com.cinchapi.common.process.Processes Maven / Gradle / Ivy
/*
* Copyright (c) 2013-2016 Cinchapi Inc.
*
* 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.cinchapi.common.process;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import com.cinchapi.common.base.CheckedExceptions;
import com.cinchapi.common.base.Platform;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import com.google.common.util.concurrent.MoreExecutors;
/**
* Utility functions for safely handling {@link Process} objects.
*
* @author Jeff Nelson
*/
public class Processes {
/**
* Create a {@link ProcessBuilder} that, on the appropriate platforms,
* sources the standard interactive profile for the user (i.e.
* ~/.bash_profile).
*
* @param commands a string array containing the program and its arguments
* @return a {@link ProcessBuilder}
*/
public static ProcessBuilder getBuilder(String... commands) {
ProcessBuilder pb = new ProcessBuilder(commands);
if(!Platform.isWindows()) {
Map env = pb.environment();
env.put("BASH_ENV",
System.getProperty("user.home") + "/.bash_profile");
}
return pb;
}
/**
* Create a {@link ProcessBuilder} that, on the appropriate platforms,
* sources the standard interactive profile for the user (i.e.
* ~/.bash_profile) and supports the use of the pipe (|) redirection on
* platforms that allow it.
*
* @param commands a string array containing the program and its arguments
* @return a {@link ProcessBuilder}
*/
public static ProcessBuilder getBuilderWithPipeSupport(String... commands) {
if(!Platform.isWindows()) {
List listCommands = Lists
.newArrayListWithCapacity(commands.length + 2);
// Need to invoke a shell in which the commands can be run. That
// shell will properly interpret the pipe(|).
listCommands.add("/bin/sh");
listCommands.add("-c");
for (String command : commands) {
listCommands.add(command);
}
return getBuilder(listCommands.toArray(commands));
}
else {
return getBuilder(commands);
}
}
/**
* Return the pid of the current process.
*
* @return pid.
*/
public static String getCurrentPid() {
return ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
}
/**
* Get the stderr for {@code process}.
*
* @param process
* @return a collection of error lines
*/
public static List getStdErr(Process process) {
return readStream(process.getErrorStream());
}
/**
* Get the stdout for {@code process}.
*
* @param process
* @return a collection of output lines
*/
public static List getStdOut(Process process) {
return readStream(process.getInputStream());
}
/**
* Check if the process with the processId is running.
*
* @param pid Id for the input process.
* @return true if its running, false if not.
*/
public static boolean isPidRunning(String pid) {
Process process = null;
try {
if(Platform.isLinux() || Platform.isMacOsX()
|| Platform.isSolaris()) {
ProcessBuilder pb = getBuilderWithPipeSupport(
"ps aux | grep ");
process = pb.start();
}
else if(Platform.isWindows()) {
process = Runtime.getRuntime().exec(
"TASKLIST /fi \"PID eq " + pid + "\" /fo csv /nh");
}
else {
throw new UnsupportedOperationException(
"Cannot check pid on the underlying platform");
}
}
catch (IOException e) {
throw CheckedExceptions.wrapAsRuntimeException(e);
}
if(process != null) {
ProcessResult result = waitForSuccessfulCompletion(process);
for (String line : result.out()) {
if(line.contains(pid)) {
return true;
}
}
return false;
}
else {
return true;
}
}
/**
* Execute {@link Process#waitFor()} while reading everything from the
* {@code process}'s standard out and error to prevent the process from
* hanging.
*
* @param process the {@link Process} for which to wait
* @return a map containing all the {@link ProcessData} (e.g. exit code,
* stdout and stderr)
*/
public static ProcessResult waitFor(Process process) {
AtomicBoolean finished = new AtomicBoolean(false);
List stdout = Lists.newArrayList();
List stderr = Lists.newArrayList();
CountDownLatch latch = new CountDownLatch(2);
try {
// Asynchronously exhaust stdout so process doesn't hang
executor.execute(() -> {
try {
InputStreamReader reader = new InputStreamReader(
process.getInputStream());
while (!finished.get()) {
stdout.addAll(CharStreams.readLines(reader));
}
reader.close();
latch.countDown();
}
catch (IOException e) {
throw CheckedExceptions.wrapAsRuntimeException(e);
}
});
// Asynchronously exhaust stderr so process doesn't hang
executor.execute(() -> {
try {
InputStreamReader reader = new InputStreamReader(
process.getErrorStream());
while (!finished.get()) {
stderr.addAll(CharStreams.readLines(reader));
}
reader.close();
latch.countDown();
}
catch (IOException e) {
throw CheckedExceptions.wrapAsRuntimeException(e);
}
});
int code = process.waitFor();
finished.set(true);
latch.await();
return new ProcessResult(code, stdout, stderr);
}
catch (InterruptedException e) {
throw CheckedExceptions.wrapAsRuntimeException(e);
}
}
/**
* Similar to {@link Process#waitFor()} but will throw a
* {@link RuntimeException} if the process does not have an exit code of
* {@code 0}.
*
* @param process
* @return the {@link ProcessResult}
*/
public static ProcessResult waitForSuccessfulCompletion(Process process) {
ProcessResult result = waitFor(process);
if(result.exitCode() != 0) {
List msg = result.out().isEmpty() ? result.err()
: result.out();
throw new RuntimeException(msg.toString());
}
return result;
}
/**
* Read an input stream.
*
* @param stream
* @return the lines in the stream
*/
private static List readStream(InputStream stream) {
try (BufferedReader out = new BufferedReader(
new InputStreamReader(stream))) {
String line;
List output = Lists.newArrayList();
while ((line = out.readLine()) != null) {
output.add(line);
}
return output;
}
catch (Exception e) {
throw CheckedExceptions.wrapAsRuntimeException(e);
}
}
/**
* An {@link Executor} that is used to asynchronously read input from a
* processe's standard out and error streams.
*/
private static final ExecutorService executor = MoreExecutors
.getExitingExecutorService(
(ThreadPoolExecutor) Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2));
private Processes() {} /* no-op */
/**
* The result of a {@link Process} that has been
* {@link Processes#waitFor(Process) waited on}.
*
* @author Jeff Nelson
*/
public static final class ProcessResult {
private final int exitCode;
private final List stderr;
private final List stdout;
/**
* Construct a new instance.
*
* @param exitCode
* @param stdout
* @param stderr
*/
private ProcessResult(int exitCode, List stdout,
List stderr) {
this.exitCode = exitCode;
this.stdout = stdout;
this.stderr = stderr;
}
/**
* Return the {@link Process process's} standard error.
*
* @return stderr
*/
public List err() {
return Collections.unmodifiableList(stderr);
}
/**
* Return the {@link Process process's} exit code.
*
* @return the exit code
*/
public int exitCode() {
return exitCode;
}
/**
* Return the {@link Process process's} standard output.
*
* @return stdout
*/
public List out() {
return Collections.unmodifiableList(stdout);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy