/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.utils.executor; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecuteResultHandler; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteWatchdog; import org.apache.commons.exec.OS; import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.TempFileServiceAccess; /** * A {@link CommandLineExecutor} that executes the given commands locally. * * @author Robert Mischke * */ public class LocalApacheCommandLineExecutor extends AbstractCommandLineExecutor implements CommandLineExecutor { /** * Sets the execution flag for the specified file. */ private static final String LINUX_MAKE_FILE_EXECUTABLE_TEMPLATE = "chmod +x %s"; private File workDir; private final Log log = LogFactory.getLog(getClass()); private DefaultExecutor executor; private ExecuteWatchdog watchdog; private PipedInputStream pipedStdInputStream; private PipedInputStream pipedErrInputStream; private DefaultExecuteResultHandler resultHandler; private PipedOutputStream pipedStdOutputStream; private PipedOutputStream pipedErrOutputStream; private ExtendedPumpStreamHandler streamHandler; private ProcessExtractor processExtractor; private boolean cancelRequested = false; /** * Creates a local executor with the given path as its working directory. If the given path is not a directory, it is created. * * @param workDirPath the directory on the local system to use for execution * * @throws IOException if the given {@link File} is not a directory and also could not be created */ public LocalApacheCommandLineExecutor(File workDirPath) throws IOException { this.workDir = workDirPath; } @Override public void start(String commandString) throws IOException { start(commandString, null); } private void checkAndCreateWorkDir() throws IOException { if (!workDir.isDirectory()) { // try to create the work directory workDir.mkdirs(); if (!workDir.isDirectory()) { throw new IOException("Failed to create provided work directory " + workDir.getAbsolutePath()); } } } private void executeCommand(CommandLine cmd, final InputStream stdinStream) throws IOException { pipedStdOutputStream = new PipedOutputStream(); pipedErrOutputStream = new PipedOutputStream(); pipedStdInputStream = new PipedInputStream(pipedStdOutputStream); pipedErrInputStream = new PipedInputStream(pipedErrOutputStream); watchdog = new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT); executor = new DefaultExecutor(); executor.setWatchdog(watchdog); resultHandler = new DefaultExecuteResultHandler(); streamHandler = new ExtendedPumpStreamHandler(pipedStdOutputStream, pipedErrOutputStream, stdinStream); executor.setStreamHandler(streamHandler); executor.setWorkingDirectory(workDir); // set a special watchdog to get access to the process object processExtractor = new ProcessExtractor(); executor.setWatchdog(processExtractor); // this block is synchronized to avoid a race condition where the initial cancelRequested check in this block is already passed and // then the cancel method is called, which would lead to an uncanceled process. synchronized (this) { if (cancelRequested) { resultHandler.onProcessComplete(1); } else { if (env.isEmpty()) { executor.execute(cmd, resultHandler); } else { executor.execute(cmd, env, resultHandler); } } } } @Override public void start(String commandString, final InputStream stdinStream) throws IOException { checkAndCreateWorkDir(); CommandLine cmd = ProcessUtils.constructCommandLine(commandString); executeCommand(cmd, stdinStream); } // Executes a script by writing the script into a temporary file, setting the executable bit and executing the script. private void executeShebangScript(String scriptString, final InputStream stdinStream) throws IOException, InterruptedException { checkAndCreateWorkDir(); // write the scriptString into a file in the temp directory final File scriptFile = TempFileServiceAccess.getInstance().createTempFileWithFixedFilename("script"); log.debug(StringUtils.format("Writing script to %s", scriptFile.getAbsolutePath())); FileUtils.writeStringToFile(scriptFile, scriptString, false); log.debug(scriptFile.exists()); log.debug(scriptFile.length()); // make the file executable String makeExecutableCmd = StringUtils.format(LINUX_MAKE_FILE_EXECUTABLE_TEMPLATE, scriptFile.getAbsolutePath()); // we cannot call start on the same executor twice since the canceling does not support this yet LocalApacheCommandLineExecutor makeExecutableExecutor = new LocalApacheCommandLineExecutor(workDir); makeExecutableExecutor.start(makeExecutableCmd); int exitCode = makeExecutableExecutor.waitForTermination(); log.debug(StringUtils.format("chmod +x finished with exit code %d", exitCode)); log.debug("scriptFile.exists(): " + scriptFile.exists()); // execute the script CommandLine cmd = new CommandLine(scriptFile.getAbsolutePath()); executeCommand(cmd, stdinStream); // if an interpreter is chosen, which is not available on the system, the streams are not closed correctly // TODO check for errors during execution, e.g. interpreter is not available // TODO if we dispose the script now, it might be deleted before it is executed //TempFileServiceAccess.getInstance().disposeManagedTempDirOrFile(scriptFile); } /** * Executes a script by writing the script into a temporary file, setting the executable bit and executing the script. * * On Linux, if the first line of the script starts either with "#!/bin/sh" or with "#!/bin/bash", the script temporarily will be * written to a file and executed. Other interpreters are currently not supported, instead, all lines will be concatenated and executed * as a single shell command. * * @param scriptString The script that should be executed. * @param stdinStream the input stream to read standard input data from, or "null" to disable * @throws IOException On IO errors. * @throws InterruptedException If interrupted while waiting for an operation to finish. */ public void executeScript(String scriptString, final InputStream stdinStream) throws IOException, InterruptedException { if (OS.isFamilyWindows()) { this.startMultiLineCommand(scriptString.split("\r?\n|\r")); } else if (OS.isFamilyUnix()) { // check if the scriptString contains a shebang at its start // we currently only support sh and bash, since these are available on all major Linux distributions if (scriptString.startsWith("#!/bin/sh\n") || scriptString.startsWith("#!/bin/bash\n")) { executeShebangScript(scriptString, stdinStream); } else { this.startMultiLineCommand(scriptString.split("\r?\n|\r")); } } } /** * Destroys the running process manually. */ public void manuallyDestroyProcess() { watchdog.destroyProcess(); } @Override public String getWorkDirPath() { return workDir.getAbsolutePath(); } @Override public InputStream getStdout() { return pipedStdInputStream; } @Override public InputStream getStderr() { return pipedErrInputStream; } @Override public int waitForTermination() throws IOException, InterruptedException { resultHandler.waitFor(); int exitValue = resultHandler.getExitValue(); return exitValue; } public DefaultExecuteResultHandler getResultHandler() { return resultHandler; } /** * This class overrides the normal {@link PumpStreamHandler} because the closeWhenExhausted flag when creating a pump must be set. * * @author Sascha Zur * */ private class ExtendedPumpStreamHandler extends PumpStreamHandler { ExtendedPumpStreamHandler( PipedOutputStream pipedStdOutputStream, PipedOutputStream pipedErrOutputStream, InputStream stdinStream) { super(pipedStdOutputStream, pipedErrOutputStream, stdinStream); } @Override protected Thread createPump(final InputStream is, final OutputStream os) { return createPump(is, os, true); } } @Override public void uploadFileToWorkdir(File localFile, String remoteLocation) throws IOException { File targetFile = new File(workDir, remoteLocation); log.debug("Local copy from " + localFile.getAbsolutePath() + " to " + targetFile.getAbsolutePath()); FileUtils.copyFile(localFile, targetFile); }; @Override public void downloadFileFromWorkdir(String remoteLocation, File localFile) throws IOException { FileUtils.copyFile(new File(workDir, remoteLocation), localFile); } @Override public void downloadWorkdir(File localDir) throws IOException { FileUtils.copyDirectory(workDir, localDir); } @Override public void remoteCopy(String remoteSource, String remoteTarget) throws IOException { FileUtils.copyFile(new File(remoteSource), new File(remoteTarget)); } @Override public void uploadDirectoryToWorkdir(File localDirectory, String remoteLocation) throws IOException { File targetDirectory = new File(workDir, remoteLocation); targetDirectory.mkdirs(); FileUtils.copyDirectory(localDirectory, targetDirectory); } @Override public void downloadDirectoryFromWorkdir(String remoteLocation, File localDirectory) throws IOException { FileUtils.copyDirectory(new File(workDir, remoteLocation), localDirectory); } @Override public void downloadFile(String remoteLocation, File localFile) throws IOException { FileUtils.copyFile(new File(remoteLocation), localFile); } @Override public void downloadDirectory(String remoteLocation, File localDirectory) throws IOException { FileUtils.copyDirectory(new File(remoteLocation), localDirectory); } @Override public void uploadFile(File localFile, String remoteLocation) throws IOException { FileUtils.copyFile(localFile, new File(remoteLocation)); } @Override public void uploadDirectory(File localDirectory, String remoteLocation) throws IOException { FileUtils.copyDirectory(localDirectory, new File(remoteLocation)); } public File getWorkDir() { return workDir; } public void setWorkDir(File workDir) { this.workDir = workDir; } /** * Requests cancellation of the started process. This will kill all descending processes too. If cancel is called before start, the * execution will not be started. * * TODO This implementation currently only supports one call of start(). If it is necessary that multiple commands are started with the * same LocalApacheCommandLineExecutor, this method needs to be modified to receive the process object which should be killed. ~rode_to * * TODO what happens if this is called multiple times? * * @return true, if the process was canceled, otherwise false. */ public synchronized boolean cancel() { cancelRequested = true; // start() has not been called yet if (processExtractor == null) { return false; } Process process = processExtractor.getProcess(); // the process was not started yet if (process == null) { return false; } try { int pid = ProcessUtils.getPid(process); ProcessUtils.killProcessTree(pid); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | IOException | InterruptedException e) { log.error("Unable to cancel the process.", e); return false; } return true; } }