/* * Copyright (C) 2014 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.tools.subprocess; import com.facebook.tools.ErrorMessage; import com.facebook.tools.io.IO; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; class SubprocessImpl implements Subprocess { private final List<String> command; private final Process process; private final ExecutorService stdoutExecutorService; private final ExecutorService stderrExecutorService; private final String name; private final Output stdout; private final Output stderr; private final Future<?> stdoutFuture; private final Future<?> stderrFuture; private final AtomicBoolean consumedStdout = new AtomicBoolean(false); private final Thread shutdownHook; SubprocessImpl( List<String> command, Process process, IO echo, int outputBytesLimit, boolean streaming ) { this.command = new ArrayList<>(command); this.process = process; StringBuilder name = new StringBuilder(80); Iterator<String> nameIterator = command.iterator(); while (nameIterator.hasNext()) { name.append(nameIterator.next()); if (nameIterator.hasNext()) { name.append(' '); } } this.name = name.toString(); InputStream processInputStream = process.getInputStream(); InputStream processErrorStream = process.getErrorStream(); if (echo != null) { processInputStream = new EchoInputStream(processInputStream, echo.out); processErrorStream = new EchoInputStream(processErrorStream, echo.err); } stdout = new Output(processInputStream, outputBytesLimit, streaming); stderr = new Output(processErrorStream, outputBytesLimit, false); stdoutExecutorService = Executors.newCachedThreadPool(new NamedDaemonThreadFactory(name + "-stdout")); stderrExecutorService = Executors.newCachedThreadPool(new NamedDaemonThreadFactory(name + "-stderr")); stdoutFuture = stdoutExecutorService.submit(stdout); stderrFuture = stderrExecutorService.submit(stderr); shutdownHook = new Thread( new Runnable() { @Override public void run() { //noinspection EmptyTryBlock,UnusedDeclaration try ( InputStream inputStream = process.getInputStream(); OutputStream outputStream = process.getOutputStream(); InputStream errorStream = process.getErrorStream() ) { } catch (IOException | RuntimeException ignored) { } process.destroy(); } } ); Runtime.getRuntime().addShutdownHook(shutdownHook); } @Override public List<String> command() { return command; } @Override public int waitFor() { stdout.background(); try { int result = process.waitFor(); stdoutFuture.get(); stderrFuture.get(); return result; } catch (InterruptedException e) { throw new ErrorMessage(e, "Interrupted while waiting for: %s", name); } catch (ExecutionException e) { throw new ErrorMessage(e, "Error while waiting for: %s", name); } finally { close(); } } @Override public int waitFor(OutputStream out) { try { InputStream in = getStdOut(); byte[] buffer = new byte[4096]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { throw new ErrorMessage(e, "Error while waiting for %s", name); } try { return process.waitFor(); } catch (InterruptedException e) { throw new ErrorMessage(e, "Interrupted while waiting for %s", name); } finally { close(); } } @Override public int waitFor(File outFile) { try (OutputStream out = new FileOutputStream(outFile)) { return waitFor(out); } catch (IOException e) { throw new ErrorMessage(e, "Error saving to %s", outFile); } } @Override public void kill() { //noinspection EmptyTryBlock,UnusedDeclaration try ( Output stdout = this.stdout; Output stderr = this.stderr; InputStream inputStream = process.getInputStream(); OutputStream outputStream = process.getOutputStream(); InputStream errorStream = process.getErrorStream() ) { } catch (IOException | RuntimeException ignored) { } process.destroy(); stdoutFuture.cancel(true); stderrFuture.cancel(true); stdoutExecutorService.shutdownNow(); stderrExecutorService.shutdownNow(); Runtime.getRuntime().removeShutdownHook(shutdownHook); } @Override public void close() { kill(); } @Override public String getOutput() { waitFor(); return new String(stdout.getContent(), StandardCharsets.UTF_8); } @Override public String getError() { waitFor(); return new String(stderr.getContent(), StandardCharsets.UTF_8); } @Override public BufferedInputStream getStream() { return new BufferedInputStream(getStdOut()); } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getStdOut(), StandardCharsets.UTF_8)); } @Override public void background() { stdout.background(); } @Override public void send(String content) { OutputStream outputStream = process.getOutputStream(); try { outputStream.write(content.getBytes(StandardCharsets.UTF_8)); outputStream.flush(); } catch (IOException e) { throw new ErrorMessage(e, "Error while sending content to %s", name); } } @Override public int returnCode() { waitFor(); return process.exitValue(); } @Override public boolean succeeded() { return returnCode() == 0; } @Override public boolean failed() { return returnCode() != 0; } @Override public Iterator<String> iterator() { final BufferedReader reader = getReader(); return new Iterator<String>() { private String line; private boolean pending = false; @Override public boolean hasNext() { if (pending) { return true; } try { line = reader.readLine(); } catch (IOException e) { throw new ErrorMessage(e, "Error while running %s", name); } pending = true; return line != null; } @Override public String next() { if (!hasNext()) { throw new NoSuchElementException(); } pending = false; return line; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } @Override public String toString() { return "SubprocessImpl{" + "name='" + name + '\'' + ", consumedStdout=" + consumedStdout + '}'; } private InputStream getStdOut() { if (!consumedStdout.compareAndSet(false, true)) { throw new IllegalStateException("Already consumed stdout: " + name); } return stdout; } private static class NamedDaemonThreadFactory implements ThreadFactory { private final String name; private NamedDaemonThreadFactory(String name) { this.name = name; } @Override public Thread newThread(Runnable task) { Thread thread = Executors.defaultThreadFactory().newThread(task); thread.setName(name); thread.setDaemon(true); return thread; } } private static class EchoInputStream extends FilterInputStream { private final OutputStream echo; private EchoInputStream(InputStream in, OutputStream echo) { super(in); this.echo = echo; } @Override public int read() throws IOException { int read = in.read(); if (read != -1) { echo.write(read); } return read; } @Override public int read(byte[] result) throws IOException { int read = in.read(result); if (read != -1) { echo.write(result, 0, read); } return read; } @Override public int read(byte[] result, int offset, int length) throws IOException { int read = in.read(result, offset, length); if (read != -1) { echo.write(result, offset, read); } return read; } } }