| /* |
| * 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.application.PathManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.CharsetToolkit; |
| import com.intellij.util.EventDispatcher; |
| 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.File; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.util.List; |
| import java.util.Map; |
| 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; |
| |
| @Nullable private String myMessage; |
| @Nullable private File myMessageFile; |
| private boolean myIsDestroyed; |
| private boolean myNeedsDestroy; |
| private volatile String myDestroyReason; |
| private volatile boolean myWasCancelled; |
| protected final GeneralCommandLine myCommandLine; |
| protected Process myProcess; |
| protected SvnProcessHandler 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(prepareParameters(command)); |
| myExitCodeReference = new AtomicReference<Integer>(); |
| } |
| |
| @NotNull |
| private List<String> prepareParameters(@NotNull Command command) { |
| List<String> parameters = command.getParameters(); |
| |
| detectAndRemoveMessage(parameters); |
| |
| return parameters; |
| } |
| |
| private void detectAndRemoveMessage(@NotNull List<String> parameters) { |
| int index = parameters.indexOf("-m"); |
| index = index < 0 ? parameters.indexOf("--message") : index; |
| |
| if (index >= 0 && index + 1 < parameters.size()) { |
| myMessage = parameters.get(index + 1); |
| parameters.remove(index + 1); |
| parameters.remove(index); |
| } |
| } |
| |
| /** |
| * 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 { |
| beforeCreateProcess(); |
| 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); |
| } |
| } |
| } |
| |
| protected void cleanup() { |
| cleanupMessageFile(); |
| } |
| |
| protected void beforeCreateProcess() throws SvnBindException { |
| EncodingEnvironmentUtil.fixDefaultEncodingIfMac(myCommandLine, null); |
| setupLocale(); |
| ensureMessageFile(); |
| } |
| |
| private void setupLocale() { |
| String locale = Registry.stringValue("svn.executable.locale"); |
| Map<String, String> environment = myCommandLine.getEnvironment(); |
| |
| // TODO: check if we need to set LC_ALL to configured locale or just clear it |
| environment.put("LC_ALL", ""); |
| environment.put("LC_MESSAGES", locale); |
| environment.put("LANG", locale); |
| } |
| |
| private void ensureMessageFile() throws SvnBindException { |
| if (myMessage != null) { |
| myMessageFile = createTempFile("commit-message", ".txt"); |
| try { |
| FileUtil.writeToFile(myMessageFile, myMessage); |
| } |
| catch (IOException e) { |
| throw new SvnBindException(e); |
| } |
| myCommandLine.addParameters("-F", myMessageFile.getAbsolutePath()); |
| myCommandLine.addParameters("--config-option", "config:miscellany:log-encoding=" + CharsetToolkit.UTF8); |
| } |
| } |
| |
| private void cleanupMessageFile() { |
| deleteTempFile(myMessageFile); |
| } |
| |
| @NotNull |
| protected static File getSvnFolder() { |
| File vcsFolder = new File(PathManager.getSystemPath(), "vcs"); |
| |
| return new File(vcsFolder, "svn"); |
| } |
| |
| @NotNull |
| protected static File createTempFile(@NotNull String prefix, @NotNull String extension) throws SvnBindException { |
| try { |
| return FileUtil.createTempFile(getSvnFolder(), prefix, extension); |
| } |
| catch (IOException e) { |
| throw new SvnBindException(e); |
| } |
| } |
| |
| protected static void deleteTempFile(@Nullable File file) { |
| if (file != null) { |
| boolean wasDeleted = FileUtil.delete(file); |
| |
| if (!wasDeleted) { |
| LOG.info("Failed to delete temp file " + file.getAbsolutePath()); |
| } |
| } |
| } |
| |
| @NotNull |
| protected SvnProcessHandler createProcessHandler() { |
| return new SvnProcessHandler(myProcess, myCommandLine.getCommandLineString(), needsUtf8Output(), needsBinaryOutput()); |
| } |
| |
| protected boolean needsBinaryOutput() { |
| // TODO: Add ability so that command could indicate output type it needs by itself |
| return myCommand.is(SvnCommandName.cat) || (myCommand.is(SvnCommandName.diff) && !myCommand.getParameters().contains("--xml")); |
| } |
| |
| protected boolean needsUtf8Output() { |
| return myCommand.getParameters().contains("--xml"); |
| } |
| |
| @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(); |
| } |
| |
| public ProcessOutput getProcessOutput() { |
| return outputAdapter.getOutput(); |
| } |
| |
| @NotNull |
| public ByteArrayOutputStream getBinaryOutput() { |
| return myHandler.getBinaryOutput(); |
| } |
| |
| // 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 { |
| try { |
| start(); |
| boolean finished; |
| do { |
| finished = waitFor(500); |
| if (!finished && (wasError() || needsDestroy() || checkCancelled())) { |
| waitFor(1000); |
| doDestroyProcess(); |
| break; |
| } |
| } |
| while (!finished); |
| } |
| finally { |
| cleanup(); |
| } |
| } |
| |
| public void run(int timeout) throws SvnBindException { |
| try { |
| start(); |
| boolean finished = waitFor(timeout); |
| if (!finished) { |
| outputAdapter.getOutput().setTimeout(); |
| doDestroyProcess(); |
| } |
| } |
| finally { |
| cleanup(); |
| } |
| } |
| |
| 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); |
| } |
| } |
| } |
| } |