test/jdk/java/lang/ProcessHandle/JavaChild.java
changeset 47216 71c04702a3d5
parent 44640 590dec7cadb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/lang/ProcessHandle/JavaChild.java	Tue Sep 12 19:03:39 2017 +0200
@@ -0,0 +1,550 @@
+/*
+ * Copyright (c) 2014, 2017, 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.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.concurrent.ExecutionException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Optional;
+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-" + pid();
+        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 (" + pid() + "):" + 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.pid(),
+                                            "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.pid());
+                                        System.err.printf("  Process not terminated: " +
+                                                "pid: %d%n", c.pid());
+                                    }
+                                    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.pid());
+                            p.forEachOutputLine(JavaChild::sendRaw);
+                            p.onJavaChildExit().thenAccept((p1) -> {
+                                int excode = p1.exitValue();
+                                sendResult("child_exit", p1.pid(), 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.pid());
+                                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.pid());
+                                p.getOutputStream().close();
+                            }
+                        }
+                        break;
+                    case "property":
+                        String name = args[nextArg++];
+                        sendResult(action, name, System.getProperty(name));
+                        break;
+                    case "threaddump":
+                        Thread.dumpStack();
+                        break;
+                    case "waitpid":
+                        long pid = Long.parseLong(args[nextArg++]);
+                        Optional<String> s = ProcessHandle.of(pid).map(ph -> waitAlive(ph));
+                        sendResult(action, s.orElse("pid not valid: " + pid));
+                        break;
+                    default:
+                        throw new Error("JavaChild action unknown: " + action);
+                }
+            }
+        } catch (Throwable t) {
+            t.printStackTrace(System.err);
+            System.exit(1);
+        }
+    }
+
+    private static String waitAlive(ProcessHandle ph) {
+        String status;
+        try {
+            boolean isAlive = ph.onExit().get().isAlive();
+            status = Boolean.toString(isAlive);
+        } catch (InterruptedException | ExecutionException ex ) {
+            status = "interrupted";
+        }
+        return status;
+    }
+
+    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("  waitpid <pid> - wait for the pid to exit");
+        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.pid(), ++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();
+        }
+
+    }
+}