/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.vfs.newvfs;
import com.intellij.codeInsight.daemon.impl.FileStatusMap;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.TransactionId;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
import com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
import com.intellij.openapi.vfs.newvfs.persistent.RefreshWorker;
import com.intellij.util.concurrency.Semaphore;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author max
*/
public class RefreshSessionImpl extends RefreshSession {
private static final Logger LOG = Logger.getInstance(RefreshSession.class);
private static final AtomicLong ID_COUNTER = new AtomicLong(0);
private final long myId = ID_COUNTER.incrementAndGet();
private final boolean myIsAsync;
private final boolean myIsRecursive;
private final Runnable myFinishRunnable;
private final Throwable myStartTrace;
private final Semaphore mySemaphore = new Semaphore();
private List<VirtualFile> myWorkQueue = new ArrayList<>();
private List<VFileEvent> myEvents = new ArrayList<>();
private volatile boolean myHaveEventsToFire;
private volatile RefreshWorker myWorker;
private volatile boolean myCancelled;
private final TransactionId myTransaction;
RefreshSessionImpl(boolean async, boolean recursive, @Nullable Runnable finishRunnable, @Nullable TransactionId context) {
myIsAsync = async;
myIsRecursive = recursive;
myFinishRunnable = finishRunnable;
myTransaction = context;
LOG.assertTrue(context == ModalityState.NON_MODAL || context != ModalityState.any(), "Refresh session should have a specific modality");
myStartTrace = rememberStartTrace();
}
private Throwable rememberStartTrace() {
if (ApplicationManager.getApplication().isUnitTestMode() &&
(myIsAsync || !ApplicationManager.getApplication().isDispatchThread())) {
return new Throwable();
}
return null;
}
RefreshSessionImpl(@NotNull List<VFileEvent> events) {
this(false, false, null, null);
myEvents.addAll(events);
}
@Override
public long getId() {
return myId;
}
@Override
public void addAllFiles(@NotNull Collection<? extends VirtualFile> files) {
for (VirtualFile file : files) {
if (file == null) {
LOG.error("null passed among " + files);
}
else {
myWorkQueue.add(file);
}
}
}
@Override
public void addFile(@NotNull VirtualFile file) {
myWorkQueue.add(file);
}
@Override
public boolean isAsynchronous() {
return myIsAsync;
}
@Override
public void launch() {
mySemaphore.down();
((RefreshQueueImpl)RefreshQueue.getInstance()).execute(this);
}
public void scan() {
List<VirtualFile> workQueue = myWorkQueue;
myWorkQueue = new ArrayList<>();
boolean haveEventsToFire = myFinishRunnable != null || !myEvents.isEmpty();
if (!workQueue.isEmpty()) {
LocalFileSystem fs = LocalFileSystem.getInstance();
if (fs instanceof LocalFileSystemImpl) {
((LocalFileSystemImpl)fs).markSuspiciousFilesDirty(workQueue);
}
long t = 0;
if (LOG.isTraceEnabled()) {
LOG.trace("scanning " + workQueue);
t = System.currentTimeMillis();
}
int count = 0;
refresh: do {
if (LOG.isTraceEnabled()) LOG.trace("try=" + count);
for (VirtualFile file : workQueue) {
if (myCancelled) break refresh;
NewVirtualFile nvf = (NewVirtualFile)file;
if (!myIsRecursive && !myIsAsync) {
nvf.markDirty(); // always scan when non-recursive AND synchronous - needed e.g. when refreshing project files on open
}
RefreshWorker worker = new RefreshWorker(nvf, myIsRecursive);
myWorker = worker;
worker.scan();
haveEventsToFire |= myEvents.addAll(worker.getEvents());
}
count++;
if (LOG.isTraceEnabled()) LOG.trace("events=" + myEvents.size());
}
while (!myCancelled && myIsRecursive && count < 3 && workQueue.stream().anyMatch(f -> ((NewVirtualFile)f).isDirty()));
if (t != 0) {
t = System.currentTimeMillis() - t;
LOG.trace((myCancelled ? "cancelled, " : "done, ") + t + " ms, events " + myEvents);
}
}
myWorker = null;
myHaveEventsToFire = haveEventsToFire;
}
void cancel() {
myCancelled = true;
RefreshWorker worker = myWorker;
if (worker != null) {
worker.cancel();
}
}
void fireEvents() {
if (!myHaveEventsToFire || ApplicationManager.getApplication().isDisposed()) {
mySemaphore.up();
return;
}
try {
if (LOG.isDebugEnabled()) LOG.debug("events are about to fire: " + myEvents);
WriteAction.run(this::fireEventsInWriteAction);
}
finally {
mySemaphore.up();
}
}
private void fireEventsInWriteAction() {
final VirtualFileManagerEx manager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
manager.fireBeforeRefreshStart(myIsAsync);
try {
while (!myWorkQueue.isEmpty() || !myEvents.isEmpty()) {
PersistentFS.getInstance().processEvents(mergeEventsAndReset());
scan();
}
}
catch (AssertionError e) {
if (FileStatusMap.CHANGES_NOT_ALLOWED_DURING_HIGHLIGHTING.equals(e.getMessage())) {
throw new AssertionError("VFS changes are not allowed during highlighting", myStartTrace);
}
throw e;
}
finally {
try {
manager.fireAfterRefreshFinish(myIsAsync);
}
finally {
if (myFinishRunnable != null) {
myFinishRunnable.run();
}
}
}
}
public void waitFor() {
mySemaphore.waitFor();
}
private List<VFileEvent> mergeEventsAndReset() {
Set<VFileEvent> mergedEvents = new LinkedHashSet<>(myEvents);
List<VFileEvent> events = new ArrayList<>(mergedEvents);
myEvents = new ArrayList<>();
return events;
}
@Nullable
TransactionId getTransaction() {
return myTransaction;
}
@Override
public String toString() {
return myWorkQueue.size() <= 1 ? "" : myWorkQueue.size() + " roots in queue.";
}
}