1 /*
   2  * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package jdk.testlibrary;
  25 
  26 import java.io.IOException;
  27 import java.io.PrintStream;
  28 import java.lang.management.ManagementFactory;
  29 import java.lang.management.RuntimeMXBean;
  30 import java.lang.reflect.Field;
  31 import java.lang.reflect.Method;
  32 import java.util.ArrayList;
  33 import java.util.Collections;
  34 import java.util.concurrent.CountDownLatch;
  35 import java.util.Map;
  36 import java.util.concurrent.Future;
  37 import java.util.concurrent.TimeUnit;
  38 import java.util.concurrent.TimeoutException;
  39 import java.util.function.Predicate;
  40 import java.util.function.Consumer;
  41 import java.util.stream.Collectors;
  42 
  43 import sun.management.VMManagement;
  44 
  45 public final class ProcessTools {
  46     private static final class LineForwarder extends StreamPumper.LinePump {
  47         private final PrintStream ps;
  48         private final String prefix;
  49         LineForwarder(String prefix, PrintStream os) {
  50             this.ps = os;
  51             this.prefix = prefix;
  52         }
  53         @Override
  54         protected void processLine(String line) {
  55             ps.println("[" + prefix + "] " + line);
  56         }
  57     }
  58 
  59     private ProcessTools() {
  60     }
  61 
  62     /**
  63      * <p>Starts a process from its builder.</p>
  64      * <span>The default redirects of STDOUT and STDERR are started</span>
  65      * @param name The process name
  66      * @param processBuilder The process builder
  67      * @return Returns the initialized process
  68      * @throws IOException
  69      */
  70     public static Process startProcess(String name,
  71                                        ProcessBuilder processBuilder)
  72     throws IOException {
  73         return startProcess(name, processBuilder, (Consumer)null);
  74     }
  75 
  76     /**
  77      * <p>Starts a process from its builder.</p>
  78      * <span>The default redirects of STDOUT and STDERR are started</span>
  79      * <p>It is possible to monitor the in-streams via the provided {@code consumer}
  80      * @param name The process name
  81      * @param consumer {@linkplain Consumer} instance to process the in-streams
  82      * @param processBuilder The process builder
  83      * @return Returns the initialized process
  84      * @throws IOException
  85      */
  86     public static Process startProcess(String name,
  87                                        ProcessBuilder processBuilder,
  88                                        Consumer<String> consumer)
  89     throws IOException {
  90         Process p = null;
  91         try {
  92             p = startProcess(
  93                 name,
  94                 processBuilder,
  95                 line -> {
  96                     if (consumer != null) {
  97                         consumer.accept(line);
  98                     }
  99                     return false;
 100                 },
 101                 -1,
 102                 TimeUnit.NANOSECONDS
 103             );
 104         } catch (InterruptedException | TimeoutException e) {
 105             // can't ever happen
 106         }
 107         return p;
 108     }
 109 
 110     /**
 111      * <p>Starts a process from its builder.</p>
 112      * <span>The default redirects of STDOUT and STDERR are started</span>
 113      * <p>
 114      * It is possible to wait for the process to get to a warmed-up state
 115      * via {@linkplain Predicate} condition on the STDOUT
 116      * </p>
 117      * @param name The process name
 118      * @param processBuilder The process builder
 119      * @param linePredicate The {@linkplain Predicate} to use on the STDOUT
 120      *                      Used to determine the moment the target app is
 121      *                      properly warmed-up.
 122      *                      It can be null - in that case the warmup is skipped.
 123      * @param timeout The timeout for the warmup waiting; -1 = no wait; 0 = wait forever
 124      * @param unit The timeout {@linkplain TimeUnit}
 125      * @return Returns the initialized {@linkplain Process}
 126      * @throws IOException
 127      * @throws InterruptedException
 128      * @throws TimeoutException
 129      */
 130     public static Process startProcess(String name,
 131                                        ProcessBuilder processBuilder,
 132                                        final Predicate<String> linePredicate,
 133                                        long timeout,
 134                                        TimeUnit unit)
 135     throws IOException, InterruptedException, TimeoutException {
 136         System.out.println("["+name+"]:" + processBuilder.command().stream().collect(Collectors.joining(" ")));
 137         Process p = processBuilder.start();
 138         StreamPumper stdout = new StreamPumper(p.getInputStream());
 139         StreamPumper stderr = new StreamPumper(p.getErrorStream());
 140 
 141         stdout.addPump(new LineForwarder(name, System.out));
 142         stderr.addPump(new LineForwarder(name, System.err));
 143         CountDownLatch latch = new CountDownLatch(1);
 144         if (linePredicate != null) {
 145             StreamPumper.LinePump pump = new StreamPumper.LinePump() {
 146                 @Override
 147                 protected void processLine(String line) {
 148                     if (latch.getCount() > 0 && linePredicate.test(line)) {
 149                         latch.countDown();
 150                     }
 151                 }
 152             };
 153             stdout.addPump(pump);
 154             stderr.addPump(pump);
 155         } else {
 156             latch.countDown();
 157         }
 158         Future<Void> stdoutTask = stdout.process();
 159         Future<Void> stderrTask = stderr.process();
 160 
 161         try {
 162             if (timeout > -1) {
 163                 if (timeout == 0) {
 164                     latch.await();
 165                 } else {
 166                     if (!latch.await(Utils.adjustTimeout(timeout), unit)) {
 167                         throw new TimeoutException();
 168                     }
 169                 }
 170             }
 171         } catch (TimeoutException | InterruptedException e) {
 172             System.err.println("Failed to start a process (thread dump follows)");
 173             for(Map.Entry<Thread, StackTraceElement[]> s : Thread.getAllStackTraces().entrySet()) {
 174                 printStack(s.getKey(), s.getValue());
 175             }
 176 
 177             if (p.isAlive()) {
 178                 p.destroyForcibly();
 179             }
 180 
 181             stdoutTask.cancel(true);
 182             stderrTask.cancel(true);
 183             throw e;
 184         }
 185 
 186         return p;
 187     }
 188 
 189     /**
 190      * <p>Starts a process from its builder.</p>
 191      * <span>The default redirects of STDOUT and STDERR are started</span>
 192      * <p>
 193      * It is possible to wait for the process to get to a warmed-up state
 194      * via {@linkplain Predicate} condition on the STDOUT
 195      * </p>
 196      * @param name The process name
 197      * @param processBuilder The process builder
 198      * @param linePredicate The {@linkplain Predicate} to use on the STDOUT
 199      *                      Used to determine the moment the target app is
 200      *                      properly warmed-up.
 201      *                      It can be null - in that case the warmup is skipped.
 202      * @return Returns the initialized {@linkplain Process}
 203      * @throws IOException
 204      * @throws InterruptedException
 205      * @throws TimeoutException
 206      */
 207     public static Process startProcess(String name,
 208                                        ProcessBuilder processBuilder,
 209                                        final Predicate<String> linePredicate)
 210     throws IOException, InterruptedException, TimeoutException {
 211         return startProcess(name, processBuilder, linePredicate, 0, TimeUnit.SECONDS);
 212     }
 213 
 214     /**
 215      * Get the process id of the current running Java process
 216      *
 217      * @return Process id
 218      */
 219     public static int getProcessId() throws Exception {
 220 
 221         // Get the current process id using a reflection hack
 222         RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
 223         Field jvm = runtime.getClass().getDeclaredField("jvm");
 224 
 225         jvm.setAccessible(true);
 226         VMManagement mgmt = (sun.management.VMManagement) jvm.get(runtime);
 227 
 228         Method pid_method = mgmt.getClass().getDeclaredMethod("getProcessId");
 229 
 230         pid_method.setAccessible(true);
 231 
 232         int pid = (Integer) pid_method.invoke(mgmt);
 233 
 234         return pid;
 235     }
 236 
 237     /**
 238      * Get platform specific VM arguments (e.g. -d64 on 64bit Solaris)
 239      *
 240      * @return String[] with platform specific arguments, empty if there are
 241      *         none
 242      */
 243     public static String[] getPlatformSpecificVMArgs() {
 244         String osName = System.getProperty("os.name");
 245         String dataModel = System.getProperty("sun.arch.data.model");
 246 
 247         if (osName.equals("SunOS") && dataModel.equals("64")) {
 248             return new String[] { "-d64" };
 249         }
 250 
 251         return new String[] {};
 252     }
 253 
 254     /**
 255      * Create ProcessBuilder using the java launcher from the jdk to be tested
 256      * and with any platform specific arguments prepended
 257      */
 258     public static ProcessBuilder createJavaProcessBuilder(String... command)
 259             throws Exception {
 260         String javapath = JDKToolFinder.getJDKTool("java");
 261 
 262         ArrayList<String> args = new ArrayList<>();
 263         args.add(javapath);
 264         Collections.addAll(args, getPlatformSpecificVMArgs());
 265         Collections.addAll(args, command);
 266 
 267         // Reporting
 268         StringBuilder cmdLine = new StringBuilder();
 269         for (String cmd : args)
 270             cmdLine.append(cmd).append(' ');
 271         System.out.println("Command line: [" + cmdLine.toString() + "]");
 272 
 273         return new ProcessBuilder(args.toArray(new String[args.size()]));
 274     }
 275 
 276     private static void printStack(Thread t, StackTraceElement[] stack) {
 277         System.out.println("\t" +  t +
 278                            " stack: (length = " + stack.length + ")");
 279         if (t != null) {
 280             for (StackTraceElement stack1 : stack) {
 281                 System.out.println("\t" + stack1);
 282             }
 283             System.out.println();
 284         }
 285     }
 286 
 287     /**
 288      * Executes a test jvm process, waits for it to finish and returns the process output.
 289      * The default jvm options from jtreg, test.vm.opts and test.java.opts, are added.
 290      * The java from the test.jdk is used to execute the command.
 291      *
 292      * The command line will be like:
 293      * {test.jdk}/bin/java {test.vm.opts} {test.java.opts} cmds
 294      *
 295      * The jvm process will have exited before this method returns.
 296      *
 297      * @param cmds User specifed arguments.
 298      * @return The output from the process.
 299      */
 300     public static OutputAnalyzer executeTestJvm(String... cmds) throws Exception {
 301         ProcessBuilder pb = createJavaProcessBuilder(Utils.addTestJavaOpts(cmds));
 302         return executeProcess(pb);
 303     }
 304 
 305     /**
 306      * Executes a process, waits for it to finish and returns the process output.
 307      * The process will have exited before this method returns.
 308      * @param pb The ProcessBuilder to execute.
 309      * @return The {@linkplain OutputAnalyzer} instance wrapping the process.
 310      */
 311     public static OutputAnalyzer executeProcess(ProcessBuilder pb) throws Exception {
 312         OutputAnalyzer output = null;
 313         Process p = null;
 314         boolean failed = false;
 315         try {
 316             p = pb.start();
 317             output = new OutputAnalyzer(p);
 318             p.waitFor();
 319 
 320             return output;
 321         } catch (Throwable t) {
 322             if (p != null) {
 323                 p.destroyForcibly().waitFor();
 324             }
 325 
 326             failed = true;
 327             System.out.println("executeProcess() failed: " + t);
 328             throw t;
 329         } finally {
 330             if (failed) {
 331                 System.err.println(getProcessLog(pb, output));
 332             }
 333         }
 334     }
 335 
 336     /**
 337      * Executes a process, waits for it to finish and returns the process output.
 338      *
 339      * The process will have exited before this method returns.
 340      *
 341      * @param cmds The command line to execute.
 342      * @return The output from the process.
 343      */
 344     public static OutputAnalyzer executeProcess(String... cmds) throws Throwable {
 345         return executeProcess(new ProcessBuilder(cmds));
 346     }
 347 
 348     /**
 349      * Used to log command line, stdout, stderr and exit code from an executed process.
 350      * @param pb The executed process.
 351      * @param output The output from the process.
 352      */
 353     public static String getProcessLog(ProcessBuilder pb, OutputAnalyzer output) {
 354         String stderr = output == null ? "null" : output.getStderr();
 355         String stdout = output == null ? "null" : output.getStdout();
 356         String exitValue = output == null ? "null": Integer.toString(output.getExitValue());
 357         StringBuilder logMsg = new StringBuilder();
 358         final String nl = System.getProperty("line.separator");
 359         logMsg.append("--- ProcessLog ---" + nl);
 360         logMsg.append("cmd: " + getCommandLine(pb) + nl);
 361         logMsg.append("exitvalue: " + exitValue + nl);
 362         logMsg.append("stderr: " + stderr + nl);
 363         logMsg.append("stdout: " + stdout + nl);
 364 
 365         return logMsg.toString();
 366     }
 367 
 368     /**
 369      * @return The full command line for the ProcessBuilder.
 370      */
 371     public static String getCommandLine(ProcessBuilder pb) {
 372         if (pb == null) {
 373             return "null";
 374         }
 375         StringBuilder cmd = new StringBuilder();
 376         for (String s : pb.command()) {
 377             cmd.append(s).append(" ");
 378         }
 379         return cmd.toString().trim();
 380     }
 381 }