replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / commands / GitTask.java
1 /*
2  * Copyright 2000-2016 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 git4idea.commands;
17
18 import com.intellij.concurrency.JobScheduler;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.progress.ProgressIndicator;
23 import com.intellij.openapi.progress.ProgressManager;
24 import com.intellij.openapi.progress.Task;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.Computable;
27 import com.intellij.openapi.util.Disposer;
28 import com.intellij.openapi.util.Key;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.openapi.vcs.VcsException;
31 import git4idea.GitVcs;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import java.util.concurrent.ScheduledFuture;
36 import java.util.concurrent.TimeUnit;
37 import java.util.concurrent.atomic.AtomicBoolean;
38 import java.util.concurrent.atomic.AtomicReference;
39
40 /**
41  * All Git commands are cancellable when called via {@link GitHandler}. <br/>
42  * To execute the command synchronously, call {@link GitHandler#runInCurrentThread(Runnable)}
43  * or better {@link Git#runCommand(Computable)}.<br/>
44  * To execute in the background or under a modal progress, use the standard {@link Task}. <br/>
45  * To watch the progress, call {@link GitStandardProgressAnalyzer#createListener(ProgressIndicator)}.
46  *
47  * @deprecated To remove in IDEA 2017.
48  */
49 @Deprecated
50 public class GitTask {
51
52   private static final Logger LOG = Logger.getInstance(GitTask.class);
53
54   private final Project myProject;
55   private final GitHandler myHandler;
56   private final String myTitle;
57   private GitProgressAnalyzer myProgressAnalyzer;
58   private ProgressIndicator myProgressIndicator;
59
60   public GitTask(Project project, GitHandler handler, String title) {
61     myProject = project;
62     myHandler = handler;
63     myTitle = title;
64   }
65
66   /**
67    * Executes this task synchronously, with a modal progress dialog.
68    * @return Result of the task execution.
69    */
70   @SuppressWarnings("unused")
71   public GitTaskResult executeModal() {
72     return execute(true);
73   }
74
75   /**
76    * Executes this task asynchronously, in background. Calls the resultHandler when finished.
77    * @param resultHandler callback called after the task has finished or was cancelled by user or automatically.
78    */
79   public void executeAsync(final GitTaskResultHandler resultHandler) {
80     execute(false, false, resultHandler);
81   }
82
83   public void executeInBackground(boolean sync, final GitTaskResultHandler resultHandler) {
84     execute(sync, false, resultHandler);
85   }
86
87   // this is always sync
88   @NotNull
89   public GitTaskResult execute(boolean modal) {
90     final AtomicReference<GitTaskResult> result = new AtomicReference<>(GitTaskResult.INITIAL);
91     execute(true, modal, new GitTaskResultHandlerAdapter() {
92       @Override
93       protected void run(GitTaskResult res) {
94         result.set(res);
95       }
96     });
97     return result.get();
98   }
99
100   /**
101    * The most general execution method.
102    * @param sync  Set to {@code true} to make the calling thread wait for the task execution.
103    * @param modal If {@code true}, the task will be modal with a modal progress dialog. If false, the task will be executed in
104    * background. {@code modal} implies {@code sync}, i.e. if modal then sync doesn't matter: you'll wait anyway.
105    * @param resultHandler Handle the result.
106    * @see #execute(boolean)
107    */
108   public void execute(boolean sync, boolean modal, final GitTaskResultHandler resultHandler) {
109     final Object LOCK = new Object();
110     final AtomicBoolean completed = new AtomicBoolean();
111
112     if (modal) {
113       final ModalTask task = new ModalTask(myProject, myHandler, myTitle) {
114         @Override public void onSuccess() {
115           commonOnSuccess(LOCK, resultHandler);
116           completed.set(true);
117         }
118         @Override public void onCancel() {
119           commonOnCancel(LOCK, resultHandler);
120           completed.set(true);
121         }
122         @Override public void onThrowable(@NotNull Throwable error) {
123           super.onThrowable(error);
124           commonOnCancel(LOCK, resultHandler);
125           completed.set(true);
126         }
127       };
128       ApplicationManager.getApplication().invokeAndWait(() -> ProgressManager.getInstance().run(task));
129     } else {
130       final BackgroundableTask task = new BackgroundableTask(myProject, myHandler, myTitle) {
131         @Override public void onSuccess() {
132           commonOnSuccess(LOCK, resultHandler);
133           completed.set(true);
134         }
135         @Override public void onCancel() {
136           commonOnCancel(LOCK, resultHandler);
137           completed.set(true);
138         }
139       };
140       if (myProgressIndicator == null) {
141         GitVcs.runInBackground(task);
142       } else {
143         task.runAlone();
144       }
145     }
146
147     if (sync) {
148       while (!completed.get()) {
149         try {
150           synchronized (LOCK) {
151             LOCK.wait(50);
152           }
153         } catch (InterruptedException e) {
154           LOG.info(e);
155         }
156       }
157     }
158   }
159
160   private void commonOnSuccess(final Object LOCK, final GitTaskResultHandler resultHandler) {
161     GitTaskResult res = !myHandler.errors().isEmpty() ? GitTaskResult.GIT_ERROR : GitTaskResult.OK;
162     resultHandler.run(res);
163     synchronized (LOCK) {
164       LOCK.notifyAll();
165     }
166   }
167
168   private void commonOnCancel(final Object LOCK, final GitTaskResultHandler resultHandler) {
169     resultHandler.run(GitTaskResult.CANCELLED);
170     synchronized (LOCK) {
171       LOCK.notifyAll();
172     }
173   }
174
175   private void addListeners(final TaskExecution task, final ProgressIndicator indicator) {
176     if (indicator != null) {
177       indicator.setIndeterminate(myProgressAnalyzer == null);
178     }
179     // When receives an error line, adds a VcsException to the GitHandler.
180     final GitLineHandlerListener listener = new GitLineHandlerListener() {
181       @Override
182       public void processTerminated(int exitCode) {
183         if (exitCode != 0 && !myHandler.isIgnoredErrorCode(exitCode)) {
184           if (myHandler.errors().isEmpty()) {
185             myHandler.addError(new VcsException(myHandler.getLastOutput()));
186           }
187         }
188       }
189
190       @Override
191       public void startFailed(Throwable exception) {
192         myHandler.addError(new VcsException("Git start failed: " + exception.getMessage(), exception));
193       }
194
195       @Override
196       public void onLineAvailable(String line, Key outputType) {
197         if (GitHandlerUtil.isErrorLine(line.trim())) {
198           myHandler.addError(new VcsException(line));
199         } else if (!StringUtil.isEmptyOrSpaces(line)) {
200           myHandler.addLastOutput(line);
201         }
202         if (indicator != null) {
203           indicator.setText2(line);
204         }
205         if (myProgressAnalyzer != null && indicator != null) {
206           final double fraction = myProgressAnalyzer.analyzeProgress(line);
207           if (fraction >= 0) {
208             indicator.setFraction(fraction);
209           }
210         }
211       }
212     };
213
214     if (myHandler instanceof GitLineHandler) {
215       ((GitLineHandler)myHandler).addLineListener(listener);
216     } else {
217       myHandler.addListener(listener);
218     }
219
220     // disposes the timer
221     myHandler.addListener(new GitHandlerListener() {
222       @Override
223       public void processTerminated(int exitCode) {
224         task.dispose();
225       }
226
227       @Override
228       public void startFailed(Throwable exception) {
229         task.dispose();
230       }
231     });
232   }
233
234   public void setProgressAnalyzer(GitProgressAnalyzer progressAnalyzer) {
235     myProgressAnalyzer = progressAnalyzer;
236   }
237
238   public void setProgressIndicator(ProgressIndicator progressIndicator) {
239     myProgressIndicator = progressIndicator;
240   }
241
242   /**
243    * We're using this interface here to work with Task, because standard {@link Task#run(com.intellij.openapi.progress.ProgressIndicator)}
244    * is busy with timers.
245    */
246   private interface TaskExecution {
247     void execute(ProgressIndicator indicator);
248     void dispose();
249   }
250
251   // To add to {@link com.intellij.openapi.progress.BackgroundTaskQueue} a task must be {@link Task.Backgroundable},
252   // so we can't have a single class representing a task: we have BackgroundableTask and ModalTask.
253   // To minimize code duplication we use GitTaskDelegate.
254
255   private abstract class BackgroundableTask extends Task.Backgroundable implements TaskExecution {
256     private final GitTaskDelegate myDelegate;
257
258     public BackgroundableTask(@Nullable final Project project, @NotNull GitHandler handler, @NotNull final String processTitle) {
259       super(project, processTitle, true);
260       myDelegate = new GitTaskDelegate(myProject, handler, this);
261     }
262
263     @Override
264     public final void run(@NotNull ProgressIndicator indicator) {
265       myDelegate.run(indicator);
266     }
267
268     public final void runAlone() {
269       if (ApplicationManager.getApplication().isDispatchThread()) {
270         ApplicationManager.getApplication().executeOnPooledThread(() -> justRun());
271       } else {
272         justRun();
273       }
274     }
275
276     private void justRun() {
277       String oldTitle = myProgressIndicator.getText();
278       myProgressIndicator.setText(myTitle);
279       myDelegate.run(myProgressIndicator);
280       myProgressIndicator.setText(oldTitle);
281       if (myProgressIndicator.isCanceled()) {
282         onCancel();
283       } else {
284         onSuccess();
285       }
286     }
287
288     @Override
289     public void execute(ProgressIndicator indicator) {
290       addListeners(this, indicator);
291       GitHandlerUtil.runInCurrentThread(myHandler, indicator, false, myTitle);
292     }
293
294     @Override
295     public void dispose() {
296       Disposer.dispose(myDelegate);
297     }
298   }
299
300   private abstract class ModalTask extends Task.Modal implements TaskExecution {
301     private final GitTaskDelegate myDelegate;
302
303     public ModalTask(@Nullable final Project project, @NotNull GitHandler handler, @NotNull final String processTitle) {
304       super(project, processTitle, true);
305       myDelegate = new GitTaskDelegate(myProject, handler, this);
306     }
307
308     @Override
309     public final void run(@NotNull ProgressIndicator indicator) {
310       myDelegate.run(indicator);
311     }
312
313     @Override
314     public void execute(ProgressIndicator indicator) {
315       addListeners(this, indicator);
316       GitHandlerUtil.runInCurrentThread(myHandler, indicator, false, myTitle);
317     }
318
319     @Override
320     public void dispose() {
321       Disposer.dispose(myDelegate);
322     }
323   }
324
325   /**
326    * Does the work which is common for BackgroundableTask and ModalTask.
327    * Actually - starts a timer which checks if current progress indicator is cancelled.
328    * If yes, kills the GitHandler.
329    */
330   private static class GitTaskDelegate implements Disposable {
331     private final GitHandler myHandler;
332     private ProgressIndicator myIndicator;
333     private final TaskExecution myTask;
334     private ScheduledFuture<?> myTimer;
335     private final Project myProject;
336
337     public GitTaskDelegate(Project project, GitHandler handler, TaskExecution task) {
338       myProject = project;
339       myHandler = handler;
340       myTask = task;
341       Disposer.register(myProject, this);
342     }
343
344     public void run(ProgressIndicator indicator) {
345       myIndicator = indicator;
346       myTimer = JobScheduler.getScheduler().scheduleWithFixedDelay(
347         ()-> {
348           if (myIndicator != null && myIndicator.isCanceled()) {
349             try {
350               if (myHandler != null) {
351                 myHandler.destroyProcess();
352               }
353             }
354             finally {
355               Disposer.dispose(this);
356             }
357           }
358       }, 0, 200, TimeUnit.MILLISECONDS);
359       myTask.execute(indicator);
360     }
361
362     @Override
363     public void dispose() {
364       if (myTimer != null) {
365         myTimer.cancel(false);
366       }
367     }
368   }
369
370 }