/* * Autopsy Forensic Browser * * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * 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.sleuthkit.autopsy.coreutils; import com.sun.javafx.PlatformUtil; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Writer; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import org.sleuthkit.autopsy.core.UserPreferences; /** * Executes a command line using an operating system process with a configurable * timeout and pluggable logic to kill or continue the process on timeout. */ public final class ExecUtil { private static final long DEFAULT_TIMEOUT = 5; private static final TimeUnit DEFAULT_TIMEOUT_UNITS = TimeUnit.SECONDS; /** * The execute() methods do a wait() with a timeout on the executing process * and query a process terminator each time the timeout expires to determine * whether or not to kill the process. See * DataSourceIngestModuleProcessTerminator and * FileIngestModuleProcessTerminator as examples of ProcessTerminator * implementations. */ public interface ProcessTerminator { /** * Decides whether or not to terminate a process being run by a * ExcUtil.execute() methods. * * @return True or false. */ boolean shouldTerminateProcess(); } /** * Process terminator that can be used to kill a processes after it exceeds * a maximum allowable run time. */ public static class TimedProcessTerminator implements ProcessTerminator { private final long startTimeInSeconds; private final long maxRunTimeInSeconds; /** * Creates a process terminator that can be used to kill a process after * it has run for a given period of time. * * @param maxRunTimeInSeconds The maximum allowable run time in seconds. */ public TimedProcessTerminator(long maxRunTimeInSeconds) { this.maxRunTimeInSeconds = maxRunTimeInSeconds; this.startTimeInSeconds = (new Date().getTime()) / 1000; } /** * Creates a process terminator that can be used to kill a process after * it has run for a given period of time. Maximum allowable run time is * set via Autopsy Options panel. If the process termination * functionality is disabled then the maximum allowable time is set to * MAX_INT seconds. */ public TimedProcessTerminator() { if (UserPreferences.getIsTimeOutEnabled() && UserPreferences.getProcessTimeOutHrs() > 0) { // user specified time out this.maxRunTimeInSeconds = UserPreferences.getProcessTimeOutHrs() * 3600; } else { // never time out this.maxRunTimeInSeconds = Long.MAX_VALUE; } this.startTimeInSeconds = (new Date().getTime()) / 1000; } @Override public boolean shouldTerminateProcess() { long currentTimeInSeconds = (new Date().getTime()) / 1000; return (currentTimeInSeconds - this.startTimeInSeconds) > this.maxRunTimeInSeconds; } } /** * Runs a process without a timeout and terminator. * * @param processBuilder A process builder used to configure and construct * the process to be run. * * @return the exit value of the process * * @throws SecurityException if a security manager exists and vetoes any * aspect of running the process. * @throws IOException if an I/O error occurs. */ public static int execute(ProcessBuilder processBuilder) throws SecurityException, IOException { return ExecUtil.execute(processBuilder, 30, TimeUnit.DAYS, new ProcessTerminator() { @Override public boolean shouldTerminateProcess() { return false; } }); } /** * Runs a process using the default timeout and a custom terminator. * * @param processBuilder A process builder used to configure and construct * the process to be run. * @param terminator The terminator. * * @return the exit value of the process * * @throws SecurityException if a security manager exists and vetoes any * aspect of running the process. * @throws IOException if an I/O error occurs. */ public static int execute(ProcessBuilder processBuilder, ProcessTerminator terminator) throws SecurityException, IOException { return ExecUtil.execute(processBuilder, ExecUtil.DEFAULT_TIMEOUT, ExecUtil.DEFAULT_TIMEOUT_UNITS, terminator); } /** * Runs a process using a custom terminator. * * @param processBuilder A process builder used to configure and construct * the process to be run. * @param timeOut The duration of the timeout. * @param units The units for the timeout. * @param terminator The terminator. * * @return the exit value of the process * * @throws SecurityException if a security manager exists and vetoes any * aspect of running the process. * @throws IOException if an I/o error occurs. */ public static int execute(ProcessBuilder processBuilder, long timeOut, TimeUnit units, ProcessTerminator terminator) throws SecurityException, IOException { Process process = processBuilder.start(); try { do { process.waitFor(timeOut, units); if (process.isAlive() && terminator.shouldTerminateProcess()) { killProcess(process); } } while (process.isAlive()); } catch (InterruptedException ex) { if (process.isAlive()) { killProcess(process); } Logger.getLogger(ExecUtil.class.getName()).log(Level.INFO, "Thread interrupted while running {0}", processBuilder.command().get(0)); // NON-NLS Thread.currentThread().interrupt(); } return process.exitValue(); } /** * Kills a process and its children * * @param process The parent process to kill */ public static void killProcess(Process process) { if (process == null) { return; } try { if (PlatformUtil.isWindows()) { Win32Process parentProcess = new Win32Process(process); List<Win32Process> children = parentProcess.getChildren(); children.stream().forEach((child) -> { child.terminate(); }); parentProcess.terminate(); } else { process.destroyForcibly(); } } catch (Exception ex) { logger.log(Level.WARNING, "Error occurred when attempting to kill process: {0}", ex.getMessage()); // NON-NLS } } /** * EVERYTHING FOLLOWING THIS LINE IS DEPRECATED AND SLATED FOR REMOVAL */ private static final Logger logger = Logger.getLogger(ExecUtil.class.getName()); private Process proc = null; private ExecUtil.StreamToStringRedirect errorStringRedirect = null; private ExecUtil.StreamToStringRedirect outputStringRedirect = null; private ExecUtil.StreamToWriterRedirect outputWriterRedirect = null; private int exitValue = -100; /** * Execute a process. Redirect asynchronously stdout to a string and stderr * to nowhere. Use only for small outputs, otherwise use the execute() * variant with Writer. * * @param aCommand command to be executed * @param params parameters of the command * * @return string buffer with captured stdout */ @Deprecated public synchronized String execute(final String aCommand, final String... params) throws IOException, InterruptedException { // build command array String[] arrayCommand = new String[params.length + 1]; arrayCommand[0] = aCommand; StringBuilder arrayCommandToLog = new StringBuilder(); arrayCommandToLog.append(aCommand).append(" "); for (int i = 1; i < arrayCommand.length; i++) { arrayCommand[i] = params[i - 1]; arrayCommandToLog.append(arrayCommand[i]).append(" "); } final Runtime rt = Runtime.getRuntime(); logger.log(Level.INFO, "Executing {0}", arrayCommandToLog.toString()); //NON-NLS proc = rt.exec(arrayCommand); //stderr redirect errorStringRedirect = new ExecUtil.StreamToStringRedirect(proc.getErrorStream(), "ERROR"); //NON-NLS errorStringRedirect.start(); //stdout redirect outputStringRedirect = new ExecUtil.StreamToStringRedirect(proc.getInputStream(), "OUTPUT"); //NON-NLS outputStringRedirect.start(); //wait for process to complete and capture error core this.exitValue = proc.waitFor(); // wait for output redirectors to finish writing / reading outputStringRedirect.join(); errorStringRedirect.join(); return outputStringRedirect.getOutput(); } /** * Execute a process. Redirect asynchronously stdout to a passed in writer * and stderr to nowhere. * * @param stdoutWriter file writer to write stdout to * @param aCommand command to be executed * @param params parameters of the command * * @return string buffer with captured stdout */ @Deprecated public synchronized void execute(final Writer stdoutWriter, final String aCommand, final String... params) throws IOException, InterruptedException { // build command array String[] arrayCommand = new String[params.length + 1]; arrayCommand[0] = aCommand; StringBuilder arrayCommandToLog = new StringBuilder(); arrayCommandToLog.append(aCommand).append(" "); for (int i = 1; i < arrayCommand.length; i++) { arrayCommand[i] = params[i - 1]; arrayCommandToLog.append(arrayCommand[i]).append(" "); } final Runtime rt = Runtime.getRuntime(); logger.log(Level.INFO, "Executing {0}", arrayCommandToLog.toString()); //NON-NLS proc = rt.exec(arrayCommand); //stderr redirect errorStringRedirect = new ExecUtil.StreamToStringRedirect(proc.getErrorStream(), "ERROR"); //NON-NLS errorStringRedirect.start(); //stdout redirect outputWriterRedirect = new ExecUtil.StreamToWriterRedirect(proc.getInputStream(), stdoutWriter); outputWriterRedirect.start(); //wait for process to complete and capture error core this.exitValue = proc.waitFor(); logger.log(Level.INFO, "{0} exit value: {1}", new Object[]{aCommand, exitValue}); //NON-NLS // wait for them to finish writing / reading outputWriterRedirect.join(); errorStringRedirect.join(); //gc process with its streams //proc = null; } /** * Interrupt the running process and stop its stream redirect threads */ @Deprecated public synchronized void stop() { if (errorStringRedirect != null) { errorStringRedirect.stopRun(); errorStringRedirect = null; } if (outputStringRedirect != null) { outputStringRedirect.stopRun(); outputStringRedirect = null; } if (outputWriterRedirect != null) { outputWriterRedirect.stopRun(); outputWriterRedirect = null; } if (proc != null) { proc.destroy(); proc = null; } } /** * Gets the exit value returned by the subprocess used to execute a command. * * @return The exit value or the distinguished value -100 if this method is * called before the exit value is set. */ @Deprecated synchronized public int getExitValue() { return this.exitValue; } /** * Asynchronously read the output of a given input stream and write to a * string to be returned. Any exception during execution of the command is * managed in this thread. * */ private static class StreamToStringRedirect extends Thread { private static final Logger logger = Logger.getLogger(StreamToStringRedirect.class.getName()); private final InputStream is; private final StringBuffer output = new StringBuffer(); private volatile boolean doRun = false; StreamToStringRedirect(final InputStream anIs, final String aType) { this.is = anIs; this.doRun = true; } /** * Asynchronous read of the input stream. <br /> Will report output as * its its displayed. * * @see java.lang.Thread#run() */ @Override public final void run() { final String SEP = System.getProperty("line.separator"); InputStreamReader isr; BufferedReader br = null; try { isr = new InputStreamReader(this.is); br = new BufferedReader(isr); String line = null; while (doRun && (line = br.readLine()) != null) { this.output.append(line).append(SEP); } } catch (final IOException ex) { logger.log(Level.WARNING, "Error redirecting stream to string buffer", ex); //NON-NLS } finally { if (br != null) { try { br.close(); } catch (IOException ex) { logger.log(Level.SEVERE, "Error closing stream reader", ex); //NON-NLS } } } } /** * Stop running the stream redirect. The thread will exit out gracefully * after the current readLine() on stream unblocks */ public void stopRun() { doRun = false; } /** * Get output filled asynchronously. <br /> Should be called after * execution * * @return final output */ public final String getOutput() { return this.output.toString(); } } /** * Asynchronously read the output of a given input stream and write to a * file writer passed in by the client. Client is responsible for closing * the writer. * * Any exception during execution of the command is managed in this thread. * */ private static class StreamToWriterRedirect extends Thread { private static final Logger logger = Logger.getLogger(StreamToStringRedirect.class.getName()); private final InputStream is; private volatile boolean doRun = false; private Writer writer = null; StreamToWriterRedirect(final InputStream anIs, final Writer writer) { this.is = anIs; this.writer = writer; this.doRun = true; } /** * Asynchronous read of the input stream. <br /> Will report output as * its its displayed. * * @see java.lang.Thread#run() */ @Override public final void run() { final String SEP = System.getProperty("line.separator"); InputStreamReader isr; BufferedReader br = null; try { isr = new InputStreamReader(this.is); br = new BufferedReader(isr); String line = null; while (doRun && (line = br.readLine()) != null) { writer.append(line).append(SEP); } } catch (final IOException ex) { logger.log(Level.SEVERE, "Error reading output and writing to file writer", ex); //NON-NLS } finally { try { if (doRun) { writer.flush(); } if (br != null) { br.close(); } } catch (IOException ex) { logger.log(Level.SEVERE, "Error flushing file writer", ex); //NON-NLS } } } /** * Stop running the stream redirect. The thread will exit out gracefully * after the current readLine() on stream unblocks */ public void stopRun() { doRun = false; } } }