| /* |
| * 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 org.jetbrains.idea.svn.commandLine; |
| |
| import com.intellij.execution.ExecutionException; |
| import com.intellij.execution.configurations.EncodingEnvironmentUtil; |
| import com.intellij.execution.configurations.GeneralCommandLine; |
| import com.intellij.execution.process.*; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.util.EventDispatcher; |
| import com.intellij.util.io.BaseDataReader; |
| import com.intellij.util.io.BinaryOutputReader; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.tmatesoft.svn.core.SVNCancelException; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStreamWriter; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** |
| * Created with IntelliJ IDEA. |
| * User: Irina.Chernushina |
| * Date: 1/25/12 |
| * Time: 12:58 PM |
| */ |
| public class CommandExecutor { |
| static final Logger LOG = Logger.getInstance(CommandExecutor.class.getName()); |
| private final AtomicReference<Integer> myExitCodeReference; |
| |
| private boolean myIsDestroyed; |
| private boolean myNeedsDestroy; |
| private volatile String myDestroyReason; |
| private volatile boolean myWasCancelled; |
| protected final GeneralCommandLine myCommandLine; |
| protected Process myProcess; |
| protected OSProcessHandler myHandler; |
| private OutputStreamWriter myProcessWriter; |
| // TODO: Try to implement commands in a way that they manually indicate if they need full output - to prevent situations |
| // TODO: when large amount of data needs to be stored instead of just sequential processing. |
| private CapturingProcessAdapter outputAdapter; |
| private final Object myLock; |
| |
| private final EventDispatcher<LineCommandListener> myListeners = EventDispatcher.create(LineCommandListener.class); |
| |
| private final AtomicBoolean myWasError = new AtomicBoolean(false); |
| @Nullable private final LineCommandListener myResultBuilder; |
| @NotNull private final Command myCommand; |
| |
| public CommandExecutor(@NotNull @NonNls String exePath, @NotNull Command command) { |
| myCommand = command; |
| myResultBuilder = command.getResultBuilder(); |
| if (myResultBuilder != null) |
| { |
| myListeners.addListener(myResultBuilder); |
| // cancel tracker should be executed after result builder |
| myListeners.addListener(new CommandCancelTracker()); |
| } |
| myLock = new Object(); |
| myCommandLine = new GeneralCommandLine(); |
| myCommandLine.setExePath(exePath); |
| myCommandLine.setWorkDirectory(command.getWorkingDirectory()); |
| if (command.getConfigDir() != null) { |
| myCommandLine.addParameters("--config-dir", command.getConfigDir().getPath()); |
| } |
| myCommandLine.addParameter(command.getName().getName()); |
| myCommandLine.addParameters(command.getParameters()); |
| myExitCodeReference = new AtomicReference<Integer>(); |
| } |
| |
| /** |
| * Indicates if process was destroyed "manually" by command execution logic. |
| * |
| * @return |
| */ |
| public boolean isManuallyDestroyed() { |
| return myIsDestroyed; |
| } |
| |
| public String getDestroyReason() { |
| return myDestroyReason; |
| } |
| |
| public void start() throws SvnBindException { |
| synchronized (myLock) { |
| checkNotStarted(); |
| |
| try { |
| EncodingEnvironmentUtil.fixDefaultEncodingIfMac(myCommandLine, null); |
| |
| myProcess = createProcess(); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug(myCommandLine.toString()); |
| } |
| myHandler = createProcessHandler(); |
| myProcessWriter = new OutputStreamWriter(myHandler.getProcessInput()); |
| startHandlingStreams(); |
| } catch (ExecutionException e) { |
| // TODO: currently startFailed() is not used for some real logic in svn4idea plugin |
| listeners().startFailed(e); |
| throw new SvnBindException(e); |
| } |
| } |
| } |
| |
| @NotNull |
| protected OSProcessHandler createProcessHandler() { |
| return needsBinaryOutput() |
| ? new BinaryOSProcessHandler(myProcess, myCommandLine.getCommandLineString()) |
| : new OSProcessHandler(myProcess, myCommandLine.getCommandLineString()); |
| } |
| |
| private boolean needsBinaryOutput() { |
| return SvnCommandName.cat.equals(myCommand.getName()); |
| } |
| |
| @NotNull |
| protected Process createProcess() throws ExecutionException { |
| return myCommandLine.createProcess(); |
| } |
| |
| protected void startHandlingStreams() { |
| outputAdapter = new CapturingProcessAdapter(); |
| myHandler.addProcessListener(outputAdapter); |
| myHandler.addProcessListener(new ProcessTracker()); |
| myHandler.addProcessListener(new ResultBuilderNotifier(listeners())); |
| myHandler.addProcessListener(new CommandOutputLogger()); |
| myHandler.startNotify(); |
| } |
| |
| public String getOutput() { |
| return outputAdapter.getOutput().getStdout(); |
| } |
| |
| public String getErrorOutput() { |
| return outputAdapter.getOutput().getStderr(); |
| } |
| |
| @Nullable |
| public ByteArrayOutputStream getBinaryOutput() { |
| return myHandler instanceof BinaryOSProcessHandler ? ((BinaryOSProcessHandler)myHandler).myBinaryOutput : null; |
| } |
| |
| // TODO: Carefully here - do not modify command from threads other than the one started command execution |
| @NotNull |
| public Command getCommand() { |
| return myCommand; |
| } |
| |
| /** |
| * Wait for process termination |
| * @param timeout |
| */ |
| public boolean waitFor(int timeout) { |
| checkStarted(); |
| final OSProcessHandler handler; |
| synchronized (myLock) { |
| // TODO: This line seems to cause situation when exitCode is not set before SvnLineCommand.runCommand() is finished. |
| // TODO: Carefully analyze behavior (on all operating systems) and fix. |
| if (myIsDestroyed) return true; |
| handler = myHandler; |
| } |
| if (timeout == -1) { |
| return handler.waitFor(); |
| } |
| else { |
| return handler.waitFor(timeout); |
| } |
| } |
| |
| public void cancel() { |
| synchronized (myLock) { |
| checkStarted(); |
| destroyProcess(); |
| } |
| } |
| |
| public void run() throws SvnBindException { |
| start(); |
| boolean finished; |
| do { |
| finished = waitFor(500); |
| if (!finished && (wasError() || needsDestroy() || checkCancelled())) { |
| waitFor(1000); |
| doDestroyProcess(); |
| break; |
| } |
| } |
| while (!finished); |
| } |
| |
| public void addListener(final LineCommandListener listener) { |
| synchronized (myLock) { |
| myListeners.addListener(listener); |
| } |
| } |
| |
| protected LineCommandListener listeners() { |
| synchronized (myLock) { |
| return myListeners.getMulticaster(); |
| } |
| } |
| |
| public boolean checkCancelled() { |
| if (!myWasCancelled && myCommand.getCanceller() != null) { |
| try { |
| myCommand.getCanceller().checkCancelled(); |
| } |
| catch (SVNCancelException e) { |
| // indicates command should be cancelled |
| myWasCancelled = true; |
| } |
| } |
| |
| return myWasCancelled; |
| } |
| |
| public void destroyProcess() { |
| synchronized (myLock) { |
| myNeedsDestroy = true; |
| } |
| } |
| |
| public void destroyProcess(@Nullable String destroyReason) { |
| synchronized (myLock) { |
| myDestroyReason = destroyReason; |
| myNeedsDestroy = true; |
| } |
| } |
| |
| /** |
| * ProcessHandler.destroyProcess() implementations could acquire read lock in its implementation - like OSProcessManager.getInstance(). |
| * Some commands are called under write lock - which is generally bad idea, but such logic is not refactored yet. |
| * To prevent deadlocks this method should only be called from thread that started the process. |
| */ |
| public void doDestroyProcess() { |
| synchronized (myLock) { |
| if (!myIsDestroyed) { |
| LOG.info("Destroying process by command: " + getCommandText()); |
| myIsDestroyed = true; |
| myHandler.destroyProcess(); |
| } |
| } |
| } |
| |
| public boolean needsDestroy() { |
| synchronized (myLock) { |
| return myNeedsDestroy; |
| } |
| } |
| |
| public String getCommandText() { |
| synchronized (myLock) { |
| return StringUtil.join(myCommandLine.getExePath(), " ", myCommand.getText()); |
| } |
| } |
| |
| /** |
| * check that process is not started yet |
| * |
| * @throws IllegalStateException if process has been already started |
| */ |
| private void checkNotStarted() { |
| if (isStarted()) { |
| throw new IllegalStateException("The process has been already started"); |
| } |
| } |
| |
| /** |
| * check that process is started |
| * |
| * @throws IllegalStateException if process has not been started |
| */ |
| protected void checkStarted() { |
| if (! isStarted()) { |
| throw new IllegalStateException("The process is not started yet"); |
| } |
| } |
| |
| /** |
| * @return true if process is started |
| */ |
| public boolean isStarted() { |
| synchronized (myLock) { |
| return myProcess != null; |
| } |
| } |
| |
| public SvnCommandName getCommandName() { |
| return myCommand.getName(); |
| } |
| |
| public Integer getExitCodeReference() { |
| return myExitCodeReference.get(); |
| } |
| |
| public void setExitCodeReference(int value) { |
| myExitCodeReference.set(value); |
| } |
| |
| public Boolean wasError() { |
| return myWasError.get(); |
| } |
| |
| public void write(String value) throws SvnBindException { |
| try { |
| synchronized (myLock) { |
| myProcessWriter.write(value); |
| myProcessWriter.flush(); |
| } |
| } |
| catch (IOException e) { |
| throw new SvnBindException(e); |
| } |
| } |
| |
| public void logCommand() { |
| LOG.info("Command text " + getCommandText()); |
| LOG.info("Command output " + getOutput()); |
| } |
| |
| private class CommandCancelTracker extends LineCommandAdapter { |
| @Override |
| public void onLineAvailable(String line, Key outputType) { |
| if (myResultBuilder != null && myResultBuilder.isCanceled()) { |
| LOG.info("Cancelling command: " + getCommandText()); |
| destroyProcess(); |
| } |
| } |
| } |
| |
| private class ProcessTracker extends ProcessAdapter { |
| |
| @Override |
| public void processTerminated(ProcessEvent event) { |
| setExitCodeReference(event.getExitCode()); |
| } |
| |
| @Override |
| public void onTextAvailable(ProcessEvent event, Key outputType) { |
| if (ProcessOutputTypes.STDERR == outputType) { |
| myWasError.set(true); |
| } |
| } |
| } |
| |
| private static class BinaryOSProcessHandler extends OSProcessHandler { |
| |
| @NotNull private final ByteArrayOutputStream myBinaryOutput; |
| |
| public BinaryOSProcessHandler(@NotNull final Process process, @Nullable final String commandLine) { |
| super(process, commandLine); |
| myBinaryOutput = new ByteArrayOutputStream(); |
| } |
| |
| @NotNull |
| @Override |
| protected BaseDataReader createOutputDataReader(BaseDataReader.SleepingPolicy sleepingPolicy) { |
| return new SimpleBinaryOutputReader(myProcess.getInputStream(), sleepingPolicy); |
| } |
| |
| private class SimpleBinaryOutputReader extends BinaryOutputReader { |
| |
| public SimpleBinaryOutputReader(@NotNull InputStream stream, SleepingPolicy sleepingPolicy) { |
| super(stream, sleepingPolicy); |
| start(); |
| } |
| |
| @Override |
| protected void onBinaryAvailable(@NotNull byte[] data, int size) { |
| myBinaryOutput.write(data, 0, size); |
| } |
| |
| @Override |
| protected Future<?> executeOnPooledThread(Runnable runnable) { |
| return BinaryOSProcessHandler.this.executeOnPooledThread(runnable); |
| } |
| } |
| } |
| } |