/* * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.jpackage.test; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.StringReader; import java.nio.file.Path; import java.util.*; import java.util.regex.Pattern; import java.util.spi.ToolProvider; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.test.Functional.ThrowingSupplier; public final class Executor extends CommandArguments { public Executor() { saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE)); } public Executor setExecutable(String v) { return setExecutable(Path.of(v)); } public Executor setExecutable(Path v) { executable = Objects.requireNonNull(v); toolProvider = null; return this; } public Executor setToolProvider(ToolProvider v) { toolProvider = Objects.requireNonNull(v); executable = null; return this; } public Executor setToolProvider(JavaTool v) { return setToolProvider(v.asToolProvider()); } public Executor setDirectory(Path v) { directory = v; return this; } public Executor setExecutable(JavaTool v) { return setExecutable(v.getPath()); } /** * Configures this instance to save full output that command will produce. * This function is mutual exclusive with * saveFirstLineOfOutput() function. * * @return this */ public Executor saveOutput() { saveOutputType.remove(SaveOutputType.FIRST_LINE); saveOutputType.add(SaveOutputType.FULL); return this; } /** * Configures how to save output that command will produce. If * v is true, the function call is equivalent to * saveOutput() call. If v is false, * the function will result in not preserving command output. * * @return this */ public Executor saveOutput(boolean v) { if (v) { saveOutput(); } else { saveOutputType.remove(SaveOutputType.FIRST_LINE); saveOutputType.remove(SaveOutputType.FULL); } return this; } /** * Configures this instance to save only the first line out output that * command will produce. This function is mutual exclusive with * saveOutput() function. * * @return this */ public Executor saveFirstLineOfOutput() { saveOutputType.add(SaveOutputType.FIRST_LINE); saveOutputType.remove(SaveOutputType.FULL); return this; } /** * Configures this instance to dump all output that command will produce to * System.out and System.err. Can be used together with saveOutput() and * saveFirstLineOfOutput() to save command output and also copy it in the * default output streams. * * @return this */ public Executor dumpOutput() { return dumpOutput(true); } public Executor dumpOutput(boolean v) { if (v) { saveOutputType.add(SaveOutputType.DUMP); } else { saveOutputType.remove(SaveOutputType.DUMP); } return this; } public class Result { Result(int exitCode) { this.exitCode = exitCode; } public String getFirstLineOfOutput() { return output.get(0); } public List getOutput() { return output; } public String getPrintableCommandLine() { return Executor.this.getPrintableCommandLine(); } public Result assertExitCodeIs(int expectedExitCode) { TKit.assertEquals(expectedExitCode, exitCode, String.format( "Check command %s exited with %d code", getPrintableCommandLine(), expectedExitCode)); return this; } public Result assertExitCodeIsZero() { return assertExitCodeIs(0); } final int exitCode; private List output; } public Result execute() { if (toolProvider != null && directory != null) { throw new IllegalArgumentException( "Can't change directory when using tool provider"); } return ThrowingSupplier.toSupplier(() -> { if (toolProvider != null) { return runToolProvider(); } if (executable != null) { return runExecutable(); } throw new IllegalStateException("No command to execute"); }).get(); } public String executeAndGetFirstLineOfOutput() { return saveFirstLineOfOutput().execute().assertExitCodeIsZero().getFirstLineOfOutput(); } public List executeAndGetOutput() { return saveOutput().execute().assertExitCodeIsZero().getOutput(); } private boolean withSavedOutput() { return saveOutputType.contains(SaveOutputType.FULL) || saveOutputType.contains( SaveOutputType.FIRST_LINE); } private Path executablePath() { if (directory == null || executable.isAbsolute()) { return executable; } // If relative path to executable is used it seems to be broken when // ProcessBuilder changes the directory. On Windows it changes the // directory first and on Linux it looks up for executable before // changing the directory. So to stay of safe side, use absolute path // to executable. return executable.toAbsolutePath(); } private Result runExecutable() throws IOException, InterruptedException { List command = new ArrayList<>(); command.add(executablePath().toString()); command.addAll(args); ProcessBuilder builder = new ProcessBuilder(command); StringBuilder sb = new StringBuilder(getPrintableCommandLine()); if (withSavedOutput()) { builder.redirectErrorStream(true); sb.append("; save output"); } else if (saveOutputType.contains(SaveOutputType.DUMP)) { builder.inheritIO(); sb.append("; inherit I/O"); } else { builder.redirectError(ProcessBuilder.Redirect.DISCARD); builder.redirectOutput(ProcessBuilder.Redirect.DISCARD); sb.append("; discard I/O"); } if (directory != null) { builder.directory(directory.toFile()); sb.append(String.format("; in directory [%s]", directory)); } TKit.trace("Execute " + sb.toString() + "..."); Process process = builder.start(); List outputLines = null; if (withSavedOutput()) { try (BufferedReader outReader = new BufferedReader( new InputStreamReader(process.getInputStream()))) { if (saveOutputType.contains(SaveOutputType.DUMP) || saveOutputType.contains(SaveOutputType.FULL)) { outputLines = outReader.lines().collect(Collectors.toList()); } else { outputLines = Arrays.asList( outReader.lines().findFirst().orElse(null)); } } finally { if (saveOutputType.contains(SaveOutputType.DUMP) && outputLines != null) { outputLines.stream().forEach(System.out::println); if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { // Pick the first line of saved output if there is one for (String line: outputLines) { outputLines = List.of(line); break; } } } } } Result reply = new Result(process.waitFor()); TKit.trace("Done. Exit code: " + reply.exitCode); if (outputLines != null) { reply.output = Collections.unmodifiableList(outputLines); } return reply; } private Result runToolProvider(PrintStream out, PrintStream err) { TKit.trace("Execute " + getPrintableCommandLine() + "..."); Result reply = new Result(toolProvider.run(out, err, args.toArray( String[]::new))); TKit.trace("Done. Exit code: " + reply.exitCode); return reply; } private Result runToolProvider() throws IOException { if (!withSavedOutput()) { if (saveOutputType.contains(SaveOutputType.DUMP)) { return runToolProvider(System.out, System.err); } PrintStream nullPrintStream = new PrintStream(new OutputStream() { @Override public void write(int b) { // Nop } }); return runToolProvider(nullPrintStream, nullPrintStream); } try (ByteArrayOutputStream buf = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(buf)) { Result reply = runToolProvider(ps, ps); ps.flush(); try (BufferedReader bufReader = new BufferedReader(new StringReader( buf.toString()))) { if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { String firstLine = bufReader.lines().findFirst().orElse(null); if (firstLine != null) { reply.output = List.of(firstLine); } } else if (saveOutputType.contains(SaveOutputType.FULL)) { reply.output = bufReader.lines().collect( Collectors.toUnmodifiableList()); } if (saveOutputType.contains(SaveOutputType.DUMP)) { Stream lines; if (saveOutputType.contains(SaveOutputType.FULL)) { lines = reply.output.stream(); } else { lines = bufReader.lines(); } lines.forEach(System.out::println); } } return reply; } } public String getPrintableCommandLine() { final String exec; String format = "[%s](%d)"; if (toolProvider == null && executable == null) { exec = ""; } else if (toolProvider != null) { format = "tool provider " + format; exec = toolProvider.name(); } else { exec = executablePath().toString(); } return String.format(format, printCommandLine(exec, args), args.size() + 1); } private static String printCommandLine(String executable, List args) { // Want command line printed in a way it can be easily copy/pasted // to be executed manally Pattern regex = Pattern.compile("\\s"); return Stream.concat(Stream.of(executable), args.stream()).map( v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect( Collectors.joining(" ")); } private ToolProvider toolProvider; private Path executable; private Set saveOutputType; private Path directory; private static enum SaveOutputType { NONE, FULL, FIRST_LINE, DUMP }; }