1 /*
   2  * Copyright (c) 2019, 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 package jdk.jpackage.test;
  24 
  25 import java.io.BufferedReader;
  26 import java.io.ByteArrayOutputStream;
  27 import java.io.IOException;
  28 import java.io.InputStreamReader;
  29 import java.io.OutputStream;
  30 import java.io.PrintStream;
  31 import java.io.StringReader;
  32 import java.nio.file.Path;
  33 import java.util.*;
  34 import java.util.regex.Pattern;
  35 import java.util.spi.ToolProvider;
  36 import java.util.stream.Collectors;
  37 import java.util.stream.Stream;
  38 import jdk.jpackage.test.Functional.ThrowingSupplier;
  39 
  40 public final class Executor extends CommandArguments<Executor> {
  41 
  42     public Executor() {
  43         saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE));
  44     }
  45 
  46     public Executor setExecutable(String v) {
  47         return setExecutable(Path.of(v));
  48     }
  49 
  50     public Executor setExecutable(Path v) {
  51         executable = Objects.requireNonNull(v);
  52         toolProvider = null;
  53         return this;
  54     }
  55 
  56     public Executor setToolProvider(ToolProvider v) {
  57         toolProvider = Objects.requireNonNull(v);
  58         executable = null;
  59         return this;
  60     }
  61 
  62     public Executor setToolProvider(JavaTool v) {
  63         return setToolProvider(v.asToolProvider());
  64     }
  65 
  66     public Executor setDirectory(Path v) {
  67         directory = v;
  68         return this;
  69     }
  70 
  71     public Executor setExecutable(JavaTool v) {
  72         return setExecutable(v.getPath());
  73     }
  74 
  75     /**
  76      * Configures this instance to save full output that command will produce.
  77      * This function is mutual exclusive with
  78      * saveFirstLineOfOutput() function.
  79      *
  80      * @return this
  81      */
  82     public Executor saveOutput() {
  83         saveOutputType.remove(SaveOutputType.FIRST_LINE);
  84         saveOutputType.add(SaveOutputType.FULL);
  85         return this;
  86     }
  87 
  88     /**
  89      * Configures how to save output that command will produce. If
  90      * <code>v</code> is <code>true</code>, the function call is equivalent to
  91      * <code>saveOutput()</code> call. If <code>v</code> is <code>false</code>,
  92      * the function will result in not preserving command output.
  93      *
  94      * @return this
  95      */
  96     public Executor saveOutput(boolean v) {
  97         if (v) {
  98             saveOutput();
  99         } else {
 100             saveOutputType.remove(SaveOutputType.FIRST_LINE);
 101             saveOutputType.remove(SaveOutputType.FULL);
 102         }
 103         return this;
 104     }
 105 
 106     /**
 107      * Configures this instance to save only the first line out output that
 108      * command will produce. This function is mutual exclusive with
 109      * saveOutput() function.
 110      *
 111      * @return this
 112      */
 113     public Executor saveFirstLineOfOutput() {
 114         saveOutputType.add(SaveOutputType.FIRST_LINE);
 115         saveOutputType.remove(SaveOutputType.FULL);
 116         return this;
 117     }
 118 
 119     /**
 120      * Configures this instance to dump all output that command will produce to
 121      * System.out and System.err. Can be used together with saveOutput() and
 122      * saveFirstLineOfOutput() to save command output and also copy it in the
 123      * default output streams.
 124      *
 125      * @return this
 126      */
 127     public Executor dumpOutput() {
 128         return dumpOutput(true);
 129     }
 130 
 131     public Executor dumpOutput(boolean v) {
 132         if (v) {
 133             saveOutputType.add(SaveOutputType.DUMP);
 134         } else {
 135             saveOutputType.remove(SaveOutputType.DUMP);
 136         }
 137         return this;
 138     }
 139 
 140     public class Result {
 141 
 142         Result(int exitCode) {
 143             this.exitCode = exitCode;
 144         }
 145 
 146         public String getFirstLineOfOutput() {
 147             return output.get(0);
 148         }
 149 
 150         public List<String> getOutput() {
 151             return output;
 152         }
 153 
 154         public String getPrintableCommandLine() {
 155             return Executor.this.getPrintableCommandLine();
 156         }
 157 
 158         public Result assertExitCodeIs(int expectedExitCode) {
 159             TKit.assertEquals(expectedExitCode, exitCode, String.format(
 160                     "Check command %s exited with %d code",
 161                     getPrintableCommandLine(), expectedExitCode));
 162             return this;
 163         }
 164 
 165         public Result assertExitCodeIsZero() {
 166             return assertExitCodeIs(0);
 167         }
 168 
 169         final int exitCode;
 170         private List<String> output;
 171     }
 172 
 173     public Result execute() {
 174         if (toolProvider != null && directory != null) {
 175             throw new IllegalArgumentException(
 176                     "Can't change directory when using tool provider");
 177         }
 178 
 179         return ThrowingSupplier.toSupplier(() -> {
 180             if (toolProvider != null) {
 181                 return runToolProvider();
 182             }
 183 
 184             if (executable != null) {
 185                 return runExecutable();
 186             }
 187 
 188             throw new IllegalStateException("No command to execute");
 189         }).get();
 190     }
 191 
 192     public String executeAndGetFirstLineOfOutput() {
 193         return saveFirstLineOfOutput().execute().assertExitCodeIsZero().getFirstLineOfOutput();
 194     }
 195 
 196     public List<String> executeAndGetOutput() {
 197         return saveOutput().execute().assertExitCodeIsZero().getOutput();
 198     }
 199 
 200     private boolean withSavedOutput() {
 201         return saveOutputType.contains(SaveOutputType.FULL) || saveOutputType.contains(
 202                 SaveOutputType.FIRST_LINE);
 203     }
 204 
 205     private Path executablePath() {
 206         if (directory == null || executable.isAbsolute()) {
 207             return executable;
 208         }
 209 
 210         // If relative path to executable is used it seems to be broken when
 211         // ProcessBuilder changes the directory. On Windows it changes the
 212         // directory first and on Linux it looks up for executable before
 213         // changing the directory. So to stay of safe side, use absolute path
 214         // to executable.
 215         return executable.toAbsolutePath();
 216     }
 217 
 218     private Result runExecutable() throws IOException, InterruptedException {
 219         List<String> command = new ArrayList<>();
 220         command.add(executablePath().toString());
 221         command.addAll(args);
 222         ProcessBuilder builder = new ProcessBuilder(command);
 223         StringBuilder sb = new StringBuilder(getPrintableCommandLine());
 224         if (withSavedOutput()) {
 225             builder.redirectErrorStream(true);
 226             sb.append("; save output");
 227         } else if (saveOutputType.contains(SaveOutputType.DUMP)) {
 228             builder.inheritIO();
 229             sb.append("; inherit I/O");
 230         } else {
 231             builder.redirectError(ProcessBuilder.Redirect.DISCARD);
 232             builder.redirectOutput(ProcessBuilder.Redirect.DISCARD);
 233             sb.append("; discard I/O");
 234         }
 235         if (directory != null) {
 236             builder.directory(directory.toFile());
 237             sb.append(String.format("; in directory [%s]", directory));
 238         }
 239 
 240         TKit.trace("Execute " + sb.toString() + "...");
 241         Process process = builder.start();
 242 
 243         List<String> outputLines = null;
 244         if (withSavedOutput()) {
 245             try (BufferedReader outReader = new BufferedReader(
 246                     new InputStreamReader(process.getInputStream()))) {
 247                 if (saveOutputType.contains(SaveOutputType.DUMP)
 248                         || saveOutputType.contains(SaveOutputType.FULL)) {
 249                     outputLines = outReader.lines().collect(Collectors.toList());
 250                 } else {
 251                     outputLines = Arrays.asList(
 252                             outReader.lines().findFirst().orElse(null));
 253                 }
 254             } finally {
 255                 if (saveOutputType.contains(SaveOutputType.DUMP) && outputLines != null) {
 256                     outputLines.stream().forEach(System.out::println);
 257                     if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) {
 258                         // Pick the first line of saved output if there is one
 259                         for (String line: outputLines) {
 260                             outputLines = List.of(line);
 261                             break;
 262                         }
 263                     }
 264                 }
 265             }
 266         }
 267 
 268         Result reply = new Result(process.waitFor());
 269         TKit.trace("Done. Exit code: " + reply.exitCode);
 270 
 271         if (outputLines != null) {
 272             reply.output = Collections.unmodifiableList(outputLines);
 273         }
 274         return reply;
 275     }
 276 
 277     private Result runToolProvider(PrintStream out, PrintStream err) {
 278         TKit.trace("Execute " + getPrintableCommandLine() + "...");
 279         Result reply = new Result(toolProvider.run(out, err, args.toArray(
 280                 String[]::new)));
 281         TKit.trace("Done. Exit code: " + reply.exitCode);
 282         return reply;
 283     }
 284 
 285 
 286     private Result runToolProvider() throws IOException {
 287         if (!withSavedOutput()) {
 288             if (saveOutputType.contains(SaveOutputType.DUMP)) {
 289                 return runToolProvider(System.out, System.err);
 290             }
 291 
 292             PrintStream nullPrintStream = new PrintStream(new OutputStream() {
 293                 @Override
 294                 public void write(int b) {
 295                     // Nop
 296                 }
 297             });
 298             return runToolProvider(nullPrintStream, nullPrintStream);
 299         }
 300 
 301         try (ByteArrayOutputStream buf = new ByteArrayOutputStream();
 302                 PrintStream ps = new PrintStream(buf)) {
 303             Result reply = runToolProvider(ps, ps);
 304             ps.flush();
 305             try (BufferedReader bufReader = new BufferedReader(new StringReader(
 306                     buf.toString()))) {
 307                 if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) {
 308                     String firstLine = bufReader.lines().findFirst().orElse(null);
 309                     if (firstLine != null) {
 310                         reply.output = List.of(firstLine);
 311                     }
 312                 } else if (saveOutputType.contains(SaveOutputType.FULL)) {
 313                     reply.output = bufReader.lines().collect(
 314                             Collectors.toUnmodifiableList());
 315                 }
 316 
 317                 if (saveOutputType.contains(SaveOutputType.DUMP)) {
 318                     Stream<String> lines;
 319                     if (saveOutputType.contains(SaveOutputType.FULL)) {
 320                         lines = reply.output.stream();
 321                     } else {
 322                         lines = bufReader.lines();
 323                     }
 324                     lines.forEach(System.out::println);
 325                 }
 326             }
 327             return reply;
 328         }
 329     }
 330 
 331     public String getPrintableCommandLine() {
 332         final String exec;
 333         String format = "[%s](%d)";
 334         if (toolProvider == null && executable == null) {
 335             exec = "<null>";
 336         } else if (toolProvider != null) {
 337             format = "tool provider " + format;
 338             exec = toolProvider.name();
 339         } else {
 340             exec = executablePath().toString();
 341         }
 342 
 343         return String.format(format, printCommandLine(exec, args),
 344                 args.size() + 1);
 345     }
 346 
 347     private static String printCommandLine(String executable, List<String> args) {
 348         // Want command line printed in a way it can be easily copy/pasted
 349         // to be executed manally
 350         Pattern regex = Pattern.compile("\\s");
 351         return Stream.concat(Stream.of(executable), args.stream()).map(
 352                 v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect(
 353                         Collectors.joining(" "));
 354     }
 355 
 356     private ToolProvider toolProvider;
 357     private Path executable;
 358     private Set<SaveOutputType> saveOutputType;
 359     private Path directory;
 360 
 361     private static enum SaveOutputType {
 362         NONE, FULL, FIRST_LINE, DUMP
 363     };
 364 }