/* * 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 com.addthis.hydra.util; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.util.Arrays; import java.util.concurrent.TimeUnit; import com.addthis.codec.annotations.Time; import com.google.common.io.ByteStreams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Wrapper around Java Process execution API. Does not attempt to * consume the output stream or the error stream. Do not use this * class if you expect your process to fill those streams. If the * stdout or stderr streams are full then the process will block * and not complete successfully. */ public class ProcessExecutor { private static final Logger log = LoggerFactory.getLogger(ProcessExecutor.class); @Time(TimeUnit.SECONDS) private static final int DEFAULT_WAIT = 10; /** * Name of command and arguments to execute. */ @Nonnull private final String[] cmdarray; /** * Optionally specify standard input to process. */ @Nullable private final String stdin; /** * Number of seconds to wait for process to complete. */ @Time(TimeUnit.SECONDS) private final int wait; /** * Exit code of completed processes. Only valid * if {@link #execute()} returns true. Initial value * is {@code Integer.MIN_VALUE} to eliminate mistaking * an uninitialized value for successful exit. */ private int exitValue = Integer.MIN_VALUE; /** * Standard output of completed processes. Possibly null. */ private String stdout; /** * Standard error of completed processes. Possibly null. */ private String stderr; private ProcessExecutor(@Nonnull String[] cmdarray, @Nullable String stdin, @Time(TimeUnit.SECONDS) int wait) { this.cmdarray = cmdarray; this.stdin = stdin; this.wait = wait; } public boolean execute() { boolean completed = false; Process process = null; try { process = Runtime.getRuntime().exec(cmdarray); if (stdin != null) { OutputStreamWriter osw = new OutputStreamWriter(process.getOutputStream()); osw.write(stdin); osw.close(); } completed = process.waitFor(wait, TimeUnit.SECONDS); if (!completed) { stdout = readAvailable(process.getInputStream()); stderr = readAvailable(process.getErrorStream()); process.destroyForcibly(); } else { stdout = new String(ByteStreams.toByteArray(process.getInputStream())); stderr = new String(ByteStreams.toByteArray(process.getErrorStream())); exitValue = process.exitValue(); } if (!completed) { log.error("Process '{}' was killed. It did not complete after {} seconds. " + "Stdout was '{}' and stderr was '{}'", Arrays.toString(cmdarray), wait, stderr, stderr); } else if (exitValue != 0) { log.error("Process '{}' returned non-zero exit value {}. " + "Stdout was '{}' and stderr was '{}'", Arrays.toString(cmdarray), exitValue, stderr, stderr); } } catch (IOException|InterruptedException ex) { log.error("Exception occurred attempting to execute process '{}'", Arrays.toString(cmdarray), ex); if (process != null) { process.destroyForcibly(); } } return completed; } private static String readAvailable(InputStream inputStream) throws IOException { int available = inputStream.available(); if (available > 0) { byte[] data = new byte[available]; inputStream.read(data); return new String(data); } else { return null; } } public int exitValue() { return exitValue; } public String stdout() { return stdout; } public String stderr() { return stderr; } public static class Builder { // required fields @Nonnull private final String[] cmdarray; // optional fields @Nullable private String stdin; private int wait = DEFAULT_WAIT; public Builder(@Nonnull String[] cmdarray) { this.cmdarray = cmdarray; } public Builder setStdin(String stdin) { this.stdin = stdin; return this; } public Builder setWait(int wait) { this.wait = wait; return this; } public ProcessExecutor build() { return new ProcessExecutor(cmdarray, stdin, wait); } } }