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 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(); 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 } | 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 static Executor of(String... cmdline) { 43 return new Executor().setExecutable(cmdline[0]).addArguments( 44 Arrays.copyOfRange(cmdline, 1, cmdline.length)); 45 } 46 47 public Executor() { 48 saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE)); 49 } 50 51 public Executor setExecutable(String v) { 52 return setExecutable(Path.of(v)); 53 } 54 55 public Executor setExecutable(Path v) { 56 executable = Objects.requireNonNull(v); 57 toolProvider = null; 58 return this; 59 } 60 61 public Executor setToolProvider(ToolProvider v) { 62 toolProvider = Objects.requireNonNull(v); 63 executable = null; 64 return this; 65 } 66 158 159 public String getPrintableCommandLine() { 160 return Executor.this.getPrintableCommandLine(); 161 } 162 163 public Result assertExitCodeIs(int expectedExitCode) { 164 TKit.assertEquals(expectedExitCode, exitCode, String.format( 165 "Check command %s exited with %d code", 166 getPrintableCommandLine(), expectedExitCode)); 167 return this; 168 } 169 170 public Result assertExitCodeIsZero() { 171 return assertExitCodeIs(0); 172 } 173 174 final int exitCode; 175 private List<String> output; 176 } 177 178 public Result executeWithoutExitCodeCheck() { 179 if (toolProvider != null && directory != null) { 180 throw new IllegalArgumentException( 181 "Can't change directory when using tool provider"); 182 } 183 184 return ThrowingSupplier.toSupplier(() -> { 185 if (toolProvider != null) { 186 return runToolProvider(); 187 } 188 189 if (executable != null) { 190 return runExecutable(); 191 } 192 193 throw new IllegalStateException("No command to execute"); 194 }).get(); 195 } 196 197 public Result execute(int expectedCode) { 198 return executeWithoutExitCodeCheck().assertExitCodeIs(expectedCode); 199 } 200 201 public Result execute() { 202 return execute(0); 203 } 204 205 public String executeAndGetFirstLineOfOutput() { 206 return saveFirstLineOfOutput().execute().getFirstLineOfOutput(); 207 } 208 209 public List<String> executeAndGetOutput() { 210 return saveOutput().execute().getOutput(); 211 } 212 213 private boolean withSavedOutput() { 214 return saveOutputType.contains(SaveOutputType.FULL) || saveOutputType.contains( 215 SaveOutputType.FIRST_LINE); 216 } 217 218 private Path executablePath() { 219 if (directory == null 220 || executable.isAbsolute() 221 || !Set.of(".", "..").contains(executable.getName(0).toString())) { 222 return executable; 223 } 224 225 // If relative path to executable is used it seems to be broken when 226 // ProcessBuilder changes the directory. On Windows it changes the 227 // directory first and on Linux it looks up for executable before 228 // changing the directory. So to stay of safe side, use absolute path 229 // to executable. 230 return executable.toAbsolutePath(); 231 } 232 233 private Result runExecutable() throws IOException, InterruptedException { 234 List<String> command = new ArrayList<>(); 235 command.add(executablePath().toString()); 236 command.addAll(args); 237 ProcessBuilder builder = new ProcessBuilder(command); 238 StringBuilder sb = new StringBuilder(getPrintableCommandLine()); 239 if (withSavedOutput()) { 240 builder.redirectErrorStream(true); 241 sb.append("; save output"); 242 } else if (saveOutputType.contains(SaveOutputType.DUMP)) { 243 builder.inheritIO(); 244 sb.append("; inherit I/O"); 245 } else { 246 builder.redirectError(ProcessBuilder.Redirect.DISCARD); 247 builder.redirectOutput(ProcessBuilder.Redirect.DISCARD); 248 sb.append("; discard I/O"); 249 } 250 if (directory != null) { 251 builder.directory(directory.toFile()); 252 sb.append(String.format("; in directory [%s]", directory)); 253 } 254 255 trace("Execute " + sb.toString() + "..."); 256 Process process = builder.start(); 257 258 List<String> outputLines = null; 259 if (withSavedOutput()) { 260 try (BufferedReader outReader = new BufferedReader( 261 new InputStreamReader(process.getInputStream()))) { 262 if (saveOutputType.contains(SaveOutputType.DUMP) 263 || saveOutputType.contains(SaveOutputType.FULL)) { 264 outputLines = outReader.lines().collect(Collectors.toList()); 265 } else { 266 outputLines = Arrays.asList( 267 outReader.lines().findFirst().orElse(null)); 268 } 269 } finally { 270 if (saveOutputType.contains(SaveOutputType.DUMP) && outputLines != null) { 271 outputLines.stream().forEach(System.out::println); 272 if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { 273 // Pick the first line of saved output if there is one 274 for (String line: outputLines) { 275 outputLines = List.of(line); 276 break; 277 } 278 } 279 } 280 } 281 } 282 283 Result reply = new Result(process.waitFor()); 284 trace("Done. Exit code: " + reply.exitCode); 285 286 if (outputLines != null) { 287 reply.output = Collections.unmodifiableList(outputLines); 288 } 289 return reply; 290 } 291 292 private Result runToolProvider(PrintStream out, PrintStream err) { 293 trace("Execute " + getPrintableCommandLine() + "..."); 294 Result reply = new Result(toolProvider.run(out, err, args.toArray( 295 String[]::new))); 296 trace("Done. Exit code: " + reply.exitCode); 297 return reply; 298 } 299 300 301 private Result runToolProvider() throws IOException { 302 if (!withSavedOutput()) { 303 if (saveOutputType.contains(SaveOutputType.DUMP)) { 304 return runToolProvider(System.out, System.err); 305 } 306 307 PrintStream nullPrintStream = new PrintStream(new OutputStream() { 308 @Override 309 public void write(int b) { 310 // Nop 311 } 312 }); 313 return runToolProvider(nullPrintStream, nullPrintStream); 314 } 315 316 try (ByteArrayOutputStream buf = new ByteArrayOutputStream(); 349 if (toolProvider == null && executable == null) { 350 exec = "<null>"; 351 } else if (toolProvider != null) { 352 format = "tool provider " + format; 353 exec = toolProvider.name(); 354 } else { 355 exec = executablePath().toString(); 356 } 357 358 return String.format(format, printCommandLine(exec, args), 359 args.size() + 1); 360 } 361 362 private static String printCommandLine(String executable, List<String> args) { 363 // Want command line printed in a way it can be easily copy/pasted 364 // to be executed manally 365 Pattern regex = Pattern.compile("\\s"); 366 return Stream.concat(Stream.of(executable), args.stream()).map( 367 v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect( 368 Collectors.joining(" ")); 369 } 370 371 private static void trace(String msg) { 372 TKit.trace(String.format("exec: %s", msg)); 373 } 374 375 private ToolProvider toolProvider; 376 private Path executable; 377 private Set<SaveOutputType> saveOutputType; 378 private Path directory; 379 380 private static enum SaveOutputType { 381 NONE, FULL, FIRST_LINE, DUMP 382 }; 383 } |