package me.pbox.jrun; import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.*; /** * The only public class in library with the only public method run(). * * @author Mike Mirzayanov (mirzayanovmr@gmail.com) */ public class ProcessRunner { private static final int BUFFER_SIZE = 1024 * 1024; private static final int TRUNCATE_LIMIT = 5 * 1024 * 1024; /** * @param directory Directory. * @param commandLine Command line. * @return Returns string array. Each element is a single item in terms of command line argument. * Works with quotes correct. */ private static String[] parseCommandLine(File directory, String commandLine) { // Seems to be file name path? if (new File(directory, commandLine).exists()) { return new String[]{commandLine}; } // Hack to generalize situation. commandLine += " "; // Number of slashes in the last block modulo 2. int slashes = 0; // If we are inside quotes. boolean quoted = false; // Current item. StringBuilder current = new StringBuilder(); // Result. List<String> items = new ArrayList<>(); for (int i = 0; i < commandLine.length(); i++) { char c = commandLine.charAt(i); if (c == '\\') { slashes ^= 1; if (slashes == 0) { current.append('\\'); } } else { if (c == '\"') { if (slashes == 0) { quoted = !quoted; } else { current.append('\"'); } } else { if (slashes == 1) { current.append('\\'); } if (c <= ' ' && !quoted) { if (current.length() > 0) { items.add(current.toString()); current.setLength(0); } } else { current.append(c); } } slashes = 0; } } return items.toArray(new String[items.size()]); } private static <T> T timedCall(Callable<T> c, long timeout, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException { ExecutorService pool = Executors.newFixedThreadPool(1); try { FutureTask<T> task = new FutureTask<>(c); pool.execute(task); return task.get(timeout, timeUnit); } finally { pool.shutdown(); } } /** * Executes command line and returns its exitCode, output * (stdout) and error (stderr). Output and error will be * truncated to 5MB if needed. Exit code will be equal * to -1 if some error happened and it is impossible to * run process. You may check comment in order to get * details. * <p/> * It will use ProcessBuilder. * <p/> * Doesn't support non-ASCII characters in directory * or commandLine. * * @param commandLine Process command line in * form "exec_file param_1 param_2 ... param_n". * @param params Encapsulates directory, time limit (in milliseconds), redirection files. * @return Result: exitCode, output, error and comment. */ public static Outcome run(String commandLine, final Params params) { String[] tokens = parseCommandLine(params.getDirectory(), commandLine); ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.directory(params.getDirectory()); processBuilder.command(tokens); final StringBuilder output = new StringBuilder(); final StringBuilder error = new StringBuilder(); final Process process; final List<String> errors = new ArrayList<>(); int exitCode = -1; try { process = processBuilder.start(); Thread writeInputThread = null; if (params.getRedirectInputFile() != null) { writeInputThread = new Thread() { @Override public void run() { OutputStream outputStream = new BufferedOutputStream(process.getOutputStream()); InputStream inputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(params.getRedirectInputFile())); byte[] buffer = new byte[BUFFER_SIZE]; while (true) { int readByteCount = inputStream.read(buffer); if (readByteCount == -1) { break; } else { outputStream.write(buffer, 0, readByteCount); outputStream.flush(); } } } catch (FileNotFoundException e) { errors.add("Can't find input file " + params.getRedirectInputFile() + "."); } catch (IOException e) { errors.add("Can't read input file " + params.getRedirectInputFile() + "."); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { // No operations. } try { outputStream.close(); } catch (IOException e) { // No operations. } } } }; } Thread readOutputThread = new Thread() { @Override public void run() { BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); Writer redirectOutputWriter; try { redirectOutputWriter = params.getRedirectOutputFile() == null ? null : new BufferedWriter(new FileWriter(params.getRedirectOutputFile())); } catch (IOException e) { errors.add("Can't write output file " + params.getRedirectOutputFile() + "."); return; } try { char[] buffer = new char[BUFFER_SIZE]; while (true) { int readCharCount = reader.read(buffer); if (readCharCount == -1) { break; } if (redirectOutputWriter != null) { redirectOutputWriter.write(buffer, 0, readCharCount); redirectOutputWriter.flush(); } if (output.length() < TRUNCATE_LIMIT) { readCharCount = Math.min(readCharCount, TRUNCATE_LIMIT - output.length()); output.append(buffer, 0, readCharCount); } } } catch (IOException ignored) { errors.add("Can't handle output of the process."); } finally { try { if (redirectOutputWriter != null) { redirectOutputWriter.close(); } } catch (IOException ignored) { // No operations. } try { reader.close(); } catch (IOException ignored) { // No operations. } } } }; Thread readErrorThread = new Thread() { @Override public void run() { BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); Writer redirectErrorWriter; try { redirectErrorWriter = params.getRedirectErrorFile() == null ? null : new BufferedWriter(new FileWriter(params.getRedirectErrorFile())); } catch (IOException e) { errors.add("Can't write error file " + params.getRedirectErrorFile() + "."); return; } try { char[] buffer = new char[BUFFER_SIZE]; while (true) { int readCharCount = reader.read(buffer); if (readCharCount == -1) { break; } if (redirectErrorWriter != null) { redirectErrorWriter.write(buffer, 0, readCharCount); redirectErrorWriter.flush(); } if (error.length() < TRUNCATE_LIMIT) { readCharCount = Math.min(readCharCount, TRUNCATE_LIMIT - error.length()); error.append(buffer, 0, readCharCount); } } } catch (IOException ignored) { errors.add("Can't handle error of the process."); } finally { try { if (redirectErrorWriter != null) { redirectErrorWriter.close(); } } catch (IOException ignored) { // No operations. } try { reader.close(); } catch (IOException ignored) { // No operations. } } } }; if (writeInputThread != null) { writeInputThread.start(); } readOutputThread.start(); readErrorThread.start(); try { exitCode = timedCall(new Callable<Integer>() { public Integer call() throws Exception { return process.waitFor(); } }, params.getTimeLimit() == 0 ? Integer.MAX_VALUE : params.getTimeLimit(), TimeUnit.MILLISECONDS); } catch (TimeoutException e) { errors.add("Process timed out [timeLimit=" + params.getTimeLimit() + "]"); } catch (ExecutionException e) { errors.add("Process failed [commandLine=" + commandLine + "]"); } finally { if (writeInputThread != null) { writeInputThread.join(TimeUnit.MINUTES.toMillis(1)); } readOutputThread.join(TimeUnit.MINUTES.toMillis(1)); readErrorThread.join(TimeUnit.MINUTES.toMillis(1)); process.destroy(); } return new Outcome(exitCode, output.toString(), error.toString(), errors); } catch (Exception e) { return new Outcome( -1, output.toString(), error.toString(), Arrays.asList(e.getMessage()) ); } } }