remove allowStartingDumbModeInside implementation
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / project / DumbServiceImpl.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.project;
17
18 import com.intellij.ide.IdeBundle;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.application.*;
21 import com.intellij.openapi.application.impl.ApplicationImpl;
22 import com.intellij.openapi.diagnostic.Attachment;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
25 import com.intellij.openapi.progress.*;
26 import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase;
27 import com.intellij.openapi.progress.util.ProgressIndicatorBase;
28 import com.intellij.openapi.startup.StartupManager;
29 import com.intellij.openapi.ui.MessageType;
30 import com.intellij.openapi.util.*;
31 import com.intellij.openapi.wm.AppIconScheme;
32 import com.intellij.openapi.wm.IdeFrame;
33 import com.intellij.openapi.wm.WindowManager;
34 import com.intellij.openapi.wm.ex.ProgressIndicatorEx;
35 import com.intellij.openapi.wm.ex.StatusBarEx;
36 import com.intellij.ui.AppIcon;
37 import com.intellij.util.ExceptionUtil;
38 import com.intellij.util.concurrency.Semaphore;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.containers.Queue;
41 import com.intellij.util.io.storage.HeavyProcessLatch;
42 import com.intellij.util.ui.UIUtil;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
45 import org.jetbrains.annotations.TestOnly;
46
47 import javax.swing.*;
48 import java.util.ArrayList;
49 import java.util.HashSet;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.concurrent.atomic.AtomicReference;
53
54 public class DumbServiceImpl extends DumbService implements Disposable, ModificationTracker {
55   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.project.DumbServiceImpl");
56   private static Throwable ourForcedTrace;
57   private final AtomicReference<State> myState = new AtomicReference<>(State.SMART);
58   private volatile Throwable myDumbStart;
59   private volatile TransactionId myDumbStartTransaction;
60   private final DumbModeListener myPublisher;
61   private long myModificationCount;
62   private final Set<Object> myQueuedEquivalences = new HashSet<>();
63   private final Queue<DumbModeTask> myUpdatesQueue = new Queue<>(5);
64
65   /**
66    * Per-task progress indicators. Modified from EDT only.
67    * The task is removed from this map after it's finished or when the project is disposed. 
68    */
69   private final Map<DumbModeTask, ProgressIndicatorEx> myProgresses = ContainerUtil.newConcurrentMap();
70   
71   private final Queue<Runnable> myRunWhenSmartQueue = new Queue<>(5);
72   private final Project myProject;
73   private final ThreadLocal<Integer> myAlternativeResolution = new ThreadLocal<>();
74
75   public DumbServiceImpl(Project project) {
76     myProject = project;
77     myPublisher = project.getMessageBus().syncPublisher(DUMB_MODE);
78   }
79
80   @SuppressWarnings("MethodOverridesStaticMethodOfSuperclass")
81   public static DumbServiceImpl getInstance(@NotNull Project project) {
82     return (DumbServiceImpl)DumbService.getInstance(project);
83   }
84
85   @Override
86   public void cancelTask(@NotNull DumbModeTask task) {
87     if (ApplicationManager.getApplication().isInternal()) LOG.info("cancel " + task);
88     ProgressIndicatorEx indicator = myProgresses.get(task);
89     if (indicator != null) {
90       indicator.cancel();
91     }
92   }
93
94   @Override
95   public void dispose() {
96     ApplicationManager.getApplication().assertIsDispatchThread();
97     myUpdatesQueue.clear();
98     myQueuedEquivalences.clear();
99     myRunWhenSmartQueue.clear();
100     for (DumbModeTask task : new ArrayList<>(myProgresses.keySet())) {
101       cancelTask(task);
102       Disposer.dispose(task);
103     }
104   }
105
106   @Override
107   public Project getProject() {
108     return myProject;
109   }
110
111   @Override
112   public boolean isAlternativeResolveEnabled() {
113     return myAlternativeResolution.get() != null;
114   }
115
116   @Override
117   public void setAlternativeResolveEnabled(boolean enabled) {
118     Integer oldValue = myAlternativeResolution.get();
119     int newValue = (oldValue == null ? 0 : oldValue) + (enabled ? 1 : -1);
120     assert newValue >= 0 : "Non-paired alternative resolution mode";
121     myAlternativeResolution.set(newValue == 0 ? null : newValue);
122   }
123
124   @Override
125   public ModificationTracker getModificationTracker() {
126     return this;
127   }
128
129   @Override
130   public boolean isDumb() {
131     return myState.get() != State.SMART;
132   }
133
134   @TestOnly
135   public void setDumb(boolean dumb) {
136     if (dumb) {
137       myState.set(State.RUNNING_DUMB_TASKS);
138       myPublisher.enteredDumbMode();
139     }
140     else {
141       myState.set(State.WAITING_FOR_FINISH);
142       updateFinished();
143     }
144   }
145
146   @Override
147   public void runWhenSmart(@NotNull Runnable runnable) {
148     StartupManager.getInstance(myProject).runWhenProjectIsInitialized(() -> {
149       synchronized (myRunWhenSmartQueue) {
150         if (isDumb()) {
151           myRunWhenSmartQueue.addLast(runnable);
152           return;
153         }
154       }
155
156       runnable.run();
157     });
158   }
159
160   @Override
161   public void queueTask(@NotNull final DumbModeTask task) {
162     TransactionId contextTransaction = TransactionGuard.getInstance().getContextTransaction();
163
164     final Throwable trace = ourForcedTrace != null ? ourForcedTrace : new Throwable(); // please report exceptions here to peter
165     if (LOG.isDebugEnabled()) LOG.debug("Scheduling task " + task);
166     final Application application = ApplicationManager.getApplication();
167
168     if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
169       final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
170       if (indicator != null) {
171         indicator.pushState();
172       }
173       AccessToken token = HeavyProcessLatch.INSTANCE.processStarted("Performing indexing task");
174       try {
175         task.performInDumbMode(indicator != null ? indicator : new EmptyProgressIndicator());
176       }
177       finally {
178         token.finish();
179         if (indicator != null) {
180           indicator.popState();
181         }
182         Disposer.dispose(task);
183       }
184       return;
185     }
186
187     Runnable runnable = () -> queueTaskOnEdt(task, contextTransaction, trace);
188     if (application.isDispatchThread()) {
189       runnable.run(); // will log errors if not already in a write-safe context
190     } else {
191       TransactionGuard.submitTransaction(myProject, runnable);
192     }
193   }
194
195   private void queueTaskOnEdt(@NotNull DumbModeTask task,
196                               @Nullable TransactionId contextTransaction,
197                               @NotNull Throwable trace) {
198     if (!addTaskToQueue(task)) return;
199
200     if (myState.get() == State.SMART || myState.get() == State.WAITING_FOR_FINISH) {
201       WriteAction.run(() -> enterDumbMode(contextTransaction, trace));
202       ApplicationManager.getApplication().invokeLater(this::startBackgroundProcess);
203     }
204   }
205
206   private boolean addTaskToQueue(@NotNull DumbModeTask task) {
207     if (!myQueuedEquivalences.add(task.getEquivalenceObject())) {
208       Disposer.dispose(task);
209       return false;
210     }
211
212     myProgresses.put(task, new ProgressIndicatorBase());
213     Disposer.register(task, () -> {
214       ApplicationManager.getApplication().assertIsDispatchThread();
215       myProgresses.remove(task);
216     });
217     myUpdatesQueue.addLast(task);
218     return true;
219   }
220
221   private void enterDumbMode(@Nullable TransactionId contextTransaction, @NotNull Throwable trace) {
222     boolean wasSmart = !isDumb();
223     synchronized (myRunWhenSmartQueue) {
224       myState.set(State.SCHEDULED_TASKS);
225     }
226     myDumbStart = trace;
227     myDumbStartTransaction = contextTransaction;
228     myModificationCount++;
229     if (wasSmart) {
230       try {
231         myPublisher.enteredDumbMode();
232       }
233       catch (Throwable e) {
234         LOG.error(e);
235       }
236     }
237   }
238
239   @NotNull
240   public static AccessToken forceDumbModeStartTrace(@NotNull Throwable trace) {
241     ApplicationManager.getApplication().assertIsDispatchThread();
242     final Throwable prev = ourForcedTrace;
243     ourForcedTrace = trace;
244     return new AccessToken() {
245       @Override
246       public void finish() {
247         //noinspection AssignmentToStaticFieldFromInstanceMethod
248         ourForcedTrace = prev;
249       }
250     };
251   }
252
253   private void queueUpdateFinished() {
254     if (myState.compareAndSet(State.RUNNING_DUMB_TASKS, State.WAITING_FOR_FINISH)) {
255       StartupManager.getInstance(myProject).runWhenProjectIsInitialized(
256         () -> TransactionGuard.getInstance().submitTransaction(myProject, myDumbStartTransaction, () ->
257           WriteAction.run(
258             () -> updateFinished())));
259     }
260   }
261
262   private void updateFinished() {
263     synchronized (myRunWhenSmartQueue) {
264       if (!myState.compareAndSet(State.WAITING_FOR_FINISH, State.SMART)) {
265         return;
266       }
267     }
268     myDumbStart = null;
269     myModificationCount++;
270     if (myProject.isDisposed()) return;
271
272     if (ApplicationManager.getApplication().isInternal()) LOG.info("updateFinished");
273
274     notifyUpdateFinished();
275   }
276
277   private void notifyUpdateFinished() {
278     try {
279       myPublisher.exitDumbMode();
280       FileEditorManagerEx.getInstanceEx(myProject).refreshIcons();
281     }
282     finally {
283       // It may happen that one of the pending runWhenSmart actions triggers new dumb mode;
284       // in this case we should quit processing pending actions and postpone them until the newly started dumb mode finishes.
285       while (!isDumb()) {
286         final Runnable runnable;
287         synchronized (myRunWhenSmartQueue) {
288           if (myRunWhenSmartQueue.isEmpty()) {
289             break;
290           }
291           runnable = myRunWhenSmartQueue.pullFirst();
292         }
293         try {
294           runnable.run();
295         }
296         catch (ProcessCanceledException e) {
297           LOG.error("Task canceled: " + runnable, new Attachment("pce.trace", ExceptionUtil.getThrowableText(e)));
298         }
299         catch (Throwable e) {
300           LOG.error("Error executing task " + runnable, e);
301         }
302       }
303     }
304   }
305
306   @Override
307   public void showDumbModeNotification(@NotNull final String message) {
308     UIUtil.invokeLaterIfNeeded(() -> {
309       final IdeFrame ideFrame = WindowManager.getInstance().getIdeFrame(myProject);
310       if (ideFrame != null) {
311         StatusBarEx statusBar = (StatusBarEx)ideFrame.getStatusBar();
312         statusBar.notifyProgressByBalloon(MessageType.WARNING, message, null, null);
313       }
314     });
315   }
316
317   @Override
318   public void waitForSmartMode() {
319     if (!isDumb()) {
320       return;
321     }
322
323     final Application application = ApplicationManager.getApplication();
324     if (application.isReadAccessAllowed() || application.isDispatchThread()) {
325       throw new AssertionError("Don't invoke waitForSmartMode from inside read action in dumb mode");
326     }
327
328     final Semaphore semaphore = new Semaphore();
329     semaphore.down();
330     runWhenSmart(semaphore::up);
331     while (true) {
332       if (semaphore.waitFor(50)) {
333         return;
334       }
335       ProgressManager.checkCanceled();
336     }
337   }
338
339   @Override
340   public JComponent wrapGently(@NotNull JComponent dumbUnawareContent, @NotNull Disposable parentDisposable) {
341     final DumbUnawareHider wrapper = new DumbUnawareHider(dumbUnawareContent);
342     wrapper.setContentVisible(!isDumb());
343     getProject().getMessageBus().connect(parentDisposable).subscribe(DUMB_MODE, new DumbModeListener() {
344
345       @Override
346       public void enteredDumbMode() {
347         wrapper.setContentVisible(false);
348       }
349
350       @Override
351       public void exitDumbMode() {
352         wrapper.setContentVisible(true);
353       }
354     });
355
356     return wrapper;
357   }
358
359   @Override
360   public void smartInvokeLater(@NotNull final Runnable runnable) {
361     smartInvokeLater(runnable, ModalityState.defaultModalityState());
362   }
363
364   @Override
365   public void smartInvokeLater(@NotNull final Runnable runnable, @NotNull ModalityState modalityState) {
366     ApplicationManager.getApplication().invokeLater(() -> {
367       if (isDumb()) {
368         runWhenSmart(() -> smartInvokeLater(runnable, modalityState));
369       } else {
370         runnable.run();
371       }
372     }, modalityState, myProject.getDisposed());
373   }
374
375   @Override
376   public void completeJustSubmittedTasks() {
377     assert myProject.isInitialized();
378     if (myState.get() != State.SCHEDULED_TASKS) {
379       return;
380     }
381
382     while (isDumb()) {
383       showModalProgress();
384     }
385   }
386
387   private void showModalProgress() {
388     try {
389       ((ApplicationImpl)ApplicationManager.getApplication()).executeSuspendingWriteAction(myProject, IdeBundle.message("progress.indexing"), () ->
390         runBackgroundProcess(ProgressManager.getInstance().getProgressIndicator()));
391     }
392     finally {
393       if (myState.get() != State.SMART) {
394         if (myState.get() != State.WAITING_FOR_FINISH) throw new AssertionError(myState.get());
395         WriteAction.run(() -> updateFinished());
396       }
397     }
398   }
399
400   private void startBackgroundProcess() {
401     try {
402       ProgressManager.getInstance().run(new Task.Backgroundable(myProject, IdeBundle.message("progress.indexing"), false) {
403         @Override
404         public void run(@NotNull final ProgressIndicator visibleIndicator) {
405           runBackgroundProcess(visibleIndicator);
406         }
407       });
408     }
409     catch (Throwable e) {
410       queueUpdateFinished();
411       LOG.error("Failed to start background index update task", e);
412     }
413   }
414
415   private void runBackgroundProcess(@NotNull final ProgressIndicator visibleIndicator) {
416     if (!myState.compareAndSet(State.SCHEDULED_TASKS, State.RUNNING_DUMB_TASKS)) return;
417
418     final ShutDownTracker shutdownTracker = ShutDownTracker.getInstance();
419     final Thread self = Thread.currentThread();
420     try (AccessToken ignored = HeavyProcessLatch.INSTANCE.processStarted("Performing indexing tasks")) {
421       shutdownTracker.registerStopperThread(self);
422
423       if (visibleIndicator instanceof ProgressIndicatorEx) {
424         ((ProgressIndicatorEx)visibleIndicator).addStateDelegate(new AppIconProgress());
425       }
426
427       DumbModeTask task = null;
428       while (true) {
429         Pair<DumbModeTask, ProgressIndicatorEx> pair = getNextTask(task);
430         if (pair == null) break;
431
432         task = pair.first;
433         ProgressIndicatorEx taskIndicator = pair.second;
434         if (visibleIndicator instanceof ProgressIndicatorEx) {
435           taskIndicator.addStateDelegate(new AbstractProgressIndicatorExBase() {
436             @Override
437             protected void delegateProgressChange(@NotNull IndicatorAction action) {
438               super.delegateProgressChange(action);
439               action.execute((ProgressIndicatorEx)visibleIndicator);
440             }
441           });
442         }
443         runSingleTask(task, taskIndicator);
444       }
445     }
446     catch (Throwable unexpected) {
447       LOG.error(unexpected);
448     }
449     finally {
450       shutdownTracker.unregisterStopperThread(self);
451     }
452   }
453
454   private static void runSingleTask(final DumbModeTask task, final ProgressIndicatorEx taskIndicator) {
455     if (ApplicationManager.getApplication().isInternal()) LOG.info("Running dumb mode task: " + task);
456     
457     // nested runProcess is needed for taskIndicator to be honored in ProgressManager.checkCanceled calls deep inside tasks 
458     ProgressManager.getInstance().runProcess(() -> {
459       try {
460         taskIndicator.checkCanceled();
461
462         taskIndicator.setIndeterminate(true);
463         taskIndicator.setText(IdeBundle.message("progress.indexing.scanning"));
464
465         task.performInDumbMode(taskIndicator);
466       }
467       catch (ProcessCanceledException ignored) {
468       }
469       catch (Throwable unexpected) {
470         LOG.error(unexpected);
471       }
472     }, taskIndicator);
473   }
474
475   @Nullable private Pair<DumbModeTask, ProgressIndicatorEx> getNextTask(@Nullable final DumbModeTask prevTask) {
476     final Ref<Pair<DumbModeTask, ProgressIndicatorEx>> result = Ref.create();
477     invokeAndWaitIfNeeded(() -> {
478       if (myProject.isDisposed()) return;
479       if (prevTask != null) {
480         Disposer.dispose(prevTask);
481       }
482
483       while (true) {
484         if (myUpdatesQueue.isEmpty()) {
485           queueUpdateFinished();
486           return;
487         }
488
489         DumbModeTask queuedTask = myUpdatesQueue.pullFirst();
490         myQueuedEquivalences.remove(queuedTask.getEquivalenceObject());
491         ProgressIndicatorEx indicator = myProgresses.get(queuedTask);
492         if (indicator.isCanceled()) {
493           Disposer.dispose(queuedTask);
494           continue;
495         }
496
497         result.set(Pair.create(queuedTask, indicator));
498         return;
499       }
500     });
501     return result.get();
502   }
503
504   private static void invokeAndWaitIfNeeded(Runnable runnable) {
505     Application app = ApplicationManager.getApplication();
506     if (app.isDispatchThread()) {
507       runnable.run();
508       return;
509     }
510
511     Semaphore semaphore = new Semaphore();
512     semaphore.down();
513     app.invokeLater(() -> {
514       try {
515         runnable.run();
516       } finally {
517         semaphore.up();
518       }
519     }, ModalityState.any());
520     try {
521       semaphore.waitFor();
522     }
523     catch (ProcessCanceledException ignore) {
524     }
525   }
526
527   @Override
528   public long getModificationCount() {
529     return myModificationCount;
530   }
531
532   @Nullable
533   public Throwable getDumbModeStartTrace() {
534     return myDumbStart;
535   }
536
537   private class AppIconProgress extends ProgressIndicatorBase {
538     private double lastFraction;
539
540     @Override
541     public void setFraction(final double fraction) {
542       if (fraction - lastFraction < 0.01d) return;
543       lastFraction = fraction;
544       UIUtil.invokeLaterIfNeeded(
545         () -> AppIcon.getInstance().setProgress(myProject, "indexUpdate", AppIconScheme.Progress.INDEXING, fraction, true));
546     }
547
548     @Override
549     public void finish(@NotNull TaskInfo task) {
550       if (lastFraction != 0) { // we should call setProgress at least once before
551         UIUtil.invokeLaterIfNeeded(() -> {
552           AppIcon appIcon = AppIcon.getInstance();
553           if (appIcon.hideProgress(myProject, "indexUpdate")) {
554             appIcon.requestAttention(myProject, false);
555             appIcon.setOkBadge(myProject, true);
556           }
557         });
558       }
559     }
560   }
561
562   private enum State {
563     /** Non-dumb mode. For all other states, {@link #isDumb()} returns true. */
564     SMART,
565
566     /**
567      * A state between entering dumb mode ({@link #queueTaskOnEdt}) and actually starting the background progress later ({@link #runBackgroundProcess}).
568      * In this state, it's possible to call {@link #completeJustSubmittedTasks()} and perform all submitted the tasks modally.
569      * This state can happen after {@link #SMART} or {@link #WAITING_FOR_FINISH}. Followed by {@link #RUNNING_DUMB_TASKS}.
570      */
571     SCHEDULED_TASKS,
572
573     /**
574      * Indicates that a background thread is currently executing dumb tasks.
575      */
576     RUNNING_DUMB_TASKS,
577
578     /**
579      * Set after background execution ({@link #RUNNING_DUMB_TASKS}) finishes, until the dumb mode can be exited
580      * (in a write-safe context on EDT when project is initialized). If new tasks are queued at this state, it's switched to {@link #SCHEDULED_TASKS}.
581      */
582     WAITING_FOR_FINISH
583   }
584 }