/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jorphan.exec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.jorphan.util.JOrphanUtils;
/**
* Utility class for invoking native system applications
*/
public class SystemCommand {
public static final int POLL_INTERVAL = 100;
private final File directory;
private final Map<String, String> env;
private Map<String, String> executionEnvironment;
private final InputStream stdin;
private final OutputStream stdout;
private final boolean stdoutWasNull;
private final OutputStream stderr;
private final long timeoutMillis;
private final int pollInterval;
/**
* @param env Environment variables appended to environment (may be null)
* @param directory File working directory (may be null)
*/
public SystemCommand(File directory, Map<String, String> env) {
this(directory, 0L, POLL_INTERVAL, env, (InputStream) null, (OutputStream) null, (OutputStream) null);
}
/**
*
* @param env Environment variables appended to environment (may be null)
* @param directory File working directory (may be null)
* @param timeoutMillis timeout in Milliseconds
* @param pollInterval Value used to poll for Process execution end
* @param stdin File name that will contain data to be input to process (may be null)
* @param stdout File name that will contain out stream (may be null)
* @param stderr File name that will contain err stream (may be null)
* @throws IOException if the input file is not found or output cannot be written
*/
public SystemCommand(File directory, long timeoutMillis, int pollInterval, Map<String, String> env, String stdin, String stdout, String stderr) throws IOException {
this(directory, timeoutMillis, pollInterval, env, checkIn(stdin), checkOut(stdout), checkOut(stderr));
}
private static InputStream checkIn(String stdin) throws FileNotFoundException {
String in = JOrphanUtils.nullifyIfEmptyTrimmed(stdin);
if (in == null) {
return null;
} else {
return new FileInputStream(in);
}
}
private static OutputStream checkOut(String path) throws IOException {
String in = JOrphanUtils.nullifyIfEmptyTrimmed(path);
if (in == null) {
return null;
} else {
return new FileOutputStream(path);
}
}
/**
*
* @param env Environment variables appended to environment (may be null)
* @param directory File working directory (may be null)
* @param timeoutMillis timeout in Milliseconds
* @param pollInterval Value used to poll for Process execution end
* @param stdin File name that will contain data to be input to process (may be null)
* @param stdout File name that will contain out stream (may be null)
* @param stderr File name that will contain err stream (may be null)
*/
public SystemCommand(File directory, long timeoutMillis, int pollInterval, Map<String, String> env, InputStream stdin, OutputStream stdout, OutputStream stderr) {
super();
this.timeoutMillis = timeoutMillis;
this.directory = directory;
this.env = env;
this.pollInterval = pollInterval;
this.stdin = stdin;
this.stdoutWasNull = stdout == null;
if (stdout == null) {
this.stdout = new ByteArrayOutputStream(); // capture the output
} else {
this.stdout = stdout;
}
this.stderr = stderr;
}
/**
* @param arguments List of strings, not null
* @return return code
* @throws InterruptedException when execution was interrupted
* @throws IOException when I/O error occurs while execution
*/
public int run(List<String> arguments) throws InterruptedException, IOException {
return run(arguments, stdin, stdout, stderr);
}
// helper method to allow input and output to be changed for chaining
private int run(List<String> arguments, InputStream in, OutputStream out, OutputStream err) throws InterruptedException, IOException {
Process proc = null;
final ProcessBuilder procBuild = new ProcessBuilder(arguments);
if (env != null) {
procBuild.environment().putAll(env);
}
this.executionEnvironment = Collections.unmodifiableMap(procBuild.environment());
procBuild.directory(directory);
if (err == null) {
procBuild.redirectErrorStream(true);
}
try
{
proc = procBuild.start();
final OutputStream procOut = proc.getOutputStream();
final InputStream procErr = proc.getErrorStream();
final InputStream procIn = proc.getInputStream();
final StreamCopier swerr;
if (err != null){
swerr = new StreamCopier(procErr, err);
swerr.start();
} else {
swerr = null;
}
final StreamCopier swout = new StreamCopier(procIn, out);
swout.start();
final StreamCopier swin;
if (in != null) {
swin = new StreamCopier(in, procOut);
swin.start();
} else {
swin = null;
procOut.close(); // ensure the application does not hang if it requests input
}
int exitVal = waitForEndWithTimeout(proc, timeoutMillis);
swout.join();
if (swerr != null) {
swerr.join();
}
if (swin != null) {
swin.interrupt(); // the copying thread won't generally detect EOF
swin.join();
}
procErr.close();
procIn.close();
procOut.close();
return exitVal;
} finally {
if(proc != null) {
try {
proc.destroy();
} catch (Exception ignored) {
// Ignored
}
}
}
}
/**
* Pipe the output of one command into another
*
* @param arguments1 first command to run
* @param arguments2 second command to run
* @return exit status
* @throws InterruptedException when execution gets interrupted
* @throws IOException when I/O error occurs while execution
*/
public int run(List<String> arguments1, List<String> arguments2) throws InterruptedException, IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); // capture the intermediate output
int exitCode=run(arguments1,stdin,out, stderr);
if (exitCode == 0) {
exitCode = run(arguments2,new ByteArrayInputStream(out.toByteArray()),stdout,stderr);
}
return exitCode;
}
/**
* Wait for end of proc execution or timeout if timeoutInMillis is greater than 0
* @param proc Process
* @param timeoutInMillis long timeout in ms
* @return proc exit value
* @throws InterruptedException
*/
private int waitForEndWithTimeout(Process proc, long timeoutInMillis) throws InterruptedException {
if (timeoutInMillis <= 0L) {
return proc.waitFor();
} else {
long now = System.currentTimeMillis();
long finish = now + timeoutInMillis;
while(System.currentTimeMillis() < finish) {
try {
return proc.exitValue();
} catch (IllegalThreadStateException e) { // not yet terminated
Thread.sleep(pollInterval);
}
}
try {
return proc.exitValue();
} catch (IllegalThreadStateException e) { // not yet terminated
// N.B. proc.destroy() is called by the finally clause in the run() method
throw new InterruptedException( "Process timeout out after " + timeoutInMillis + " milliseconds" );
}
}
}
/**
* @return Out/Err stream contents
*/
public String getOutResult() {
if (stdoutWasNull) { // we are capturing output
return stdout.toString(); // Default charset is probably appropriate here.
} else {
return "";
}
}
/**
* @return the executionEnvironment
*/
public Map<String, String> getExecutionEnvironment() {
return executionEnvironment;
}
}