blob: f7781c332ba5f25f175f0070fe3fd65c2e84c475 [file] [log] [blame]
// 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
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef __WINDOWS__
#include <unistd.h>
#endif // __WINDOWS__
#include <sys/types.h>
#ifdef __linux__
#include <sys/prctl.h>
#endif // __linux__
#include <sys/types.h>
#include <initializer_list>
#include <string>
#include <glog/logging.h>
#include <process/future.hpp>
#include <process/reap.hpp>
#include <process/subprocess.hpp>
#include <stout/error.hpp>
#include <stout/lambda.hpp>
#include <stout/foreach.hpp>
#include <stout/option.hpp>
#include <stout/os.hpp>
#include <stout/strings.hpp>
#include <stout/try.hpp>
#include <stout/os/chdir.hpp>
#include <stout/os/close.hpp>
#include <stout/os/dup.hpp>
#include <stout/os/exec.hpp>
#include <stout/os/fcntl.hpp>
#include <stout/os/signals.hpp>
#ifdef __WINDOWS__
#include "windows/subprocess.hpp"
#else
#include "posix/subprocess.hpp"
#endif // __WINDOWS__
using std::map;
using std::string;
using std::vector;
namespace process {
using InputFileDescriptors = Subprocess::IO::InputFileDescriptors;
using OutputFileDescriptors = Subprocess::IO::OutputFileDescriptors;
Subprocess::ParentHook::ParentHook(
const lambda::function<Try<Nothing>(pid_t)>& _parent_setup)
: parent_setup(_parent_setup) {}
Subprocess::ChildHook::ChildHook(
const lambda::function<Try<Nothing>()>& _child_setup)
: child_setup(_child_setup) {}
Subprocess::ChildHook Subprocess::ChildHook::CHDIR(
const std::string& working_directory)
{
return Subprocess::ChildHook([working_directory]() -> Try<Nothing> {
const Try<Nothing> result = os::chdir(working_directory);
if (result.isError()) {
return Error(result.error());
}
return Nothing();
});
}
Subprocess::ChildHook Subprocess::ChildHook::SETSID()
{
return Subprocess::ChildHook([]() -> Try<Nothing> {
// TODO(josephw): By default, child processes on Windows do not
// terminate when the parent terminates. We need to implement
// `JobObject` support to change this default.
#ifndef __WINDOWS__
// Put child into its own process session to prevent the parent
// suicide on child process SIGKILL/SIGTERM.
if (::setsid() == -1) {
return Error("Could not setsid");
}
#endif // __WINDOWS__
return Nothing();
});
}
#ifndef __WINDOWS__
Subprocess::ChildHook Subprocess::ChildHook::DUP2(int oldFd, int newFd)
{
return Subprocess::ChildHook([oldFd, newFd]() -> Try<Nothing> {
return os::dup2(oldFd, newFd);
});
}
#endif // __WINDOWS__
#ifdef __linux__
static void signalHandler(int signal)
{
// Send SIGKILL to every process in the process group of the
// calling process.
kill(0, SIGKILL);
_exit(EXIT_FAILURE);
}
#endif // __linux__
Subprocess::ChildHook Subprocess::ChildHook::SUPERVISOR()
{
return Subprocess::ChildHook([]() -> Try<Nothing> {
#ifdef __linux__
// Send SIGTERM to the current process if the parent exits.
// NOTE:: This function should always succeed because we are passing
// in a valid signal.
prctl(PR_SET_PDEATHSIG, SIGTERM);
// Put the current process into a separate process group so that
// we can kill it and all its children easily.
if (setpgid(0, 0) != 0) {
return Error("Could not start supervisor process.");
}
// Install a SIGTERM handler which will kill the current process
// group. Since we already setup the death signal above, the
// signal handler will be triggered when the parent exits.
if (os::signals::install(SIGTERM, &signalHandler) != 0) {
return Error("Could not start supervisor process.");
}
pid_t pid = fork();
if (pid == -1) {
return Error("Could not start supervisor process.");
} else if (pid == 0) {
// Child. This is the process that is going to exec the
// process if zero is returned.
// We setup death signal for the process as well in case
// someone, though unlikely, accidentally kill the parent of
// this process (the bookkeeping process).
prctl(PR_SET_PDEATHSIG, SIGKILL);
// NOTE: We don't need to clear the signal handler explicitly
// because the subsequent 'exec' will clear them.
return Nothing();
} else {
// Parent. This is the bookkeeping process which will wait for
// the child process to finish.
// Close the files to prevent interference on the communication
// between the parent and the child process.
::close(STDIN_FILENO);
::close(STDOUT_FILENO);
::close(STDERR_FILENO);
// Block until the child process finishes.
int status = 0;
while (waitpid(pid, &status, 0) == -1) {
if (errno != EINTR) {
_exit(EXIT_FAILURE);
}
}
// Forward the exit status if the child process exits normally.
if (WIFEXITED(status)) {
_exit(WEXITSTATUS(status));
}
_exit(EXIT_FAILURE);
}
#endif // __linux__
return Nothing();
});
}
Subprocess::IO Subprocess::FD(int_fd fd, IO::FDType type)
{
return Subprocess::IO(
[fd, type]() -> Try<InputFileDescriptors> {
int_fd prepared_fd = -1;
switch (type) {
case IO::DUPLICATED: {
Try<int_fd> dup = os::dup(fd);
if (dup.isError()) {
return Error(dup.error());
}
prepared_fd = dup.get();
break;
}
case IO::OWNED: {
prepared_fd = fd;
break;
}
// NOTE: By not setting a default we leverage the compiler
// errors when the enumeration is augmented to find all
// the cases we need to provide. Same for below.
}
InputFileDescriptors fds;
fds.read = prepared_fd;
return fds;
},
[fd, type]() -> Try<OutputFileDescriptors> {
int_fd prepared_fd = -1;
switch (type) {
case IO::DUPLICATED: {
Try<int_fd> dup = os::dup(fd);
if (dup.isError()) {
return Error(dup.error());
}
prepared_fd = dup.get();
break;
}
case IO::OWNED: {
prepared_fd = fd;
break;
}
// NOTE: By not setting a default we leverage the compiler
// errors when the enumeration is augmented to find all
// the cases we need to provide. Same for below.
}
OutputFileDescriptors fds;
fds.write = prepared_fd;
return fds;
});
}
namespace internal {
static void cleanup(
const Future<Option<int>>& result,
Promise<Option<int>>* promise,
const Subprocess& subprocess)
{
CHECK(!result.isPending());
CHECK(!result.isDiscarded());
if (result.isFailed()) {
promise->fail(result.failure());
} else {
promise->set(result.get());
}
delete promise;
}
static void close(std::initializer_list<int_fd> fds)
{
foreach (int_fd fd, fds) {
if (fd >= 0) {
os::close(fd);
}
}
}
// This function will invoke `os::close` on all specified file
// descriptors that are valid (i.e., not `None` and >= 0).
static void close(
const Subprocess::IO::InputFileDescriptors& stdinfds,
const Subprocess::IO::OutputFileDescriptors& stdoutfds,
const Subprocess::IO::OutputFileDescriptors& stderrfds)
{
close(
{stdinfds.read,
stdinfds.write.getOrElse(-1),
stdoutfds.read.getOrElse(-1),
stdoutfds.write,
stderrfds.read.getOrElse(-1),
stderrfds.write});
}
} // namespace internal {
// Executes a subprocess.
//
// NOTE: On Windows, components of the `path` and `argv` that need to be quoted
// are expected to have been quoted before they are passed to `subprocess. For
// example, either of these may contain paths with spaces in them, like
// `C:\"Program Files"\foo.exe`, where notably the character sequence `\"`
// is not escaped quote, but instead a path separator and the start of a path
// component. Since the semantics of quoting are shell-dependent, it is not
// practical to attempt to re-parse the command that is passed in and properly
// escape it. Therefore, incorrectly-quoted command arguments will probably
// lead the child process to terminate with an error.
Try<Subprocess> subprocess(
const string& path,
vector<string> argv,
const Subprocess::IO& in,
const Subprocess::IO& out,
const Subprocess::IO& err,
const flags::FlagsBase* flags,
const Option<map<string, string>>& environment,
const Option<lambda::function<
pid_t(const lambda::function<int()>&)>>& _clone,
const vector<Subprocess::ParentHook>& parent_hooks,
const vector<Subprocess::ChildHook>& child_hooks,
const vector<int_fd>& whitelist_fds)
{
// TODO(hausdorff): We should error out on Windows here if we are passing
// parameters that aren't used.
// File descriptors for redirecting stdin/stdout/stderr.
// These file descriptors are used for different purposes depending
// on the specified I/O modes.
// See `Subprocess::PIPE`, `Subprocess::PATH`, and `Subprocess::FD`.
InputFileDescriptors stdinfds;
OutputFileDescriptors stdoutfds;
OutputFileDescriptors stderrfds;
// Prepare the file descriptor(s) for stdin.
Try<InputFileDescriptors> input = in.input();
if (input.isError()) {
return Error(input.error());
}
stdinfds = input.get();
// Prepare the file descriptor(s) for stdout.
Try<OutputFileDescriptors> output = out.output();
if (output.isError()) {
process::internal::close(stdinfds, stdoutfds, stderrfds);
return Error(output.error());
}
stdoutfds = output.get();
// Prepare the file descriptor(s) for stderr.
output = err.output();
if (output.isError()) {
process::internal::close(stdinfds, stdoutfds, stderrfds);
return Error(output.error());
}
stderrfds = output.get();
#ifndef __WINDOWS__
// TODO(jieyu): Consider using O_CLOEXEC for atomic close-on-exec.
Try<Nothing> cloexec = internal::cloexec(stdinfds, stdoutfds, stderrfds);
if (cloexec.isError()) {
process::internal::close(stdinfds, stdoutfds, stderrfds);
return Error("Failed to cloexec: " + cloexec.error());
}
#endif // __WINDOWS__
// Prepare the arguments. If the user specifies the 'flags', we will
// stringify them and append them to the existing arguments.
if (flags != nullptr) {
foreachvalue (const flags::Flag& flag, *flags) {
Option<string> value = flag.stringify(*flags);
if (value.isSome()) {
argv.push_back("--" + flag.effective_name().value + "=" + value.get());
}
}
}
// Parent.
Subprocess process;
// Create child, passing in stdin/stdout/stderr handles. We store the
// information and data structures we need to manage the lifecycle of the
// child (e.g., the PID of the child) in `process.data`.
//
// NOTE: We use lexical blocking around the `#ifdef` to limit use of data
// structures declared in the `#ifdef`. Since objects like `pid` will go out
// of scope at the end of the block, there is no chance we will accidentally
// define something in (say) the `__WINDOWS__` branch and then attempt to use
// it in the non-`__WINDOWS__` branch.
{
#ifndef __WINDOWS__
Try<pid_t> pid = internal::cloneChild(
path,
argv,
environment,
_clone,
parent_hooks,
child_hooks,
stdinfds,
stdoutfds,
stderrfds,
whitelist_fds);
if (pid.isError()) {
return Error(pid.error());
}
process.data->pid = pid.get();
#else
// TODO(joerg84): Consider using the childHooks and parentHooks here.
Try<os::windows::internal::ProcessData> process_data =
internal::createChildProcess(
path,
argv,
environment,
parent_hooks,
stdinfds,
stdoutfds,
stderrfds,
whitelist_fds);
if (process_data.isError()) {
// NOTE: `createChildProcess` either succeeds entirely or returns an
// `Error`. There is no need to check the value of the PID.
return Error(process_data.error());
}
// NOTE: We specifically tie the lifetime of the child process to
// the `Subprocess` object by saving the handles here so that
// there is zero chance of the `pid` getting reused.
process.data->process_data = process_data.get();
process.data->pid = process_data->pid;
#endif // __WINDOWS__
}
// For any pipes, store the parent side of the pipe so that
// the user can communicate with the subprocess.
process.data->in = stdinfds.write;
process.data->out = stdoutfds.read;
process.data->err = stderrfds.read;
// Rather than directly exposing the future from process::reap, we
// must use an explicit promise so that we can ensure we can receive
// the termination signal. Otherwise, the caller can discard the
// reap future, and we will not know when it is safe to close the
// file descriptors.
Promise<Option<int>>* promise = new Promise<Option<int>>();
process.data->status = promise->future();
// We need to bind a copy of this Subprocess into the onAny callback
// below to ensure that we don't close the file descriptors before
// the subprocess has terminated (i.e., because the caller doesn't
// keep a copy of this Subprocess around themselves).
process::reap(process.data->pid)
.onAny(lambda::bind(internal::cleanup, lambda::_1, promise, process));
return process;
}
} // namespace process {