blob: 79f8e33ef55e12e2a2caf56c76683b1a497f644d [file] [log] [blame]
/*
* Copyright 2000-2013 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 git4idea.commands;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import git4idea.GitVcs;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* A Task to run the given GitHandler with ability to cancel it.
*
* <b>Cancellation</b> is implemented with a {@link java.util.Timer} which checks whether the ProgressIndicator was cancelled and kills
* the GitHandler in that case.
*
* A GitTask may be executed synchronously ({@link #executeModal()} or asynchronously ({@link #executeAsync(GitTask.ResultHandler)}.
* Result of the execution is encapsulated in {@link GitTaskResult}.
*
* <p>
* <b>GitTaskResultHandler</b> is called from AWT-thread. Use {@link #setExecuteResultInAwt(boolean) setExecuteResultInAwt(false)}
* to execute the result {@link com.intellij.openapi.application.Application#executeOnPooledThread(Runnable) on the pooled thread}.
* </p>
*
* @author Kirill Likhodedov
*/
public class GitTask {
private static final Logger LOG = Logger.getInstance(GitTask.class);
private final Project myProject;
private final GitHandler myHandler;
private final String myTitle;
private GitProgressAnalyzer myProgressAnalyzer;
private ProgressIndicator myProgressIndicator;
public GitTask(Project project, GitHandler handler, String title) {
myProject = project;
myHandler = handler;
myTitle = title;
}
/**
* Executes this task synchronously, with a modal progress dialog.
* @return Result of the task execution.
*/
public GitTaskResult executeModal() {
return execute(true);
}
/**
* Executes the task synchronously, with a modal progress dialog.
* @param resultHandler callback which will be called after task execution.
*/
public void executeModal(GitTaskResultHandler resultHandler) {
execute(true, true, resultHandler);
}
/**
* Executes this task asynchronously, in background. Calls the resultHandler when finished.
* @param resultHandler callback called after the task has finished or was cancelled by user or automatically.
*/
public void executeAsync(final GitTaskResultHandler resultHandler) {
execute(false, false, resultHandler);
}
public void executeInBackground(boolean sync, final GitTaskResultHandler resultHandler) {
execute(sync, false, resultHandler);
}
// this is always sync
@NotNull
public GitTaskResult execute(boolean modal) {
final AtomicReference<GitTaskResult> result = new AtomicReference<GitTaskResult>(GitTaskResult.INITIAL);
execute(true, modal, new GitTaskResultHandlerAdapter() {
@Override
protected void run(GitTaskResult res) {
result.set(res);
}
});
return result.get();
}
/**
* The most general execution method.
* @param sync Set to <code>true</code> to make the calling thread wait for the task execution.
* @param modal If <code>true</code>, the task will be modal with a modal progress dialog. If false, the task will be executed in
* background. <code>modal</code> implies <code>sync</code>, i.e. if modal then sync doesn't matter: you'll wait anyway.
* @param resultHandler Handle the result.
* @see #execute(boolean)
*/
public void execute(boolean sync, boolean modal, final GitTaskResultHandler resultHandler) {
final Object LOCK = new Object();
final AtomicBoolean completed = new AtomicBoolean();
if (modal) {
final ModalTask task = new ModalTask(myProject, myHandler, myTitle) {
@Override public void onSuccess() {
commonOnSuccess(LOCK, resultHandler);
completed.set(true);
}
@Override public void onCancel() {
commonOnCancel(LOCK, resultHandler);
completed.set(true);
}
};
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
ProgressManager.getInstance().run(task);
}
}, ModalityState.defaultModalityState());
} else {
final BackgroundableTask task = new BackgroundableTask(myProject, myHandler, myTitle) {
@Override public void onSuccess() {
commonOnSuccess(LOCK, resultHandler);
completed.set(true);
}
@Override public void onCancel() {
commonOnCancel(LOCK, resultHandler);
completed.set(true);
}
};
if (myProgressIndicator == null) {
GitVcs.runInBackground(task);
} else {
task.runAlone();
}
}
if (sync) {
while (!completed.get()) {
try {
synchronized (LOCK) {
LOCK.wait(50);
}
} catch (InterruptedException e) {
LOG.info(e);
}
}
}
}
private void commonOnSuccess(final Object LOCK, final GitTaskResultHandler resultHandler) {
GitTaskResult res = !myHandler.errors().isEmpty() ? GitTaskResult.GIT_ERROR : GitTaskResult.OK;
resultHandler.run(res);
synchronized (LOCK) {
LOCK.notifyAll();
}
}
private void commonOnCancel(final Object LOCK, final GitTaskResultHandler resultHandler) {
resultHandler.run(GitTaskResult.CANCELLED);
synchronized (LOCK) {
LOCK.notifyAll();
}
}
private void addListeners(final TaskExecution task, final ProgressIndicator indicator) {
if (indicator != null) {
indicator.setIndeterminate(myProgressAnalyzer == null);
}
// When receives an error line, adds a VcsException to the GitHandler.
final GitLineHandlerListener listener = new GitLineHandlerListener() {
@Override
public void processTerminated(int exitCode) {
if (exitCode != 0 && !myHandler.isIgnoredErrorCode(exitCode)) {
if (myHandler.errors().isEmpty()) {
myHandler.addError(new VcsException(myHandler.getLastOutput()));
}
}
}
@Override
public void startFailed(Throwable exception) {
myHandler.addError(new VcsException("Git start failed: " + exception.getMessage(), exception));
}
@Override
public void onLineAvailable(String line, Key outputType) {
if (GitHandlerUtil.isErrorLine(line.trim())) {
myHandler.addError(new VcsException(line));
} else if (!StringUtil.isEmptyOrSpaces(line)) {
myHandler.addLastOutput(line);
}
if (indicator != null) {
indicator.setText2(line);
}
if (myProgressAnalyzer != null && indicator != null) {
final double fraction = myProgressAnalyzer.analyzeProgress(line);
if (fraction >= 0) {
indicator.setFraction(fraction);
}
}
}
};
if (myHandler instanceof GitLineHandler) {
((GitLineHandler)myHandler).addLineListener(listener);
} else {
myHandler.addListener(listener);
}
// disposes the timer
myHandler.addListener(new GitHandlerListener() {
@Override
public void processTerminated(int exitCode) {
task.dispose();
}
@Override
public void startFailed(Throwable exception) {
task.dispose();
}
});
}
public void setProgressAnalyzer(GitProgressAnalyzer progressAnalyzer) {
myProgressAnalyzer = progressAnalyzer;
}
public void setProgressIndicator(ProgressIndicator progressIndicator) {
myProgressIndicator = progressIndicator;
}
/**
* We're using this interface here to work with Task, because standard {@link Task#run(com.intellij.openapi.progress.ProgressIndicator)}
* is busy with timers.
*/
private interface TaskExecution {
void execute(ProgressIndicator indicator);
void dispose();
}
// To add to {@link com.intellij.openapi.progress.BackgroundTaskQueue} a task must be {@link Task.Backgroundable},
// so we can't have a single class representing a task: we have BackgroundableTask and ModalTask.
// To minimize code duplication we use GitTaskDelegate.
private abstract class BackgroundableTask extends Task.Backgroundable implements TaskExecution {
private GitTaskDelegate myDelegate;
public BackgroundableTask(@Nullable final Project project, @NotNull GitHandler handler, @NotNull final String processTitle) {
super(project, processTitle, true);
myDelegate = new GitTaskDelegate(myProject, handler, this);
}
@Override
public final void run(@NotNull ProgressIndicator indicator) {
myDelegate.run(indicator);
}
public final void runAlone() {
if (ApplicationManager.getApplication().isDispatchThread()) {
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
justRun();
}
});
} else {
justRun();
}
}
private void justRun() {
String oldTitle = myProgressIndicator.getText();
myProgressIndicator.setText(myTitle);
myDelegate.run(myProgressIndicator);
myProgressIndicator.setText(oldTitle);
if (myProgressIndicator.isCanceled()) {
onCancel();
} else {
onSuccess();
}
}
@Override
public void execute(ProgressIndicator indicator) {
addListeners(this, indicator);
GitHandlerUtil.runInCurrentThread(myHandler, indicator, false, myTitle);
}
@Override
public void dispose() {
Disposer.dispose(myDelegate);
}
}
private abstract class ModalTask extends Task.Modal implements TaskExecution {
private GitTaskDelegate myDelegate;
public ModalTask(@Nullable final Project project, @NotNull GitHandler handler, @NotNull final String processTitle) {
super(project, processTitle, true);
myDelegate = new GitTaskDelegate(myProject, handler, this);
}
@Override
public final void run(@NotNull ProgressIndicator indicator) {
myDelegate.run(indicator);
}
@Override
public void execute(ProgressIndicator indicator) {
addListeners(this, indicator);
GitHandlerUtil.runInCurrentThread(myHandler, indicator, false, myTitle);
}
@Override
public void dispose() {
Disposer.dispose(myDelegate);
}
}
/**
* Does the work which is common for BackgrounableTask and ModalTask.
* Actually - starts a timer which checks if current progress indicator is cancelled.
* If yes, kills the GitHandler.
*/
private static class GitTaskDelegate implements Disposable {
private GitHandler myHandler;
private ProgressIndicator myIndicator;
private TaskExecution myTask;
private Timer myTimer;
private Project myProject;
public GitTaskDelegate(Project project, GitHandler handler, TaskExecution task) {
myProject = project;
myHandler = handler;
myTask = task;
Disposer.register(myProject, this);
}
public void run(ProgressIndicator indicator) {
myIndicator = indicator;
myTimer = new Timer();
myTimer.schedule(new TimerTask() {
@Override
public void run() {
if (myIndicator != null && myIndicator.isCanceled()) {
try {
if (myHandler != null) {
myHandler.destroyProcess();
}
} finally {
Disposer.dispose(GitTaskDelegate.this);
}
}
}
}, 0, 200);
myTask.execute(indicator);
}
public void dispose() {
if (myTimer != null) {
myTimer.cancel();
}
}
}
}