| /* |
| * Copyright (c) 2013, 2016, 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 toolbox; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.PrintStream; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.EnumMap; |
| import java.util.HashMap; |
| import java.util.Map; |
| import static toolbox.ToolBox.lineSeparator; |
| |
| /** |
| * A utility base class to simplify the implementation of tasks. |
| * Provides support for running the task in a process and for |
| * capturing output written by the task to stdout, stderr and |
| * other writers where applicable. |
| * @param <T> the implementing subclass |
| */ |
| abstract class AbstractTask<T extends AbstractTask<T>> implements Task { |
| protected final ToolBox toolBox; |
| protected final Mode mode; |
| private final Map<OutputKind, String> redirects = new EnumMap<>(OutputKind.class); |
| private final Map<String, String> envVars = new HashMap<>(); |
| private Expect expect = Expect.SUCCESS; |
| int expectedExitCode = 0; |
| |
| /** |
| * Create a task that will execute in the specified mode. |
| * @param mode the mode |
| */ |
| protected AbstractTask(ToolBox tb, Mode mode) { |
| toolBox = tb; |
| this.mode = mode; |
| } |
| |
| /** |
| * Sets the expected outcome of the task and calls {@code run()}. |
| * @param expect the expected outcome |
| * @return the result of calling {@code run()} |
| */ |
| public Result run(Expect expect) { |
| expect(expect, Integer.MIN_VALUE); |
| return run(); |
| } |
| |
| /** |
| * Sets the expected outcome of the task and calls {@code run()}. |
| * @param expect the expected outcome |
| * @param exitCode the expected exit code if the expected outcome |
| * is {@code FAIL} |
| * @return the result of calling {@code run()} |
| */ |
| public Result run(Expect expect, int exitCode) { |
| expect(expect, exitCode); |
| return run(); |
| } |
| |
| /** |
| * Sets the expected outcome and expected exit code of the task. |
| * The exit code will not be checked if the outcome is |
| * {@code Expect.SUCCESS} or if the exit code is set to |
| * {@code Integer.MIN_VALUE}. |
| * @param expect the expected outcome |
| * @param exitCode the expected exit code |
| */ |
| protected void expect(Expect expect, int exitCode) { |
| this.expect = expect; |
| this.expectedExitCode = exitCode; |
| } |
| |
| /** |
| * Checks the exit code contained in a {@code Result} against the |
| * expected outcome and exit value |
| * @param result the result object |
| * @return the result object |
| * @throws TaskError if the exit code stored in the result object |
| * does not match the expected outcome and exit code. |
| */ |
| protected Result checkExit(Result result) throws TaskError { |
| switch (expect) { |
| case SUCCESS: |
| if (result.exitCode != 0) { |
| result.writeAll(); |
| throw new TaskError("Task " + name() + " failed: rc=" + result.exitCode); |
| } |
| break; |
| |
| case FAIL: |
| if (result.exitCode == 0) { |
| result.writeAll(); |
| throw new TaskError("Task " + name() + " succeeded unexpectedly"); |
| } |
| |
| if (expectedExitCode != Integer.MIN_VALUE |
| && result.exitCode != expectedExitCode) { |
| result.writeAll(); |
| throw new TaskError("Task " + name() + "failed with unexpected exit code " |
| + result.exitCode + ", expected " + expectedExitCode); |
| } |
| break; |
| } |
| return result; |
| } |
| |
| /** |
| * Sets an environment variable to be used by this task. |
| * @param name the name of the environment variable |
| * @param value the value for the environment variable |
| * @return this task object |
| * @throws IllegalStateException if the task mode is not {@code EXEC} |
| */ |
| public T envVar(String name, String value) { |
| if (mode != Mode.EXEC) |
| throw new IllegalStateException(); |
| envVars.put(name, value); |
| return (T) this; |
| } |
| |
| /** |
| * Redirects output from an output stream to a file. |
| * @param outputKind the name of the stream to be redirected. |
| * @param path the file |
| * @return this task object |
| * @throws IllegalStateException if the task mode is not {@code EXEC} |
| */ |
| public T redirect(OutputKind outputKind, String path) { |
| if (mode != Mode.EXEC) |
| throw new IllegalStateException(); |
| redirects.put(outputKind, path); |
| return (T) this; |
| } |
| |
| /** |
| * Returns a {@code ProcessBuilder} initialized with any |
| * redirects and environment variables that have been set. |
| * @return a {@code ProcessBuilder} |
| */ |
| protected ProcessBuilder getProcessBuilder() { |
| if (mode != Mode.EXEC) |
| throw new IllegalStateException(); |
| ProcessBuilder pb = new ProcessBuilder(); |
| if (redirects.get(OutputKind.STDOUT) != null) |
| pb.redirectOutput(new File(redirects.get(OutputKind.STDOUT))); |
| if (redirects.get(OutputKind.STDERR) != null) |
| pb.redirectError(new File(redirects.get(OutputKind.STDERR))); |
| pb.environment().putAll(envVars); |
| return pb; |
| } |
| |
| /** |
| * Collects the output from a process and saves it in a {@code Result}. |
| * @param tb the {@code ToolBox} containing the task {@code t} |
| * @param t the task initiating the process |
| * @param p the process |
| * @return a Result object containing the output from the process and its |
| * exit value. |
| * @throws InterruptedException if the thread is interrupted |
| */ |
| protected Result runProcess(ToolBox tb, Task t, Process p) throws InterruptedException { |
| if (mode != Mode.EXEC) |
| throw new IllegalStateException(); |
| ProcessOutput sysOut = new ProcessOutput(p.getInputStream()).start(); |
| ProcessOutput sysErr = new ProcessOutput(p.getErrorStream()).start(); |
| sysOut.waitUntilDone(); |
| sysErr.waitUntilDone(); |
| int rc = p.waitFor(); |
| Map<OutputKind, String> outputMap = new EnumMap<>(OutputKind.class); |
| outputMap.put(OutputKind.STDOUT, sysOut.getOutput()); |
| outputMap.put(OutputKind.STDERR, sysErr.getOutput()); |
| return checkExit(new Result(toolBox, t, rc, outputMap)); |
| } |
| |
| /** |
| * Thread-friendly class to read the output from a process until the stream |
| * is exhausted. |
| */ |
| static class ProcessOutput implements Runnable { |
| ProcessOutput(InputStream from) { |
| in = new BufferedReader(new InputStreamReader(from)); |
| out = new StringBuilder(); |
| } |
| |
| ProcessOutput start() { |
| new Thread(this).start(); |
| return this; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| String line; |
| while ((line = in.readLine()) != null) { |
| out.append(line).append(lineSeparator); |
| } |
| } catch (IOException e) { |
| } |
| synchronized (this) { |
| done = true; |
| notifyAll(); |
| } |
| } |
| |
| synchronized void waitUntilDone() throws InterruptedException { |
| boolean interrupted = false; |
| |
| // poll interrupted flag, while waiting for copy to complete |
| while (!(interrupted = Thread.interrupted()) && !done) |
| wait(1000); |
| |
| if (interrupted) |
| throw new InterruptedException(); |
| } |
| |
| String getOutput() { |
| return out.toString(); |
| } |
| |
| private final BufferedReader in; |
| private final StringBuilder out; |
| private boolean done; |
| } |
| |
| /** |
| * Utility class to simplify the handling of temporarily setting a |
| * new stream for System.out or System.err. |
| */ |
| static class StreamOutput { |
| // Functional interface to set a stream. |
| // Expected use: System::setOut, System::setErr |
| interface Initializer { |
| void set(PrintStream s); |
| } |
| |
| private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| private final PrintStream ps = new PrintStream(baos); |
| private final PrintStream prev; |
| private final Initializer init; |
| |
| StreamOutput(PrintStream s, Initializer init) { |
| prev = s; |
| init.set(ps); |
| this.init = init; |
| } |
| |
| /** |
| * Closes the stream and returns the contents that were written to it. |
| * @return the contents that were written to it. |
| */ |
| String close() { |
| init.set(prev); |
| ps.close(); |
| return baos.toString(); |
| } |
| } |
| |
| /** |
| * Utility class to simplify the handling of creating an in-memory PrintWriter. |
| */ |
| static class WriterOutput { |
| private final StringWriter sw = new StringWriter(); |
| final PrintWriter pw = new PrintWriter(sw); |
| |
| /** |
| * Closes the stream and returns the contents that were written to it. |
| * @return the contents that were written to it. |
| */ |
| String close() { |
| pw.close(); |
| return sw.toString(); |
| } |
| } |
| } |