/*
* Copyright 2013 John Leacox
*
* 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.openedit.util;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.openedit.util.ExecutorManager;
/**
* This class wraps {@link ProcessBuilder} for creating operating system processes using the safer
* {@link FinalizedProcess}.
*
* <p>
* Like {@code ProcessBuilder}, each {@code FinalizedProcessBuilder} instance manages a collection of process
* attributes. The {@link #start()} method creates a new {@link FinalizedProcess} instance with these attributes. The
* {@link #start()} method can be invoked multiple times from the same instance to create new subprocesses with
* identical or related attributes.
*
* <p>
* Each process builder manages the following attributes, in addition to the attributes from {@link ProcessBuilder}:
*
*
* <ul>
*
* <li>a <i>keepProcess</i> indicator, a boolean indicator as to whether the process should be destroyed during cleanup
* or not. By default the process will be destroyed during cleanup.</li>
*
* </ul>
*
* <p>
* Starting a new process with the default attributes is just as easy as {@link ProcessBuilder}:
*
* <pre>
* {
* @code
* FinalizedProcess process = new ProcessBuilder("myCommand", "myArg").start();
* }
* </pre>
*
* <p>
* Here is an example that redirects standard error to standard output and does not destroy the process when the process
* is closed:
*
* <pre>
* {
* @code
* FinalizedProcessBuilder pb = new FinalizedProcessBuilder("myCommand", "myArg");
* pb.redirectErrorStream(true);
* pb.keepProcess();
* FinalizedProcess process = pb.start();
* }
* </pre>
*
* @author John Leacox
* @see ProcessBuilder
*/
public class FinalizedProcessBuilder {
private final ProcessBuilder processBuilder;
private boolean keepProcess = false;
private boolean gobbleInput = true;
private boolean gobbleInputLogging = false;
private boolean gobbleError = false;
private boolean gobbleErrorLogging = false;
/**
* Constructs a process builder with the specified operating system program and arguments. This constructor does
* <i>not</i> make a copy of the {@code command} list. Subsequent updates to the list will be reflected in the state
* of the process builder. It is not checked whether {@code command} corresponds to a valid operating system
* command.
*
* @param command
* the list containing the program and its arguments (cannot be null)
* @throws NullPointerException
* if command is null
*/
public FinalizedProcessBuilder(List<String> command) {
if (command == null) {
throw new NullPointerException();
}
this.processBuilder = new ProcessBuilder(command);
}
/**
* Constructs a process builder with the specified operating system program and arguments. This is a convenience
* constructor that sets the process builder's command to a string list containing the same strings as the command
* array, in the same order. It is not checked whether command corresponds to a valid operating system command.
*
* @param command
* a string array containing the program and its arguments
*/
public FinalizedProcessBuilder(String... command) {
this.processBuilder = new ProcessBuilder(command);
}
/**
* Returns this process builder's operating system program and arguments. The returned list is <i>not</i> a copy.
* Subsequent updates to the list will be reflected in the state of this process builder.
*
* @return this process builder's program and its arguments
*/
public List<String> command() {
return processBuilder.command();
}
/**
* Sets this process builder's operating system program and arguments. This method does <i>not</i> make a copy of
* the {@code command} list. Subsequent updates to the list will be reflected in the state of the process builder.
* It is not checked whether {@code command} corresponds to a valid operating system command.
*
* @param command
* the list containing the program and its arguments (cannot be null)
* @return this process builder
* @throws NullPointerException
* if the command is null
*/
public FinalizedProcessBuilder command(List<String> command) {
if (command == null) {
throw new NullPointerException();
}
processBuilder.command(command);
return this;
}
/**
* Sets this process builder's operating system program and arguments. This is a convenience method that sets the
* command to a string list containing the same strings as the {@code command} array, in the same order. It is not
* checked whether {@code command} corresponds to a valid operating system command.
*
* @param command
* a string array containing the program and its arguments
* @return this process builder
*/
public FinalizedProcessBuilder command(String... command) {
processBuilder.command(command);
return this;
}
/**
* Returns this process builder's working directory.
*
* Subprocesses subsequently started by this object's {@link #start()} method will use this as their working
* directory. The returned value may be {@code null} -- this means to use the working directory of the current Java
* process, usually the directory named by the system property {@code user.dir}, as the working directory of the
* child process.
*
* @return this process builder's working directory
*/
public File directory() {
return processBuilder.directory();
}
/**
* Sets this process builder's working directory.
*
* Subprocesses subsequently started by this object's {@link #start()} method will use this as their working
* directory. The argument may be {@code null} -- this means to use the working directory of the current Java
* process, usually the directory named by the system property {@code user.dir}, as the working directory of the
* child process.
*
* @param directory
* the new working directory
* @return this process builder
*/
public FinalizedProcessBuilder directory(File directory) {
processBuilder.directory(directory);
return this;
}
/**
* Returns a string map view of this process builder's environment.
*
* Whenever a process builder is created, the environment is initialized to a copy of the current process
* environment (see {@link System#getenv()}). Subprocesses subsequently started by this object's {@link #start()}
* method will use this map as their environment.
*
* <p>
* The returned object may be modified using ordinary {@link java.util.Map Map} operations. These modifications will
* be visible to subprocesses started via the {@link #start()} method. Two {@code ProcessBuilder} instances always
* contain independent process environments, so changes to the returned map will never be reflected in any other
* {@code ProcessBuilder} instance or the values returned by {@link System#getenv System.getenv}.
*
* <p>
* If the system does not support environment variables, an empty map is returned.
*
* <p>
* The returned map does not permit null keys or values. Attempting to insert or query the presence of a null key or
* value will throw a {@link NullPointerException}. Attempting to query the presence of a key or value which is not
* of type {@link String} will throw a {@link ClassCastException}.
*
* <p>
* The behavior of the returned map is system-dependent. A system may not allow modifications to environment
* variables or may forbid certain variable names or values. For this reason, attempts to modify the map may fail
* with {@link UnsupportedOperationException} or {@link IllegalArgumentException} if the modification is not
* permitted by the operating system.
*
* <p>
* Since the external format of environment variable names and values is system-dependent, there may not be a
* one-to-one mapping between them and Java's Unicode strings. Nevertheless, the map is implemented in such a way
* that environment variables which are not modified by Java code will have an unmodified native representation in
* the subprocess.
*
* <p>
* The returned map and its collection views may not obey the general contract of the {@link Object#equals} and
* {@link Object#hashCode} methods.
*
* <p>
* The returned map is typically case-sensitive on all platforms.
*
* <p>
* If a security manager exists, its {@link SecurityManager#checkPermission checkPermission} method is called with a
* {@link RuntimePermission} {@code ("getenv.*")} permission. This may result in a {@link SecurityException} being
* thrown.
*
* <p>
* When passing information to a Java subprocess, <a href=System.html#EnvironmentVSSystemProperties>system
* properties</a> are generally preferred over environment variables.
*
* @return this process builder's environment
* @throws SecurityException
* if a security manager exists and its {@link SecurityManager#checkPermission checkPermission} method
* doesn't allow access to the process environment
* @see Runtime#exec(String[],String[],java.io.File)
* @see System#getenv()
*/
public Map<String, String> environment() {
return processBuilder.environment();
}
/**
* Tells whether this process builder merges standard error and standard output.
*
* <p>
* If this property is {@code true}, then any error output generated by subprocesses subsequently started by this
* object's {@link #start()} method will be merged with the standard output, so that both can be read using the
* {@link Process#getInputStream()} method. This makes it easier to correlate error messages with the corresponding
* output. The initial value is {@code false}.
*
* @return this process builder's {@code redirectErrorStream} property
*/
public boolean redirectErrorStream() {
return processBuilder.redirectErrorStream();
}
/**
* Sets this process builder's {@code redirectErrorStream} property.
*
* <p>
* If this property is {@code true}, then any error output generated by subprocesses subsequently started by this
* object's {@link #start()} method will be merged with the standard output, so that both can be read using the
* {@link Process#getInputStream()} method. This makes it easier to correlate error messages with the corresponding
* output. The initial value is {@code false}.
*
* @param redirectErrorStream
* the new property value
* @return this process builder
*/
public FinalizedProcessBuilder redirectErrorStream(boolean redirectErrorStream) {
processBuilder.redirectErrorStream(redirectErrorStream);
return this;
}
/**
* Tells whether this process builder destroys the subprocess when it is closed or not.
*
* <p>
* If this property is {@code true}, then when the subprocess is closed, the subprocess will also be destroyed via
* {@code Process#destroy()}. This prevents the subprocess from continuing to run indefinitely, even after there are
* no longer any references to it in the parent process.
*
* <p>
* If this property is {@code false}, then the subprocess will not be automatically destroyed when
* {@code FinalizedProcess#close()} is called. This is useful if the subprocess should continue running
* indefinitely.
*
* @return this process builder's {@code keepProcess} property
*/
public boolean keepProcess() {
return keepProcess;
}
/**
* Sets this process builder's {@code keepProcess} property.
*
* <p>
* If this property is {@code true}, then when the subprocess is closed, the subprocess will also be destroyed via
* {@code Process#destroy()}. This prevents the subprocess from continuing to run indefinitely, even after there are
* no longer any references to it in the parent process.
*
* <p>
* If this property is {@code false}, then the subprocess will not be automatically destroyed when
* {@code FinalizedProcess#close()} is called. This is useful if the subprocess should continue running
* indefinitely.
*
* @param keepProcess
* the new property value
* @return this process builder
*/
public FinalizedProcessBuilder keepProcess(boolean keepProcess) {
this.keepProcess = keepProcess;
return this;
}
/**
* Tells whether the created sub process will gobble the input stream or not.
*
* <p>
* If this property is {@code true}, then the sub process input stream will be automatically gobbled.
*
* <p>
* If this property is {@code false}, then the sub process input stream will not be gobbled.
*
* @return this process builder's {@code gobbleInput} property
*/
public boolean gobbleInputStream() {
return gobbleInput;
}
/**
* Sets this process builder's {@code gobbleInput} property.
*
* <p>
* If this property is {@code true}, then the sub process input stream will be automatically gobbled.
*
* <p>
* If this property is {@code false}, then the sub process input stream will not be gobbled.
*
* @param gobbleInput
* the new property value
* @return this process builder
*/
public FinalizedProcessBuilder gobbleInputStream(boolean gobbleInput) {
this.gobbleInput = gobbleInput;
return this;
}
public FinalizedProcessBuilder gobbleErrorStream(boolean gobbleError) {
this.gobbleError = gobbleError;
return this;
}
/**
* Tells whether the created sub process will gobble the input stream and log the output or not.
*
* <p>
* If this property is {@code true}, then the sub process input stream will be automatically gobbled and logged.
*
* <p>
* If this property is {@code false}, then the sub process input stream will not be gobbled.
*
* @return this process builder's {@code gobbleInputLogging} property
*/
public boolean logInputtStream() {
return gobbleInput && gobbleInputLogging;
}
/**
* Sets this process builder's {@code gobbleInputLogging} property.
*
* <p>
* If this property is {@code true}, then the sub process input stream will be automatically gobbled and logged.
*
* <p>
* If this property is {@code false}, then the sub process input stream will not be gobbled.
*
* @param gobbleInput
* the new property value
* @return this process builder
*/
public FinalizedProcessBuilder logInputtStream(boolean gobbleInput) {
this.gobbleInputLogging = gobbleInput;
return this;
}
/**
* Tells whether the created sub process will gobble the error stream or not.
*
* <p>
* If this property is {@code true}, then the sub process error stream will be automatically gobbled.
*
* <p>
* If this property is {@code false}, then the sub process error stream will not be gobbled.
*
* @return this process builder's {@code gobbleError} property
*/
public boolean gobbleErrorStream() {
return gobbleError;
}
/**
* Sets this process builder's {@code gobbleError} property.
*
* <p>
* If this property is {@code true}, then the sub process error stream will be automatically gobbled.
*
* <p>
* If this property is {@code false}, then the sub process error stream will not be gobbled.
*
* @param gobbleError
* the new property value
* @return this process builder
*/
public FinalizedProcessBuilder logErrorStream(boolean gobbleError) {
this.gobbleError = gobbleError;
return this;
}
/**
* Tells whether the created sub process will gobble the error stream and log the output or not.
*
* <p>
* If this property is {@code true}, then the sub process error stream will be automatically gobbled and logged.
*
* <p>
* If this property is {@code false}, then the sub process error stream will not be gobbled.
*
* @return this process builder's {@code gobbleErrorLogging} property
*/
public boolean logErrorStream() {
return gobbleError && gobbleErrorLogging;
}
/**
* Starts a new process using the attributes of this process builder.
*
* <p>
* The new process will invoke the command and arguments given by {@link #command()}, in a working directory as
* given by {@link #directory()}, with a process environment as given by {@link #environment()}.
*
* <p>
* This method checks that the command is a valid operating system command. Which commands are valid is
* system-dependent, but at the very least the command must be a non-empty list of non-null strings.
*
* <p>
* A minimal set of system dependent environment variables may be required to start a process on some operating
* systems. As a result, the subprocess may inherit additional environment variable settings beyond those in the
* process builder's {@link #environment()}.
*
* <p>
* If there is a security manager, its {@link SecurityManager#checkExec checkExec} method is called with the first
* component of this object's {@code command} array as its argument. This may result in a {@link SecurityException}
* being thrown.
*
* <p>
* Starting an operating system process is highly system-dependent. Among the many things that can go wrong are:
* <ul>
* <li>The operating system program file was not found.
* <li>Access to the program file was denied.
* <li>The working directory does not exist.
* </ul>
*
* <p>
* In such cases an exception will be thrown. The exact nature of the exception is system-dependent, but it will
* always be a subclass of {@link IOException}.
*
* <p>
* Subsequent modifications to this process builder will not affect the returned {@link FinalizedProcess}.
*
* @return a new {@link FinalizedProcess} object for managing the subprocess
* @throws NullPointerException
* if an element of the command list is null
* @throws IndexOutOfBoundsException
* if the command is an empty list (has size {@code 0})
* @throws SecurityException
* if a security manager exists and
* <ul>
*
* <li>its {@link SecurityManager#checkExec checkExec} method doesn't allow creation of the subprocess,
* or
*
* <li>the standard input to the subprocess was {@linkplain #redirectInput redirected from a file} and
* the security manager's {@link SecurityManager#checkRead checkRead} method denies read access to the
* file, or
*
* <li>the standard output or standard error of the subprocess was {@linkplain #redirectOutput
* redirected to a file} and the security manager's {@link SecurityManager#checkWrite checkWrite} method
* denies write access to the file
*
* </ul>
* @throws IOException
* if an I/O error occurs
*/
public FinalizedProcess start(ExecutorManager inManager) throws IOException
{
if(gobbleInput && !gobbleError)
{
processBuilder.redirectErrorStream(true);
}
Process process = processBuilder.start();
Set<StreamGobbler> gobblers = new HashSet<StreamGobbler>(2);
if (gobbleInput) {
StreamGobbler inputGobbler = new StreamGobbler(process.getInputStream(), gobbleInputLogging);
inManager.execute(inputGobbler);
gobblers.add(inputGobbler);
}
if (gobbleError) {
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), gobbleErrorLogging);
errorGobbler.setErrorStream(true);
inManager.execute(errorGobbler);
gobblers.add(errorGobbler);
}
return new FinalizedProcess(process, keepProcess, gobblers);
}
}