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