/*
* Copyright 2015-present 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.buck.util;
import com.facebook.buck.log.Logger;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.zaxxer.nuprocess.NuAbstractProcessHandler;
import com.zaxxer.nuprocess.NuProcess;
import com.zaxxer.nuprocess.NuProcessBuilder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/**
* Replacement for {@link ProcessBuilder} which provides an asynchronous callback interface to
* notify the caller on a background thread when a process starts, performs I/O, or exits.
*
* <p>Unlike {@link ProcessExecutor}, this does not automatically forward output to {@link Console}
* formatted with ANSI escapes.
*/
public class ListeningProcessExecutor {
private static final Logger LOG = Logger.get(ListeningProcessExecutor.class);
/**
* Callback API to notify the caller on a background thread when a process starts, exits, has
* stdout or stderr bytes to read, or is ready to receive bytes on stdin.
*/
public interface ProcessListener {
/** Called just after the process starts. */
void onStart(LaunchedProcess process);
/** Called just after the process exits. */
void onExit(int exitCode);
/**
* Called when the process writes bytes to stdout.
*
* <p>Before this method returns, you must set {@code buffer.position()} to indicate how many
* bytes you have consumed.
*
* <p>If you do not consume the entire buffer, any remaining bytes will be passed back to you
* upon the next invocation (for example, when implementing a UTF-8 decoder which might contain
* a byte sequence spanning multiple reads).
*
* <p>If {@code closed} is {@code true}, then stdout has been closed and no more bytes will be
* received.
*/
void onStdout(ByteBuffer buffer, boolean closed);
/**
* Called when the process writes bytes to stderr.
*
* <p>Before this method returns, you must set {@code buffer.position()} to indicate how many
* bytes you have consumed.
*
* <p>If you do not consume the entire buffer, any remaining bytes will be passed back to you
* upon the next invocation (for example, when implementing a UTF-8 decoder which might contain
* a byte sequence spanning multiple reads).
*
* <p>If {@code closed} is {@code true}, then stdout has been closed and no more bytes will be
* received.
*/
void onStderr(ByteBuffer buffer, boolean closed);
/**
* Called when the process is ready to receive bytes on stdin.
*
* <p>Before this method returns, you must set the {@code buffer}'s {@link ByteBuffer#position()
* position} and {@link ByteBuffer#limit() limit} (for example, by invoking {@link
* ByteBuffer#flip()}) to indicate how much data is in the buffer before returning from this
* method.
*
* <p>You must first call {@link LaunchedProcess#wantWrite()} at least once before this method
* will be invoked.
*
* <p>If not all of the data needed to be written will fit in {@code buffer}, you can return
* {@code true} to indicate that you would like to write more data.
*
* <p>Otherwise, return {@code false} if you have no more data to write to stdin. (You can
* always invoke {@link LaunchedProcess#wantWrite()} any time in the future.
*/
boolean onStdinReady(ByteBuffer buffer);
}
/** Represents a process which was launched by a {@link ListeningProcessExecutor}. */
public interface LaunchedProcess {
/** The capacity of each I/O buffer, in bytes. */
int BUFFER_CAPACITY = NuProcess.BUFFER_CAPACITY;
/**
* Invoke this to indicate you wish to write to the launched process's stdin.
*
* <p>Your {@link ProcessListener#onStdinReady(ByteBuffer)} method will be invoked
* asynchronously when the process is ready to receive data on stdin.
*/
void wantWrite();
/**
* Invoke this to directly write data to the launched process's stdin. This method does not
* block, and will enqueue the buffer to be written to the launched process's stdin at a later
* date.
*
* <p>If you need to be notified when the write to stdin completes, use {@link #wantWrite()} and
* {@link ProcessListener#onStdinReady(ByteBuffer)} instead.
*/
void writeStdin(ByteBuffer buffer);
/**
* Closes the stdin of the process. Call this if the process expects stdin to be closed before
* it writes to stdout.
*
* <p>If {@code force} is {@code true}, then pending writes to stdin are discarded. Otherwise,
* waits for pending writes to flush, then closes stdin.
*/
void closeStdin(boolean force);
/**
* Returns {@code true} if the process has any data queued to write to stdin, {@code false}
* otherwise.
*/
boolean hasPendingWrites();
/** Returns {@code true} if the process is running, {@code false} otherwise. */
boolean isRunning();
}
private static class LaunchedProcessImpl implements LaunchedProcess {
public final NuProcess nuProcess;
public final ProcessExecutorParams params;
public LaunchedProcessImpl(NuProcess nuProcess, ProcessExecutorParams params) {
this.nuProcess = nuProcess;
this.params = params;
}
@Override
public void wantWrite() {
nuProcess.wantWrite();
}
@Override
public void writeStdin(ByteBuffer buffer) {
nuProcess.writeStdin(buffer);
}
@Override
public void closeStdin(boolean force) {
nuProcess.closeStdin(force);
}
@Override
public boolean hasPendingWrites() {
return nuProcess.hasPendingWrites();
}
@Override
public boolean isRunning() {
return nuProcess.isRunning();
}
}
private static class ListeningProcessHandler extends NuAbstractProcessHandler {
private final ProcessExecutorParams params;
private final ProcessListener listener;
@Nullable public LaunchedProcessImpl process;
public ListeningProcessHandler(ProcessListener listener, ProcessExecutorParams params) {
this.listener = listener;
this.params = params;
}
@Override
public void onPreStart(NuProcess process) {
this.process = new LaunchedProcessImpl(process, params);
}
@Override
public void onStart(NuProcess process) {
Preconditions.checkNotNull(this.process);
Preconditions.checkState(this.process.nuProcess == process);
listener.onStart(this.process);
}
@Override
public void onExit(int exitCode) {
listener.onExit(exitCode);
}
@Override
public void onStdout(ByteBuffer buffer, boolean closed) {
listener.onStdout(buffer, closed);
}
@Override
public void onStderr(ByteBuffer buffer, boolean closed) {
listener.onStderr(buffer, closed);
}
@Override
public boolean onStdinReady(ByteBuffer buffer) {
return listener.onStdinReady(buffer);
}
}
private final ProcessRegistry processRegistry;
public ListeningProcessExecutor() {
processRegistry = ProcessRegistry.getInstance();
}
/**
* Launches a process and asynchronously sends notifications to {@code listener} on a background
* thread when the process starts, has I/O, or exits.
*/
public LaunchedProcess launchProcess(ProcessExecutorParams params, final ProcessListener listener)
throws IOException {
LOG.debug("Launching process with params %s", params);
ListeningProcessHandler processHandler = new ListeningProcessHandler(listener, params);
// Unlike with Java's ProcessBuilder, we don't need special param escaping for Win32 platforms.
NuProcessBuilder processBuilder = new NuProcessBuilder(processHandler, params.getCommand());
if (params.getEnvironment().isPresent()) {
processBuilder.environment().clear();
processBuilder.environment().putAll(params.getEnvironment().get());
}
if (params.getDirectory().isPresent()) {
processBuilder.setCwd(params.getDirectory().get());
}
NuProcess process = BgProcessKiller.startProcess(processBuilder);
if (process == null) {
throw new IOException(String.format("Could not start process with params %s", params));
}
LOG.debug("Successfully launched process %s", process);
// This should be set by onPreStart().
Preconditions.checkState(processHandler.process != null);
processRegistry.registerProcess(processHandler.process.nuProcess, params, ImmutableMap.of());
return processHandler.process;
}
/**
* Blocks the calling thread until either the process exits or the timeout expires, whichever is
* first.
*
* @return {@code Integer.MIN_VALUE} if the timeout expired or the process failed to start, or the
* exit code of the process otherwise.
*/
public int waitForProcess(LaunchedProcess process, long timeout, TimeUnit timeUnit)
throws InterruptedException {
LOG.debug("Waiting for process %s timeout %d %s", process, timeout, timeUnit);
Preconditions.checkArgument(process instanceof LaunchedProcessImpl);
LaunchedProcessImpl processImpl = (LaunchedProcessImpl) process;
int exitCode = processImpl.nuProcess.waitFor(timeout, timeUnit);
LOG.debug("Wait for process returned %d", exitCode);
return exitCode;
}
/**
* Blocks the calling thread until the process exits.
*
* @return the exit code of the process.
* @throws IOException if the process failed to start.
*/
public int waitForProcess(LaunchedProcess process) throws InterruptedException, IOException {
long infiniteWait = 0;
int exitCode = waitForProcess(process, infiniteWait, TimeUnit.SECONDS);
if (exitCode == Integer.MIN_VALUE) {
// Specifying 0 for timeout guarantees that the wait will not time out. This way we know that
// an exit code equal to Integer.MIN_VALUE must mean that the process failed to start.
Preconditions.checkArgument(process instanceof LaunchedProcessImpl);
LaunchedProcessImpl processImpl = (LaunchedProcessImpl) process;
throw new IOException(
String.format("Failed to start process %s", processImpl.params.getCommand()));
}
return exitCode;
}
/**
* Destroys a process. If {@code force} is {@code true}, then forcibly destroys the process in a
* way it cannot ignore.
*/
public void destroyProcess(LaunchedProcess process, boolean force) {
LOG.debug("Destroying process %s (force %s)", process, force);
Preconditions.checkArgument(process instanceof LaunchedProcessImpl);
LaunchedProcessImpl processImpl = (LaunchedProcessImpl) process;
processImpl.nuProcess.destroy(force);
}
}