no dumb mode permission assertion when already in dumb mode (https://youtrack.jetbrai...
[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.ide.startup.StartupManagerEx;
20 import com.intellij.openapi.Disposable;
21 import com.intellij.openapi.application.AccessToken;
22 import com.intellij.openapi.application.Application;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.application.ModalityState;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
27 import com.intellij.openapi.progress.*;
28 import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase;
29 import com.intellij.openapi.progress.util.ProgressIndicatorBase;
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.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.Map;
50
51 public class DumbServiceImpl extends DumbService implements Disposable, ModificationTracker {
52   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.project.DumbServiceImpl");
53   private volatile boolean myDumb = false;
54   private final DumbModeListener myPublisher;
55   private long myModificationCount;
56   private final Queue<DumbModeTask> myUpdatesQueue = new Queue<DumbModeTask>(5);
57
58   /**
59    * Per-task progress indicators. Modified from EDT only.
60    * The task is removed from this map after it's finished or when the project is disposed. 
61    */
62   private final Map<DumbModeTask, ProgressIndicatorEx> myProgresses = ContainerUtil.newConcurrentMap();
63   
64   private final Queue<Runnable> myRunWhenSmartQueue = new Queue<Runnable>(5);
65   private final Project myProject;
66   private final ThreadLocal<Integer> myAlternativeResolution = new ThreadLocal<Integer>();
67   private final Map<ModalityState, DumbModePermission> myPermissions = ContainerUtil.newHashMap();
68
69   public DumbServiceImpl(Project project) {
70     myProject = project;
71     myPublisher = project.getMessageBus().syncPublisher(DUMB_MODE);
72   }
73
74   @SuppressWarnings({"MethodOverridesStaticMethodOfSuperclass"})
75   public static DumbServiceImpl getInstance(@NotNull Project project) {
76     return (DumbServiceImpl)DumbService.getInstance(project);
77   }
78
79   @Override
80   public void queueTask(@NotNull final DumbModeTask task) {
81     scheduleCacheUpdate(task, true);
82   }
83
84   @Override
85   public void cancelTask(@NotNull DumbModeTask task) {
86     if (ApplicationManager.getApplication().isInternal()) LOG.info("cancel " + task);
87     ProgressIndicatorEx indicator = myProgresses.get(task);
88     if (indicator != null) {
89       indicator.cancel();
90     }
91   }
92
93   @Override
94   public void dispose() {
95     ApplicationManager.getApplication().assertIsDispatchThread();
96     myUpdatesQueue.clear();
97     myRunWhenSmartQueue.clear();
98     for (DumbModeTask task : new ArrayList<DumbModeTask>(myProgresses.keySet())) {
99       cancelTask(task);
100       Disposer.dispose(task);
101     }
102   }
103
104   @Override
105   public Project getProject() {
106     return myProject;
107   }
108
109   @Override
110   public boolean isAlternativeResolveEnabled() {
111     return myAlternativeResolution.get() != null;
112   }
113
114   @Override
115   public void allowStartingDumbModeInside(@NotNull DumbModePermission permission, @NotNull Runnable runnable) {
116     ApplicationManager.getApplication().assertIsDispatchThread();
117     ModalityState modality = ModalityState.current();
118     DumbModePermission prev = myPermissions.put(modality, permission);
119     try {
120       runnable.run();
121     }
122     finally {
123       if (prev == null) {
124         myPermissions.remove(modality);
125       } else {
126         myPermissions.put(modality, prev);
127       }
128     }
129   }
130
131   @Override
132   public void setAlternativeResolveEnabled(boolean enabled) {
133     Integer oldValue = myAlternativeResolution.get();
134     int newValue = (oldValue == null ? 0 : oldValue) + (enabled ? 1 : -1);
135     assert newValue >= 0 : "Non-paired alternative resolution mode";
136     myAlternativeResolution.set(newValue == 0 ? null : newValue);
137   }
138
139   @Override
140   public ModificationTracker getModificationTracker() {
141     return this;
142   }
143
144   @Override
145   public boolean isDumb() {
146     return myDumb;
147   }
148
149   @TestOnly
150   public void setDumb(boolean dumb) {
151     if (dumb) {
152       myDumb = true;
153       myPublisher.enteredDumbMode();
154     }
155     else {
156       updateFinished();
157     }
158   }
159
160   @Override
161   public void runWhenSmart(@NotNull Runnable runnable) {
162     if (!isDumb()) {
163       runnable.run();
164     }
165     else {
166       synchronized (myRunWhenSmartQueue) {
167         myRunWhenSmartQueue.addLast(runnable);
168       }
169     }
170   }
171
172   private void scheduleCacheUpdate(@NotNull final DumbModeTask task, boolean forceDumbMode) {
173     final Throwable trace = new Throwable();
174     if (LOG.isDebugEnabled()) LOG.debug("Scheduling task " + task, trace);
175     final Application application = ApplicationManager.getApplication();
176
177     if (application.isUnitTestMode() ||
178         application.isHeadlessEnvironment() ||
179         !forceDumbMode && !myDumb && application.isReadAccessAllowed()) {
180       final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
181       if (indicator != null) {
182         indicator.pushState();
183       }
184       AccessToken token = HeavyProcessLatch.INSTANCE.processStarted("Performing indexing task");
185       try {
186         task.performInDumbMode(indicator != null ? indicator : new EmptyProgressIndicator());
187       }
188       finally {
189         token.finish();
190         if (indicator != null) {
191           indicator.popState();
192         }
193         Disposer.dispose(task);
194       }
195       return;
196     }
197
198     UIUtil.invokeLaterIfNeeded(new DumbAwareRunnable() {
199       @Override
200       public void run() {
201         if (myProject.isDisposed()) {
202           return;
203         }
204
205         ModalityState modality = ModalityState.current();
206         final DumbModePermission permission = getDumbModePermission(modality);
207
208         myProgresses.put(task, new ProgressIndicatorBase());
209         Disposer.register(task, new Disposable() {
210           @Override
211           public void dispose() {
212             application.assertIsDispatchThread();
213             myProgresses.remove(task);
214           }
215         });
216         myUpdatesQueue.addLast(task);
217         // ok to test and set the flag like this, because the change is always done from dispatch thread
218         if (!myDumb) {
219           if (permission == null) {
220             LOG.error("Dumb mode not permitted in modal environment; see DumbService.allowStartingDumbModeInside documentation." +
221                       "\n Current modality: " + modality +
222                       "\n all permissions: " + myPermissions, trace);
223           }
224
225           // always change dumb status inside write action.
226           // This will ensure all active read actions are completed before the app goes dumb
227           application.runWriteAction(new Runnable() {
228             @Override
229             public void run() {
230               myDumb = true;
231               myModificationCount++;
232               try {
233                 myPublisher.enteredDumbMode();
234               }
235               catch (Throwable e) {
236                 LOG.error(e);
237               }
238             }
239           });
240
241           // later because we're likely in a write action and can't start a modal progress immediately
242           // and for a background progress, it doesn't matter if it starts several milliseconds later; dumb mode is already on
243           application.invokeLater(new Runnable() {
244             @Override
245             public void run() {
246               boolean modal = permission != DumbModePermission.MAY_START_BACKGROUND;
247               boolean shouldFinish = modal;
248               try {
249                 startBackgroundProcess(modal);
250               }
251               catch (Throwable e) {
252                 shouldFinish = true;
253                 LOG.error("Failed to start background index update task", e);
254               }
255               finally {
256                 if (shouldFinish) {
257                   updateFinished();
258                 }
259               }
260             }
261           }, modality, myProject.getDisposed());
262         }
263       }
264     });
265   }
266
267   @Nullable
268   private DumbModePermission getDumbModePermission(ModalityState modality) {
269     DumbModePermission permission = myPermissions.get(modality);
270     if (permission != null) {
271       return permission;
272     }
273
274     if (modality == ModalityState.NON_MODAL || !StartupManagerEx.getInstanceEx(myProject).postStartupActivityPassed()) {
275       return DumbModePermission.MAY_START_BACKGROUND;
276     }
277
278     return null;
279   }
280
281   private void updateFinished() {
282     myDumb = false;
283     myModificationCount++;
284     if (myProject.isDisposed()) return;
285
286     if (ApplicationManager.getApplication().isInternal()) LOG.info("updateFinished");
287
288     try {
289       myPublisher.exitDumbMode();
290       FileEditorManagerEx.getInstanceEx(myProject).refreshIcons();
291     }
292     finally {
293       // It may happen that one of the pending runWhenSmart actions triggers new dumb mode;
294       // in this case we should quit processing pending actions and postpone them until the newly started dumb mode finishes.
295       while (!myDumb) {
296         final Runnable runnable;
297         synchronized (myRunWhenSmartQueue) {
298           if (myRunWhenSmartQueue.isEmpty()) {
299             break;
300           }
301           runnable = myRunWhenSmartQueue.pullFirst();
302         }
303         try {
304           runnable.run();
305         }
306         catch (Throwable e) {
307           LOG.error("Error executing task " + runnable, e);
308         }
309       }
310     }
311   }
312
313   @Override
314   public void showDumbModeNotification(@NotNull final String message) {
315     UIUtil.invokeLaterIfNeeded(new Runnable() {
316       @Override
317       public void run() {
318         final IdeFrame ideFrame = WindowManager.getInstance().getIdeFrame(myProject);
319         if (ideFrame != null) {
320           StatusBarEx statusBar = (StatusBarEx)ideFrame.getStatusBar();
321           statusBar.notifyProgressByBalloon(MessageType.WARNING, message, null, null);
322         }
323       }
324     });
325   }
326
327   @Override
328   public void waitForSmartMode() {
329     if (!isDumb()) {
330       return;
331     }
332
333     final Application application = ApplicationManager.getApplication();
334     if (application.isReadAccessAllowed() || application.isDispatchThread()) {
335       throw new AssertionError("Don't invoke waitForSmartMode from inside read action in dumb mode");
336     }
337
338     final Semaphore semaphore = new Semaphore();
339     semaphore.down();
340     runWhenSmart(new Runnable() {
341       @Override
342       public void run() {
343         semaphore.up();
344       }
345     });
346     while (true) {
347       if (semaphore.waitFor(50)) {
348         return;
349       }
350       ProgressManager.checkCanceled();
351     }
352   }
353
354   @Override
355   public JComponent wrapGently(@NotNull JComponent dumbUnawareContent, @NotNull Disposable parentDisposable) {
356     final DumbUnawareHider wrapper = new DumbUnawareHider(dumbUnawareContent);
357     wrapper.setContentVisible(!isDumb());
358     getProject().getMessageBus().connect(parentDisposable).subscribe(DUMB_MODE, new DumbModeListener() {
359
360       @Override
361       public void enteredDumbMode() {
362         wrapper.setContentVisible(false);
363       }
364
365       @Override
366       public void exitDumbMode() {
367         wrapper.setContentVisible(true);
368       }
369     });
370
371     return wrapper;
372   }
373
374   @Override
375   public void smartInvokeLater(@NotNull final Runnable runnable) {
376     ApplicationManager.getApplication().invokeLater(new Runnable() {
377       @Override
378       public void run() {
379         runWhenSmart(runnable);
380       }
381
382       @Override
383       public String toString() {
384         return runnable.toString();
385       }
386     }, myProject.getDisposed());
387   }
388
389   @Override
390   public void smartInvokeLater(@NotNull final Runnable runnable, @NotNull ModalityState modalityState) {
391     ApplicationManager.getApplication().invokeLater(new Runnable() {
392       @Override
393       public void run() {
394         runWhenSmart(runnable);
395       }
396     }, modalityState, myProject.getDisposed());
397   }
398
399   private void startBackgroundProcess(final boolean modal) {
400     ProgressManager.getInstance().run(new Task.Backgroundable(myProject, IdeBundle.message("progress.indexing"), false) {
401
402       @Override
403       public void run(@NotNull final ProgressIndicator visibleIndicator) {
404         final ShutDownTracker shutdownTracker = ShutDownTracker.getInstance();
405         final Thread self = Thread.currentThread();
406         AccessToken token = HeavyProcessLatch.INSTANCE.processStarted("Performing indexing tasks");
407         try {
408           shutdownTracker.registerStopperThread(self);
409
410           if (visibleIndicator instanceof ProgressIndicatorEx) {
411             ((ProgressIndicatorEx)visibleIndicator).addStateDelegate(new AppIconProgress());
412           }
413
414           DumbModeTask task = null;
415           while (true) {
416             Pair<DumbModeTask, ProgressIndicatorEx> pair = getNextTask(task);
417             if (pair == null) break;
418             
419             task = pair.first;
420             ProgressIndicatorEx taskIndicator = pair.second;
421             if (visibleIndicator instanceof ProgressIndicatorEx) {
422               taskIndicator.addStateDelegate(new AbstractProgressIndicatorExBase() {
423                 @Override
424                 protected void delegateProgressChange(@NotNull IndicatorAction action) {
425                   super.delegateProgressChange(action);
426                   action.execute((ProgressIndicatorEx)visibleIndicator);
427                 }
428               });
429             }
430             runSingleTask(task, taskIndicator);
431           }
432         }
433         catch (Throwable unexpected) {
434           LOG.error(unexpected);
435         }
436         finally {
437           shutdownTracker.unregisterStopperThread(self);
438           token.finish();
439         }
440       }
441
442       public boolean isConditionalModal() {
443         return modal;
444       }
445
446       @Override
447       public boolean shouldStartInBackground() {
448         return !modal;
449       }
450     });
451   }
452
453   private static void runSingleTask(final DumbModeTask task, final ProgressIndicatorEx taskIndicator) {
454     if (ApplicationManager.getApplication().isInternal()) LOG.info("Running dumb mode task: " + task);
455     
456     // nested runProcess is needed for taskIndicator to be honored in ProgressManager.checkCanceled calls deep inside tasks 
457     ProgressManager.getInstance().runProcess(new Runnable() {
458       @Override
459       public void run() {
460         try {
461           taskIndicator.checkCanceled();
462
463           taskIndicator.setIndeterminate(true);
464           taskIndicator.setText(IdeBundle.message("progress.indexing.scanning"));
465
466           task.performInDumbMode(taskIndicator);
467         }
468         catch (ProcessCanceledException ignored) {
469         }
470         catch (Throwable unexpected) {
471           LOG.error(unexpected);
472         }
473       }
474     }, taskIndicator);
475   }
476
477   @Nullable private Pair<DumbModeTask, ProgressIndicatorEx> getNextTask(@Nullable final DumbModeTask prevTask) {
478     final Ref<Pair<DumbModeTask, ProgressIndicatorEx>> result = Ref.create();
479     invokeAndWaitIfNeeded(new Runnable() {
480       @Override
481       public void run() {
482         if (myProject.isDisposed()) return;
483         if (prevTask != null) {
484           Disposer.dispose(prevTask);
485         }
486
487         while (true) {
488           if (myUpdatesQueue.isEmpty()) {
489             updateFinished();
490             return;
491           }
492
493           DumbModeTask queuedTask = myUpdatesQueue.pullFirst();
494           ProgressIndicatorEx indicator = myProgresses.get(queuedTask);
495           if (indicator.isCanceled()) {
496             Disposer.dispose(queuedTask);
497             continue;
498           }
499           
500           result.set(Pair.create(queuedTask, indicator));
501           return;
502         }
503       }
504     });
505     return result.get();
506   }
507
508   private static void invokeAndWaitIfNeeded(Runnable runnable) {
509     if (SwingUtilities.isEventDispatchThread()) {
510       runnable.run();
511     }
512     else {
513       try {
514         SwingUtilities.invokeAndWait(runnable);
515       }
516       catch (InterruptedException ignore) {
517       }
518       catch (Exception e) {
519         LOG.error(e);
520       }
521     }
522   }
523
524   @Override
525   public long getModificationCount() {
526     return myModificationCount;
527   }
528
529   private class AppIconProgress extends ProgressIndicatorBase {
530     private double lastFraction;
531
532     @Override
533     public void setFraction(final double fraction) {
534       if (fraction - lastFraction < 0.01d) return;
535       lastFraction = fraction;
536       UIUtil.invokeLaterIfNeeded(new Runnable() {
537         @Override
538         public void run() {
539           AppIcon.getInstance().setProgress(myProject, "indexUpdate", AppIconScheme.Progress.INDEXING, fraction, true);
540         }
541       });
542     }
543
544     @Override
545     public void finish(@NotNull TaskInfo task) {
546       if (lastFraction != 0) { // we should call setProgress at least once before
547         UIUtil.invokeLaterIfNeeded(new Runnable() {
548           @Override
549           public void run() {
550             AppIcon appIcon = AppIcon.getInstance();
551             if (appIcon.hideProgress(myProject, "indexUpdate")) {
552               appIcon.requestAttention(myProject, false);
553               appIcon.setOkBadge(myProject, true);
554             }
555           }
556         });
557       }
558     }
559   }
560 }