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 }