blob: b476bf0a567c875ba4b38eebac6f6a074804bdc9 [file] [log] [blame]
/*
* Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import com.sun.management.OperatingSystemMXBean;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.io.PrintWriter;
import java.lang.InterruptedException;
import java.lang.Override;
import java.lang.management.ManagementFactory;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/**
* Command driven subprocess with useful child functions.
*/
public class JavaChild extends Process {
private static volatile int commandSeq = 0; // Command sequence number
private static final ProcessHandle self = ProcessHandle.current();
private static int finalStatus = 0;
private static final List<JavaChild> children = new ArrayList<>();
private static final Set<JavaChild> completedChildren =
Collections.synchronizedSet(new HashSet<>());
private final Process delegate;
private final PrintWriter inputWriter;
private final BufferedReader outputReader;
/**
* Create a JavaChild control instance that delegates to the spawned process.
* {@link #sendAction} is used to send commands via the processes stdin.
* {@link #forEachOutputLine} can be used to process output from the child
* @param delegate the process to delegate and send commands to and get responses from
*/
private JavaChild(ProcessBuilder pb) throws IOException {
allArgs = pb.command();
delegate = pb.start();
// Initialize PrintWriter with autoflush (on println)
inputWriter = new PrintWriter(delegate.getOutputStream(), true);
outputReader = new BufferedReader(new InputStreamReader(delegate.getInputStream()));
}
@Override
public void destroy() {
delegate.destroy();
}
@Override
public int exitValue() {
return delegate.exitValue();
}
@Override
public int waitFor() throws InterruptedException {
return delegate.waitFor();
}
@Override
public OutputStream getOutputStream() {
return delegate.getOutputStream();
}
@Override
public InputStream getInputStream() {
return delegate.getInputStream();
}
@Override
public InputStream getErrorStream() {
return delegate.getErrorStream();
}
@Override
public ProcessHandle toHandle() {
return delegate.toHandle();
}
@Override
public CompletableFuture<Process> onExit() {
return delegate.onExit();
}
@Override
public String toString() {
return "delegate: " + delegate.toString();
}
public List<String> getArgs() {
return allArgs;
}
public CompletableFuture<JavaChild> onJavaChildExit() {
return onExit().thenApply(ph -> this);
}
/**
* Send an action and arguments to the child via stdin.
* @param action the action
* @param args additional arguments
* @throws IOException if something goes wrong writing to the child
*/
void sendAction(String action, Object... args) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(action);
for (Object arg :args) {
sb.append(" ");
sb.append(arg);
}
String cmd = sb.toString();
synchronized (this) {
inputWriter.println(cmd);
}
}
public BufferedReader outputReader() {
return outputReader;
}
/**
* Asynchronously evaluate each line of output received back from the child process.
* @param consumer a Consumer of each line read from the child
* @return a CompletableFuture that is completed when the child closes System.out.
*/
CompletableFuture<String> forEachOutputLine(Consumer<String> consumer) {
final CompletableFuture<String> future = new CompletableFuture<>();
String name = "OutputLineReader-" + getPid();
Thread t = new Thread(() -> {
try (BufferedReader reader = outputReader()) {
String line;
while ((line = reader.readLine()) != null) {
consumer.accept(line);
}
} catch (IOException | RuntimeException ex) {
consumer.accept("IOE (" + getPid() + "):" + ex.getMessage());
future.completeExceptionally(ex);
}
future.complete("success");
}, name);
t.start();
return future;
}
/**
* Spawn a JavaChild with the provided arguments.
* Commands can be send to the child with {@link #sendAction}.
* Output lines from the child can be processed with {@link #forEachOutputLine}.
* System.err is set to inherit and is the unstructured async logging
* output for all subprocesses.
* @param args the command line arguments to JavaChild
* @return the JavaChild that was started
* @throws IOException thrown by ProcessBuilder.start
*/
static JavaChild spawnJavaChild(Object... args) throws IOException {
String[] stringArgs = new String[args.length];
for (int i = 0; i < args.length; i++) {
stringArgs[i] = args[i].toString();
}
ProcessBuilder pb = build(stringArgs);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
return new JavaChild(pb);
}
/**
* Spawn a JavaChild with the provided arguments.
* Sets the process to inherit the I/O channels.
* @param args the command line arguments to JavaChild
* @return the Process that was started
* @throws IOException thrown by ProcessBuilder.start
*/
static Process spawn(String... args) throws IOException {
ProcessBuilder pb = build(args);
pb.inheritIO();
return pb.start();
}
/**
* Return a ProcessBuilder with the javaChildArgs and
* any additional supplied args.
*
* @param args the command line arguments to JavaChild
* @return the ProcessBuilder
*/
static ProcessBuilder build(String ... args) {
ProcessBuilder pb = new ProcessBuilder();
List<String> list = new ArrayList<>(javaChildArgs);
for (String arg : args)
list.add(arg);
pb.command(list);
return pb;
}
static final String javaHome = (System.getProperty("test.jdk") != null)
? System.getProperty("test.jdk")
: System.getProperty("java.home");
static final String javaExe =
javaHome + File.separator + "bin" + File.separator + "java";
static final String classpath =
System.getProperty("java.class.path");
static final List<String> javaChildArgs =
Arrays.asList(javaExe,
"-XX:+DisplayVMOutputToStderr",
"-Dtest.jdk=" + javaHome,
"-classpath", absolutifyPath(classpath),
"JavaChild");
// Will hold the complete list of arguments which was given to Processbuilder.command()
private List<String> allArgs;
private static String absolutifyPath(String path) {
StringBuilder sb = new StringBuilder();
for (String file : path.split(File.pathSeparator)) {
if (sb.length() != 0)
sb.append(File.pathSeparator);
sb.append(new File(file).getAbsolutePath());
}
return sb.toString();
}
/**
* Main program that interprets commands from the command line args or stdin.
* Each command produces output to stdout confirming the command and
* providing results.
* System.err is used for unstructured information.
* @param args an array of strings to be interpreted as commands;
* each command uses additional arguments as needed
*/
public static void main(String[] args) {
System.out.printf("args: %s %s%n", ProcessHandle.current(), Arrays.toString(args));
interpretCommands(args);
System.exit(finalStatus);
}
/**
* Interpret an array of strings as a command line.
* @param args an array of strings to be interpreted as commands;
* each command uses additional arguments as needed
*/
private static void interpretCommands(String[] args) {
try {
int nextArg = 0;
while (nextArg < args.length) {
String action = args[nextArg++];
switch (action) {
case "help":
sendResult(action, "");
help();
break;
case "sleep":
int millis = Integer.valueOf(args[nextArg++]);
Thread.sleep(millis);
sendResult(action, Integer.toString(millis));
break;
case "cpuloop":
long cpuMillis = Long.valueOf(args[nextArg++]);
long cpuTarget = getCpuTime() + cpuMillis * 1_000_000L;
while (getCpuTime() < cpuTarget) {
// burn the cpu until the time is up
}
sendResult(action, cpuMillis);
break;
case "cputime":
sendResult(action, getCpuTime());
break;
case "out":
case "err":
String value = args[nextArg++];
sendResult(action, value);
if (action.equals("err")) {
System.err.println(value);
}
break;
case "stdin":
// Read commands from stdin; at eof, close stdin of
// children and wait for each to exit
sendResult(action, "start");
try (Reader reader = new InputStreamReader(System.in);
BufferedReader input = new BufferedReader(reader)) {
String line;
while ((line = input.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) {
String[] split = line.split("\\s");
interpretCommands(split);
}
}
// EOF on stdin, close stdin on all spawned processes
for (JavaChild p : children) {
try {
p.getOutputStream().close();
} catch (IOException ie) {
sendResult("stdin_closing", p.getPid(),
"exception", ie.getMessage());
}
}
for (JavaChild p : children) {
do {
try {
p.waitFor();
break;
} catch (InterruptedException e) {
// retry
}
} while (true);
}
// Wait for all children to be gone
Instant timeOut = Instant.now().plusSeconds(10L);
while (!completedChildren.containsAll(children)) {
if (Instant.now().isBefore(timeOut)) {
Thread.sleep(100L);
} else {
System.err.printf("Timeout waiting for " +
"children to terminate%n");
children.removeAll(completedChildren);
for (JavaChild c : children) {
sendResult("stdin_noterm", c.getPid());
System.err.printf(" Process not terminated: " +
"pid: %d%n", c.getPid());
}
System.exit(2);
}
}
}
sendResult(action, "done");
return; // normal exit from JavaChild Process
case "parent":
sendResult(action, self.parent().toString());
break;
case "pid":
sendResult(action, self.toString());
break;
case "exit":
int exitValue = (nextArg < args.length)
? Integer.valueOf(args[nextArg]) : 0;
sendResult(action, exitValue);
System.exit(exitValue);
break;
case "spawn": {
if (args.length - nextArg < 2) {
throw new RuntimeException("not enough args for respawn: " +
(args.length - 2));
}
// Spawn as many children as requested and
// pass on rest of the arguments
int ncount = Integer.valueOf(args[nextArg++]);
Object[] subargs = new String[args.length - nextArg];
System.arraycopy(args, nextArg, subargs, 0, subargs.length);
for (int i = 0; i < ncount; i++) {
JavaChild p = spawnJavaChild(subargs);
sendResult(action, p.getPid());
p.forEachOutputLine(JavaChild::sendRaw);
p.onJavaChildExit().thenAccept((p1) -> {
int excode = p1.exitValue();
sendResult("child_exit", p1.getPid(), excode);
completedChildren.add(p1);
});
children.add(p); // Add child to spawned list
}
nextArg = args.length;
break;
}
case "child": {
// Send the command to all the live children;
// ignoring those that are not alive
int sentCount = 0;
Object[] result =
Arrays.copyOfRange(args, nextArg - 1, args.length);
Object[] subargs =
Arrays.copyOfRange(args, nextArg + 1, args.length);
for (JavaChild p : children) {
if (p.isAlive()) {
sentCount++;
// overwrite with current pid
result[0] = Long.toString(p.getPid());
sendResult(action, result);
p.sendAction(args[nextArg], subargs);
}
}
if (sentCount == 0) {
sendResult(action, "n/a");
}
nextArg = args.length;
break;
}
case "child_eof" :
// Close the InputStream of all the live children;
// ignoring those that are not alive
for (JavaChild p : children) {
if (p.isAlive()) {
sendResult(action, p.getPid());
p.getOutputStream().close();
}
}
break;
case "property":
String name = args[nextArg++];
sendResult(action, name, System.getProperty(name));
break;
case "threaddump":
Thread.dumpStack();
break;
default:
throw new Error("JavaChild action unknown: " + action);
}
}
} catch (Throwable t) {
t.printStackTrace(System.err);
System.exit(1);
}
}
static synchronized void sendRaw(String s) {
System.out.println(s);
System.out.flush();
}
static void sendResult(String action, Object... results) {
sendRaw(new Event(action, results).toString());
}
static long getCpuTime() {
OperatingSystemMXBean osMbean =
(OperatingSystemMXBean)ManagementFactory.getOperatingSystemMXBean();
return osMbean.getProcessCpuTime();
}
/**
* Print command usage to stderr.
*/
private static void help() {
System.err.println("Commands:");
System.err.println(" help");
System.err.println(" pid");
System.err.println(" parent");
System.err.println(" cpuloop <loopcount>");
System.err.println(" cputime");
System.err.println(" stdin - read commands from stdin");
System.err.println(" sleep <millis>");
System.err.println(" spawn <n> command... - spawn n new children and send command");
System.err.println(" child command... - send command to all live children");
System.err.println(" child_eof - send eof to all live children");
System.err.println(" exit <exitcode>");
System.err.println(" out arg...");
System.err.println(" err arg...");
}
static class Event {
long pid;
long seq;
String command;
Object[] results;
Event(String command, Object... results) {
this(self.getPid(), ++commandSeq, command, results);
}
Event(long pid, int seq, String command, Object... results) {
this.pid = pid;
this.seq = seq;
this.command = command;
this.results = results;
}
/**
* Create a String encoding the pid, seq, command, and results.
*
* @return a String formatted to send to the stream.
*/
String format() {
StringBuilder sb = new StringBuilder();
sb.append(pid);
sb.append(":");
sb.append(seq);
sb.append(" ");
sb.append(command);
for (int i = 0; i < results.length; i++) {
sb.append(" ");
sb.append(results[i]);
}
return sb.toString();
}
Event(String encoded) {
String[] split = encoded.split("\\s");
String[] pidSeq = split[0].split(":");
pid = Long.valueOf(pidSeq[0]);
seq = Integer.valueOf(pidSeq[1]);
command = split[1];
Arrays.copyOfRange(split, 1, split.length);
}
public String toString() {
return format();
}
}
}