1 /*
   2  * Copyright (c) 2013, 2020, 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.test.lib.process;
  25 
  26 import java.io.File;
  27 import java.io.IOException;
  28 import java.io.InputStream;
  29 import java.io.OutputStream;
  30 import java.io.PrintStream;
  31 import java.nio.charset.Charset;
  32 import java.nio.file.Paths;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.Collections;
  36 import java.util.concurrent.CountDownLatch;
  37 import java.util.Map;
  38 import java.util.concurrent.ExecutionException;
  39 import java.util.concurrent.Future;
  40 import java.util.concurrent.TimeUnit;
  41 import java.util.concurrent.TimeoutException;
  42 import java.util.function.Predicate;
  43 import java.util.function.Consumer;
  44 import java.util.stream.Collectors;
  45 import java.security.AccessController;
  46 import java.security.PrivilegedActionException;
  47 import java.security.PrivilegedExceptionAction;
  48 
  49 import jdk.test.lib.JDKToolFinder;
  50 import jdk.test.lib.Platform;
  51 import jdk.test.lib.Utils;
  52 
  53 public final class ProcessTools {
  54     private static final class LineForwarder extends StreamPumper.LinePump {
  55         private final PrintStream ps;
  56         private final String prefix;
  57         LineForwarder(String prefix, PrintStream os) {
  58             this.ps = os;
  59             this.prefix = prefix;
  60         }
  61         @Override
  62         protected void processLine(String line) {
  63             ps.println("[" + prefix + "] " + line);
  64         }
  65     }
  66 
  67     private ProcessTools() {
  68     }
  69 
  70     /**
  71      * <p>Starts a process from its builder.</p>
  72      * <span>The default redirects of STDOUT and STDERR are started</span>
  73      * @param name The process name
  74      * @param processBuilder The process builder
  75      * @return Returns the initialized process
  76      * @throws IOException
  77      */
  78     public static Process startProcess(String name,
  79                                        ProcessBuilder processBuilder)
  80     throws IOException {
  81         return startProcess(name, processBuilder, (Consumer<String>)null);
  82     }
  83 
  84     /**
  85      * <p>Starts a process from its builder.</p>
  86      * <span>The default redirects of STDOUT and STDERR are started</span>
  87      * <p>It is possible to monitor the in-streams via the provided {@code consumer}
  88      * @param name The process name
  89      * @param consumer {@linkplain Consumer} instance to process the in-streams
  90      * @param processBuilder The process builder
  91      * @return Returns the initialized process
  92      * @throws IOException
  93      */
  94     @SuppressWarnings("overloads")
  95     public static Process startProcess(String name,
  96                                        ProcessBuilder processBuilder,
  97                                        Consumer<String> consumer)
  98     throws IOException {
  99         try {
 100             return startProcess(name, processBuilder, consumer, null, -1, TimeUnit.NANOSECONDS);
 101         } catch (InterruptedException | TimeoutException e) {
 102             // will never happen
 103             throw new RuntimeException(e);
 104         }
 105     }
 106 
 107     /**
 108      * <p>Starts a process from its builder.</p>
 109      * <span>The default redirects of STDOUT and STDERR are started</span>
 110      * <p>
 111      * It is possible to wait for the process to get to a warmed-up state
 112      * via {@linkplain Predicate} condition on the STDOUT
 113      * </p>
 114      * @param name The process name
 115      * @param processBuilder The process builder
 116      * @param linePredicate The {@linkplain Predicate} to use on the STDOUT
 117      *                      Used to determine the moment the target app is
 118      *                      properly warmed-up.
 119      *                      It can be null - in that case the warmup is skipped.
 120      * @param timeout The timeout for the warmup waiting; -1 = no wait; 0 = wait forever
 121      * @param unit The timeout {@linkplain TimeUnit}
 122      * @return Returns the initialized {@linkplain Process}
 123      * @throws IOException
 124      * @throws InterruptedException
 125      * @throws TimeoutException
 126      */
 127     public static Process startProcess(String name,
 128                                        ProcessBuilder processBuilder,
 129                                        final Predicate<String> linePredicate,
 130                                        long timeout,
 131                                        TimeUnit unit)
 132     throws IOException, InterruptedException, TimeoutException {
 133         return startProcess(name, processBuilder, null, linePredicate, timeout, unit);
 134     }
 135 
 136     /**
 137      * <p>Starts a process from its builder.</p>
 138      * <span>The default redirects of STDOUT and STDERR are started</span>
 139      * <p>
 140      * It is possible to wait for the process to get to a warmed-up state
 141      * via {@linkplain Predicate} condition on the STDOUT and monitor the
 142      * in-streams via the provided {@linkplain Consumer}
 143      * </p>
 144      * @param name The process name
 145      * @param processBuilder The process builder
 146      * @param lineConsumer  The {@linkplain Consumer} the lines will be forwarded to
 147      * @param linePredicate The {@linkplain Predicate} to use on the STDOUT
 148      *                      Used to determine the moment the target app is
 149      *                      properly warmed-up.
 150      *                      It can be null - in that case the warmup is skipped.
 151      * @param timeout The timeout for the warmup waiting; -1 = no wait; 0 = wait forever
 152      * @param unit The timeout {@linkplain TimeUnit}
 153      * @return Returns the initialized {@linkplain Process}
 154      * @throws IOException
 155      * @throws InterruptedException
 156      * @throws TimeoutException
 157      */
 158     public static Process startProcess(String name,
 159                                        ProcessBuilder processBuilder,
 160                                        final Consumer<String> lineConsumer,
 161                                        final Predicate<String> linePredicate,
 162                                        long timeout,
 163                                        TimeUnit unit)
 164     throws IOException, InterruptedException, TimeoutException {
 165         System.out.println("["+name+"]:" + processBuilder.command().stream().collect(Collectors.joining(" ")));
 166         Process p = privilegedStart(processBuilder);
 167         StreamPumper stdout = new StreamPumper(p.getInputStream());
 168         StreamPumper stderr = new StreamPumper(p.getErrorStream());
 169 
 170         stdout.addPump(new LineForwarder(name, System.out));
 171         stderr.addPump(new LineForwarder(name, System.err));
 172         if (lineConsumer != null) {
 173             StreamPumper.LinePump pump = new StreamPumper.LinePump() {
 174                 @Override
 175                 protected void processLine(String line) {
 176                     lineConsumer.accept(line);
 177                 }
 178             };
 179             stdout.addPump(pump);
 180             stderr.addPump(pump);
 181         }
 182 
 183 
 184         CountDownLatch latch = new CountDownLatch(1);
 185         if (linePredicate != null) {
 186             StreamPumper.LinePump pump = new StreamPumper.LinePump() {
 187                 @Override
 188                 protected void processLine(String line) {
 189                     if (latch.getCount() > 0 && linePredicate.test(line)) {
 190                         latch.countDown();
 191                     }
 192                 }
 193             };
 194             stdout.addPump(pump);
 195             stderr.addPump(pump);
 196         } else {
 197             latch.countDown();
 198         }
 199         final Future<Void> stdoutTask = stdout.process();
 200         final Future<Void> stderrTask = stderr.process();
 201 
 202         try {
 203             if (timeout > -1) {
 204                 if (timeout == 0) {
 205                     latch.await();
 206                 } else {
 207                     if (!latch.await(Utils.adjustTimeout(timeout), unit)) {
 208                         throw new TimeoutException();
 209                     }
 210                 }
 211             }
 212         } catch (TimeoutException | InterruptedException e) {
 213             System.err.println("Failed to start a process (thread dump follows)");
 214             for(Map.Entry<Thread, StackTraceElement[]> s : Thread.getAllStackTraces().entrySet()) {
 215                 printStack(s.getKey(), s.getValue());
 216             }
 217 
 218             if (p.isAlive()) {
 219                 p.destroyForcibly();
 220             }
 221 
 222             stdoutTask.cancel(true);
 223             stderrTask.cancel(true);
 224             throw e;
 225         }
 226 
 227         return new ProcessImpl(p, stdoutTask, stderrTask);
 228     }
 229 
 230     /**
 231      * <p>Starts a process from its builder.</p>
 232      * <span>The default redirects of STDOUT and STDERR are started</span>
 233      * <p>
 234      * It is possible to wait for the process to get to a warmed-up state
 235      * via {@linkplain Predicate} condition on the STDOUT. The warm-up will
 236      * wait indefinitely.
 237      * </p>
 238      * @param name The process name
 239      * @param processBuilder The process builder
 240      * @param linePredicate The {@linkplain Predicate} to use on the STDOUT
 241      *                      Used to determine the moment the target app is
 242      *                      properly warmed-up.
 243      *                      It can be null - in that case the warmup is skipped.
 244      * @return Returns the initialized {@linkplain Process}
 245      * @throws IOException
 246      * @throws InterruptedException
 247      * @throws TimeoutException
 248      */
 249     @SuppressWarnings("overloads")
 250     public static Process startProcess(String name,
 251                                        ProcessBuilder processBuilder,
 252                                        final Predicate<String> linePredicate)
 253     throws IOException, InterruptedException, TimeoutException {
 254         return startProcess(name, processBuilder, linePredicate, 0, TimeUnit.SECONDS);
 255     }
 256 
 257     /**
 258      * Get the process id of the current running Java process
 259      *
 260      * @return Process id
 261      */
 262     public static long getProcessId() throws Exception {
 263         return ProcessHandle.current().pid();
 264     }
 265 
 266 
 267 
 268     /**
 269      * Create ProcessBuilder using the java launcher from the jdk to be tested and
 270      * with any platform specific arguments prepended
 271      */
 272     public static ProcessBuilder createJavaProcessBuilder(String... command) {
 273         return createJavaProcessBuilder(false, command);
 274     }
 275 
 276     /**
 277      * Create ProcessBuilder using the java launcher from the jdk to be tested,
 278      * and with any platform specific arguments prepended.
 279      *
 280      * @param addTestVmAndJavaOptions If true, adds test.vm.opts and test.java.opts
 281      *        to the java arguments.
 282      * @param command Arguments to pass to the java command.
 283      * @return The ProcessBuilder instance representing the java command.
 284      */
 285     public static ProcessBuilder createJavaProcessBuilder(boolean addTestVmAndJavaOptions, String... command) {
 286         String javapath = JDKToolFinder.getJDKTool("java");
 287 
 288         ArrayList<String> args = new ArrayList<>();
 289         args.add(javapath);
 290 
 291         args.add("-cp");
 292         args.add(System.getProperty("java.class.path"));
 293 
 294         if (addTestVmAndJavaOptions) {
 295             Collections.addAll(args, Utils.getTestJavaOpts());
 296         }
 297 
 298         Collections.addAll(args, command);
 299 
 300         // Reporting
 301         StringBuilder cmdLine = new StringBuilder();
 302         for (String cmd : args)
 303             cmdLine.append(cmd).append(' ');
 304         System.out.println("Command line: [" + cmdLine.toString() + "]");
 305 
 306         return new ProcessBuilder(args.toArray(new String[args.size()]));
 307     }
 308 
 309     private static void printStack(Thread t, StackTraceElement[] stack) {
 310         System.out.println("\t" +  t +
 311                            " stack: (length = " + stack.length + ")");
 312         if (t != null) {
 313             for (StackTraceElement stack1 : stack) {
 314                 System.out.println("\t" + stack1);
 315             }
 316             System.out.println();
 317         }
 318     }
 319 
 320     /**
 321      * Executes a test jvm process, waits for it to finish and returns the process output.
 322      * The default jvm options from jtreg, test.vm.opts and test.java.opts, are added.
 323      * The java from the test.jdk is used to execute the command.
 324      *
 325      * The command line will be like:
 326      * {test.jdk}/bin/java {test.vm.opts} {test.java.opts} cmds
 327      *
 328      * The jvm process will have exited before this method returns.
 329      *
 330      * @param cmds User specified arguments.
 331      * @return The output from the process.
 332      */
 333     public static OutputAnalyzer executeTestJvm(String... cmds) throws Exception {
 334         ProcessBuilder pb = createJavaProcessBuilder(Utils.addTestJavaOpts(cmds));
 335         return executeProcess(pb);
 336     }
 337 
 338     /**
 339      * @see #executeTestJvm(String...)
 340      * @param cmds User specified arguments.
 341      * @return The output from the process.
 342      */
 343     public static OutputAnalyzer executeTestJava(String... cmds) throws Exception {
 344         return executeTestJvm(cmds);
 345     }
 346 
 347     /**
 348      * Executes a process, waits for it to finish and returns the process output.
 349      * The process will have exited before this method returns.
 350      * @param pb The ProcessBuilder to execute.
 351      * @return The {@linkplain OutputAnalyzer} instance wrapping the process.
 352      */
 353     public static OutputAnalyzer executeProcess(ProcessBuilder pb) throws Exception {
 354         return executeProcess(pb, null);
 355     }
 356 
 357     /**
 358      * Executes a process, pipe some text into its STDIN, waits for it
 359      * to finish and returns the process output. The process will have exited
 360      * before this method returns.
 361      * @param pb The ProcessBuilder to execute.
 362      * @param input The text to pipe into STDIN. Can be null.
 363      * @return The {@linkplain OutputAnalyzer} instance wrapping the process.
 364      */
 365     public static OutputAnalyzer executeProcess(ProcessBuilder pb, String input) throws Exception {
 366         return executeProcess(pb, input, null);
 367     }
 368 
 369     /**
 370      * Executes a process, pipe some text into its STDIN, waits for it
 371      * to finish and returns the process output. The process will have exited
 372      * before this method returns.
 373      * @param pb The ProcessBuilder to execute.
 374      * @param input The text to pipe into STDIN. Can be null.
 375      * @param cs The charset used to convert from bytes to chars or null for
 376      *           the default charset.
 377      * @return The {@linkplain OutputAnalyzer} instance wrapping the process.
 378      */
 379     public static OutputAnalyzer executeProcess(ProcessBuilder pb, String input,
 380             Charset cs) throws Exception {
 381         OutputAnalyzer output = null;
 382         Process p = null;
 383         boolean failed = false;
 384         try {
 385             p = privilegedStart(pb);
 386             if (input != null) {
 387                try (PrintStream ps = new PrintStream(p.getOutputStream())) {
 388                    ps.print(input);
 389                }
 390             }
 391 
 392             output = new OutputAnalyzer(p, cs);
 393             p.waitFor();
 394 
 395             return output;
 396         } catch (Throwable t) {
 397             if (p != null) {
 398                 p.destroyForcibly().waitFor();
 399             }
 400 
 401             failed = true;
 402             System.out.println("executeProcess() failed: " + t);
 403             throw t;
 404         } finally {
 405             if (failed) {
 406                 System.err.println(getProcessLog(pb, output));
 407             }
 408         }
 409     }
 410 
 411     /**
 412      * Executes a process, waits for it to finish and returns the process output.
 413      *
 414      * The process will have exited before this method returns.
 415      *
 416      * @param cmds The command line to execute.
 417      * @return The output from the process.
 418      */
 419     public static OutputAnalyzer executeProcess(String... cmds) throws Throwable {
 420         return executeProcess(new ProcessBuilder(cmds));
 421     }
 422 
 423     /**
 424      * Used to log command line, stdout, stderr and exit code from an executed process.
 425      * @param pb The executed process.
 426      * @param output The output from the process.
 427      */
 428     public static String getProcessLog(ProcessBuilder pb, OutputAnalyzer output) {
 429         String stderr = output == null ? "null" : output.getStderr();
 430         String stdout = output == null ? "null" : output.getStdout();
 431         String exitValue = output == null ? "null": Integer.toString(output.getExitValue());
 432         StringBuilder logMsg = new StringBuilder();
 433         final String nl = System.getProperty("line.separator");
 434         logMsg.append("--- ProcessLog ---" + nl);
 435         logMsg.append("cmd: " + getCommandLine(pb) + nl);
 436         logMsg.append("exitvalue: " + exitValue + nl);
 437         logMsg.append("stderr: " + stderr + nl);
 438         logMsg.append("stdout: " + stdout + nl);
 439 
 440         return logMsg.toString();
 441     }
 442 
 443     /**
 444      * @return The full command line for the ProcessBuilder.
 445      */
 446     public static String getCommandLine(ProcessBuilder pb) {
 447         if (pb == null) {
 448             return "null";
 449         }
 450         StringBuilder cmd = new StringBuilder();
 451         for (String s : pb.command()) {
 452             cmd.append(s).append(" ");
 453         }
 454         return cmd.toString().trim();
 455     }
 456 
 457     /**
 458      * Executes a process, waits for it to finish, prints the process output
 459      * to stdout, and returns the process output.
 460      *
 461      * The process will have exited before this method returns.
 462      *
 463      * @param cmds The command line to execute.
 464      * @return The {@linkplain OutputAnalyzer} instance wrapping the process.
 465      */
 466     public static OutputAnalyzer executeCommand(String... cmds)
 467             throws Throwable {
 468         String cmdLine = Arrays.stream(cmds).collect(Collectors.joining(" "));
 469         System.out.println("Command line: [" + cmdLine + "]");
 470         OutputAnalyzer analyzer = ProcessTools.executeProcess(cmds);
 471         System.out.println(analyzer.getOutput());
 472         return analyzer;
 473     }
 474 
 475     /**
 476      * Executes a process, waits for it to finish, prints the process output
 477      * to stdout and returns the process output.
 478      *
 479      * The process will have exited before this method returns.
 480      *
 481      * @param pb The ProcessBuilder to execute.
 482      * @return The {@linkplain OutputAnalyzer} instance wrapping the process.
 483      */
 484     public static OutputAnalyzer executeCommand(ProcessBuilder pb)
 485             throws Throwable {
 486         String cmdLine = pb.command().stream()
 487                 .map(x -> (x.contains(" ") || x.contains("$"))
 488                         ? ("'" + x + "'") : x)
 489                 .collect(Collectors.joining(" "));
 490         System.out.println("Command line: [" + cmdLine + "]");
 491         OutputAnalyzer analyzer = ProcessTools.executeProcess(pb);
 492         System.out.println(analyzer.getOutput());
 493         return analyzer;
 494     }
 495 
 496     /**
 497      * Helper method to create a process builder for launching native executable
 498      * test that uses/loads JVM.
 499      *
 500      * @param executableName The name of an executable to be launched.
 501      * @param args Arguments for the executable.
 502      * @return New ProcessBuilder instance representing the command.
 503      */
 504     public static ProcessBuilder createNativeTestProcessBuilder(String executableName,
 505                                                                 String... args) throws Exception {
 506         String executable = Paths.get(System.getProperty("test.nativepath"), executableName)
 507             .toAbsolutePath()
 508             .toString();
 509 
 510         ProcessBuilder pb = new ProcessBuilder(executable);
 511         pb.command().addAll(Arrays.asList(args));
 512         addJvmLib(pb);
 513         return pb;
 514     }
 515 
 516     /**
 517      * Adds JVM library path to the native library path.
 518      *
 519      * @param pb ProcessBuilder to be updated with JVM library path.
 520      */
 521     public static void addJvmLib(ProcessBuilder pb) throws Exception {
 522         String jvmLibDir = Platform.jvmLibDir().toString();
 523         String libPathVar = Platform.sharedLibraryPathVariableName();
 524         String currentLibPath = pb.environment().get(libPathVar);
 525 
 526         String newLibPath = jvmLibDir;
 527         if ( (currentLibPath != null) && !currentLibPath.isEmpty() ) {
 528             newLibPath = currentLibPath + File.pathSeparator + jvmLibDir;
 529         }
 530 
 531         pb.environment().put(libPathVar, newLibPath);
 532     }
 533 
 534     private static Process privilegedStart(ProcessBuilder pb) throws IOException {
 535         try {
 536             return AccessController.doPrivileged(
 537                 (PrivilegedExceptionAction<Process>) () -> pb.start());
 538         } catch (PrivilegedActionException e) {
 539             IOException t = (IOException) e.getException();
 540             throw t;
 541         }
 542     }
 543 
 544     private static class ProcessImpl extends Process {
 545 
 546         private final Process p;
 547         private final Future<Void> stdoutTask;
 548         private final Future<Void> stderrTask;
 549 
 550         public ProcessImpl(Process p, Future<Void> stdoutTask, Future<Void> stderrTask) {
 551             this.p = p;
 552             this.stdoutTask = stdoutTask;
 553             this.stderrTask = stderrTask;
 554         }
 555 
 556         @Override
 557         public OutputStream getOutputStream() {
 558             return p.getOutputStream();
 559         }
 560 
 561         @Override
 562         public InputStream getInputStream() {
 563             return p.getInputStream();
 564         }
 565 
 566         @Override
 567         public InputStream getErrorStream() {
 568             return p.getErrorStream();
 569         }
 570 
 571         @Override
 572         public int waitFor() throws InterruptedException {
 573             int rslt = p.waitFor();
 574             waitForStreams();
 575             return rslt;
 576         }
 577 
 578         @Override
 579         public int exitValue() {
 580             return p.exitValue();
 581         }
 582 
 583         @Override
 584         public void destroy() {
 585             p.destroy();
 586         }
 587 
 588         @Override
 589         public long pid() {
 590             return p.pid();
 591         }
 592 
 593         @Override
 594         public boolean isAlive() {
 595             return p.isAlive();
 596         }
 597 
 598         @Override
 599         public Process destroyForcibly() {
 600             return p.destroyForcibly();
 601         }
 602 
 603         @Override
 604         public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
 605             boolean rslt = p.waitFor(timeout, unit);
 606             if (rslt) {
 607                 waitForStreams();
 608             }
 609             return rslt;
 610         }
 611 
 612         private void waitForStreams() throws InterruptedException {
 613             try {
 614                 stdoutTask.get();
 615             } catch (ExecutionException e) {
 616             }
 617             try {
 618                 stderrTask.get();
 619             } catch (ExecutionException e) {
 620             }
 621         }
 622     }
 623 }