blob: ad0c70932414904e99e594aafdf5d67d0c7c0fac [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 com.intellij.execution.process;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.Nullable;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class ProcessHandler extends UserDataHolderBase {
private static final Logger LOG = Logger.getInstance("#com.intellij.execution.process.ProcessHandler");
/**
* todo: replace with an overridable method [nik]
*
* @deprecated
*/
public static final Key<Boolean> SILENTLY_DESTROY_ON_CLOSE = Key.create("SILENTLY_DESTROY_ON_CLOSE");
private final List<ProcessListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private static final int STATE_INITIAL = 0;
private static final int STATE_RUNNING = 1;
private static final int STATE_TERMINATING = 2;
private static final int STATE_TERMINATED = 3;
private final AtomicInteger myState = new AtomicInteger(STATE_INITIAL);
private final Semaphore myWaitSemaphore;
private final ProcessListener myEventMulticaster;
private final TasksRunner myAfterStartNotifiedRunner;
protected ProcessHandler() {
myEventMulticaster = createEventMulticaster();
myWaitSemaphore = new Semaphore();
myWaitSemaphore.down();
myAfterStartNotifiedRunner = new TasksRunner();
myListeners.add(myAfterStartNotifiedRunner);
}
public void startNotify() {
if (myState.compareAndSet(STATE_INITIAL, STATE_RUNNING)) {
myEventMulticaster.startNotified(new ProcessEvent(this));
}
else {
LOG.error("startNotify called already");
}
}
protected abstract void destroyProcessImpl();
protected abstract void detachProcessImpl();
public abstract boolean detachIsDefault();
/**
* Wait for process execution.
*
* @return true if target process has actually ended; false if we stopped watching the process execution and don't know if it has completed.
*/
public boolean waitFor() {
try {
myWaitSemaphore.waitFor();
return true;
}
catch (ProcessCanceledException e) {
return false;
}
}
public boolean waitFor(long timeoutInMilliseconds) {
try {
return myWaitSemaphore.waitFor(timeoutInMilliseconds);
}
catch (ProcessCanceledException e) {
return false;
}
}
public void destroyProcess() {
myAfterStartNotifiedRunner.execute(new Runnable() {
@Override
public void run() {
if (myState.compareAndSet(STATE_RUNNING, STATE_TERMINATING)) {
fireProcessWillTerminate(true);
destroyProcessImpl();
}
}
});
}
public void detachProcess() {
myAfterStartNotifiedRunner.execute(new Runnable() {
@Override
public void run() {
if (myState.compareAndSet(STATE_RUNNING, STATE_TERMINATING)) {
fireProcessWillTerminate(false);
detachProcessImpl();
}
}
});
}
public boolean isProcessTerminated() {
return myState.get() == STATE_TERMINATED;
}
public boolean isProcessTerminating() {
return myState.get() == STATE_TERMINATING;
}
public void addProcessListener(final ProcessListener listener) {
myListeners.add(listener);
}
public void removeProcessListener(final ProcessListener listener) {
myListeners.remove(listener);
}
protected void notifyProcessDetached() {
notifyTerminated(0, false);
}
protected void notifyProcessTerminated(final int exitCode) {
notifyTerminated(exitCode, true);
}
private void notifyTerminated(final int exitCode, final boolean willBeDestroyed) {
myAfterStartNotifiedRunner.execute(new Runnable() {
@Override
public void run() {
LOG.assertTrue(isStartNotified(), "Start notify is not called");
if (myState.compareAndSet(STATE_RUNNING, STATE_TERMINATING)) {
try {
fireProcessWillTerminate(willBeDestroyed);
}
catch (Throwable e) {
if (!isCanceledException(e)) {
LOG.error(e);
}
}
}
if (myState.compareAndSet(STATE_TERMINATING, STATE_TERMINATED)) {
try {
myEventMulticaster.processTerminated(new ProcessEvent(ProcessHandler.this, exitCode));
}
catch (Throwable e) {
if (!isCanceledException(e)) {
LOG.error(e);
}
}
finally {
myWaitSemaphore.up();
}
}
}
});
}
public void notifyTextAvailable(final String text, final Key outputType) {
final ProcessEvent event = new ProcessEvent(this, text);
myEventMulticaster.onTextAvailable(event, outputType);
}
@Nullable
public abstract OutputStream getProcessInput();
private void fireProcessWillTerminate(final boolean willBeDestroyed) {
LOG.assertTrue(isStartNotified(), "All events should be fired after startNotify is called");
myEventMulticaster.processWillTerminate(new ProcessEvent(this), willBeDestroyed);
}
public boolean isStartNotified() {
return myState.get() > STATE_INITIAL;
}
public boolean isSilentlyDestroyOnClose() {
return false;
}
private ProcessListener createEventMulticaster() {
final Class<ProcessListener> listenerClass = ProcessListener.class;
return (ProcessListener)Proxy.newProxyInstance(listenerClass.getClassLoader(), new Class[]{listenerClass}, new InvocationHandler() {
@Override
public Object invoke(Object object, Method method, Object[] params) throws Throwable {
for (ProcessListener listener : myListeners) {
try {
method.invoke(listener, params);
}
catch (Throwable e) {
if (!isCanceledException(e)) {
LOG.error(e);
}
}
}
return null;
}
});
}
private boolean isCanceledException(Throwable e) {
final boolean value = e instanceof InvocationTargetException && e.getCause() instanceof ProcessCanceledException;
if (value) {
LOG.info(e);
}
return value;
}
private final class TasksRunner extends ProcessAdapter {
private final List<Runnable> myPendingTasks = new ArrayList<Runnable>();
@Override
public void startNotified(ProcessEvent event) {
removeProcessListener(this);
// at this point it is guaranteed that nothing will be added to myPendingTasks
runPendingTasks();
}
public void execute(Runnable task) {
if (isStartNotified()) {
task.run();
}
else {
synchronized (myPendingTasks) {
myPendingTasks.add(task);
}
if (isStartNotified()) {
runPendingTasks();
}
}
}
private void runPendingTasks() {
final Runnable[] tasks;
synchronized (myPendingTasks) {
tasks = myPendingTasks.toArray(new Runnable[myPendingTasks.size()]);
myPendingTasks.clear();
}
for (Runnable task : tasks) {
task.run();
}
}
}
}