/*
* 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.io.IO;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Builds and starts an operating system process. There are two modes a process can be in:
* <dl>
* <dt>streaming</dt>
* <dd>
* An unlimited amount of output is allowed, but the command may block if it is not consumed.
* </dd>
* <dt>non-streaming</dt>
* <dd>
* The command is guranteed to not block, but the amount of output is limited (e.g., first 10k).
* </dd>
* </dl>
* Which is to say, if you want to run a quick command that you expect to produce a fixed amount of
* output (e.g., "fbpackage info foo:123"), then you want to use non-streaming mode. If you want to
* run a command that can produce a lot of data, (e.g., "ptail foo") then you want to use streaming
* mode.
* <p/>
* Streaming commands are started by calling {@link SubprocessBuilder.Builder#stream()}.
* Non-streaming commands are started by calling {@link SubprocessBuilder.Builder#start()}.
* A streaming command can be turned into a non-streaming command by calling
* {@link Subprocess#background()}.
* <p/>
* Stderr is always in non-streaming mode. This ensures you never have to worry about your command
* blocking because you haven't read error output. If you expect (and need to process) a lot of
* output to stderr, then you need to {@link SubprocessBuilder.Builder#redirectStderrToStdout}.
*
* @see Subprocess
*/
public class SubprocessBuilder {
private final ProcessBuilderWrapper builder;
public SubprocessBuilder() {
this.builder = new JavaProcessBuilderWrapper();
}
public SubprocessBuilder(ProcessBuilderWrapper builder) {
this.builder = builder;
}
public Builder forCommand(String command) {
return new Builder(command, builder);
}
public static class Builder {
private final List<String> command = new ArrayList<>();
private final Map<String, String> environmentOverrides = new LinkedHashMap<>();
private final ProcessBuilderWrapper builder;
private boolean redirectStderrToStdout = false;
private File workingDirectory = null;
private IO echoCommand = null;
private IO echoOutput = null;
private int outputBytesLimit = 512_000;
private Builder(String command, ProcessBuilderWrapper builder) {
this.command.add(command);
this.builder = builder;
}
public Builder withArguments(List<?> arguments) {
for (Object argument : arguments) {
command.add(String.valueOf(argument));
}
return this;
}
public Builder withArguments(Object... arguments) {
return withArguments(Arrays.asList(arguments));
}
public Builder withEnvironmentVariable(String key, String value) {
environmentOverrides.put(key, value);
return this;
}
public Builder withoutEnvironmentVariable(String key) {
environmentOverrides.put(key, null);
return this;
}
public Builder redirectStderrToStdout() {
redirectStderrToStdout = true;
return this;
}
public Builder withWorkingDirectory(File workingDirectory) {
this.workingDirectory = workingDirectory;
return this;
}
public Builder echoCommand(IO io) {
echoCommand = io;
return this;
}
public Builder echoOutput(IO io) {
echoOutput = io;
return this;
}
public Builder outputBytesLimit(int outputBytesLimit) {
this.outputBytesLimit = outputBytesLimit;
return this;
}
/**
* Creates and begins running the command in non-streaming mode.
*
* @return a running command
*/
public Subprocess start() {
return start(false);
}
/**
* Creates and begins running the command in streaming mode.
*
* @return a running command
*/
public Subprocess stream() {
return start(true);
}
private Subprocess start(boolean streaming) {
final Process process;
if (redirectStderrToStdout) {
process = builder.createProcess(
RedirectErrorsTo.STDOUT, environmentOverrides, workingDirectory, command
);
} else {
process = builder.createProcess(
RedirectErrorsTo.STDERR, environmentOverrides, workingDirectory, command
);
}
if (echoCommand != null) {
Iterator<String> iterator = command.iterator();
while (iterator.hasNext()) {
echoCommand.out.print(iterator.next());
if (iterator.hasNext()) {
echoCommand.out.print(' ');
}
}
echoCommand.out.println();
}
return new SubprocessImpl(command, process, echoOutput, outputBytesLimit, streaming);
}
}
}