summaryrefslogtreecommitdiff
path: root/utils/process
diff options
context:
space:
mode:
Diffstat (limited to 'utils/process')
-rw-r--r--utils/process/.gitignore1
-rw-r--r--utils/process/Kyuafile13
-rw-r--r--utils/process/Makefile.am.inc113
-rw-r--r--utils/process/child.cpp385
-rw-r--r--utils/process/child.hpp113
-rw-r--r--utils/process/child.ipp110
-rw-r--r--utils/process/child_fwd.hpp45
-rw-r--r--utils/process/child_test.cpp846
-rw-r--r--utils/process/deadline_killer.cpp54
-rw-r--r--utils/process/deadline_killer.hpp58
-rw-r--r--utils/process/deadline_killer_fwd.hpp45
-rw-r--r--utils/process/deadline_killer_test.cpp108
-rw-r--r--utils/process/exceptions.cpp91
-rw-r--r--utils/process/exceptions.hpp78
-rw-r--r--utils/process/exceptions_test.cpp63
-rw-r--r--utils/process/executor.cpp869
-rw-r--r--utils/process/executor.hpp231
-rw-r--r--utils/process/executor.ipp182
-rw-r--r--utils/process/executor_fwd.hpp49
-rw-r--r--utils/process/executor_test.cpp940
-rw-r--r--utils/process/fdstream.cpp76
-rw-r--r--utils/process/fdstream.hpp66
-rw-r--r--utils/process/fdstream_fwd.hpp45
-rw-r--r--utils/process/fdstream_test.cpp73
-rw-r--r--utils/process/helpers.cpp74
-rw-r--r--utils/process/isolation.cpp207
-rw-r--r--utils/process/isolation.hpp60
-rw-r--r--utils/process/isolation_test.cpp622
-rw-r--r--utils/process/operations.cpp273
-rw-r--r--utils/process/operations.hpp56
-rw-r--r--utils/process/operations_fwd.hpp49
-rw-r--r--utils/process/operations_test.cpp471
-rw-r--r--utils/process/status.cpp200
-rw-r--r--utils/process/status.hpp84
-rw-r--r--utils/process/status_fwd.hpp45
-rw-r--r--utils/process/status_test.cpp209
-rw-r--r--utils/process/system.cpp59
-rw-r--r--utils/process/system.hpp66
-rw-r--r--utils/process/systembuf.cpp152
-rw-r--r--utils/process/systembuf.hpp71
-rw-r--r--utils/process/systembuf_fwd.hpp45
-rw-r--r--utils/process/systembuf_test.cpp166
42 files changed, 7563 insertions, 0 deletions
diff --git a/utils/process/.gitignore b/utils/process/.gitignore
new file mode 100644
index 000000000000..fb3291b39e0c
--- /dev/null
+++ b/utils/process/.gitignore
@@ -0,0 +1 @@
+helpers
diff --git a/utils/process/Kyuafile b/utils/process/Kyuafile
new file mode 100644
index 000000000000..92e62cfac6fc
--- /dev/null
+++ b/utils/process/Kyuafile
@@ -0,0 +1,13 @@
+syntax(2)
+
+test_suite("kyua")
+
+atf_test_program{name="child_test"}
+atf_test_program{name="deadline_killer_test"}
+atf_test_program{name="exceptions_test"}
+atf_test_program{name="executor_test"}
+atf_test_program{name="fdstream_test"}
+atf_test_program{name="isolation_test"}
+atf_test_program{name="operations_test"}
+atf_test_program{name="status_test"}
+atf_test_program{name="systembuf_test"}
diff --git a/utils/process/Makefile.am.inc b/utils/process/Makefile.am.inc
new file mode 100644
index 000000000000..3cff02e7e455
--- /dev/null
+++ b/utils/process/Makefile.am.inc
@@ -0,0 +1,113 @@
+# Copyright 2010 The Kyua Authors.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Google Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+libutils_a_SOURCES += utils/process/child.cpp
+libutils_a_SOURCES += utils/process/child.hpp
+libutils_a_SOURCES += utils/process/child.ipp
+libutils_a_SOURCES += utils/process/child_fwd.hpp
+libutils_a_SOURCES += utils/process/deadline_killer.cpp
+libutils_a_SOURCES += utils/process/deadline_killer.hpp
+libutils_a_SOURCES += utils/process/deadline_killer_fwd.hpp
+libutils_a_SOURCES += utils/process/exceptions.cpp
+libutils_a_SOURCES += utils/process/exceptions.hpp
+libutils_a_SOURCES += utils/process/executor.cpp
+libutils_a_SOURCES += utils/process/executor.hpp
+libutils_a_SOURCES += utils/process/executor.ipp
+libutils_a_SOURCES += utils/process/executor_fwd.hpp
+libutils_a_SOURCES += utils/process/fdstream.cpp
+libutils_a_SOURCES += utils/process/fdstream.hpp
+libutils_a_SOURCES += utils/process/fdstream_fwd.hpp
+libutils_a_SOURCES += utils/process/isolation.cpp
+libutils_a_SOURCES += utils/process/isolation.hpp
+libutils_a_SOURCES += utils/process/operations.cpp
+libutils_a_SOURCES += utils/process/operations.hpp
+libutils_a_SOURCES += utils/process/operations_fwd.hpp
+libutils_a_SOURCES += utils/process/status.cpp
+libutils_a_SOURCES += utils/process/status.hpp
+libutils_a_SOURCES += utils/process/status_fwd.hpp
+libutils_a_SOURCES += utils/process/system.cpp
+libutils_a_SOURCES += utils/process/system.hpp
+libutils_a_SOURCES += utils/process/systembuf.cpp
+libutils_a_SOURCES += utils/process/systembuf.hpp
+libutils_a_SOURCES += utils/process/systembuf_fwd.hpp
+
+if WITH_ATF
+tests_utils_processdir = $(pkgtestsdir)/utils/process
+
+tests_utils_process_DATA = utils/process/Kyuafile
+EXTRA_DIST += $(tests_utils_process_DATA)
+
+tests_utils_process_PROGRAMS = utils/process/child_test
+utils_process_child_test_SOURCES = utils/process/child_test.cpp
+utils_process_child_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_child_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/deadline_killer_test
+utils_process_deadline_killer_test_SOURCES = \
+ utils/process/deadline_killer_test.cpp
+utils_process_deadline_killer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_deadline_killer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/exceptions_test
+utils_process_exceptions_test_SOURCES = utils/process/exceptions_test.cpp
+utils_process_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/executor_test
+utils_process_executor_test_SOURCES = utils/process/executor_test.cpp
+utils_process_executor_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_executor_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/fdstream_test
+utils_process_fdstream_test_SOURCES = utils/process/fdstream_test.cpp
+utils_process_fdstream_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_fdstream_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/isolation_test
+utils_process_isolation_test_SOURCES = utils/process/isolation_test.cpp
+utils_process_isolation_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_isolation_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/helpers
+utils_process_helpers_SOURCES = utils/process/helpers.cpp
+
+tests_utils_process_PROGRAMS += utils/process/operations_test
+utils_process_operations_test_SOURCES = utils/process/operations_test.cpp
+utils_process_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/status_test
+utils_process_status_test_SOURCES = utils/process/status_test.cpp
+utils_process_status_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_status_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+
+tests_utils_process_PROGRAMS += utils/process/systembuf_test
+utils_process_systembuf_test_SOURCES = utils/process/systembuf_test.cpp
+utils_process_systembuf_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS)
+utils_process_systembuf_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS)
+endif
diff --git a/utils/process/child.cpp b/utils/process/child.cpp
new file mode 100644
index 000000000000..fef09ccaad3b
--- /dev/null
+++ b/utils/process/child.cpp
@@ -0,0 +1,385 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/child.ipp"
+
+extern "C" {
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <iostream>
+#include <memory>
+
+#include "utils/defs.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/process/exceptions.hpp"
+#include "utils/process/fdstream.hpp"
+#include "utils/process/operations.hpp"
+#include "utils/process/system.hpp"
+#include "utils/process/status.hpp"
+#include "utils/sanity.hpp"
+#include "utils/signals/interrupts.hpp"
+
+
+namespace utils {
+namespace process {
+
+
+/// Private implementation fields for child objects.
+struct child::impl : utils::noncopyable {
+ /// The process identifier.
+ pid_t _pid;
+
+ /// The input stream for the process' stdout and stderr. May be NULL.
+ std::auto_ptr< process::ifdstream > _output;
+
+ /// Initializes private implementation data.
+ ///
+ /// \param pid The process identifier.
+ /// \param output The input stream. Grabs ownership of the pointer.
+ impl(const pid_t pid, process::ifdstream* output) :
+ _pid(pid), _output(output) {}
+};
+
+
+} // namespace process
+} // namespace utils
+
+
+namespace fs = utils::fs;
+namespace process = utils::process;
+namespace signals = utils::signals;
+
+
+namespace {
+
+
+/// Exception-based version of dup(2).
+///
+/// \param old_fd The file descriptor to duplicate.
+/// \param new_fd The file descriptor to use as the duplicate. This is
+/// closed if it was open before the copy happens.
+///
+/// \throw process::system_error If the call to dup2(2) fails.
+static void
+safe_dup(const int old_fd, const int new_fd)
+{
+ if (process::detail::syscall_dup2(old_fd, new_fd) == -1) {
+ const int original_errno = errno;
+ throw process::system_error(F("dup2(%s, %s) failed") % old_fd % new_fd,
+ original_errno);
+ }
+}
+
+
+/// Exception-based version of open(2) to open (or create) a file for append.
+///
+/// \param filename The file to open in append mode.
+///
+/// \return The file descriptor for the opened or created file.
+///
+/// \throw process::system_error If the call to open(2) fails.
+static int
+open_for_append(const fs::path& filename)
+{
+ const int fd = process::detail::syscall_open(
+ filename.c_str(), O_CREAT | O_WRONLY | O_APPEND,
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+ if (fd == -1) {
+ const int original_errno = errno;
+ throw process::system_error(F("Failed to create %s because open(2) "
+ "failed") % filename, original_errno);
+ }
+ return fd;
+}
+
+
+/// Logs the execution of another program.
+///
+/// \param program The binary to execute.
+/// \param args The arguments to pass to the binary, without the program name.
+static void
+log_exec(const fs::path& program, const process::args_vector& args)
+{
+ std::string plain_command = program.str();
+ for (process::args_vector::const_iterator iter = args.begin();
+ iter != args.end(); ++iter)
+ plain_command += F(" %s") % *iter;
+ LD(F("Executing %s") % plain_command);
+}
+
+
+} // anonymous namespace
+
+
+/// Prints out a fatal error and aborts.
+void
+utils::process::detail::report_error_and_abort(void)
+{
+ std::cerr << "Caught unknown exception\n";
+ std::abort();
+}
+
+
+/// Prints out a fatal error and aborts.
+///
+/// \param error The error to display.
+void
+utils::process::detail::report_error_and_abort(const std::runtime_error& error)
+{
+ std::cerr << "Caught runtime_error: " << error.what() << '\n';
+ std::abort();
+}
+
+
+/// Creates a new child.
+///
+/// \param implptr A dynamically-allocated impl object with the contents of the
+/// new child.
+process::child::child(impl *implptr) :
+ _pimpl(implptr)
+{
+}
+
+
+/// Destructor for child.
+process::child::~child(void)
+{
+}
+
+
+/// Helper function for fork().
+///
+/// Please note: if you update this function to change the return type or to
+/// raise different errors, do not forget to update fork() accordingly.
+///
+/// \return In the case of the parent, a new child object returned as a
+/// dynamically-allocated object because children classes are unique and thus
+/// noncopyable. In the case of the child, a NULL pointer.
+///
+/// \throw process::system_error If the calls to pipe(2) or fork(2) fail.
+std::auto_ptr< process::child >
+process::child::fork_capture_aux(void)
+{
+ std::cout.flush();
+ std::cerr.flush();
+
+ int fds[2];
+ if (detail::syscall_pipe(fds) == -1)
+ throw process::system_error("pipe(2) failed", errno);
+
+ std::auto_ptr< signals::interrupts_inhibiter > inhibiter(
+ new signals::interrupts_inhibiter);
+ pid_t pid = detail::syscall_fork();
+ if (pid == -1) {
+ inhibiter.reset(NULL); // Unblock signals.
+ ::close(fds[0]);
+ ::close(fds[1]);
+ throw process::system_error("fork(2) failed", errno);
+ } else if (pid == 0) {
+ inhibiter.reset(NULL); // Unblock signals.
+ ::setsid();
+
+ try {
+ ::close(fds[0]);
+ safe_dup(fds[1], STDOUT_FILENO);
+ safe_dup(fds[1], STDERR_FILENO);
+ ::close(fds[1]);
+ } catch (const system_error& e) {
+ std::cerr << F("Failed to set up subprocess: %s\n") % e.what();
+ std::abort();
+ }
+ return std::auto_ptr< process::child >(NULL);
+ } else {
+ ::close(fds[1]);
+ LD(F("Spawned process %s: stdout and stderr inherited") % pid);
+ signals::add_pid_to_kill(pid);
+ inhibiter.reset(NULL); // Unblock signals.
+ return std::auto_ptr< process::child >(
+ new process::child(new impl(pid, new process::ifdstream(fds[0]))));
+ }
+}
+
+
+/// Helper function for fork().
+///
+/// Please note: if you update this function to change the return type or to
+/// raise different errors, do not forget to update fork() accordingly.
+///
+/// \param stdout_file The name of the file in which to store the stdout.
+/// If this has the magic value /dev/stdout, then the parent's stdout is
+/// reused without applying any redirection.
+/// \param stderr_file The name of the file in which to store the stderr.
+/// If this has the magic value /dev/stderr, then the parent's stderr is
+/// reused without applying any redirection.
+///
+/// \return In the case of the parent, a new child object returned as a
+/// dynamically-allocated object because children classes are unique and thus
+/// noncopyable. In the case of the child, a NULL pointer.
+///
+/// \throw process::system_error If the call to fork(2) fails.
+std::auto_ptr< process::child >
+process::child::fork_files_aux(const fs::path& stdout_file,
+ const fs::path& stderr_file)
+{
+ std::cout.flush();
+ std::cerr.flush();
+
+ std::auto_ptr< signals::interrupts_inhibiter > inhibiter(
+ new signals::interrupts_inhibiter);
+ pid_t pid = detail::syscall_fork();
+ if (pid == -1) {
+ inhibiter.reset(NULL); // Unblock signals.
+ throw process::system_error("fork(2) failed", errno);
+ } else if (pid == 0) {
+ inhibiter.reset(NULL); // Unblock signals.
+ ::setsid();
+
+ try {
+ if (stdout_file != fs::path("/dev/stdout")) {
+ const int stdout_fd = open_for_append(stdout_file);
+ safe_dup(stdout_fd, STDOUT_FILENO);
+ ::close(stdout_fd);
+ }
+ if (stderr_file != fs::path("/dev/stderr")) {
+ const int stderr_fd = open_for_append(stderr_file);
+ safe_dup(stderr_fd, STDERR_FILENO);
+ ::close(stderr_fd);
+ }
+ } catch (const system_error& e) {
+ std::cerr << F("Failed to set up subprocess: %s\n") % e.what();
+ std::abort();
+ }
+ return std::auto_ptr< process::child >(NULL);
+ } else {
+ LD(F("Spawned process %s: stdout=%s, stderr=%s") % pid % stdout_file %
+ stderr_file);
+ signals::add_pid_to_kill(pid);
+ inhibiter.reset(NULL); // Unblock signals.
+ return std::auto_ptr< process::child >(
+ new process::child(new impl(pid, NULL)));
+ }
+}
+
+
+/// Spawns a new binary and multiplexes and captures its stdout and stderr.
+///
+/// If the subprocess cannot be completely set up for any reason, it attempts to
+/// dump an error message to its stderr channel and it then calls std::abort().
+///
+/// \param program The binary to execute.
+/// \param args The arguments to pass to the binary, without the program name.
+///
+/// \return A new child object, returned as a dynamically-allocated object
+/// because children classes are unique and thus noncopyable.
+///
+/// \throw process::system_error If the process cannot be spawned due to a
+/// system call error.
+std::auto_ptr< process::child >
+process::child::spawn_capture(const fs::path& program, const args_vector& args)
+{
+ std::auto_ptr< child > child = fork_capture_aux();
+ if (child.get() == NULL)
+ exec(program, args);
+ log_exec(program, args);
+ return child;
+}
+
+
+/// Spawns a new binary and redirects its stdout and stderr to files.
+///
+/// If the subprocess cannot be completely set up for any reason, it attempts to
+/// dump an error message to its stderr channel and it then calls std::abort().
+///
+/// \param program The binary to execute.
+/// \param args The arguments to pass to the binary, without the program name.
+/// \param stdout_file The name of the file in which to store the stdout.
+/// \param stderr_file The name of the file in which to store the stderr.
+///
+/// \return A new child object, returned as a dynamically-allocated object
+/// because children classes are unique and thus noncopyable.
+///
+/// \throw process::system_error If the process cannot be spawned due to a
+/// system call error.
+std::auto_ptr< process::child >
+process::child::spawn_files(const fs::path& program,
+ const args_vector& args,
+ const fs::path& stdout_file,
+ const fs::path& stderr_file)
+{
+ std::auto_ptr< child > child = fork_files_aux(stdout_file, stderr_file);
+ if (child.get() == NULL)
+ exec(program, args);
+ log_exec(program, args);
+ return child;
+}
+
+
+/// Returns the process identifier of this child.
+///
+/// \return A process identifier.
+int
+process::child::pid(void) const
+{
+ return _pimpl->_pid;
+}
+
+
+/// Gets the input stream corresponding to the stdout and stderr of the child.
+///
+/// \pre The child must have been started by fork_capture().
+///
+/// \return A reference to the input stream connected to the output of the test
+/// case.
+std::istream&
+process::child::output(void)
+{
+ PRE(_pimpl->_output.get() != NULL);
+ return *_pimpl->_output;
+}
+
+
+/// Blocks to wait for completion.
+///
+/// \return The termination status of the child process.
+///
+/// \throw process::system_error If the call to waitpid(2) fails.
+process::status
+process::child::wait(void)
+{
+ return process::wait(_pimpl->_pid);
+}
diff --git a/utils/process/child.hpp b/utils/process/child.hpp
new file mode 100644
index 000000000000..2c9450f6500a
--- /dev/null
+++ b/utils/process/child.hpp
@@ -0,0 +1,113 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/child.hpp
+/// Spawning and manipulation of children processes.
+///
+/// The child module provides a set of functions to spawn subprocesses with
+/// different settings, and the corresponding set of classes to interact with
+/// said subprocesses. The interfaces to fork subprocesses are very simplified
+/// and only provide the minimum functionality required by the rest of the
+/// project.
+///
+/// Be aware that the semantics of the fork and wait methods exposed by this
+/// module are slightly different from that of the native calls. Any process
+/// spawned by fork here will be isolated in its own session; once any of
+/// such children processes is awaited for, its whole process group will be
+/// terminated. This is the semantics we want in the above layers to ensure
+/// that test programs (and, for that matter, external utilities) do not leak
+/// subprocesses on the system.
+
+#if !defined(UTILS_PROCESS_CHILD_HPP)
+#define UTILS_PROCESS_CHILD_HPP
+
+#include "utils/process/child_fwd.hpp"
+
+#include <istream>
+#include <memory>
+#include <stdexcept>
+
+#include "utils/defs.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/process/operations_fwd.hpp"
+#include "utils/process/status_fwd.hpp"
+
+namespace utils {
+namespace process {
+
+
+namespace detail {
+
+void report_error_and_abort(void) UTILS_NORETURN;
+void report_error_and_abort(const std::runtime_error&) UTILS_NORETURN;
+
+
+} // namespace detail
+
+
+/// Child process spawner and controller.
+class child : noncopyable {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::auto_ptr< impl > _pimpl;
+
+ static std::auto_ptr< child > fork_capture_aux(void);
+
+ static std::auto_ptr< child > fork_files_aux(const fs::path&,
+ const fs::path&);
+
+ explicit child(impl *);
+
+public:
+ ~child(void);
+
+ template< typename Hook >
+ static std::auto_ptr< child > fork_capture(Hook);
+ std::istream& output(void);
+
+ template< typename Hook >
+ static std::auto_ptr< child > fork_files(Hook, const fs::path&,
+ const fs::path&);
+
+ static std::auto_ptr< child > spawn_capture(
+ const fs::path&, const args_vector&);
+ static std::auto_ptr< child > spawn_files(
+ const fs::path&, const args_vector&, const fs::path&, const fs::path&);
+
+ int pid(void) const;
+
+ status wait(void);
+};
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_CHILD_HPP)
diff --git a/utils/process/child.ipp b/utils/process/child.ipp
new file mode 100644
index 000000000000..aa90373652fd
--- /dev/null
+++ b/utils/process/child.ipp
@@ -0,0 +1,110 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#if !defined(UTILS_PROCESS_CHILD_IPP)
+#define UTILS_PROCESS_CHILD_IPP
+
+#include <cstdlib>
+
+#include "utils/process/child.hpp"
+
+namespace utils {
+namespace process {
+
+
+/// Spawns a new subprocess and redirects its stdout and stderr to files.
+///
+/// If the subprocess cannot be completely set up for any reason, it attempts to
+/// dump an error message to its stderr channel and it then calls std::abort().
+///
+/// \param hook The function to execute in the subprocess. Must not return.
+/// \param stdout_file The name of the file in which to store the stdout.
+/// \param stderr_file The name of the file in which to store the stderr.
+///
+/// \return A new child object, returned as a dynamically-allocated object
+/// because children classes are unique and thus noncopyable.
+///
+/// \throw process::system_error If the process cannot be spawned due to a
+/// system call error.
+template< typename Hook >
+std::auto_ptr< child >
+child::fork_files(Hook hook, const fs::path& stdout_file,
+ const fs::path& stderr_file)
+{
+ std::auto_ptr< child > child = fork_files_aux(stdout_file, stderr_file);
+ if (child.get() == NULL) {
+ try {
+ hook();
+ std::abort();
+ } catch (const std::runtime_error& e) {
+ detail::report_error_and_abort(e);
+ } catch (...) {
+ detail::report_error_and_abort();
+ }
+ }
+
+ return child;
+}
+
+
+/// Spawns a new subprocess and multiplexes and captures its stdout and stderr.
+///
+/// If the subprocess cannot be completely set up for any reason, it attempts to
+/// dump an error message to its stderr channel and it then calls std::abort().
+///
+/// \param hook The function to execute in the subprocess. Must not return.
+///
+/// \return A new child object, returned as a dynamically-allocated object
+/// because children classes are unique and thus noncopyable.
+///
+/// \throw process::system_error If the process cannot be spawned due to a
+/// system call error.
+template< typename Hook >
+std::auto_ptr< child >
+child::fork_capture(Hook hook)
+{
+ std::auto_ptr< child > child = fork_capture_aux();
+ if (child.get() == NULL) {
+ try {
+ hook();
+ std::abort();
+ } catch (const std::runtime_error& e) {
+ detail::report_error_and_abort(e);
+ } catch (...) {
+ detail::report_error_and_abort();
+ }
+ }
+
+ return child;
+}
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_CHILD_IPP)
diff --git a/utils/process/child_fwd.hpp b/utils/process/child_fwd.hpp
new file mode 100644
index 000000000000..4d4caa17d58c
--- /dev/null
+++ b/utils/process/child_fwd.hpp
@@ -0,0 +1,45 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/child_fwd.hpp
+/// Forward declarations for utils/process/child.hpp
+
+#if !defined(UTILS_PROCESS_CHILD_FWD_HPP)
+#define UTILS_PROCESS_CHILD_FWD_HPP
+
+namespace utils {
+namespace process {
+
+
+class child;
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_CHILD_FWD_HPP)
diff --git a/utils/process/child_test.cpp b/utils/process/child_test.cpp
new file mode 100644
index 000000000000..69de9991ae13
--- /dev/null
+++ b/utils/process/child_test.cpp
@@ -0,0 +1,846 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/child.ipp"
+
+extern "C" {
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdarg>
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <stdexcept>
+
+#include <atf-c++.hpp>
+
+#include "utils/defs.hpp"
+#include "utils/env.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/process/exceptions.hpp"
+#include "utils/process/status.hpp"
+#include "utils/process/system.hpp"
+#include "utils/sanity.hpp"
+#include "utils/test_utils.ipp"
+
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace process = utils::process;
+
+
+namespace {
+
+
+/// Checks if the current subprocess is in its own session.
+static void
+child_check_own_session(void)
+{
+ std::exit((::getsid(::getpid()) == ::getpid()) ?
+ EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+
+/// Body for a process that prints a simple message and exits.
+///
+/// \tparam ExitStatus The exit status for the subprocess.
+/// \tparam Message A single character that will be prepended to the printed
+/// messages. This would ideally be a string, but we cannot templatize a
+/// function with an object nor a pointer.
+template< int ExitStatus, char Message >
+static void
+child_simple_function(void)
+{
+ std::cout << "To stdout: " << Message << "\n";
+ std::cerr << "To stderr: " << Message << "\n";
+ std::exit(ExitStatus);
+}
+
+
+/// Functor for the body of a process that prints a simple message and exits.
+class child_simple_functor {
+ /// The exit status that the subprocess will yield.
+ int _exitstatus;
+
+ /// The message to print on stdout and stderr.
+ std::string _message;
+
+public:
+ /// Constructs a new functor.
+ ///
+ /// \param exitstatus The exit status that the subprocess will yield.
+ /// \param message The message to print on stdout and stderr.
+ child_simple_functor(const int exitstatus, const std::string& message) :
+ _exitstatus(exitstatus),
+ _message(message)
+ {
+ }
+
+ /// Body for the subprocess.
+ void
+ operator()(void)
+ {
+ std::cout << "To stdout: " << _message << "\n";
+ std::cerr << "To stderr: " << _message << "\n";
+ std::exit(_exitstatus);
+ }
+};
+
+
+/// Body for a process that prints many messages to stdout and exits.
+///
+/// The goal of this body is to validate that any buffering performed on the
+/// parent process to read the output of the subprocess works correctly.
+static void
+child_printer_function(void)
+{
+ for (std::size_t i = 0; i < 100; i++)
+ std::cout << "This is a message to stdout, sequence " << i << "\n";
+ std::cout.flush();
+ std::cerr << "Exiting\n";
+ std::exit(EXIT_SUCCESS);
+}
+
+
+/// Functor for the body of a process that runs child_printer_function.
+class child_printer_functor {
+public:
+ /// Body for the subprocess.
+ void
+ operator()(void)
+ {
+ child_printer_function();
+ }
+};
+
+
+/// Body for a child process that throws an exception.
+static void
+child_throw_exception(void)
+{
+ throw std::runtime_error("A loose exception");
+}
+
+
+/// Body for a child process that creates a pidfile.
+static void
+child_write_pid(void)
+{
+ std::ofstream output("pidfile");
+ output << ::getpid() << "\n";
+ output.close();
+ std::exit(EXIT_SUCCESS);
+}
+
+
+/// A child process that returns.
+///
+/// The fork() wrappers are supposed to capture this condition and terminate the
+/// child before the code returns to the fork() call point.
+static void
+child_return(void)
+{
+}
+
+
+/// A child process that raises an exception.
+///
+/// The fork() wrappers are supposed to capture this condition and terminate the
+/// child before the code returns to the fork() call point.
+///
+/// \tparam Type The type of the exception to raise.
+/// \tparam Value The value passed to the constructor of the exception type. In
+/// general, this only makes sense if Type is a primitive type so that, in
+/// the end, the code becomes "throw int(123)".
+///
+/// \throw Type An exception of the provided type.
+template< class Type, Type Value >
+void
+child_raise_exception(void)
+{
+ throw Type(Value);
+}
+
+
+/// Calculates the path to the test helpers binary.
+///
+/// \param tc A pointer to the caller test case, needed to extract the value of
+/// the "srcdir" property.
+///
+/// \return The path to the helpers binary.
+static fs::path
+get_helpers(const atf::tests::tc* tc)
+{
+ return fs::path(tc->get_config_var("srcdir")) / "helpers";
+}
+
+
+/// Mock fork(2) that just returns an error.
+///
+/// \tparam Errno The value to set as the errno of the failed call.
+///
+/// \return Always -1.
+template< int Errno >
+static pid_t
+fork_fail(void) throw()
+{
+ errno = Errno;
+ return -1;
+}
+
+
+/// Mock open(2) that fails if the 'raise-error' file is opened.
+///
+/// \tparam Errno The value to set as the errno if the known failure triggers.
+/// \param path The path to the file to be opened.
+/// \param flags The open flags.
+/// \param ... The file mode creation, if flags contains O_CREAT.
+///
+/// \return The opened file handle or -1 on error.
+template< int Errno >
+static int
+open_fail(const char* path, const int flags, ...) throw()
+{
+ if (std::strcmp(path, "raise-error") == 0) {
+ errno = Errno;
+ return -1;
+ } else {
+ va_list ap;
+ va_start(ap, flags);
+ const int mode = va_arg(ap, int);
+ va_end(ap);
+ return ::open(path, flags, mode);
+ }
+}
+
+
+/// Mock pipe(2) that just returns an error.
+///
+/// \tparam Errno The value to set as the errno of the failed call.
+///
+/// \return Always -1.
+template< int Errno >
+static pid_t
+pipe_fail(int* /* fildes */) throw()
+{
+ errno = Errno;
+ return -1;
+}
+
+
+/// Helper for child tests to validate inheritance of stdout/stderr.
+///
+/// This function ensures that passing one of /dev/stdout or /dev/stderr to
+/// the child__fork_files fork method does the right thing. The idea is that we
+/// call fork with the given parameters and then make our child redirect one of
+/// its file descriptors to a specific file without going through the process
+/// library. We then validate if this redirection worked and got the expected
+/// output.
+///
+/// \param fork_stdout The path to pass to the fork call as the stdout file.
+/// \param fork_stderr The path to pass to the fork call as the stderr file.
+/// \param child_file The file to explicitly in the subchild.
+/// \param child_fd The file descriptor to which to attach child_file.
+static void
+do_inherit_test(const char* fork_stdout, const char* fork_stderr,
+ const char* child_file, const int child_fd)
+{
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ logging::set_inmemory();
+
+ const int fd = ::open(child_file, O_CREAT | O_WRONLY | O_TRUNC, 0644);
+ if (fd != child_fd) {
+ if (::dup2(fd, child_fd) == -1)
+ std::abort();
+ ::close(fd);
+ }
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_simple_function< 123, 'Z' >,
+ fs::path(fork_stdout), fs::path(fork_stderr));
+ const process::status status = child->wait();
+ if (!status.exited() || status.exitstatus() != 123)
+ std::abort();
+ std::exit(EXIT_SUCCESS);
+ } else {
+ int status;
+ ATF_REQUIRE(::waitpid(pid, &status, 0) != -1);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+ ATF_REQUIRE(atf::utils::grep_file("stdout: Z", "stdout.txt"));
+ ATF_REQUIRE(atf::utils::grep_file("stderr: Z", "stderr.txt"));
+ }
+}
+
+
+/// Performs a "child__fork_capture__ok_*" test.
+///
+/// This test basically ensures that the child__fork_capture class spawns a
+/// process whose output is captured in an input stream.
+///
+/// \tparam Hook The type of the fork hook to use.
+/// \param hook The hook to the fork call.
+template< class Hook >
+static void
+child__fork_capture__ok(Hook hook)
+{
+ std::cout << "This unflushed message should not propagate to the child";
+ std::cerr << "This unflushed message should not propagate to the child";
+ std::auto_ptr< process::child > child = process::child::fork_capture(hook);
+ std::cout.flush();
+ std::cerr.flush();
+
+ std::istream& output = child->output();
+ for (std::size_t i = 0; i < 100; i++) {
+ std::string line;
+ ATF_REQUIRE(std::getline(output, line).good());
+ ATF_REQUIRE_EQ((F("This is a message to stdout, "
+ "sequence %s") % i).str(), line);
+ }
+
+ std::string line;
+ ATF_REQUIRE(std::getline(output, line).good());
+ ATF_REQUIRE_EQ("Exiting", line);
+
+ process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__ok_function);
+ATF_TEST_CASE_BODY(child__fork_capture__ok_function)
+{
+ child__fork_capture__ok(child_printer_function);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__ok_functor);
+ATF_TEST_CASE_BODY(child__fork_capture__ok_functor)
+{
+ child__fork_capture__ok(child_printer_functor());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__catch_exceptions);
+ATF_TEST_CASE_BODY(child__fork_capture__catch_exceptions)
+{
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_throw_exception);
+
+ std::string message;
+ std::istream& output = child->output();
+ ATF_REQUIRE(std::getline(output, message).good());
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+
+ ATF_REQUIRE_MATCH("Caught.*A loose exception", message);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__new_session);
+ATF_TEST_CASE_BODY(child__fork_capture__new_session)
+{
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_check_own_session);
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__pipe_fail);
+ATF_TEST_CASE_BODY(child__fork_capture__pipe_fail)
+{
+ process::detail::syscall_pipe = pipe_fail< 23 >;
+ try {
+ process::child::fork_capture(child_simple_function< 1, 'A' >);
+ fail("Expected exception but none raised");
+ } catch (const process::system_error& e) {
+ ATF_REQUIRE(atf::utils::grep_string("pipe.*failed", e.what()));
+ ATF_REQUIRE_EQ(23, e.original_errno());
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_cannot_exit);
+ATF_TEST_CASE_BODY(child__fork_capture__fork_cannot_exit)
+{
+ const pid_t parent_pid = ::getpid();
+ atf::utils::create_file("to-not-be-deleted", "");
+
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_return);
+ if (::getpid() != parent_pid) {
+ // If we enter this clause, it is because the hook returned.
+ ::unlink("to-not-be-deleted");
+ std::exit(EXIT_SUCCESS);
+ }
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted")));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_cannot_unwind);
+ATF_TEST_CASE_BODY(child__fork_capture__fork_cannot_unwind)
+{
+ const pid_t parent_pid = ::getpid();
+ atf::utils::create_file("to-not-be-deleted", "");
+ try {
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_raise_exception< int, 123 >);
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted")));
+ } catch (const int i) {
+ // If we enter this clause, it is because an exception leaked from the
+ // hook.
+ INV(parent_pid != ::getpid());
+ INV(i == 123);
+ ::unlink("to-not-be-deleted");
+ std::exit(EXIT_SUCCESS);
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_fail);
+ATF_TEST_CASE_BODY(child__fork_capture__fork_fail)
+{
+ process::detail::syscall_fork = fork_fail< 89 >;
+ try {
+ process::child::fork_capture(child_simple_function< 1, 'A' >);
+ fail("Expected exception but none raised");
+ } catch (const process::system_error& e) {
+ ATF_REQUIRE(atf::utils::grep_string("fork.*failed", e.what()));
+ ATF_REQUIRE_EQ(89, e.original_errno());
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__ok_function);
+ATF_TEST_CASE_BODY(child__fork_files__ok_function)
+{
+ const fs::path file1("file1.txt");
+ const fs::path file2("file2.txt");
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_simple_function< 15, 'Z' >, file1, file2);
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(15, status.exitstatus());
+
+ ATF_REQUIRE( atf::utils::grep_file("^To stdout: Z$", file1.str()));
+ ATF_REQUIRE(!atf::utils::grep_file("^To stdout: Z$", file2.str()));
+
+ ATF_REQUIRE( atf::utils::grep_file("^To stderr: Z$", file2.str()));
+ ATF_REQUIRE(!atf::utils::grep_file("^To stderr: Z$", file1.str()));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__ok_functor);
+ATF_TEST_CASE_BODY(child__fork_files__ok_functor)
+{
+ const fs::path filea("fileA.txt");
+ const fs::path fileb("fileB.txt");
+
+ atf::utils::create_file(filea.str(), "Initial stdout\n");
+ atf::utils::create_file(fileb.str(), "Initial stderr\n");
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_simple_functor(16, "a functor"), filea, fileb);
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(16, status.exitstatus());
+
+ ATF_REQUIRE( atf::utils::grep_file("^Initial stdout$", filea.str()));
+ ATF_REQUIRE(!atf::utils::grep_file("^Initial stdout$", fileb.str()));
+
+ ATF_REQUIRE( atf::utils::grep_file("^To stdout: a functor$", filea.str()));
+ ATF_REQUIRE(!atf::utils::grep_file("^To stdout: a functor$", fileb.str()));
+
+ ATF_REQUIRE( atf::utils::grep_file("^Initial stderr$", fileb.str()));
+ ATF_REQUIRE(!atf::utils::grep_file("^Initial stderr$", filea.str()));
+
+ ATF_REQUIRE( atf::utils::grep_file("^To stderr: a functor$", fileb.str()));
+ ATF_REQUIRE(!atf::utils::grep_file("^To stderr: a functor$", filea.str()));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__catch_exceptions);
+ATF_TEST_CASE_BODY(child__fork_files__catch_exceptions)
+{
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_throw_exception,
+ fs::path("unused.out"), fs::path("stderr"));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+
+ ATF_REQUIRE(atf::utils::grep_file("Caught.*A loose exception", "stderr"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__new_session);
+ATF_TEST_CASE_BODY(child__fork_files__new_session)
+{
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_check_own_session,
+ fs::path("unused.out"), fs::path("unused.err"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__inherit_stdout);
+ATF_TEST_CASE_BODY(child__fork_files__inherit_stdout)
+{
+ do_inherit_test("/dev/stdout", "stderr.txt", "stdout.txt", STDOUT_FILENO);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__inherit_stderr);
+ATF_TEST_CASE_BODY(child__fork_files__inherit_stderr)
+{
+ do_inherit_test("stdout.txt", "/dev/stderr", "stderr.txt", STDERR_FILENO);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_cannot_exit);
+ATF_TEST_CASE_BODY(child__fork_files__fork_cannot_exit)
+{
+ const pid_t parent_pid = ::getpid();
+ atf::utils::create_file("to-not-be-deleted", "");
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_return, fs::path("out"), fs::path("err"));
+ if (::getpid() != parent_pid) {
+ // If we enter this clause, it is because the hook returned.
+ ::unlink("to-not-be-deleted");
+ std::exit(EXIT_SUCCESS);
+ }
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted")));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_cannot_unwind);
+ATF_TEST_CASE_BODY(child__fork_files__fork_cannot_unwind)
+{
+ const pid_t parent_pid = ::getpid();
+ atf::utils::create_file("to-not-be-deleted", "");
+ try {
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_raise_exception< int, 123 >, fs::path("out"),
+ fs::path("err"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted")));
+ } catch (const int i) {
+ // If we enter this clause, it is because an exception leaked from the
+ // hook.
+ INV(parent_pid != ::getpid());
+ INV(i == 123);
+ ::unlink("to-not-be-deleted");
+ std::exit(EXIT_SUCCESS);
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_fail);
+ATF_TEST_CASE_BODY(child__fork_files__fork_fail)
+{
+ process::detail::syscall_fork = fork_fail< 1234 >;
+ try {
+ process::child::fork_files(child_simple_function< 1, 'A' >,
+ fs::path("a.txt"), fs::path("b.txt"));
+ fail("Expected exception but none raised");
+ } catch (const process::system_error& e) {
+ ATF_REQUIRE(atf::utils::grep_string("fork.*failed", e.what()));
+ ATF_REQUIRE_EQ(1234, e.original_errno());
+ }
+ ATF_REQUIRE(!fs::exists(fs::path("a.txt")));
+ ATF_REQUIRE(!fs::exists(fs::path("b.txt")));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__create_stdout_fail);
+ATF_TEST_CASE_BODY(child__fork_files__create_stdout_fail)
+{
+ process::detail::syscall_open = open_fail< ENOENT >;
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_simple_function< 1, 'A' >, fs::path("raise-error"),
+ fs::path("created"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+ ATF_REQUIRE(!fs::exists(fs::path("raise-error")));
+ ATF_REQUIRE(!fs::exists(fs::path("created")));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__create_stderr_fail);
+ATF_TEST_CASE_BODY(child__fork_files__create_stderr_fail)
+{
+ process::detail::syscall_open = open_fail< ENOENT >;
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_simple_function< 1, 'A' >, fs::path("created"),
+ fs::path("raise-error"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+ ATF_REQUIRE(fs::exists(fs::path("created")));
+ ATF_REQUIRE(!fs::exists(fs::path("raise-error")));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__absolute_path);
+ATF_TEST_CASE_BODY(child__spawn__absolute_path)
+{
+ std::vector< std::string > args;
+ args.push_back("return-code");
+ args.push_back("12");
+
+ const fs::path program = get_helpers(this);
+ INV(program.is_absolute());
+ std::auto_ptr< process::child > child = process::child::spawn_files(
+ program, args, fs::path("out"), fs::path("err"));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(12, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__relative_path);
+ATF_TEST_CASE_BODY(child__spawn__relative_path)
+{
+ std::vector< std::string > args;
+ args.push_back("return-code");
+ args.push_back("13");
+
+ ATF_REQUIRE(::mkdir("root", 0755) != -1);
+ ATF_REQUIRE(::symlink(get_helpers(this).c_str(), "root/helpers") != -1);
+
+ std::auto_ptr< process::child > child = process::child::spawn_files(
+ fs::path("root/helpers"), args, fs::path("out"), fs::path("err"));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(13, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__basename_only);
+ATF_TEST_CASE_BODY(child__spawn__basename_only)
+{
+ std::vector< std::string > args;
+ args.push_back("return-code");
+ args.push_back("14");
+
+ ATF_REQUIRE(::symlink(get_helpers(this).c_str(), "helpers") != -1);
+
+ std::auto_ptr< process::child > child = process::child::spawn_files(
+ fs::path("helpers"), args, fs::path("out"), fs::path("err"));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(14, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__no_path);
+ATF_TEST_CASE_BODY(child__spawn__no_path)
+{
+ logging::set_inmemory();
+
+ std::vector< std::string > args;
+ args.push_back("return-code");
+ args.push_back("14");
+
+ const fs::path helpers = get_helpers(this);
+ utils::setenv("PATH", helpers.branch_path().c_str());
+ std::auto_ptr< process::child > child = process::child::spawn_capture(
+ fs::path(helpers.leaf_name()), args);
+
+ std::string line;
+ ATF_REQUIRE(std::getline(child->output(), line).good());
+ ATF_REQUIRE_MATCH("Failed to execute", line);
+ ATF_REQUIRE(!std::getline(child->output(), line));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__no_args);
+ATF_TEST_CASE_BODY(child__spawn__no_args)
+{
+ std::vector< std::string > args;
+ std::auto_ptr< process::child > child = process::child::spawn_capture(
+ get_helpers(this), args);
+
+ std::string line;
+ ATF_REQUIRE(std::getline(child->output(), line).good());
+ ATF_REQUIRE_EQ("Must provide a helper name", line);
+ ATF_REQUIRE(!std::getline(child->output(), line));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_FAILURE, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__some_args);
+ATF_TEST_CASE_BODY(child__spawn__some_args)
+{
+ std::vector< std::string > args;
+ args.push_back("print-args");
+ args.push_back("foo");
+ args.push_back(" bar baz ");
+ std::auto_ptr< process::child > child = process::child::spawn_capture(
+ get_helpers(this), args);
+
+ std::string line;
+ ATF_REQUIRE(std::getline(child->output(), line).good());
+ ATF_REQUIRE_EQ("argv[0] = " + get_helpers(this).str(), line);
+ ATF_REQUIRE(std::getline(child->output(), line).good());
+ ATF_REQUIRE_EQ("argv[1] = print-args", line);
+ ATF_REQUIRE(std::getline(child->output(), line));
+ ATF_REQUIRE_EQ("argv[2] = foo", line);
+ ATF_REQUIRE(std::getline(child->output(), line));
+ ATF_REQUIRE_EQ("argv[3] = bar baz ", line);
+ ATF_REQUIRE(std::getline(child->output(), line));
+ ATF_REQUIRE_EQ("argv[4] = NULL", line);
+ ATF_REQUIRE(!std::getline(child->output(), line));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__missing_program);
+ATF_TEST_CASE_BODY(child__spawn__missing_program)
+{
+ std::vector< std::string > args;
+ std::auto_ptr< process::child > child = process::child::spawn_capture(
+ fs::path("a/b/c"), args);
+
+ std::string line;
+ ATF_REQUIRE(std::getline(child->output(), line).good());
+ const std::string exp = "Failed to execute a/b/c: ";
+ ATF_REQUIRE_EQ(exp, line.substr(0, exp.length()));
+ ATF_REQUIRE(!std::getline(child->output(), line));
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(child__pid);
+ATF_TEST_CASE_BODY(child__pid)
+{
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_write_pid);
+
+ const int pid = child->pid();
+
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+
+ std::ifstream input("pidfile");
+ ATF_REQUIRE(input);
+ int read_pid;
+ input >> read_pid;
+ input.close();
+
+ ATF_REQUIRE_EQ(read_pid, pid);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ utils::avoid_coredump_on_crash();
+
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__ok_function);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__ok_functor);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__catch_exceptions);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__new_session);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__pipe_fail);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_cannot_exit);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_cannot_unwind);
+ ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_fail);
+
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__ok_function);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__ok_functor);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__catch_exceptions);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__new_session);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__inherit_stdout);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__inherit_stderr);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_cannot_exit);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_cannot_unwind);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_fail);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__create_stdout_fail);
+ ATF_ADD_TEST_CASE(tcs, child__fork_files__create_stderr_fail);
+
+ ATF_ADD_TEST_CASE(tcs, child__spawn__absolute_path);
+ ATF_ADD_TEST_CASE(tcs, child__spawn__relative_path);
+ ATF_ADD_TEST_CASE(tcs, child__spawn__basename_only);
+ ATF_ADD_TEST_CASE(tcs, child__spawn__no_path);
+ ATF_ADD_TEST_CASE(tcs, child__spawn__no_args);
+ ATF_ADD_TEST_CASE(tcs, child__spawn__some_args);
+ ATF_ADD_TEST_CASE(tcs, child__spawn__missing_program);
+
+ ATF_ADD_TEST_CASE(tcs, child__pid);
+}
diff --git a/utils/process/deadline_killer.cpp b/utils/process/deadline_killer.cpp
new file mode 100644
index 000000000000..ed733e402f76
--- /dev/null
+++ b/utils/process/deadline_killer.cpp
@@ -0,0 +1,54 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/deadline_killer.hpp"
+
+#include "utils/datetime.hpp"
+#include "utils/process/operations.hpp"
+
+namespace datetime = utils::datetime;
+namespace process = utils::process;
+
+
+/// Constructor.
+///
+/// \param delta Time to the timer activation.
+/// \param pid PID of the process (and process group) to kill.
+process::deadline_killer::deadline_killer(const datetime::delta& delta,
+ const int pid) :
+ signals::timer(delta), _pid(pid)
+{
+}
+
+
+/// Timer activation callback.
+void
+process::deadline_killer::callback(void)
+{
+ process::terminate_group(_pid);
+}
diff --git a/utils/process/deadline_killer.hpp b/utils/process/deadline_killer.hpp
new file mode 100644
index 000000000000..8b337a0f9d8c
--- /dev/null
+++ b/utils/process/deadline_killer.hpp
@@ -0,0 +1,58 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/deadline_killer.hpp
+/// Timer to kill a process on activation.
+
+#if !defined(UTILS_PROCESS_DEADLINE_KILLER_HPP)
+#define UTILS_PROCESS_DEADLINE_KILLER_HPP
+
+#include "utils/process/deadline_killer_fwd.hpp"
+
+#include "utils/signals/timer.hpp"
+
+namespace utils {
+namespace process {
+
+
+/// Timer that forcibly kills a process group on activation.
+class deadline_killer : public utils::signals::timer {
+ /// PID of the process (and process group) to kill.
+ const int _pid;
+
+ void callback(void);
+
+public:
+ deadline_killer(const datetime::delta&, const int);
+};
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_DEADLINE_KILLER_HPP)
diff --git a/utils/process/deadline_killer_fwd.hpp b/utils/process/deadline_killer_fwd.hpp
new file mode 100644
index 000000000000..fca3c5dc57c7
--- /dev/null
+++ b/utils/process/deadline_killer_fwd.hpp
@@ -0,0 +1,45 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/deadline_killer_fwd.hpp
+/// Forward declarations for utils/process/deadline_killer.hpp
+
+#if !defined(UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP)
+#define UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP
+
+namespace utils {
+namespace process {
+
+
+class deadline_killer;
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP)
diff --git a/utils/process/deadline_killer_test.cpp b/utils/process/deadline_killer_test.cpp
new file mode 100644
index 000000000000..06c89660ac31
--- /dev/null
+++ b/utils/process/deadline_killer_test.cpp
@@ -0,0 +1,108 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/deadline_killer.hpp"
+
+extern "C" {
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#include "utils/datetime.hpp"
+#include "utils/process/child.ipp"
+#include "utils/process/status.hpp"
+
+namespace datetime = utils::datetime;
+namespace process = utils::process;
+
+
+namespace {
+
+
+/// Body of a child process that sleeps and then exits.
+///
+/// \tparam Seconds The delay the subprocess has to sleep for.
+template< int Seconds >
+static void
+child_sleep(void)
+{
+ ::sleep(Seconds);
+ std::exit(EXIT_SUCCESS);
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(activation);
+ATF_TEST_CASE_BODY(activation)
+{
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_sleep< 60 >);
+
+ datetime::timestamp start = datetime::timestamp::now();
+ process::deadline_killer killer(datetime::delta(1, 0), child->pid());
+ const process::status status = child->wait();
+ killer.unprogram();
+ datetime::timestamp end = datetime::timestamp::now();
+
+ ATF_REQUIRE(killer.fired());
+ ATF_REQUIRE(end - start <= datetime::delta(10, 0));
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGKILL, status.termsig());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(no_activation);
+ATF_TEST_CASE_BODY(no_activation)
+{
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_sleep< 1 >);
+
+ datetime::timestamp start = datetime::timestamp::now();
+ process::deadline_killer killer(datetime::delta(60, 0), child->pid());
+ const process::status status = child->wait();
+ killer.unprogram();
+ datetime::timestamp end = datetime::timestamp::now();
+
+ ATF_REQUIRE(!killer.fired());
+ ATF_REQUIRE(end - start <= datetime::delta(10, 0));
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, activation);
+ ATF_ADD_TEST_CASE(tcs, no_activation);
+}
diff --git a/utils/process/exceptions.cpp b/utils/process/exceptions.cpp
new file mode 100644
index 000000000000..d7590c330499
--- /dev/null
+++ b/utils/process/exceptions.cpp
@@ -0,0 +1,91 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/exceptions.hpp"
+
+#include <cstring>
+
+#include "utils/format/macros.hpp"
+
+namespace process = utils::process;
+
+
+/// Constructs a new error with a plain-text message.
+///
+/// \param message The plain-text error message.
+process::error::error(const std::string& message) :
+ std::runtime_error(message)
+{
+}
+
+
+/// Destructor for the error.
+process::error::~error(void) throw()
+{
+}
+
+
+/// Constructs a new error based on an errno code.
+///
+/// \param message_ The message describing what caused the error.
+/// \param errno_ The error code.
+process::system_error::system_error(const std::string& message_,
+ const int errno_) :
+ error(F("%s: %s") % message_ % strerror(errno_)),
+ _original_errno(errno_)
+{
+}
+
+
+/// Destructor for the error.
+process::system_error::~system_error(void) throw()
+{
+}
+
+
+/// \return The original errno value.
+int
+process::system_error::original_errno(void) const throw()
+{
+ return _original_errno;
+}
+
+
+/// Constructs a new timeout_error.
+///
+/// \param message_ The message describing what caused the error.
+process::timeout_error::timeout_error(const std::string& message_) :
+ error(message_)
+{
+}
+
+
+/// Destructor for the error.
+process::timeout_error::~timeout_error(void) throw()
+{
+}
diff --git a/utils/process/exceptions.hpp b/utils/process/exceptions.hpp
new file mode 100644
index 000000000000..3bf740459864
--- /dev/null
+++ b/utils/process/exceptions.hpp
@@ -0,0 +1,78 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/exceptions.hpp
+/// Exception types raised by the process module.
+
+#if !defined(UTILS_PROCESS_EXCEPTIONS_HPP)
+#define UTILS_PROCESS_EXCEPTIONS_HPP
+
+#include <stdexcept>
+
+namespace utils {
+namespace process {
+
+
+/// Base exceptions for process errors.
+class error : public std::runtime_error {
+public:
+ explicit error(const std::string&);
+ ~error(void) throw();
+};
+
+
+/// Exceptions for errno-based errors.
+///
+/// TODO(jmmv): This code is duplicated in, at least, utils::fs. Figure
+/// out a way to reuse this exception while maintaining the correct inheritance
+/// (i.e. be able to keep it as a child of process::error).
+class system_error : public error {
+ /// Error number describing this libc error condition.
+ int _original_errno;
+
+public:
+ explicit system_error(const std::string&, const int);
+ ~system_error(void) throw();
+
+ int original_errno(void) const throw();
+};
+
+
+/// Denotes that a deadline was exceeded.
+class timeout_error : public error {
+public:
+ explicit timeout_error(const std::string&);
+ ~timeout_error(void) throw();
+};
+
+
+} // namespace process
+} // namespace utils
+
+
+#endif // !defined(UTILS_PROCESS_EXCEPTIONS_HPP)
diff --git a/utils/process/exceptions_test.cpp b/utils/process/exceptions_test.cpp
new file mode 100644
index 000000000000..375b635fc173
--- /dev/null
+++ b/utils/process/exceptions_test.cpp
@@ -0,0 +1,63 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/exceptions.hpp"
+
+#include <cerrno>
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+#include "utils/format/macros.hpp"
+
+namespace process = utils::process;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(error);
+ATF_TEST_CASE_BODY(error)
+{
+ const process::error e("Some text");
+ ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(system_error);
+ATF_TEST_CASE_BODY(system_error)
+{
+ const process::system_error e("Call failed", ENOENT);
+ const std::string expected = F("Call failed: %s") % std::strerror(ENOENT);
+ ATF_REQUIRE_EQ(expected, e.what());
+ ATF_REQUIRE_EQ(ENOENT, e.original_errno());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, error);
+ ATF_ADD_TEST_CASE(tcs, system_error);
+}
diff --git a/utils/process/executor.cpp b/utils/process/executor.cpp
new file mode 100644
index 000000000000..dbdf31268f86
--- /dev/null
+++ b/utils/process/executor.cpp
@@ -0,0 +1,869 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/executor.ipp"
+
+#if defined(HAVE_CONFIG_H)
+#include "config.h"
+#endif
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+}
+
+#include <fstream>
+#include <map>
+#include <memory>
+#include <stdexcept>
+
+#include "utils/datetime.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/auto_cleaners.hpp"
+#include "utils/fs/exceptions.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/optional.ipp"
+#include "utils/passwd.hpp"
+#include "utils/process/child.ipp"
+#include "utils/process/deadline_killer.hpp"
+#include "utils/process/isolation.hpp"
+#include "utils/process/operations.hpp"
+#include "utils/process/status.hpp"
+#include "utils/sanity.hpp"
+#include "utils/signals/interrupts.hpp"
+#include "utils/signals/timer.hpp"
+
+namespace datetime = utils::datetime;
+namespace executor = utils::process::executor;
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace passwd = utils::passwd;
+namespace process = utils::process;
+namespace signals = utils::signals;
+
+using utils::none;
+using utils::optional;
+
+
+namespace {
+
+
+/// Template for temporary directories created by the executor.
+static const char* work_directory_template = PACKAGE_TARNAME ".XXXXXX";
+
+
+/// Mapping of active subprocess PIDs to their execution data.
+typedef std::map< int, executor::exec_handle > exec_handles_map;
+
+
+} // anonymous namespace
+
+
+/// Basename of the file containing the stdout of the subprocess.
+const char* utils::process::executor::detail::stdout_name = "stdout.txt";
+
+
+/// Basename of the file containing the stderr of the subprocess.
+const char* utils::process::executor::detail::stderr_name = "stderr.txt";
+
+
+/// Basename of the subdirectory in which the subprocess is actually executed.
+///
+/// This is a subdirectory of the "unique work directory" generated for the
+/// subprocess so that our code can create control files on disk and not
+/// get them clobbered by the subprocess's activity.
+const char* utils::process::executor::detail::work_subdir = "work";
+
+
+/// Prepares a subprocess to run a user-provided hook in a controlled manner.
+///
+/// \param unprivileged_user User to switch to if not none.
+/// \param control_directory Path to the subprocess-specific control directory.
+/// \param work_directory Path to the subprocess-specific work directory.
+void
+utils::process::executor::detail::setup_child(
+ const optional< passwd::user > unprivileged_user,
+ const fs::path& control_directory,
+ const fs::path& work_directory)
+{
+ logging::set_inmemory();
+ process::isolate_path(unprivileged_user, control_directory);
+ process::isolate_child(unprivileged_user, work_directory);
+}
+
+
+/// Internal implementation for the exit_handle class.
+struct utils::process::executor::exec_handle::impl : utils::noncopyable {
+ /// PID of the process being run.
+ int pid;
+
+ /// Path to the subprocess-specific work directory.
+ fs::path control_directory;
+
+ /// Path to the subprocess's stdout file.
+ const fs::path stdout_file;
+
+ /// Path to the subprocess's stderr file.
+ const fs::path stderr_file;
+
+ /// Start time.
+ datetime::timestamp start_time;
+
+ /// User the subprocess is running as if different than the current one.
+ const optional< passwd::user > unprivileged_user;
+
+ /// Timer to kill the subprocess on activation.
+ process::deadline_killer timer;
+
+ /// Number of owners of the on-disk state.
+ executor::detail::refcnt_t state_owners;
+
+ /// Constructor.
+ ///
+ /// \param pid_ PID of the forked process.
+ /// \param control_directory_ Path to the subprocess-specific work
+ /// directory.
+ /// \param stdout_file_ Path to the subprocess's stdout file.
+ /// \param stderr_file_ Path to the subprocess's stderr file.
+ /// \param start_time_ Timestamp of when this object was constructed.
+ /// \param timeout Maximum amount of time the subprocess can run for.
+ /// \param unprivileged_user_ User the subprocess is running as if
+ /// different than the current one.
+ /// \param [in,out] state_owners_ Number of owners of the on-disk state.
+ /// For first-time processes, this should be a new counter set to 0;
+ /// for followup processes, this should point to the same counter used
+ /// by the preceding process.
+ impl(const int pid_,
+ const fs::path& control_directory_,
+ const fs::path& stdout_file_,
+ const fs::path& stderr_file_,
+ const datetime::timestamp& start_time_,
+ const datetime::delta& timeout,
+ const optional< passwd::user > unprivileged_user_,
+ executor::detail::refcnt_t state_owners_) :
+ pid(pid_),
+ control_directory(control_directory_),
+ stdout_file(stdout_file_),
+ stderr_file(stderr_file_),
+ start_time(start_time_),
+ unprivileged_user(unprivileged_user_),
+ timer(timeout, pid_),
+ state_owners(state_owners_)
+ {
+ (*state_owners)++;
+ POST(*state_owners > 0);
+ }
+};
+
+
+/// Constructor.
+///
+/// \param pimpl Constructed internal implementation.
+executor::exec_handle::exec_handle(std::shared_ptr< impl > pimpl) :
+ _pimpl(pimpl)
+{
+}
+
+
+/// Destructor.
+executor::exec_handle::~exec_handle(void)
+{
+}
+
+
+/// Returns the PID of the process being run.
+///
+/// \return A PID.
+int
+executor::exec_handle::pid(void) const
+{
+ return _pimpl->pid;
+}
+
+
+/// Returns the path to the subprocess-specific control directory.
+///
+/// This is where the executor may store control files.
+///
+/// \return The path to a directory that exists until cleanup() is called.
+fs::path
+executor::exec_handle::control_directory(void) const
+{
+ return _pimpl->control_directory;
+}
+
+
+/// Returns the path to the subprocess-specific work directory.
+///
+/// This is guaranteed to be clear of files created by the executor.
+///
+/// \return The path to a directory that exists until cleanup() is called.
+fs::path
+executor::exec_handle::work_directory(void) const
+{
+ return _pimpl->control_directory / detail::work_subdir;
+}
+
+
+/// Returns the path to the subprocess's stdout file.
+///
+/// \return The path to a file that exists until cleanup() is called.
+const fs::path&
+executor::exec_handle::stdout_file(void) const
+{
+ return _pimpl->stdout_file;
+}
+
+
+/// Returns the path to the subprocess's stderr file.
+///
+/// \return The path to a file that exists until cleanup() is called.
+const fs::path&
+executor::exec_handle::stderr_file(void) const
+{
+ return _pimpl->stderr_file;
+}
+
+
+/// Internal implementation for the exit_handle class.
+struct utils::process::executor::exit_handle::impl : utils::noncopyable {
+ /// Original PID of the terminated subprocess.
+ ///
+ /// Note that this PID is no longer valid and cannot be used on system
+ /// tables!
+ const int original_pid;
+
+ /// Termination status of the subprocess, or none if it timed out.
+ const optional< process::status > status;
+
+ /// The user the process ran as, if different than the current one.
+ const optional< passwd::user > unprivileged_user;
+
+ /// Timestamp of when the subprocess was spawned.
+ const datetime::timestamp start_time;
+
+ /// Timestamp of when wait() or wait_any() returned this object.
+ const datetime::timestamp end_time;
+
+ /// Path to the subprocess-specific work directory.
+ const fs::path control_directory;
+
+ /// Path to the subprocess's stdout file.
+ const fs::path stdout_file;
+
+ /// Path to the subprocess's stderr file.
+ const fs::path stderr_file;
+
+ /// Number of owners of the on-disk state.
+ ///
+ /// This will be 1 if this exit_handle is the last holder of the on-disk
+ /// state, in which case cleanup() invocations will wipe the disk state.
+ /// For all other cases, this will hold a higher value.
+ detail::refcnt_t state_owners;
+
+ /// Mutable pointer to the corresponding executor state.
+ ///
+ /// This object references a member of the executor_handle that yielded this
+ /// exit_handle instance. We need this direct access to clean up after
+ /// ourselves when the handle is destroyed.
+ exec_handles_map& all_exec_handles;
+
+ /// Whether the subprocess state has been cleaned yet or not.
+ ///
+ /// Used to keep track of explicit calls to the public cleanup().
+ bool cleaned;
+
+ /// Constructor.
+ ///
+ /// \param original_pid_ Original PID of the terminated subprocess.
+ /// \param status_ Termination status of the subprocess, or none if
+ /// timed out.
+ /// \param unprivileged_user_ The user the process ran as, if different than
+ /// the current one.
+ /// \param start_time_ Timestamp of when the subprocess was spawned.
+ /// \param end_time_ Timestamp of when wait() or wait_any() returned this
+ /// object.
+ /// \param control_directory_ Path to the subprocess-specific work
+ /// directory.
+ /// \param stdout_file_ Path to the subprocess's stdout file.
+ /// \param stderr_file_ Path to the subprocess's stderr file.
+ /// \param [in,out] state_owners_ Number of owners of the on-disk state.
+ /// \param [in,out] all_exec_handles_ Global object keeping track of all
+ /// active executions for an executor. This is a pointer to a member of
+ /// the executor_handle object.
+ impl(const int original_pid_,
+ const optional< process::status > status_,
+ const optional< passwd::user > unprivileged_user_,
+ const datetime::timestamp& start_time_,
+ const datetime::timestamp& end_time_,
+ const fs::path& control_directory_,
+ const fs::path& stdout_file_,
+ const fs::path& stderr_file_,
+ detail::refcnt_t state_owners_,
+ exec_handles_map& all_exec_handles_) :
+ original_pid(original_pid_), status(status_),
+ unprivileged_user(unprivileged_user_),
+ start_time(start_time_), end_time(end_time_),
+ control_directory(control_directory_),
+ stdout_file(stdout_file_), stderr_file(stderr_file_),
+ state_owners(state_owners_),
+ all_exec_handles(all_exec_handles_), cleaned(false)
+ {
+ }
+
+ /// Destructor.
+ ~impl(void)
+ {
+ if (!cleaned) {
+ LW(F("Implicitly cleaning up exit_handle for exec_handle %s; "
+ "ignoring errors!") % original_pid);
+ try {
+ cleanup();
+ } catch (const std::runtime_error& error) {
+ LE(F("Subprocess cleanup failed: %s") % error.what());
+ }
+ }
+ }
+
+ /// Cleans up the subprocess on-disk state.
+ ///
+ /// \throw engine::error If the cleanup fails, especially due to the
+ /// inability to remove the work directory.
+ void
+ cleanup(void)
+ {
+ PRE(*state_owners > 0);
+ if (*state_owners == 1) {
+ LI(F("Cleaning up exit_handle for exec_handle %s") % original_pid);
+ fs::rm_r(control_directory);
+ } else {
+ LI(F("Not cleaning up exit_handle for exec_handle %s; "
+ "%s owners left") % original_pid % (*state_owners - 1));
+ }
+ // We must decrease our reference only after we have successfully
+ // cleaned up the control directory. Otherwise, the rm_r call would
+ // throw an exception, which would in turn invoke the implicit cleanup
+ // from the destructor, which would make us crash due to an invalid
+ // reference count.
+ (*state_owners)--;
+ // Marking this object as clean here, even if we did not do actually the
+ // cleaning above, is fine (albeit a bit confusing). Note that "another
+ // owner" refers to a handle for a different PID, so that handle will be
+ // the one issuing the cleanup.
+ all_exec_handles.erase(original_pid);
+ cleaned = true;
+ }
+};
+
+
+/// Constructor.
+///
+/// \param pimpl Constructed internal implementation.
+executor::exit_handle::exit_handle(std::shared_ptr< impl > pimpl) :
+ _pimpl(pimpl)
+{
+}
+
+
+/// Destructor.
+executor::exit_handle::~exit_handle(void)
+{
+}
+
+
+/// Cleans up the subprocess status.
+///
+/// This function should be called explicitly as it provides the means to
+/// control any exceptions raised during cleanup. Do not rely on the destructor
+/// to clean things up.
+///
+/// \throw engine::error If the cleanup fails, especially due to the inability
+/// to remove the work directory.
+void
+executor::exit_handle::cleanup(void)
+{
+ PRE(!_pimpl->cleaned);
+ _pimpl->cleanup();
+ POST(_pimpl->cleaned);
+}
+
+
+/// Gets the current number of owners of the on-disk data.
+///
+/// \return A shared reference counter. Even though this function is marked as
+/// const, the return value is intentionally mutable because we need to update
+/// reference counts from different but related processes. This is why this
+/// method is not public.
+std::shared_ptr< std::size_t >
+executor::exit_handle::state_owners(void) const
+{
+ return _pimpl->state_owners;
+}
+
+
+/// Returns the original PID corresponding to the terminated subprocess.
+///
+/// \return An exec_handle.
+int
+executor::exit_handle::original_pid(void) const
+{
+ return _pimpl->original_pid;
+}
+
+
+/// Returns the process termination status of the subprocess.
+///
+/// \return A process termination status, or none if the subprocess timed out.
+const optional< process::status >&
+executor::exit_handle::status(void) const
+{
+ return _pimpl->status;
+}
+
+
+/// Returns the user the process ran as if different than the current one.
+///
+/// \return None if the credentials of the process were the same as the current
+/// one, or else a user.
+const optional< passwd::user >&
+executor::exit_handle::unprivileged_user(void) const
+{
+ return _pimpl->unprivileged_user;
+}
+
+
+/// Returns the timestamp of when the subprocess was spawned.
+///
+/// \return A timestamp.
+const datetime::timestamp&
+executor::exit_handle::start_time(void) const
+{
+ return _pimpl->start_time;
+}
+
+
+/// Returns the timestamp of when wait() or wait_any() returned this object.
+///
+/// \return A timestamp.
+const datetime::timestamp&
+executor::exit_handle::end_time(void) const
+{
+ return _pimpl->end_time;
+}
+
+
+/// Returns the path to the subprocess-specific control directory.
+///
+/// This is where the executor may store control files.
+///
+/// \return The path to a directory that exists until cleanup() is called.
+fs::path
+executor::exit_handle::control_directory(void) const
+{
+ return _pimpl->control_directory;
+}
+
+
+/// Returns the path to the subprocess-specific work directory.
+///
+/// This is guaranteed to be clear of files created by the executor.
+///
+/// \return The path to a directory that exists until cleanup() is called.
+fs::path
+executor::exit_handle::work_directory(void) const
+{
+ return _pimpl->control_directory / detail::work_subdir;
+}
+
+
+/// Returns the path to the subprocess's stdout file.
+///
+/// \return The path to a file that exists until cleanup() is called.
+const fs::path&
+executor::exit_handle::stdout_file(void) const
+{
+ return _pimpl->stdout_file;
+}
+
+
+/// Returns the path to the subprocess's stderr file.
+///
+/// \return The path to a file that exists until cleanup() is called.
+const fs::path&
+executor::exit_handle::stderr_file(void) const
+{
+ return _pimpl->stderr_file;
+}
+
+
+/// Internal implementation for the executor_handle.
+///
+/// Because the executor is a singleton, these essentially is a container for
+/// global variables.
+struct utils::process::executor::executor_handle::impl : utils::noncopyable {
+ /// Numeric counter of executed subprocesses.
+ ///
+ /// This is used to generate a unique identifier for each subprocess as an
+ /// easy mechanism to discern their unique work directories.
+ size_t last_subprocess;
+
+ /// Interrupts handler.
+ std::auto_ptr< signals::interrupts_handler > interrupts_handler;
+
+ /// Root work directory for all executed subprocesses.
+ std::auto_ptr< fs::auto_directory > root_work_directory;
+
+ /// Mapping of PIDs to the data required at run time.
+ exec_handles_map all_exec_handles;
+
+ /// Whether the executor state has been cleaned yet or not.
+ ///
+ /// Used to keep track of explicit calls to the public cleanup().
+ bool cleaned;
+
+ /// Constructor.
+ impl(void) :
+ last_subprocess(0),
+ interrupts_handler(new signals::interrupts_handler()),
+ root_work_directory(new fs::auto_directory(
+ fs::auto_directory::mkdtemp_public(work_directory_template))),
+ cleaned(false)
+ {
+ }
+
+ /// Destructor.
+ ~impl(void)
+ {
+ if (!cleaned) {
+ LW("Implicitly cleaning up executor; ignoring errors!");
+ try {
+ cleanup();
+ cleaned = true;
+ } catch (const std::runtime_error& error) {
+ LE(F("Executor global cleanup failed: %s") % error.what());
+ }
+ }
+ }
+
+ /// Cleans up the executor state.
+ void
+ cleanup(void)
+ {
+ PRE(!cleaned);
+
+ for (exec_handles_map::const_iterator iter = all_exec_handles.begin();
+ iter != all_exec_handles.end(); ++iter) {
+ const int& pid = (*iter).first;
+ const exec_handle& data = (*iter).second;
+
+ process::terminate_group(pid);
+ int status;
+ if (::waitpid(pid, &status, 0) == -1) {
+ // Should not happen.
+ LW(F("Failed to wait for PID %s") % pid);
+ }
+
+ try {
+ fs::rm_r(data.control_directory());
+ } catch (const fs::error& e) {
+ LE(F("Failed to clean up subprocess work directory %s: %s") %
+ data.control_directory() % e.what());
+ }
+ }
+ all_exec_handles.clear();
+
+ try {
+ // The following only causes the work directory to be deleted, not
+ // any of its contents, so we expect this to always succeed. This
+ // *should* be sufficient because, in the loop above, we have
+ // individually wiped the subdirectories of any still-unclean
+ // subprocesses.
+ root_work_directory->cleanup();
+ } catch (const fs::error& e) {
+ LE(F("Failed to clean up executor work directory %s: %s; this is "
+ "an internal error") % root_work_directory->directory()
+ % e.what());
+ }
+ root_work_directory.reset(NULL);
+
+ interrupts_handler->unprogram();
+ interrupts_handler.reset(NULL);
+ }
+
+ /// Common code to run after any of the wait calls.
+ ///
+ /// \param original_pid The PID of the terminated subprocess.
+ /// \param status The exit status of the terminated subprocess.
+ ///
+ /// \return A pointer to an object describing the waited-for subprocess.
+ executor::exit_handle
+ post_wait(const int original_pid, const process::status& status)
+ {
+ PRE(original_pid == status.dead_pid());
+ LI(F("Waited for subprocess with exec_handle %s") % original_pid);
+
+ process::terminate_group(status.dead_pid());
+
+ const exec_handles_map::iterator iter = all_exec_handles.find(
+ original_pid);
+ exec_handle& data = (*iter).second;
+ data._pimpl->timer.unprogram();
+
+ // It is tempting to assert here (and old code did) that, if the timer
+ // has fired, the process has been forcibly killed by us. This is not
+ // always the case though: for short-lived processes and with very short
+ // timeouts (think 1ms), it is possible for scheduling decisions to
+ // allow the subprocess to finish while at the same time cause the timer
+ // to fire. So we do not assert this any longer and just rely on the
+ // timer expiration to check if the process timed out or not. If the
+ // process did finish but the timer expired... oh well, we do not detect
+ // this correctly but we don't care because this should not really
+ // happen.
+
+ if (!fs::exists(data.stdout_file())) {
+ std::ofstream new_stdout(data.stdout_file().c_str());
+ }
+ if (!fs::exists(data.stderr_file())) {
+ std::ofstream new_stderr(data.stderr_file().c_str());
+ }
+
+ return exit_handle(std::shared_ptr< exit_handle::impl >(
+ new exit_handle::impl(
+ data.pid(),
+ data._pimpl->timer.fired() ?
+ none : utils::make_optional(status),
+ data._pimpl->unprivileged_user,
+ data._pimpl->start_time, datetime::timestamp::now(),
+ data.control_directory(),
+ data.stdout_file(),
+ data.stderr_file(),
+ data._pimpl->state_owners,
+ all_exec_handles)));
+ }
+};
+
+
+/// Constructor.
+executor::executor_handle::executor_handle(void) throw() : _pimpl(new impl())
+{
+}
+
+
+/// Destructor.
+executor::executor_handle::~executor_handle(void)
+{
+}
+
+
+/// Queries the path to the root of the work directory for all subprocesses.
+///
+/// \return A path.
+const fs::path&
+executor::executor_handle::root_work_directory(void) const
+{
+ return _pimpl->root_work_directory->directory();
+}
+
+
+/// Cleans up the executor state.
+///
+/// This function should be called explicitly as it provides the means to
+/// control any exceptions raised during cleanup. Do not rely on the destructor
+/// to clean things up.
+///
+/// \throw engine::error If there are problems cleaning up the executor.
+void
+executor::executor_handle::cleanup(void)
+{
+ PRE(!_pimpl->cleaned);
+ _pimpl->cleanup();
+ _pimpl->cleaned = true;
+}
+
+
+/// Initializes the executor.
+///
+/// \pre This function can only be called if there is no other executor_handle
+/// object alive.
+///
+/// \return A handle to the operations of the executor.
+executor::executor_handle
+executor::setup(void)
+{
+ return executor_handle();
+}
+
+
+/// Pre-helper for the spawn() method.
+///
+/// \return The created control directory for the subprocess.
+fs::path
+executor::executor_handle::spawn_pre(void)
+{
+ signals::check_interrupt();
+
+ ++_pimpl->last_subprocess;
+
+ const fs::path control_directory =
+ _pimpl->root_work_directory->directory() /
+ (F("%s") % _pimpl->last_subprocess);
+ fs::mkdir_p(control_directory / detail::work_subdir, 0755);
+
+ return control_directory;
+}
+
+
+/// Post-helper for the spawn() method.
+///
+/// \param control_directory Control directory as returned by spawn_pre().
+/// \param stdout_file Path to the subprocess' stdout.
+/// \param stderr_file Path to the subprocess' stderr.
+/// \param timeout Maximum amount of time the subprocess can run for.
+/// \param unprivileged_user If not none, user to switch to before execution.
+/// \param child The process created by spawn().
+///
+/// \return The execution handle of the started subprocess.
+executor::exec_handle
+executor::executor_handle::spawn_post(
+ const fs::path& control_directory,
+ const fs::path& stdout_file,
+ const fs::path& stderr_file,
+ const datetime::delta& timeout,
+ const optional< passwd::user > unprivileged_user,
+ std::auto_ptr< process::child > child)
+{
+ const exec_handle handle(std::shared_ptr< exec_handle::impl >(
+ new exec_handle::impl(
+ child->pid(),
+ control_directory,
+ stdout_file,
+ stderr_file,
+ datetime::timestamp::now(),
+ timeout,
+ unprivileged_user,
+ detail::refcnt_t(new detail::refcnt_t::element_type(0)))));
+ INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) ==
+ _pimpl->all_exec_handles.end(),
+ F("PID %s already in all_exec_handles; not properly cleaned "
+ "up or reused too fast") % handle.pid());;
+ _pimpl->all_exec_handles.insert(exec_handles_map::value_type(
+ handle.pid(), handle));
+ LI(F("Spawned subprocess with exec_handle %s") % handle.pid());
+ return handle;
+}
+
+
+/// Pre-helper for the spawn_followup() method.
+void
+executor::executor_handle::spawn_followup_pre(void)
+{
+ signals::check_interrupt();
+}
+
+
+/// Post-helper for the spawn_followup() method.
+///
+/// \param base Exit handle of the subprocess to use as context.
+/// \param timeout Maximum amount of time the subprocess can run for.
+/// \param child The process created by spawn_followup().
+///
+/// \return The execution handle of the started subprocess.
+executor::exec_handle
+executor::executor_handle::spawn_followup_post(
+ const exit_handle& base,
+ const datetime::delta& timeout,
+ std::auto_ptr< process::child > child)
+{
+ INV(*base.state_owners() > 0);
+ const exec_handle handle(std::shared_ptr< exec_handle::impl >(
+ new exec_handle::impl(
+ child->pid(),
+ base.control_directory(),
+ base.stdout_file(),
+ base.stderr_file(),
+ datetime::timestamp::now(),
+ timeout,
+ base.unprivileged_user(),
+ base.state_owners())));
+ INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) ==
+ _pimpl->all_exec_handles.end(),
+ F("PID %s already in all_exec_handles; not properly cleaned "
+ "up or reused too fast") % handle.pid());;
+ _pimpl->all_exec_handles.insert(exec_handles_map::value_type(
+ handle.pid(), handle));
+ LI(F("Spawned subprocess with exec_handle %s") % handle.pid());
+ return handle;
+}
+
+
+/// Waits for completion of any forked process.
+///
+/// \param exec_handle The handle of the process to wait for.
+///
+/// \return A pointer to an object describing the waited-for subprocess.
+executor::exit_handle
+executor::executor_handle::wait(const exec_handle exec_handle)
+{
+ signals::check_interrupt();
+ const process::status status = process::wait(exec_handle.pid());
+ return _pimpl->post_wait(exec_handle.pid(), status);
+}
+
+
+/// Waits for completion of any forked process.
+///
+/// \return A pointer to an object describing the waited-for subprocess.
+executor::exit_handle
+executor::executor_handle::wait_any(void)
+{
+ signals::check_interrupt();
+ const process::status status = process::wait_any();
+ return _pimpl->post_wait(status.dead_pid(), status);
+}
+
+
+/// Checks if an interrupt has fired.
+///
+/// Calls to this function should be sprinkled in strategic places through the
+/// code protected by an interrupts_handler object.
+///
+/// This is just a wrapper over signals::check_interrupt() to avoid leaking this
+/// dependency to the caller.
+///
+/// \throw signals::interrupted_error If there has been an interrupt.
+void
+executor::executor_handle::check_interrupt(void) const
+{
+ signals::check_interrupt();
+}
diff --git a/utils/process/executor.hpp b/utils/process/executor.hpp
new file mode 100644
index 000000000000..858ad9c815aa
--- /dev/null
+++ b/utils/process/executor.hpp
@@ -0,0 +1,231 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/executor.hpp
+/// Multiprogrammed process executor with isolation guarantees.
+///
+/// This module provides a mechanism to invoke more than one process
+/// concurrently while at the same time ensuring that each process is run
+/// in a clean container and in a "safe" work directory that gets cleaned
+/// up automatically on termination.
+///
+/// The intended workflow for using this module is the following:
+///
+/// 1) Initialize the executor using setup(). Keep the returned object
+/// around through the lifetime of the next operations. Only one
+/// instance of the executor can be alive at once.
+/// 2) Spawn one or more processes with spawn(). On the caller side, keep
+/// track of any per-process data you may need using the returned
+/// exec_handle, which is unique among the set of active processes.
+/// 3) Call wait() or wait_any() to wait for completion of a process started
+/// in the previous step. Repeat as desired.
+/// 4) Use the returned exit_handle object by wait() or wait_any() to query
+/// the status of the terminated process and/or to access any of its
+/// data files.
+/// 5) Invoke cleanup() on the exit_handle to wipe any stale data.
+/// 6) Invoke cleanup() on the object returned by setup().
+///
+/// It is the responsibility of the caller to ensure that calls to
+/// spawn() and spawn_followup() are balanced with wait() and wait_any() calls.
+///
+/// Processes executed in this manner have access to two different "unique"
+/// directories: the first is the "work directory", which is an empty directory
+/// that acts as the subprocess' work directory; the second is the "control
+/// directory", which is the location where the in-process code may place files
+/// that are not clobbered by activities in the work directory.
+
+#if !defined(UTILS_PROCESS_EXECUTOR_HPP)
+#define UTILS_PROCESS_EXECUTOR_HPP
+
+#include "utils/process/executor_fwd.hpp"
+
+#include <cstddef>
+#include <memory>
+
+#include "utils/datetime_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/optional.hpp"
+#include "utils/passwd_fwd.hpp"
+#include "utils/process/child_fwd.hpp"
+#include "utils/process/status_fwd.hpp"
+
+namespace utils {
+namespace process {
+namespace executor {
+
+
+namespace detail {
+
+
+extern const char* stdout_name;
+extern const char* stderr_name;
+extern const char* work_subdir;
+
+
+/// Shared reference counter.
+typedef std::shared_ptr< std::size_t > refcnt_t;
+
+
+void setup_child(const utils::optional< utils::passwd::user >,
+ const utils::fs::path&, const utils::fs::path&);
+
+
+} // namespace detail
+
+
+/// Maintenance data held while a subprocess is being executed.
+///
+/// This data structure exists from the moment a subprocess is executed via
+/// executor::spawn() to when it is cleaned up with exit_handle::cleanup().
+///
+/// The caller NEED NOT maintain this object alive for the execution of the
+/// subprocess. However, the PID contained in here can be used to match
+/// exec_handle objects with corresponding exit_handle objects via their
+/// original_pid() method.
+///
+/// Objects of this type can be copied around but their implementation is
+/// shared. The implication of this is that only the last copy of a given exit
+/// handle will execute the automatic cleanup() on destruction.
+class exec_handle {
+ struct impl;
+
+ /// Pointer to internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class executor_handle;
+ exec_handle(std::shared_ptr< impl >);
+
+public:
+ ~exec_handle(void);
+
+ int pid(void) const;
+ utils::fs::path control_directory(void) const;
+ utils::fs::path work_directory(void) const;
+ const utils::fs::path& stdout_file(void) const;
+ const utils::fs::path& stderr_file(void) const;
+};
+
+
+/// Container for the data of a process termination.
+///
+/// This handle provides access to the details of the process that terminated
+/// and serves as the owner of the remaining on-disk files. The caller is
+/// expected to call cleanup() before destruction to remove the on-disk state.
+///
+/// Objects of this type can be copied around but their implementation is
+/// shared. The implication of this is that only the last copy of a given exit
+/// handle will execute the automatic cleanup() on destruction.
+class exit_handle {
+ struct impl;
+
+ /// Pointer to internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class executor_handle;
+ exit_handle(std::shared_ptr< impl >);
+
+ detail::refcnt_t state_owners(void) const;
+
+public:
+ ~exit_handle(void);
+
+ void cleanup(void);
+
+ int original_pid(void) const;
+ const utils::optional< utils::process::status >& status(void) const;
+ const utils::optional< utils::passwd::user >& unprivileged_user(void) const;
+ const utils::datetime::timestamp& start_time() const;
+ const utils::datetime::timestamp& end_time() const;
+ utils::fs::path control_directory(void) const;
+ utils::fs::path work_directory(void) const;
+ const utils::fs::path& stdout_file(void) const;
+ const utils::fs::path& stderr_file(void) const;
+};
+
+
+/// Handler for the livelihood of the executor.
+///
+/// Objects of this type can be copied around (because we do not have move
+/// semantics...) but their implementation is shared. Only one instance of the
+/// executor can exist at any point in time.
+class executor_handle {
+ struct impl;
+ /// Pointer to internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend executor_handle setup(void);
+ executor_handle(void) throw();
+
+ utils::fs::path spawn_pre(void);
+ exec_handle spawn_post(const utils::fs::path&,
+ const utils::fs::path&,
+ const utils::fs::path&,
+ const utils::datetime::delta&,
+ const utils::optional< utils::passwd::user >,
+ std::auto_ptr< utils::process::child >);
+
+ void spawn_followup_pre(void);
+ exec_handle spawn_followup_post(const exit_handle&,
+ const utils::datetime::delta&,
+ std::auto_ptr< utils::process::child >);
+
+public:
+ ~executor_handle(void);
+
+ const utils::fs::path& root_work_directory(void) const;
+
+ void cleanup(void);
+
+ template< class Hook >
+ exec_handle spawn(Hook,
+ const datetime::delta&,
+ const utils::optional< utils::passwd::user >,
+ const utils::optional< utils::fs::path > = utils::none,
+ const utils::optional< utils::fs::path > = utils::none);
+
+ template< class Hook >
+ exec_handle spawn_followup(Hook,
+ const exit_handle&,
+ const datetime::delta&);
+
+ exit_handle wait(const exec_handle);
+ exit_handle wait_any(void);
+
+ void check_interrupt(void) const;
+};
+
+
+executor_handle setup(void);
+
+
+} // namespace executor
+} // namespace process
+} // namespace utils
+
+
+#endif // !defined(UTILS_PROCESS_EXECUTOR_HPP)
diff --git a/utils/process/executor.ipp b/utils/process/executor.ipp
new file mode 100644
index 000000000000..e91f994673d7
--- /dev/null
+++ b/utils/process/executor.ipp
@@ -0,0 +1,182 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#if !defined(UTILS_PROCESS_EXECUTOR_IPP)
+#define UTILS_PROCESS_EXECUTOR_IPP
+
+#include "utils/process/executor.hpp"
+
+#include "utils/fs/path.hpp"
+#include "utils/optional.ipp"
+#include "utils/passwd.hpp"
+#include "utils/process/child.ipp"
+
+namespace utils {
+namespace process {
+
+
+namespace executor {
+namespace detail {
+
+/// Functor to execute a hook in a child process.
+///
+/// The hook is executed after the process has been isolated per the logic in
+/// utils::process::isolation based on the input parameters at construction
+/// time.
+template< class Hook >
+class run_child {
+ /// Function or functor to invoke in the child.
+ Hook _hook;
+
+ /// Directory where the hook may place control files.
+ const fs::path& _control_directory;
+
+ /// Directory to enter when running the subprocess.
+ ///
+ /// This is a subdirectory of _control_directory but is separate so that
+ /// subprocess operations do not inadvertently affect our files.
+ const fs::path& _work_directory;
+
+ /// User to switch to when running the subprocess.
+ ///
+ /// If not none, the subprocess will be executed as the provided user and
+ /// the control and work directories will be writable by this user.
+ const optional< passwd::user > _unprivileged_user;
+
+public:
+ /// Constructor.
+ ///
+ /// \param hook Function or functor to invoke in the child.
+ /// \param control_directory Directory where control files can be placed.
+ /// \param work_directory Directory to enter when running the subprocess.
+ /// \param unprivileged_user If set, user to switch to before execution.
+ run_child(Hook hook,
+ const fs::path& control_directory,
+ const fs::path& work_directory,
+ const optional< passwd::user > unprivileged_user) :
+ _hook(hook),
+ _control_directory(control_directory),
+ _work_directory(work_directory),
+ _unprivileged_user(unprivileged_user)
+ {
+ }
+
+ /// Body of the subprocess.
+ void
+ operator()(void)
+ {
+ executor::detail::setup_child(_unprivileged_user,
+ _control_directory, _work_directory);
+ _hook(_control_directory);
+ }
+};
+
+} // namespace detail
+} // namespace executor
+
+
+/// Forks and executes a subprocess asynchronously.
+///
+/// \tparam Hook Type of the hook.
+/// \param hook Function or functor to run in the subprocess.
+/// \param timeout Maximum amount of time the subprocess can run for.
+/// \param unprivileged_user If not none, user to switch to before execution.
+/// \param stdout_target If not none, file to which to write the stdout of the
+/// test case.
+/// \param stderr_target If not none, file to which to write the stderr of the
+/// test case.
+///
+/// \return A handle for the background operation. Used to match the result of
+/// the execution returned by wait_any() with this invocation.
+template< class Hook >
+executor::exec_handle
+executor::executor_handle::spawn(
+ Hook hook,
+ const datetime::delta& timeout,
+ const optional< passwd::user > unprivileged_user,
+ const optional< fs::path > stdout_target,
+ const optional< fs::path > stderr_target)
+{
+ const fs::path unique_work_directory = spawn_pre();
+
+ const fs::path stdout_path = stdout_target ?
+ stdout_target.get() : (unique_work_directory / detail::stdout_name);
+ const fs::path stderr_path = stderr_target ?
+ stderr_target.get() : (unique_work_directory / detail::stderr_name);
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ detail::run_child< Hook >(hook,
+ unique_work_directory,
+ unique_work_directory / detail::work_subdir,
+ unprivileged_user),
+ stdout_path, stderr_path);
+
+ return spawn_post(unique_work_directory, stdout_path, stderr_path,
+ timeout, unprivileged_user, child);
+}
+
+
+/// Forks and executes a subprocess asynchronously in the context of another.
+///
+/// By context we understand the on-disk state of a previously-executed process,
+/// thus the new subprocess spawned by this function will run with the same
+/// control and work directories as another process.
+///
+/// \tparam Hook Type of the hook.
+/// \param hook Function or functor to run in the subprocess.
+/// \param base Context of the subprocess in which to run this one. The
+/// exit_handle provided here must remain alive throughout the existence of
+/// this other object because the original exit_handle is the one that owns
+/// the on-disk state.
+/// \param timeout Maximum amount of time the subprocess can run for.
+///
+/// \return A handle for the background operation. Used to match the result of
+/// the execution returned by wait_any() with this invocation.
+template< class Hook >
+executor::exec_handle
+executor::executor_handle::spawn_followup(Hook hook,
+ const exit_handle& base,
+ const datetime::delta& timeout)
+{
+ spawn_followup_pre();
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ detail::run_child< Hook >(hook,
+ base.control_directory(),
+ base.work_directory(),
+ base.unprivileged_user()),
+ base.stdout_file(), base.stderr_file());
+
+ return spawn_followup_post(base, timeout, child);
+}
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_EXECUTOR_IPP)
diff --git a/utils/process/executor_fwd.hpp b/utils/process/executor_fwd.hpp
new file mode 100644
index 000000000000..ec63227993f3
--- /dev/null
+++ b/utils/process/executor_fwd.hpp
@@ -0,0 +1,49 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/executor_fwd.hpp
+/// Forward declarations for utils/process/executor.hpp
+
+#if !defined(UTILS_PROCESS_EXECUTOR_FWD_HPP)
+#define UTILS_PROCESS_EXECUTOR_FWD_HPP
+
+namespace utils {
+namespace process {
+namespace executor {
+
+
+class exec_handle;
+class executor_handle;
+class exit_handle;
+
+
+} // namespace executor
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_EXECUTOR_FWD_HPP)
diff --git a/utils/process/executor_test.cpp b/utils/process/executor_test.cpp
new file mode 100644
index 000000000000..13ae69bd44ed
--- /dev/null
+++ b/utils/process/executor_test.cpp
@@ -0,0 +1,940 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/executor.ipp"
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#include "utils/datetime.hpp"
+#include "utils/defs.hpp"
+#include "utils/env.hpp"
+#include "utils/format/containers.ipp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/optional.ipp"
+#include "utils/passwd.hpp"
+#include "utils/process/status.hpp"
+#include "utils/sanity.hpp"
+#include "utils/signals/exceptions.hpp"
+#include "utils/stacktrace.hpp"
+#include "utils/text/exceptions.hpp"
+#include "utils/text/operations.ipp"
+
+namespace datetime = utils::datetime;
+namespace executor = utils::process::executor;
+namespace fs = utils::fs;
+namespace passwd = utils::passwd;
+namespace process = utils::process;
+namespace signals = utils::signals;
+namespace text = utils::text;
+
+using utils::none;
+using utils::optional;
+
+
+/// Large timeout for the processes we spawn.
+///
+/// This number is supposed to be (much) larger than the timeout of the test
+/// cases that use it so that children processes are not killed spuriously.
+static const datetime::delta infinite_timeout(1000000, 0);
+
+
+static void do_exit(const int) UTILS_NORETURN;
+
+
+/// Terminates a subprocess without invoking destructors.
+///
+/// This is just a simple wrapper over _exit(2) because we cannot use std::exit
+/// on exit from a subprocess. The reason is that we do not want to invoke any
+/// destructors as otherwise we'd clear up the global executor state by mistake.
+/// This wouldn't be a major problem if it wasn't because doing so deletes
+/// on-disk files and we want to leave them in place so that the parent process
+/// can test for them!
+///
+/// \param exit_code Code to exit with.
+static void
+do_exit(const int exit_code)
+{
+ std::cout.flush();
+ std::cerr.flush();
+ ::_exit(exit_code);
+}
+
+
+/// Subprocess that creates a cookie file in its work directory.
+class child_create_cookie {
+ /// Name of the cookie to create.
+ const std::string _cookie_name;
+
+public:
+ /// Constructor.
+ ///
+ /// \param cookie_name Name of the cookie to create.
+ child_create_cookie(const std::string& cookie_name) :
+ _cookie_name(cookie_name)
+ {
+ }
+
+ /// Runs the subprocess.
+ void
+ operator()(const fs::path& /* control_directory */)
+ UTILS_NORETURN
+ {
+ std::cout << "Creating cookie: " << _cookie_name << " (stdout)\n";
+ std::cerr << "Creating cookie: " << _cookie_name << " (stderr)\n";
+ atf::utils::create_file(_cookie_name, "");
+ do_exit(EXIT_SUCCESS);
+ }
+};
+
+
+static void child_delete_all(const fs::path&) UTILS_NORETURN;
+
+
+/// Subprocess that deletes all files in the current directory.
+///
+/// This is intended to validate that the test runs in an empty directory,
+/// separate from any control files that the executor may have created.
+///
+/// \param control_directory Directory where control files separate from the
+/// work directory can be placed.
+static void
+child_delete_all(const fs::path& control_directory)
+{
+ const fs::path cookie = control_directory / "exec_was_called";
+ std::ofstream control_file(cookie.c_str());
+ if (!control_file) {
+ std::cerr << "Failed to create " << cookie << '\n';
+ std::abort();
+ }
+
+ const int exit_code = ::system("rm *") == -1
+ ? EXIT_FAILURE : EXIT_SUCCESS;
+ do_exit(exit_code);
+}
+
+
+static void child_dump_unprivileged_user(const fs::path&) UTILS_NORETURN;
+
+
+/// Subprocess that dumps user configuration.
+static void
+child_dump_unprivileged_user(const fs::path& /* control_directory */)
+{
+ const passwd::user current_user = passwd::current_user();
+ std::cout << F("UID = %s\n") % current_user.uid;
+ do_exit(EXIT_SUCCESS);
+}
+
+
+/// Subprocess that returns a specific exit code.
+class child_exit {
+ /// Exit code to return.
+ int _exit_code;
+
+public:
+ /// Constructor.
+ ///
+ /// \param exit_code Exit code to return.
+ child_exit(const int exit_code) : _exit_code(exit_code)
+ {
+ }
+
+ /// Runs the subprocess.
+ void
+ operator()(const fs::path& /* control_directory */)
+ UTILS_NORETURN
+ {
+ do_exit(_exit_code);
+ }
+};
+
+
+static void child_pause(const fs::path&) UTILS_NORETURN;
+
+
+/// Subprocess that just blocks.
+static void
+child_pause(const fs::path& /* control_directory */)
+{
+ sigset_t mask;
+ sigemptyset(&mask);
+ for (;;) {
+ ::sigsuspend(&mask);
+ }
+ std::abort();
+}
+
+
+static void child_print(const fs::path&) UTILS_NORETURN;
+
+
+/// Subprocess that writes to stdout and stderr.
+static void
+child_print(const fs::path& /* control_directory */)
+{
+ std::cout << "stdout: some text\n";
+ std::cerr << "stderr: some other text\n";
+
+ do_exit(EXIT_SUCCESS);
+}
+
+
+/// Subprocess that sleeps for a period of time before exiting.
+class child_sleep {
+ /// Seconds to sleep for before termination.
+ int _seconds;
+
+public:
+ /// Construtor.
+ ///
+ /// \param seconds Seconds to sleep for before termination.
+ child_sleep(const int seconds) : _seconds(seconds)
+ {
+ }
+
+ /// Runs the subprocess.
+ void
+ operator()(const fs::path& /* control_directory */)
+ UTILS_NORETURN
+ {
+ ::sleep(_seconds);
+ do_exit(EXIT_SUCCESS);
+ }
+};
+
+
+static void child_spawn_blocking_child(const fs::path&) UTILS_NORETURN;
+
+
+/// Subprocess that spawns a subchild that gets stuck.
+///
+/// Used by the caller to validate that the whole process tree is terminated
+/// when this subprocess is killed.
+static void
+child_spawn_blocking_child(
+ const fs::path& /* control_directory */)
+{
+ pid_t pid = ::fork();
+ if (pid == -1) {
+ std::cerr << "Cannot fork subprocess\n";
+ do_exit(EXIT_FAILURE);
+ } else if (pid == 0) {
+ for (;;)
+ ::pause();
+ } else {
+ const fs::path name = fs::path(utils::getenv("CONTROL_DIR").get()) /
+ "pid";
+ std::ofstream pidfile(name.c_str());
+ if (!pidfile) {
+ std::cerr << "Failed to create the pidfile\n";
+ do_exit(EXIT_FAILURE);
+ }
+ pidfile << pid;
+ pidfile.close();
+ do_exit(EXIT_SUCCESS);
+ }
+}
+
+
+static void child_validate_isolation(const fs::path&) UTILS_NORETURN;
+
+
+/// Subprocess that checks if isolate_child() has been called.
+static void
+child_validate_isolation(const fs::path& /* control_directory */)
+{
+ if (utils::getenv("HOME").get() == "fake-value") {
+ std::cerr << "HOME not reset\n";
+ do_exit(EXIT_FAILURE);
+ }
+ if (utils::getenv("LANG")) {
+ std::cerr << "LANG not unset\n";
+ do_exit(EXIT_FAILURE);
+ }
+ do_exit(EXIT_SUCCESS);
+}
+
+
+/// Invokes executor::spawn() with default arguments.
+///
+/// \param handle The executor on which to invoke spawn().
+/// \param args Arguments to the binary.
+/// \param timeout Maximum time the program can run for.
+/// \param unprivileged_user If set, user to switch to when running the child
+/// program.
+/// \param stdout_target If not none, file to which to write the stdout of the
+/// test case.
+/// \param stderr_target If not none, file to which to write the stderr of the
+/// test case.
+///
+/// \return The exec handle for the spawned binary.
+template< class Hook >
+static executor::exec_handle
+do_spawn(executor::executor_handle& handle, Hook hook,
+ const datetime::delta& timeout = infinite_timeout,
+ const optional< passwd::user > unprivileged_user = none,
+ const optional< fs::path > stdout_target = none,
+ const optional< fs::path > stderr_target = none)
+{
+ const executor::exec_handle exec_handle = handle.spawn< Hook >(
+ hook, timeout, unprivileged_user, stdout_target, stderr_target);
+ return exec_handle;
+}
+
+
+/// Checks for a specific exit status in the status of a exit_handle.
+///
+/// \param exit_status The expected exit status.
+/// \param status The value of exit_handle.status().
+///
+/// \post Terminates the calling test case if the status does not match the
+/// required value.
+static void
+require_exit(const int exit_status, const optional< process::status > status)
+{
+ ATF_REQUIRE(status);
+ ATF_REQUIRE(status.get().exited());
+ ATF_REQUIRE_EQ(exit_status, status.get().exitstatus());
+}
+
+
+/// Ensures that a killed process is gone.
+///
+/// The way we do this is by sending an idempotent signal to the given PID
+/// and checking if the signal was delivered. If it was, the process is
+/// still alive; if it was not, then it is gone.
+///
+/// Note that this might be inaccurate for two reasons:
+///
+/// 1) The system may have spawned a new process with the same pid as
+/// our subchild... but in practice, this does not happen because
+/// most systems do not immediately reuse pid numbers. If that
+/// happens... well, we get a false test failure.
+///
+/// 2) We ran so fast that even if the process was sent a signal to
+/// die, it has not had enough time to process it yet. This is why
+/// we retry this a few times.
+///
+/// \param pid PID of the process to check.
+static void
+ensure_dead(const pid_t pid)
+{
+ int attempts = 30;
+retry:
+ if (::kill(pid, SIGCONT) != -1 || errno != ESRCH) {
+ if (attempts > 0) {
+ std::cout << "Subprocess not dead yet; retrying wait\n";
+ --attempts;
+ ::usleep(100000);
+ goto retry;
+ }
+ ATF_FAIL(F("The subprocess %s of our child was not killed") % pid);
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__run_one);
+ATF_TEST_CASE_BODY(integration__run_one)
+{
+ executor::executor_handle handle = executor::setup();
+
+ const executor::exec_handle exec_handle = do_spawn(handle, child_exit(41));
+
+ executor::exit_handle exit_handle = handle.wait_any();
+ ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid());
+ require_exit(41, exit_handle.status());
+ exit_handle.cleanup();
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__run_many);
+ATF_TEST_CASE_BODY(integration__run_many)
+{
+ static const std::size_t num_children = 30;
+
+ executor::executor_handle handle = executor::setup();
+
+ std::size_t total_children = 0;
+ std::map< int, int > exp_exit_statuses;
+ std::map< int, datetime::timestamp > exp_start_times;
+ for (std::size_t i = 0; i < num_children; ++i) {
+ const datetime::timestamp start_time = datetime::timestamp::from_values(
+ 2014, 12, 8, 9, 40, 0, i);
+
+ for (std::size_t j = 0; j < 3; j++) {
+ const std::size_t id = i * 3 + j;
+
+ datetime::set_mock_now(start_time);
+ const int pid = do_spawn(handle, child_exit(id)).pid();
+ exp_exit_statuses.insert(std::make_pair(pid, id));
+ exp_start_times.insert(std::make_pair(pid, start_time));
+ ++total_children;
+ }
+ }
+
+ for (std::size_t i = 0; i < total_children; ++i) {
+ const datetime::timestamp end_time = datetime::timestamp::from_values(
+ 2014, 12, 8, 9, 50, 10, i);
+ datetime::set_mock_now(end_time);
+ executor::exit_handle exit_handle = handle.wait_any();
+ const int original_pid = exit_handle.original_pid();
+
+ const int exit_status = exp_exit_statuses.find(original_pid)->second;
+ const datetime::timestamp& start_time = exp_start_times.find(
+ original_pid)->second;
+
+ require_exit(exit_status, exit_handle.status());
+
+ ATF_REQUIRE_EQ(start_time, exit_handle.start_time());
+ ATF_REQUIRE_EQ(end_time, exit_handle.end_time());
+
+ exit_handle.cleanup();
+
+ ATF_REQUIRE(!atf::utils::file_exists(
+ exit_handle.stdout_file().str()));
+ ATF_REQUIRE(!atf::utils::file_exists(
+ exit_handle.stderr_file().str()));
+ ATF_REQUIRE(!atf::utils::file_exists(
+ exit_handle.work_directory().str()));
+ }
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__parameters_and_output);
+ATF_TEST_CASE_BODY(integration__parameters_and_output)
+{
+ executor::executor_handle handle = executor::setup();
+
+ const executor::exec_handle exec_handle = do_spawn(handle, child_print);
+
+ executor::exit_handle exit_handle = handle.wait_any();
+
+ ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid());
+
+ require_exit(EXIT_SUCCESS, exit_handle.status());
+
+ const fs::path stdout_file = exit_handle.stdout_file();
+ ATF_REQUIRE(atf::utils::compare_file(
+ stdout_file.str(), "stdout: some text\n"));
+ const fs::path stderr_file = exit_handle.stderr_file();
+ ATF_REQUIRE(atf::utils::compare_file(
+ stderr_file.str(), "stderr: some other text\n"));
+
+ exit_handle.cleanup();
+ ATF_REQUIRE(!fs::exists(stdout_file));
+ ATF_REQUIRE(!fs::exists(stderr_file));
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__custom_output_files);
+ATF_TEST_CASE_BODY(integration__custom_output_files)
+{
+ executor::executor_handle handle = executor::setup();
+
+ const fs::path stdout_file("custom-stdout.txt");
+ const fs::path stderr_file("custom-stderr.txt");
+
+ const executor::exec_handle exec_handle = do_spawn(
+ handle, child_print, infinite_timeout, none,
+ utils::make_optional(stdout_file),
+ utils::make_optional(stderr_file));
+
+ executor::exit_handle exit_handle = handle.wait_any();
+
+ ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid());
+
+ require_exit(EXIT_SUCCESS, exit_handle.status());
+
+ ATF_REQUIRE_EQ(stdout_file, exit_handle.stdout_file());
+ ATF_REQUIRE_EQ(stderr_file, exit_handle.stderr_file());
+
+ exit_handle.cleanup();
+
+ handle.cleanup();
+
+ // Must compare after cleanup to ensure the files did not get deleted.
+ ATF_REQUIRE(atf::utils::compare_file(
+ stdout_file.str(), "stdout: some text\n"));
+ ATF_REQUIRE(atf::utils::compare_file(
+ stderr_file.str(), "stderr: some other text\n"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__timestamps);
+ATF_TEST_CASE_BODY(integration__timestamps)
+{
+ executor::executor_handle handle = executor::setup();
+
+ const datetime::timestamp start_time = datetime::timestamp::from_values(
+ 2014, 12, 8, 9, 35, 10, 1000);
+ const datetime::timestamp end_time = datetime::timestamp::from_values(
+ 2014, 12, 8, 9, 35, 20, 2000);
+
+ datetime::set_mock_now(start_time);
+ do_spawn(handle, child_exit(70));
+
+ datetime::set_mock_now(end_time);
+ executor::exit_handle exit_handle = handle.wait_any();
+
+ require_exit(70, exit_handle.status());
+
+ ATF_REQUIRE_EQ(start_time, exit_handle.start_time());
+ ATF_REQUIRE_EQ(end_time, exit_handle.end_time());
+ exit_handle.cleanup();
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__files);
+ATF_TEST_CASE_BODY(integration__files)
+{
+ executor::executor_handle handle = executor::setup();
+
+ do_spawn(handle, child_create_cookie("cookie.12345"));
+
+ executor::exit_handle exit_handle = handle.wait_any();
+
+ ATF_REQUIRE(atf::utils::file_exists(
+ (exit_handle.work_directory() / "cookie.12345").str()));
+
+ exit_handle.cleanup();
+
+ ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stdout_file().str()));
+ ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stderr_file().str()));
+ ATF_REQUIRE(!atf::utils::file_exists(exit_handle.work_directory().str()));
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__followup);
+ATF_TEST_CASE_BODY(integration__followup)
+{
+ executor::executor_handle handle = executor::setup();
+
+ (void)handle.spawn(child_create_cookie("cookie.1"), infinite_timeout, none);
+ executor::exit_handle exit_1_handle = handle.wait_any();
+
+ (void)handle.spawn_followup(child_create_cookie("cookie.2"), exit_1_handle,
+ infinite_timeout);
+ executor::exit_handle exit_2_handle = handle.wait_any();
+
+ ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_2_handle.stdout_file());
+ ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_2_handle.stderr_file());
+ ATF_REQUIRE_EQ(exit_1_handle.control_directory(),
+ exit_2_handle.control_directory());
+ ATF_REQUIRE_EQ(exit_1_handle.work_directory(),
+ exit_2_handle.work_directory());
+
+ (void)handle.spawn_followup(child_create_cookie("cookie.3"), exit_2_handle,
+ infinite_timeout);
+ exit_2_handle.cleanup();
+ exit_1_handle.cleanup();
+ executor::exit_handle exit_3_handle = handle.wait_any();
+
+ ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_3_handle.stdout_file());
+ ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_3_handle.stderr_file());
+ ATF_REQUIRE_EQ(exit_1_handle.control_directory(),
+ exit_3_handle.control_directory());
+ ATF_REQUIRE_EQ(exit_1_handle.work_directory(),
+ exit_3_handle.work_directory());
+
+ ATF_REQUIRE(atf::utils::file_exists(
+ (exit_1_handle.work_directory() / "cookie.1").str()));
+ ATF_REQUIRE(atf::utils::file_exists(
+ (exit_1_handle.work_directory() / "cookie.2").str()));
+ ATF_REQUIRE(atf::utils::file_exists(
+ (exit_1_handle.work_directory() / "cookie.3").str()));
+
+ ATF_REQUIRE(atf::utils::compare_file(
+ exit_1_handle.stdout_file().str(),
+ "Creating cookie: cookie.1 (stdout)\n"
+ "Creating cookie: cookie.2 (stdout)\n"
+ "Creating cookie: cookie.3 (stdout)\n"));
+
+ ATF_REQUIRE(atf::utils::compare_file(
+ exit_1_handle.stderr_file().str(),
+ "Creating cookie: cookie.1 (stderr)\n"
+ "Creating cookie: cookie.2 (stderr)\n"
+ "Creating cookie: cookie.3 (stderr)\n"));
+
+ exit_3_handle.cleanup();
+
+ ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stdout_file().str()));
+ ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stderr_file().str()));
+ ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.work_directory().str()));
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__output_files_always_exist);
+ATF_TEST_CASE_BODY(integration__output_files_always_exist)
+{
+ executor::executor_handle handle = executor::setup();
+
+ // This test is racy: we specify a very short timeout for the subprocess so
+ // that we cause the subprocess to exit before it has had time to set up the
+ // output files. However, for scheduling reasons, the subprocess may
+ // actually run to completion before the timer triggers. Retry this a few
+ // times to attempt to catch a "good test".
+ for (int i = 0; i < 50; i++) {
+ const executor::exec_handle exec_handle =
+ do_spawn(handle, child_exit(0), datetime::delta(0, 100000));
+ executor::exit_handle exit_handle = handle.wait(exec_handle);
+ ATF_REQUIRE(fs::exists(exit_handle.stdout_file()));
+ ATF_REQUIRE(fs::exists(exit_handle.stderr_file()));
+ exit_handle.cleanup();
+ }
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE(integration__timeouts);
+ATF_TEST_CASE_HEAD(integration__timeouts)
+{
+ set_md_var("timeout", "60");
+}
+ATF_TEST_CASE_BODY(integration__timeouts)
+{
+ executor::executor_handle handle = executor::setup();
+
+ const executor::exec_handle exec_handle1 =
+ do_spawn(handle, child_sleep(30), datetime::delta(2, 0));
+ const executor::exec_handle exec_handle2 =
+ do_spawn(handle, child_sleep(40), datetime::delta(5, 0));
+ const executor::exec_handle exec_handle3 =
+ do_spawn(handle, child_exit(15));
+
+ {
+ executor::exit_handle exit_handle = handle.wait_any();
+ ATF_REQUIRE_EQ(exec_handle3.pid(), exit_handle.original_pid());
+ require_exit(15, exit_handle.status());
+ exit_handle.cleanup();
+ }
+
+ {
+ executor::exit_handle exit_handle = handle.wait_any();
+ ATF_REQUIRE_EQ(exec_handle1.pid(), exit_handle.original_pid());
+ ATF_REQUIRE(!exit_handle.status());
+ const datetime::delta duration =
+ exit_handle.end_time() - exit_handle.start_time();
+ ATF_REQUIRE(duration < datetime::delta(10, 0));
+ ATF_REQUIRE(duration >= datetime::delta(2, 0));
+ exit_handle.cleanup();
+ }
+
+ {
+ executor::exit_handle exit_handle = handle.wait_any();
+ ATF_REQUIRE_EQ(exec_handle2.pid(), exit_handle.original_pid());
+ ATF_REQUIRE(!exit_handle.status());
+ const datetime::delta duration =
+ exit_handle.end_time() - exit_handle.start_time();
+ ATF_REQUIRE(duration < datetime::delta(10, 0));
+ ATF_REQUIRE(duration >= datetime::delta(4, 0));
+ exit_handle.cleanup();
+ }
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE(integration__unprivileged_user);
+ATF_TEST_CASE_HEAD(integration__unprivileged_user)
+{
+ set_md_var("require.config", "unprivileged-user");
+ set_md_var("require.user", "root");
+}
+ATF_TEST_CASE_BODY(integration__unprivileged_user)
+{
+ executor::executor_handle handle = executor::setup();
+
+ const passwd::user unprivileged_user = passwd::find_user_by_name(
+ get_config_var("unprivileged-user"));
+
+ do_spawn(handle, child_dump_unprivileged_user,
+ infinite_timeout, utils::make_optional(unprivileged_user));
+
+ executor::exit_handle exit_handle = handle.wait_any();
+ ATF_REQUIRE(atf::utils::compare_file(
+ exit_handle.stdout_file().str(),
+ F("UID = %s\n") % unprivileged_user.uid));
+ exit_handle.cleanup();
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__auto_cleanup);
+ATF_TEST_CASE_BODY(integration__auto_cleanup)
+{
+ std::vector< int > pids;
+ std::vector< fs::path > paths;
+ {
+ executor::executor_handle handle = executor::setup();
+
+ pids.push_back(do_spawn(handle, child_exit(10)).pid());
+ pids.push_back(do_spawn(handle, child_exit(20)).pid());
+
+ // This invocation is never waited for below. This is intentional: we
+ // want the destructor to clean the "leaked" test automatically so that
+ // the clean up of the parent work directory also happens correctly.
+ pids.push_back(do_spawn(handle, child_pause).pid());
+
+ executor::exit_handle exit_handle1 = handle.wait_any();
+ paths.push_back(exit_handle1.stdout_file());
+ paths.push_back(exit_handle1.stderr_file());
+ paths.push_back(exit_handle1.work_directory());
+
+ executor::exit_handle exit_handle2 = handle.wait_any();
+ paths.push_back(exit_handle2.stdout_file());
+ paths.push_back(exit_handle2.stderr_file());
+ paths.push_back(exit_handle2.work_directory());
+ }
+ for (std::vector< int >::const_iterator iter = pids.begin();
+ iter != pids.end(); ++iter) {
+ ensure_dead(*iter);
+ }
+ for (std::vector< fs::path >::const_iterator iter = paths.begin();
+ iter != paths.end(); ++iter) {
+ ATF_REQUIRE(!atf::utils::file_exists((*iter).str()));
+ }
+}
+
+
+/// Ensures that interrupting an executor cleans things up correctly.
+///
+/// This test scenario is tricky. We spawn a master child process that runs the
+/// executor code and we send a signal to it externally. The child process
+/// spawns a bunch of tests that block indefinitely and tries to wait for their
+/// results. When the signal is received, we expect an interrupt_error to be
+/// raised, which in turn should clean up all test resources and exit the master
+/// child process successfully.
+///
+/// \param signo Signal to deliver to the executor.
+static void
+do_signal_handling_test(const int signo)
+{
+ static const char* cookie = "spawned.txt";
+
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ static const std::size_t num_children = 3;
+
+ optional< fs::path > root_work_directory;
+ try {
+ executor::executor_handle handle = executor::setup();
+ root_work_directory = handle.root_work_directory();
+
+ for (std::size_t i = 0; i < num_children; ++i) {
+ std::cout << "Spawned child number " << i << '\n';
+ do_spawn(handle, child_pause);
+ }
+
+ std::cout << "Creating " << cookie << " cookie\n";
+ atf::utils::create_file(cookie, "");
+
+ std::cout << "Waiting for subprocess termination\n";
+ for (std::size_t i = 0; i < num_children; ++i) {
+ executor::exit_handle exit_handle = handle.wait_any();
+ // We may never reach this point in the test, but if we do let's
+ // make sure the subprocess was terminated as expected.
+ if (exit_handle.status()) {
+ if (exit_handle.status().get().signaled() &&
+ exit_handle.status().get().termsig() == SIGKILL) {
+ // OK.
+ } else {
+ std::cerr << "Child exited with unexpected code: "
+ << exit_handle.status().get();
+ std::exit(EXIT_FAILURE);
+ }
+ } else {
+ std::cerr << "Child timed out\n";
+ std::exit(EXIT_FAILURE);
+ }
+ exit_handle.cleanup();
+ }
+ std::cerr << "Terminating without reception of signal\n";
+ std::exit(EXIT_FAILURE);
+ } catch (const signals::interrupted_error& unused_error) {
+ std::cerr << "Terminating due to interrupted_error\n";
+ // We never kill ourselves until the cookie is created, so it is
+ // guaranteed that the optional root_work_directory has been
+ // initialized at this point.
+ if (atf::utils::file_exists(root_work_directory.get().str())) {
+ // Some cleanup did not happen; error out.
+ std::exit(EXIT_FAILURE);
+ } else {
+ std::exit(EXIT_SUCCESS);
+ }
+ }
+ std::abort();
+ }
+
+ std::cout << "Waiting for " << cookie << " cookie creation\n";
+ while (!atf::utils::file_exists(cookie)) {
+ // Wait for processes.
+ }
+ ATF_REQUIRE(::unlink(cookie) != -1);
+ std::cout << "Killing process\n";
+ ATF_REQUIRE(::kill(pid, signo) != -1);
+
+ int status;
+ std::cout << "Waiting for process termination\n";
+ ATF_REQUIRE(::waitpid(pid, &status, 0) != -1);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__signal_handling);
+ATF_TEST_CASE_BODY(integration__signal_handling)
+{
+ // This test scenario is racy so run it multiple times to have higher
+ // chances of exposing problems.
+ const std::size_t rounds = 20;
+
+ for (std::size_t i = 0; i < rounds; ++i) {
+ std::cout << F("Testing round %s\n") % i;
+ do_signal_handling_test(SIGHUP);
+ do_signal_handling_test(SIGINT);
+ do_signal_handling_test(SIGTERM);
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__isolate_child_is_called);
+ATF_TEST_CASE_BODY(integration__isolate_child_is_called)
+{
+ executor::executor_handle handle = executor::setup();
+
+ utils::setenv("HOME", "fake-value");
+ utils::setenv("LANG", "es_ES");
+ do_spawn(handle, child_validate_isolation);
+
+ executor::exit_handle exit_handle = handle.wait_any();
+ require_exit(EXIT_SUCCESS, exit_handle.status());
+ exit_handle.cleanup();
+
+ handle.cleanup();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__process_group_is_terminated);
+ATF_TEST_CASE_BODY(integration__process_group_is_terminated)
+{
+ utils::setenv("CONTROL_DIR", fs::current_path().str());
+
+ executor::executor_handle handle = executor::setup();
+ do_spawn(handle, child_spawn_blocking_child);
+
+ executor::exit_handle exit_handle = handle.wait_any();
+ require_exit(EXIT_SUCCESS, exit_handle.status());
+ exit_handle.cleanup();
+
+ handle.cleanup();
+
+ if (!fs::exists(fs::path("pid")))
+ fail("The pid file was not created");
+
+ std::ifstream pidfile("pid");
+ ATF_REQUIRE(pidfile);
+ pid_t pid;
+ pidfile >> pid;
+ pidfile.close();
+
+ ensure_dead(pid);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__prevent_clobbering_control_files);
+ATF_TEST_CASE_BODY(integration__prevent_clobbering_control_files)
+{
+ executor::executor_handle handle = executor::setup();
+
+ do_spawn(handle, child_delete_all);
+
+ executor::exit_handle exit_handle = handle.wait_any();
+ require_exit(EXIT_SUCCESS, exit_handle.status());
+ ATF_REQUIRE(atf::utils::file_exists(
+ (exit_handle.control_directory() / "exec_was_called").str()));
+ ATF_REQUIRE(!atf::utils::file_exists(
+ (exit_handle.work_directory() / "exec_was_called").str()));
+ exit_handle.cleanup();
+
+ handle.cleanup();
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, integration__run_one);
+ ATF_ADD_TEST_CASE(tcs, integration__run_many);
+
+ ATF_ADD_TEST_CASE(tcs, integration__parameters_and_output);
+ ATF_ADD_TEST_CASE(tcs, integration__custom_output_files);
+ ATF_ADD_TEST_CASE(tcs, integration__timestamps);
+ ATF_ADD_TEST_CASE(tcs, integration__files);
+
+ ATF_ADD_TEST_CASE(tcs, integration__followup);
+
+ ATF_ADD_TEST_CASE(tcs, integration__output_files_always_exist);
+ ATF_ADD_TEST_CASE(tcs, integration__timeouts);
+ ATF_ADD_TEST_CASE(tcs, integration__unprivileged_user);
+ ATF_ADD_TEST_CASE(tcs, integration__auto_cleanup);
+ ATF_ADD_TEST_CASE(tcs, integration__signal_handling);
+ ATF_ADD_TEST_CASE(tcs, integration__isolate_child_is_called);
+ ATF_ADD_TEST_CASE(tcs, integration__process_group_is_terminated);
+ ATF_ADD_TEST_CASE(tcs, integration__prevent_clobbering_control_files);
+}
diff --git a/utils/process/fdstream.cpp b/utils/process/fdstream.cpp
new file mode 100644
index 000000000000..ccd7a1f91b0c
--- /dev/null
+++ b/utils/process/fdstream.cpp
@@ -0,0 +1,76 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/fdstream.hpp"
+
+#include "utils/noncopyable.hpp"
+#include "utils/process/systembuf.hpp"
+
+
+namespace utils {
+namespace process {
+
+
+/// Private implementation fields for ifdstream.
+struct ifdstream::impl : utils::noncopyable {
+ /// The systembuf backing this file descriptor.
+ systembuf _systembuf;
+
+ /// Initializes private implementation data.
+ ///
+ /// \param fd The file descriptor.
+ impl(const int fd) : _systembuf(fd) {}
+};
+
+
+} // namespace process
+} // namespace utils
+
+
+namespace process = utils::process;
+
+
+/// Constructs a new ifdstream based on an open file descriptor.
+///
+/// This grabs ownership of the file descriptor.
+///
+/// \param fd The file descriptor to read from. Must be open and valid.
+process::ifdstream::ifdstream(const int fd) :
+ std::istream(NULL),
+ _pimpl(new impl(fd))
+{
+ rdbuf(&_pimpl->_systembuf);
+}
+
+
+/// Destroys an ifdstream object.
+///
+/// \post The file descriptor attached to this stream is closed.
+process::ifdstream::~ifdstream(void)
+{
+}
diff --git a/utils/process/fdstream.hpp b/utils/process/fdstream.hpp
new file mode 100644
index 000000000000..e785b0ac4282
--- /dev/null
+++ b/utils/process/fdstream.hpp
@@ -0,0 +1,66 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/fdstream.hpp
+/// Provides the utils::process::ifdstream class.
+
+#if !defined(UTILS_PROCESS_FDSTREAM_HPP)
+#define UTILS_PROCESS_FDSTREAM_HPP
+
+#include "utils/process/fdstream_fwd.hpp"
+
+#include <istream>
+#include <memory>
+
+#include "utils/noncopyable.hpp"
+
+namespace utils {
+namespace process {
+
+
+/// An input stream backed by a file descriptor.
+///
+/// This class grabs ownership of the file descriptor. I.e. when the class is
+/// destroyed, the file descriptor is closed unconditionally.
+class ifdstream : public std::istream, noncopyable
+{
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::auto_ptr< impl > _pimpl;
+
+public:
+ explicit ifdstream(const int);
+ ~ifdstream(void);
+};
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_FDSTREAM_HPP)
diff --git a/utils/process/fdstream_fwd.hpp b/utils/process/fdstream_fwd.hpp
new file mode 100644
index 000000000000..8d369ea0bfa5
--- /dev/null
+++ b/utils/process/fdstream_fwd.hpp
@@ -0,0 +1,45 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/fdstream_fwd.hpp
+/// Forward declarations for utils/process/fdstream.hpp
+
+#if !defined(UTILS_PROCESS_FDSTREAM_FWD_HPP)
+#define UTILS_PROCESS_FDSTREAM_FWD_HPP
+
+namespace utils {
+namespace process {
+
+
+class ifdstream;
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_FDSTREAM_FWD_HPP)
diff --git a/utils/process/fdstream_test.cpp b/utils/process/fdstream_test.cpp
new file mode 100644
index 000000000000..8420568216f0
--- /dev/null
+++ b/utils/process/fdstream_test.cpp
@@ -0,0 +1,73 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/fdstream.hpp"
+
+extern "C" {
+#include <unistd.h>
+}
+
+#include <atf-c++.hpp>
+
+#include "utils/process/systembuf.hpp"
+
+using utils::process::ifdstream;
+using utils::process::systembuf;
+
+
+ATF_TEST_CASE(ifdstream);
+ATF_TEST_CASE_HEAD(ifdstream)
+{
+ set_md_var("descr", "Tests the ifdstream class");
+}
+ATF_TEST_CASE_BODY(ifdstream)
+{
+ int fds[2];
+ ATF_REQUIRE(::pipe(fds) != -1);
+
+ ifdstream rend(fds[0]);
+
+ systembuf wbuf(fds[1]);
+ std::ostream wend(&wbuf);
+
+ // XXX This assumes that the pipe's buffer is big enough to accept
+ // the data written without blocking!
+ wend << "1Test 1message\n";
+ wend.flush();
+ std::string tmp;
+ rend >> tmp;
+ ATF_REQUIRE_EQ(tmp, "1Test");
+ rend >> tmp;
+ ATF_REQUIRE_EQ(tmp, "1message");
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, ifdstream);
+}
diff --git a/utils/process/helpers.cpp b/utils/process/helpers.cpp
new file mode 100644
index 000000000000..15deecd95f24
--- /dev/null
+++ b/utils/process/helpers.cpp
@@ -0,0 +1,74 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <sstream>
+
+
+static int
+print_args(int argc, char* argv[])
+{
+ for (int i = 0; i < argc; i++)
+ std::cout << "argv[" << i << "] = " << argv[i] << "\n";
+ std::cout << "argv[" << argc << "] = NULL";
+ return EXIT_SUCCESS;
+}
+
+
+static int
+return_code(int argc, char* argv[])
+{
+ if (argc != 3)
+ std::abort();
+
+ std::istringstream iss(argv[2]);
+ int code;
+ iss >> code;
+ return code;
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ if (argc < 2) {
+ std::cerr << "Must provide a helper name\n";
+ std::exit(EXIT_FAILURE);
+ }
+
+ if (std::strcmp(argv[1], "print-args") == 0) {
+ return print_args(argc, argv);
+ } else if (std::strcmp(argv[1], "return-code") == 0) {
+ return return_code(argc, argv);
+ } else {
+ std::cerr << "Unknown helper\n";
+ return EXIT_FAILURE;
+ }
+}
diff --git a/utils/process/isolation.cpp b/utils/process/isolation.cpp
new file mode 100644
index 000000000000..90dd08d5772d
--- /dev/null
+++ b/utils/process/isolation.cpp
@@ -0,0 +1,207 @@
+// Copyright 2014 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/isolation.hpp"
+
+extern "C" {
+#include <sys/stat.h>
+
+#include <grp.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+
+#include "utils/defs.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/env.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/optional.ipp"
+#include "utils/passwd.hpp"
+#include "utils/sanity.hpp"
+#include "utils/signals/misc.hpp"
+#include "utils/stacktrace.hpp"
+
+namespace fs = utils::fs;
+namespace passwd = utils::passwd;
+namespace process = utils::process;
+namespace signals = utils::signals;
+
+using utils::optional;
+
+
+/// Magic exit code to denote an error while preparing the subprocess.
+const int process::exit_isolation_failure = 124;
+
+
+namespace {
+
+
+static void fail(const std::string&, const int) UTILS_NORETURN;
+
+
+/// Fails the process with an errno-based error message.
+///
+/// \param message The message to print. The errno-based string will be
+/// appended to this, just like in perror(3).
+/// \param original_errno The error code to format.
+static void
+fail(const std::string& message, const int original_errno)
+{
+ std::cerr << message << ": " << std::strerror(original_errno) << '\n';
+ std::exit(process::exit_isolation_failure);
+}
+
+
+/// Changes the owner of a path.
+///
+/// This function is intended to be called from a subprocess getting ready to
+/// invoke an external binary. Therefore, if there is any error during the
+/// setup, the new process is terminated with an error code.
+///
+/// \param file The path to the file or directory to affect.
+/// \param uid The UID to set on the path.
+/// \param gid The GID to set on the path.
+static void
+do_chown(const fs::path& file, const uid_t uid, const gid_t gid)
+{
+ if (::chown(file.c_str(), uid, gid) == -1)
+ fail(F("chown(%s, %s, %s) failed; UID is %s and GID is %s")
+ % file % uid % gid % ::getuid() % ::getgid(), errno);
+}
+
+
+/// Resets the environment of the process to a known state.
+///
+/// \param work_directory Path to the work directory being used.
+///
+/// \throw std::runtime_error If there is a problem setting up the environment.
+static void
+prepare_environment(const fs::path& work_directory)
+{
+ const char* to_unset[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
+ "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC",
+ "LC_TIME", NULL };
+ const char** iter;
+ for (iter = to_unset; *iter != NULL; ++iter) {
+ utils::unsetenv(*iter);
+ }
+
+ utils::setenv("HOME", work_directory.str());
+ utils::setenv("TMPDIR", work_directory.str());
+ utils::setenv("TZ", "UTC");
+}
+
+
+} // anonymous namespace
+
+
+/// Cleans up the container process to run a new child.
+///
+/// If there is any error during the setup, the new process is terminated
+/// with an error code.
+///
+/// \param unprivileged_user Unprivileged user to run the test case as.
+/// \param work_directory Path to the test case-specific work directory.
+void
+process::isolate_child(const optional< passwd::user >& unprivileged_user,
+ const fs::path& work_directory)
+{
+ isolate_path(unprivileged_user, work_directory);
+ if (::chdir(work_directory.c_str()) == -1)
+ fail(F("chdir(%s) failed") % work_directory, errno);
+
+ utils::unlimit_core_size();
+ if (!signals::reset_all()) {
+ LW("Failed to reset one or more signals to their default behavior");
+ }
+ prepare_environment(work_directory);
+ (void)::umask(0022);
+
+ if (unprivileged_user && passwd::current_user().is_root()) {
+ const passwd::user& user = unprivileged_user.get();
+
+ if (user.gid != ::getgid()) {
+ if (::setgid(user.gid) == -1)
+ fail(F("setgid(%s) failed; UID is %s and GID is %s")
+ % user.gid % ::getuid() % ::getgid(), errno);
+ if (::getuid() == 0) {
+ ::gid_t groups[1];
+ groups[0] = user.gid;
+ if (::setgroups(1, groups) == -1)
+ fail(F("setgroups(1, [%s]) failed; UID is %s and GID is %s")
+ % user.gid % ::getuid() % ::getgid(), errno);
+ }
+ }
+ if (user.uid != ::getuid()) {
+ if (::setuid(user.uid) == -1)
+ fail(F("setuid(%s) failed; UID is %s and GID is %s")
+ % user.uid % ::getuid() % ::getgid(), errno);
+ }
+ }
+}
+
+
+/// Sets up a path to be writable by a child isolated with isolate_child.
+///
+/// If there is any error during the setup, the new process is terminated
+/// with an error code.
+///
+/// The caller should use this to prepare any directory or file that the child
+/// should be able to write to *before* invoking isolate_child(). Note that
+/// isolate_child() will use isolate_path() on the work directory though.
+///
+/// \param unprivileged_user Unprivileged user to run the test case as.
+/// \param file Path to the file to modify.
+void
+process::isolate_path(const optional< passwd::user >& unprivileged_user,
+ const fs::path& file)
+{
+ if (!unprivileged_user || !passwd::current_user().is_root())
+ return;
+ const passwd::user& user = unprivileged_user.get();
+
+ const bool change_group = user.gid != ::getgid();
+ const bool change_user = user.uid != ::getuid();
+
+ if (!change_user && !change_group) {
+ // Keep same permissions.
+ } else if (change_user && change_group) {
+ do_chown(file, user.uid, user.gid);
+ } else if (!change_user && change_group) {
+ do_chown(file, ::getuid(), user.gid);
+ } else {
+ INV(change_user && !change_group);
+ do_chown(file, user.uid, ::getgid());
+ }
+}
diff --git a/utils/process/isolation.hpp b/utils/process/isolation.hpp
new file mode 100644
index 000000000000..69793a76c7b4
--- /dev/null
+++ b/utils/process/isolation.hpp
@@ -0,0 +1,60 @@
+// Copyright 2014 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/isolation.hpp
+/// Utilities to isolate a process.
+///
+/// By "isolation" in this context we mean forcing a process to run in a
+/// more-or-less deterministic environment.
+
+#if !defined(UTILS_PROCESS_ISOLATION_HPP)
+#define UTILS_PROCESS_ISOLATION_HPP
+
+#include "utils/fs/path_fwd.hpp"
+#include "utils/optional_fwd.hpp"
+#include "utils/passwd_fwd.hpp"
+
+namespace utils {
+namespace process {
+
+
+extern const int exit_isolation_failure;
+
+
+void isolate_child(const utils::optional< utils::passwd::user >&,
+ const utils::fs::path&);
+
+void isolate_path(const utils::optional< utils::passwd::user >&,
+ const utils::fs::path&);
+
+
+} // namespace process
+} // namespace utils
+
+
+#endif // !defined(UTILS_PROCESS_ISOLATION_HPP)
diff --git a/utils/process/isolation_test.cpp b/utils/process/isolation_test.cpp
new file mode 100644
index 000000000000..dc723cc65c88
--- /dev/null
+++ b/utils/process/isolation_test.cpp
@@ -0,0 +1,622 @@
+// Copyright 2014 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/isolation.hpp"
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#include "utils/defs.hpp"
+#include "utils/env.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/optional.ipp"
+#include "utils/passwd.hpp"
+#include "utils/process/child.ipp"
+#include "utils/process/status.hpp"
+#include "utils/sanity.hpp"
+#include "utils/test_utils.ipp"
+
+namespace fs = utils::fs;
+namespace passwd = utils::passwd;
+namespace process = utils::process;
+
+using utils::none;
+using utils::optional;
+
+
+namespace {
+
+
+/// Runs the given hook in a subprocess.
+///
+/// \param hook The code to run in the subprocess.
+///
+/// \return The status of the subprocess for further validation.
+///
+/// \post The subprocess.stdout and subprocess.stderr files, created in the
+/// current directory, contain the output of the subprocess.
+template< typename Hook >
+static process::status
+fork_and_run(Hook hook)
+{
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ hook, fs::path("subprocess.stdout"), fs::path("subprocess.stderr"));
+ const process::status status = child->wait();
+
+ atf::utils::cat_file("subprocess.stdout", "isolated child stdout: ");
+ atf::utils::cat_file("subprocess.stderr", "isolated child stderr: ");
+
+ return status;
+}
+
+
+/// Subprocess that validates the cleanliness of the environment.
+///
+/// \post Exits with success if the environment is clean; failure otherwise.
+static void
+check_clean_environment(void)
+{
+ fs::mkdir(fs::path("some-directory"), 0755);
+ process::isolate_child(none, fs::path("some-directory"));
+
+ bool failed = false;
+
+ const char* empty[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
+ "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC",
+ "LC_TIME", NULL };
+ const char** iter;
+ for (iter = empty; *iter != NULL; ++iter) {
+ if (utils::getenv(*iter)) {
+ failed = true;
+ std::cout << F("%s was not unset\n") % *iter;
+ }
+ }
+
+ if (utils::getenv_with_default("HOME", "") != "some-directory") {
+ failed = true;
+ std::cout << "HOME was not set to the work directory\n";
+ }
+
+ if (utils::getenv_with_default("TMPDIR", "") != "some-directory") {
+ failed = true;
+ std::cout << "TMPDIR was not set to the work directory\n";
+ }
+
+ if (utils::getenv_with_default("TZ", "") != "UTC") {
+ failed = true;
+ std::cout << "TZ was not set to UTC\n";
+ }
+
+ if (utils::getenv_with_default("LEAVE_ME_ALONE", "") != "kill-some-day") {
+ failed = true;
+ std::cout << "LEAVE_ME_ALONE was modified while it should not have "
+ "been\n";
+ }
+
+ std::exit(failed ? EXIT_FAILURE : EXIT_SUCCESS);
+}
+
+
+/// Subprocess that checks if user privileges are dropped.
+class check_drop_privileges {
+ /// The user to drop the privileges to.
+ const passwd::user _unprivileged_user;
+
+public:
+ /// Constructor.
+ ///
+ /// \param unprivileged_user The user to drop the privileges to.
+ check_drop_privileges(const passwd::user& unprivileged_user) :
+ _unprivileged_user(unprivileged_user)
+ {
+ }
+
+ /// Body of the subprocess.
+ ///
+ /// \post Exits with success if the process has dropped privileges as
+ /// expected.
+ void
+ operator()(void) const
+ {
+ fs::mkdir(fs::path("subdir"), 0755);
+ process::isolate_child(utils::make_optional(_unprivileged_user),
+ fs::path("subdir"));
+
+ if (::getuid() == 0) {
+ std::cout << "UID is still 0\n";
+ std::exit(EXIT_FAILURE);
+ }
+
+ if (::getgid() == 0) {
+ std::cout << "GID is still 0\n";
+ std::exit(EXIT_FAILURE);
+ }
+
+ ::gid_t groups[1];
+ if (::getgroups(1, groups) == -1) {
+ // Should only fail if we get more than one group notifying about
+ // not enough space in the groups variable to store the whole
+ // result.
+ INV(errno == EINVAL);
+ std::exit(EXIT_FAILURE);
+ }
+ if (groups[0] == 0) {
+ std::cout << "Primary group is still 0\n";
+ std::exit(EXIT_FAILURE);
+ }
+
+ std::ofstream output("file.txt");
+ if (!output) {
+ std::cout << "Cannot write to isolated directory; owner not "
+ "changed?\n";
+ std::exit(EXIT_FAILURE);
+ }
+
+ std::exit(EXIT_SUCCESS);
+ }
+};
+
+
+/// Subprocess that dumps core to validate core dumping abilities.
+static void
+check_enable_core_dumps(void)
+{
+ process::isolate_child(none, fs::path("."));
+ std::abort();
+}
+
+
+/// Subprocess that checks if the work directory is entered.
+class check_enter_work_directory {
+ /// Directory to enter. May be releative.
+ const fs::path _directory;
+
+public:
+ /// Constructor.
+ ///
+ /// \param directory Directory to enter.
+ check_enter_work_directory(const fs::path& directory) :
+ _directory(directory)
+ {
+ }
+
+ /// Body of the subprocess.
+ ///
+ /// \post Exits with success if the process has entered the given work
+ /// directory; false otherwise.
+ void
+ operator()(void) const
+ {
+ const fs::path exp_subdir = fs::current_path() / _directory;
+ process::isolate_child(none, _directory);
+ std::exit(fs::current_path() == exp_subdir ?
+ EXIT_SUCCESS : EXIT_FAILURE);
+ }
+};
+
+
+/// Subprocess that validates that it owns a session.
+///
+/// \post Exits with success if the process lives in its own session;
+/// failure otherwise.
+static void
+check_new_session(void)
+{
+ process::isolate_child(none, fs::path("."));
+ std::exit(::getsid(::getpid()) == ::getpid() ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+
+/// Subprocess that validates the disconnection from any terminal.
+///
+/// \post Exits with success if the environment is clean; failure otherwise.
+static void
+check_no_terminal(void)
+{
+ process::isolate_child(none, fs::path("."));
+
+ const char* const args[] = {
+ "/bin/sh",
+ "-i",
+ "-c",
+ "echo success",
+ NULL
+ };
+ ::execv("/bin/sh", UTILS_UNCONST(char*, args));
+ std::abort();
+}
+
+
+/// Subprocess that validates that it has become the leader of a process group.
+///
+/// \post Exits with success if the process lives in its own process group;
+/// failure otherwise.
+static void
+check_process_group(void)
+{
+ process::isolate_child(none, fs::path("."));
+ std::exit(::getpgid(::getpid()) == ::getpid() ?
+ EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+
+/// Subprocess that validates that the umask has been reset.
+///
+/// \post Exits with success if the umask matches the expected value; failure
+/// otherwise.
+static void
+check_umask(void)
+{
+ process::isolate_child(none, fs::path("."));
+ std::exit(::umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment);
+ATF_TEST_CASE_BODY(isolate_child__clean_environment)
+{
+ utils::setenv("HOME", "/non-existent/directory");
+ utils::setenv("TMPDIR", "/non-existent/directory");
+ utils::setenv("LANG", "C");
+ utils::setenv("LC_ALL", "C");
+ utils::setenv("LC_COLLATE", "C");
+ utils::setenv("LC_CTYPE", "C");
+ utils::setenv("LC_MESSAGES", "C");
+ utils::setenv("LC_MONETARY", "C");
+ utils::setenv("LC_NUMERIC", "C");
+ utils::setenv("LC_TIME", "C");
+ utils::setenv("LEAVE_ME_ALONE", "kill-some-day");
+ utils::setenv("TZ", "EST+5");
+
+ const process::status status = fork_and_run(check_clean_environment);
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE(isolate_child__other_user_when_unprivileged);
+ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged)
+{
+ set_md_var("require.user", "unprivileged");
+}
+ATF_TEST_CASE_BODY(isolate_child__other_user_when_unprivileged)
+{
+ const passwd::user user = passwd::current_user();
+
+ passwd::user other_user = user;
+ other_user.uid += 1;
+ other_user.gid += 1;
+ process::isolate_child(utils::make_optional(other_user), fs::path("."));
+
+ ATF_REQUIRE_EQ(user.uid, ::getuid());
+ ATF_REQUIRE_EQ(user.gid, ::getgid());
+}
+
+
+ATF_TEST_CASE(isolate_child__drop_privileges);
+ATF_TEST_CASE_HEAD(isolate_child__drop_privileges)
+{
+ set_md_var("require.config", "unprivileged-user");
+ set_md_var("require.user", "root");
+}
+ATF_TEST_CASE_BODY(isolate_child__drop_privileges)
+{
+ const passwd::user unprivileged_user = passwd::find_user_by_name(
+ get_config_var("unprivileged-user"));
+
+ const process::status status = fork_and_run(check_drop_privileges(
+ unprivileged_user));
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid);
+ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid)
+{
+ set_md_var("require.user", "unprivileged");
+}
+ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_uid)
+{
+ // Fake the current user as root so that we bypass the protections in
+ // isolate_child that prevent us from attempting a user switch when we are
+ // not root. We do this so we can trigger the setuid failure.
+ passwd::user root = passwd::user("root", 0, 0);
+ ATF_REQUIRE(root.is_root());
+ passwd::set_current_user_for_testing(root);
+
+ passwd::user unprivileged_user = passwd::current_user();
+ unprivileged_user.uid += 1;
+
+ const process::status status = fork_and_run(check_drop_privileges(
+ unprivileged_user));
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
+ ATF_REQUIRE(atf::utils::grep_file("(chown|setuid).*failed",
+ "subprocess.stderr"));
+}
+
+
+ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid);
+ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid)
+{
+ set_md_var("require.user", "unprivileged");
+}
+ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_gid)
+{
+ // Fake the current user as root so that we bypass the protections in
+ // isolate_child that prevent us from attempting a user switch when we are
+ // not root. We do this so we can trigger the setgid failure.
+ passwd::user root = passwd::user("root", 0, 0);
+ ATF_REQUIRE(root.is_root());
+ passwd::set_current_user_for_testing(root);
+
+ passwd::user unprivileged_user = passwd::current_user();
+ unprivileged_user.gid += 1;
+
+ const process::status status = fork_and_run(check_drop_privileges(
+ unprivileged_user));
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
+ ATF_REQUIRE(atf::utils::grep_file("(chown|setgid).*failed",
+ "subprocess.stderr"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps);
+ATF_TEST_CASE_BODY(isolate_child__enable_core_dumps)
+{
+ utils::require_run_coredump_tests(this);
+
+ struct ::rlimit rl;
+ if (::getrlimit(RLIMIT_CORE, &rl) == -1)
+ fail("Failed to query the core size limit");
+ if (rl.rlim_cur == 0 || rl.rlim_max == 0)
+ skip("Maximum core size is zero; cannot run test");
+ rl.rlim_cur = 0;
+ if (::setrlimit(RLIMIT_CORE, &rl) == -1)
+ fail("Failed to lower the core size limit");
+
+ const process::status status = fork_and_run(check_enable_core_dumps);
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE(status.coredump());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory);
+ATF_TEST_CASE_BODY(isolate_child__enter_work_directory)
+{
+ const fs::path directory("some/sub/directory");
+ fs::mkdir_p(directory, 0755);
+ const process::status status = fork_and_run(
+ check_enter_work_directory(directory));
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure);
+ATF_TEST_CASE_BODY(isolate_child__enter_work_directory_failure)
+{
+ const fs::path directory("some/sub/directory");
+ const process::status status = fork_and_run(
+ check_enter_work_directory(directory));
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
+ ATF_REQUIRE(atf::utils::grep_file("chdir\\(some/sub/directory\\) failed",
+ "subprocess.stderr"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session);
+ATF_TEST_CASE_BODY(isolate_child__new_session)
+{
+ const process::status status = fork_and_run(check_new_session);
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal);
+ATF_TEST_CASE_BODY(isolate_child__no_terminal)
+{
+ const process::status status = fork_and_run(check_no_terminal);
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group);
+ATF_TEST_CASE_BODY(isolate_child__process_group)
+{
+ const process::status status = fork_and_run(check_process_group);
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask);
+ATF_TEST_CASE_BODY(isolate_child__reset_umask)
+{
+ const process::status status = fork_and_run(check_umask);
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+}
+
+
+/// Executes isolate_path() and compares the on-disk changes to expected values.
+///
+/// \param unprivileged_user The user to pass to isolate_path; may be none.
+/// \param exp_uid Expected UID or none to expect the old value.
+/// \param exp_gid Expected GID or none to expect the old value.
+static void
+do_isolate_path_test(const optional< passwd::user >& unprivileged_user,
+ const optional< uid_t >& exp_uid,
+ const optional< gid_t >& exp_gid)
+{
+ const fs::path dir("dir");
+ fs::mkdir(dir, 0755);
+ struct ::stat old_sb;
+ ATF_REQUIRE(::stat(dir.c_str(), &old_sb) != -1);
+
+ process::isolate_path(unprivileged_user, dir);
+
+ struct ::stat new_sb;
+ ATF_REQUIRE(::stat(dir.c_str(), &new_sb) != -1);
+
+ if (exp_uid)
+ ATF_REQUIRE_EQ(exp_uid.get(), new_sb.st_uid);
+ else
+ ATF_REQUIRE_EQ(old_sb.st_uid, new_sb.st_uid);
+
+ if (exp_gid)
+ ATF_REQUIRE_EQ(exp_gid.get(), new_sb.st_gid);
+ else
+ ATF_REQUIRE_EQ(old_sb.st_gid, new_sb.st_gid);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user);
+ATF_TEST_CASE_BODY(isolate_path__no_user)
+{
+ do_isolate_path_test(none, none, none);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user);
+ATF_TEST_CASE_BODY(isolate_path__same_user)
+{
+ do_isolate_path_test(utils::make_optional(passwd::current_user()),
+ none, none);
+}
+
+
+ATF_TEST_CASE(isolate_path__other_user_when_unprivileged);
+ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged)
+{
+ set_md_var("require.user", "unprivileged");
+}
+ATF_TEST_CASE_BODY(isolate_path__other_user_when_unprivileged)
+{
+ passwd::user user = passwd::current_user();
+ user.uid += 1;
+ user.gid += 1;
+
+ do_isolate_path_test(utils::make_optional(user), none, none);
+}
+
+
+ATF_TEST_CASE(isolate_path__drop_privileges);
+ATF_TEST_CASE_HEAD(isolate_path__drop_privileges)
+{
+ set_md_var("require.config", "unprivileged-user");
+ set_md_var("require.user", "root");
+}
+ATF_TEST_CASE_BODY(isolate_path__drop_privileges)
+{
+ const passwd::user unprivileged_user = passwd::find_user_by_name(
+ get_config_var("unprivileged-user"));
+ do_isolate_path_test(utils::make_optional(unprivileged_user),
+ utils::make_optional(unprivileged_user.uid),
+ utils::make_optional(unprivileged_user.gid));
+}
+
+
+ATF_TEST_CASE(isolate_path__drop_privileges_only_uid);
+ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_uid)
+{
+ set_md_var("require.config", "unprivileged-user");
+ set_md_var("require.user", "root");
+}
+ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_uid)
+{
+ passwd::user unprivileged_user = passwd::find_user_by_name(
+ get_config_var("unprivileged-user"));
+ unprivileged_user.gid = ::getgid();
+ do_isolate_path_test(utils::make_optional(unprivileged_user),
+ utils::make_optional(unprivileged_user.uid),
+ none);
+}
+
+
+ATF_TEST_CASE(isolate_path__drop_privileges_only_gid);
+ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_gid)
+{
+ set_md_var("require.config", "unprivileged-user");
+ set_md_var("require.user", "root");
+}
+ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_gid)
+{
+ passwd::user unprivileged_user = passwd::find_user_by_name(
+ get_config_var("unprivileged-user"));
+ unprivileged_user.uid = ::getuid();
+ do_isolate_path_test(utils::make_optional(unprivileged_user),
+ none,
+ utils::make_optional(unprivileged_user.gid));
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, isolate_child__clean_environment);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__other_user_when_unprivileged);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_uid);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_gid);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__enable_core_dumps);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory_failure);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__new_session);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__no_terminal);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__process_group);
+ ATF_ADD_TEST_CASE(tcs, isolate_child__reset_umask);
+
+ ATF_ADD_TEST_CASE(tcs, isolate_path__no_user);
+ ATF_ADD_TEST_CASE(tcs, isolate_path__same_user);
+ ATF_ADD_TEST_CASE(tcs, isolate_path__other_user_when_unprivileged);
+ ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges);
+ ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_uid);
+ ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_gid);
+}
diff --git a/utils/process/operations.cpp b/utils/process/operations.cpp
new file mode 100644
index 000000000000..abcc49f2a443
--- /dev/null
+++ b/utils/process/operations.cpp
@@ -0,0 +1,273 @@
+// Copyright 2014 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/operations.hpp"
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/process/exceptions.hpp"
+#include "utils/process/system.hpp"
+#include "utils/process/status.hpp"
+#include "utils/sanity.hpp"
+#include "utils/signals/interrupts.hpp"
+
+namespace fs = utils::fs;
+namespace process = utils::process;
+namespace signals = utils::signals;
+
+
+/// Maximum number of arguments supported by exec.
+///
+/// We need this limit to avoid having to allocate dynamic memory in the child
+/// process to construct the arguments list, which would have side-effects in
+/// the parent's memory if we use vfork().
+#define MAX_ARGS 128
+
+
+namespace {
+
+
+/// Exception-based, type-improved version of wait(2).
+///
+/// \return The PID of the terminated process and its termination status.
+///
+/// \throw process::system_error If the call to wait(2) fails.
+static process::status
+safe_wait(void)
+{
+ LD("Waiting for any child process");
+ int stat_loc;
+ const pid_t pid = ::wait(&stat_loc);
+ if (pid == -1) {
+ const int original_errno = errno;
+ throw process::system_error("Failed to wait for any child process",
+ original_errno);
+ }
+ return process::status(pid, stat_loc);
+}
+
+
+/// Exception-based, type-improved version of waitpid(2).
+///
+/// \param pid The identifier of the process to wait for.
+///
+/// \return The termination status of the process.
+///
+/// \throw process::system_error If the call to waitpid(2) fails.
+static process::status
+safe_waitpid(const pid_t pid)
+{
+ LD(F("Waiting for pid=%s") % pid);
+ int stat_loc;
+ if (process::detail::syscall_waitpid(pid, &stat_loc, 0) == -1) {
+ const int original_errno = errno;
+ throw process::system_error(F("Failed to wait for PID %s") % pid,
+ original_errno);
+ }
+ return process::status(pid, stat_loc);
+}
+
+
+} // anonymous namespace
+
+
+/// Executes an external binary and replaces the current process.
+///
+/// This function must not use any of the logging features so that the output
+/// of the subprocess is not "polluted" by our own messages.
+///
+/// This function must also not affect the global state of the current process
+/// as otherwise we would not be able to use vfork(). Only state stored in the
+/// stack can be touched.
+///
+/// \param program The binary to execute.
+/// \param args The arguments to pass to the binary, without the program name.
+void
+process::exec(const fs::path& program, const args_vector& args) throw()
+{
+ try {
+ exec_unsafe(program, args);
+ } catch (const system_error& error) {
+ // Error message already printed by exec_unsafe.
+ std::abort();
+ }
+}
+
+
+/// Executes an external binary and replaces the current process.
+///
+/// This differs from process::exec() in that this function reports errors
+/// caused by the exec(2) system call to let the caller decide how to handle
+/// them.
+///
+/// This function must not use any of the logging features so that the output
+/// of the subprocess is not "polluted" by our own messages.
+///
+/// This function must also not affect the global state of the current process
+/// as otherwise we would not be able to use vfork(). Only state stored in the
+/// stack can be touched.
+///
+/// \param program The binary to execute.
+/// \param args The arguments to pass to the binary, without the program name.
+///
+/// \throw system_error If the exec(2) call fails.
+void
+process::exec_unsafe(const fs::path& program, const args_vector& args)
+{
+ PRE(args.size() < MAX_ARGS);
+ int original_errno = 0;
+ try {
+ const char* argv[MAX_ARGS + 1];
+
+ argv[0] = program.c_str();
+ for (args_vector::size_type i = 0; i < args.size(); i++)
+ argv[1 + i] = args[i].c_str();
+ argv[1 + args.size()] = NULL;
+
+ const int ret = ::execv(program.c_str(),
+ (char* const*)(unsigned long)(const void*)argv);
+ original_errno = errno;
+ INV(ret == -1);
+ std::cerr << "Failed to execute " << program << ": "
+ << std::strerror(original_errno) << "\n";
+ } catch (const std::runtime_error& error) {
+ std::cerr << "Failed to execute " << program << ": "
+ << error.what() << "\n";
+ std::abort();
+ } catch (...) {
+ std::cerr << "Failed to execute " << program << "; got unexpected "
+ "exception during exec\n";
+ std::abort();
+ }
+
+ // We must do this here to prevent our exception from being caught by the
+ // generic handlers above.
+ INV(original_errno != 0);
+ throw system_error("Failed to execute " + program.str(), original_errno);
+}
+
+
+/// Forcibly kills a process group started by us.
+///
+/// This function is safe to call from an signal handler context.
+///
+/// Pretty much all of our subprocesses run in their own process group so that
+/// we can terminate them and thier children should we need to. Because of
+/// this, the very first thing our subprocesses do is create a new process group
+/// for themselves.
+///
+/// The implication of the above is that simply issuing a killpg() call on the
+/// process group is racy: if the subprocess has not yet had a chance to prepare
+/// its own process group, then we will not be killing anything. To solve this,
+/// we must also kill() the process group leader itself, and we must do so after
+/// the call to killpg(). Doing this is safe because: 1) the process group must
+/// have the same ID as the PID of the process that created it; and 2) we have
+/// not yet issued a wait() call so we still own the PID.
+///
+/// The sideffect of doing what we do here is that the process group leader may
+/// receive a signal twice. But we don't care because we are forcibly
+/// terminating the process group and none of the processes can controlledly
+/// react to SIGKILL.
+///
+/// \param pgid PID or process group ID to terminate.
+void
+process::terminate_group(const int pgid)
+{
+ (void)::killpg(pgid, SIGKILL);
+ (void)::kill(pgid, SIGKILL);
+}
+
+
+/// Terminates the current process reproducing the given status.
+///
+/// The caller process is abruptly terminated. In particular, no output streams
+/// are flushed, no destructors are called, and no atexit(2) handlers are run.
+///
+/// \param status The status to "re-deliver" to the caller process.
+void
+process::terminate_self_with(const status& status)
+{
+ if (status.exited()) {
+ ::_exit(status.exitstatus());
+ } else {
+ INV(status.signaled());
+ (void)::kill(::getpid(), status.termsig());
+ UNREACHABLE_MSG(F("Signal %s terminated %s but did not terminate "
+ "ourselves") % status.termsig() % status.dead_pid());
+ }
+}
+
+
+/// Blocks to wait for completion of a subprocess.
+///
+/// \param pid Identifier of the process to wait for.
+///
+/// \return The termination status of the child process that terminated.
+///
+/// \throw process::system_error If the call to wait(2) fails.
+process::status
+process::wait(const int pid)
+{
+ const process::status status = safe_waitpid(pid);
+ {
+ signals::interrupts_inhibiter inhibiter;
+ signals::remove_pid_to_kill(pid);
+ }
+ return status;
+}
+
+
+/// Blocks to wait for completion of any subprocess.
+///
+/// \return The termination status of the child process that terminated.
+///
+/// \throw process::system_error If the call to wait(2) fails.
+process::status
+process::wait_any(void)
+{
+ const process::status status = safe_wait();
+ {
+ signals::interrupts_inhibiter inhibiter;
+ signals::remove_pid_to_kill(status.dead_pid());
+ }
+ return status;
+}
diff --git a/utils/process/operations.hpp b/utils/process/operations.hpp
new file mode 100644
index 000000000000..773f9d38bb74
--- /dev/null
+++ b/utils/process/operations.hpp
@@ -0,0 +1,56 @@
+// Copyright 2014 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/operations.hpp
+/// Collection of utilities for process management.
+
+#if !defined(UTILS_PROCESS_OPERATIONS_HPP)
+#define UTILS_PROCESS_OPERATIONS_HPP
+
+#include "utils/process/operations_fwd.hpp"
+
+#include "utils/defs.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/process/status_fwd.hpp"
+
+namespace utils {
+namespace process {
+
+
+void exec(const utils::fs::path&, const args_vector&) throw() UTILS_NORETURN;
+void exec_unsafe(const utils::fs::path&, const args_vector&) UTILS_NORETURN;
+void terminate_group(const int);
+void terminate_self_with(const status&) UTILS_NORETURN;
+status wait(const int);
+status wait_any(void);
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_OPERATIONS_HPP)
diff --git a/utils/process/operations_fwd.hpp b/utils/process/operations_fwd.hpp
new file mode 100644
index 000000000000..bd23fdc2c691
--- /dev/null
+++ b/utils/process/operations_fwd.hpp
@@ -0,0 +1,49 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/operations_fwd.hpp
+/// Forward declarations for utils/process/operations.hpp
+
+#if !defined(UTILS_PROCESS_OPERATIONS_FWD_HPP)
+#define UTILS_PROCESS_OPERATIONS_FWD_HPP
+
+#include <string>
+#include <vector>
+
+namespace utils {
+namespace process {
+
+
+/// Arguments to a program, without the program name.
+typedef std::vector< std::string > args_vector;
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_OPERATIONS_FWD_HPP)
diff --git a/utils/process/operations_test.cpp b/utils/process/operations_test.cpp
new file mode 100644
index 000000000000..e9c1ebb65a3d
--- /dev/null
+++ b/utils/process/operations_test.cpp
@@ -0,0 +1,471 @@
+// Copyright 2014 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/operations.hpp"
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#include "utils/defs.hpp"
+#include "utils/format/containers.ipp"
+#include "utils/fs/path.hpp"
+#include "utils/process/child.ipp"
+#include "utils/process/exceptions.hpp"
+#include "utils/process/status.hpp"
+#include "utils/stacktrace.hpp"
+#include "utils/test_utils.ipp"
+
+namespace fs = utils::fs;
+namespace process = utils::process;
+
+
+namespace {
+
+
+/// Type of the process::exec() and process::exec_unsafe() functions.
+typedef void (*exec_function)(const fs::path&, const process::args_vector&);
+
+
+/// Calculates the path to the test helpers binary.
+///
+/// \param tc A pointer to the caller test case, needed to extract the value of
+/// the "srcdir" property.
+///
+/// \return The path to the helpers binary.
+static fs::path
+get_helpers(const atf::tests::tc* tc)
+{
+ return fs::path(tc->get_config_var("srcdir")) / "helpers";
+}
+
+
+/// Body for a subprocess that runs exec().
+class child_exec {
+ /// Function to do the exec.
+ const exec_function _do_exec;
+
+ /// Path to the binary to exec.
+ const fs::path& _program;
+
+ /// Arguments to the binary, not including argv[0].
+ const process::args_vector& _args;
+
+public:
+ /// Constructor.
+ ///
+ /// \param do_exec Function to do the exec.
+ /// \param program Path to the binary to exec.
+ /// \param args Arguments to the binary, not including argv[0].
+ child_exec(const exec_function do_exec, const fs::path& program,
+ const process::args_vector& args) :
+ _do_exec(do_exec), _program(program), _args(args)
+ {
+ }
+
+ /// Body for the subprocess.
+ void
+ operator()(void)
+ {
+ _do_exec(_program, _args);
+ }
+};
+
+
+/// Body for a process that returns a specific exit code.
+///
+/// \tparam ExitStatus The exit status for the subprocess.
+template< int ExitStatus >
+static void
+child_exit(void)
+{
+ std::exit(ExitStatus);
+}
+
+
+static void suspend(void) UTILS_NORETURN;
+
+
+/// Blocks a subprocess from running indefinitely.
+static void
+suspend(void)
+{
+ sigset_t mask;
+ sigemptyset(&mask);
+ for (;;) {
+ ::sigsuspend(&mask);
+ }
+}
+
+
+static void write_loop(const int) UTILS_NORETURN;
+
+
+/// Provides an infinite stream of data in a subprocess.
+///
+/// \param fd Descriptor into which to write.
+static void
+write_loop(const int fd)
+{
+ const int cookie = 0x12345678;
+ for (;;) {
+ std::cerr << "Still alive in PID " << ::getpid() << '\n';
+ if (::write(fd, &cookie, sizeof(cookie)) != sizeof(cookie))
+ std::exit(EXIT_FAILURE);
+ ::sleep(1);
+ }
+}
+
+
+} // anonymous namespace
+
+
+/// Tests an exec function with no arguments.
+///
+/// \param tc The calling test case.
+/// \param do_exec The exec function to test.
+static void
+check_exec_no_args(const atf::tests::tc* tc, const exec_function do_exec)
+{
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_exec(do_exec, get_helpers(tc), process::args_vector()),
+ fs::path("stdout"), fs::path("stderr"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_FAILURE, status.exitstatus());
+ ATF_REQUIRE(atf::utils::grep_file("Must provide a helper name", "stderr"));
+}
+
+
+/// Tests an exec function with some arguments.
+///
+/// \param tc The calling test case.
+/// \param do_exec The exec function to test.
+static void
+check_exec_some_args(const atf::tests::tc* tc, const exec_function do_exec)
+{
+ process::args_vector args;
+ args.push_back("print-args");
+ args.push_back("foo");
+ args.push_back("bar");
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_exec(do_exec, get_helpers(tc), args),
+ fs::path("stdout"), fs::path("stderr"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
+ ATF_REQUIRE(atf::utils::grep_file("argv\\[1\\] = print-args", "stdout"));
+ ATF_REQUIRE(atf::utils::grep_file("argv\\[2\\] = foo", "stdout"));
+ ATF_REQUIRE(atf::utils::grep_file("argv\\[3\\] = bar", "stdout"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(exec__no_args);
+ATF_TEST_CASE_BODY(exec__no_args)
+{
+ check_exec_no_args(this, process::exec);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(exec__some_args);
+ATF_TEST_CASE_BODY(exec__some_args)
+{
+ check_exec_some_args(this, process::exec);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(exec__fail);
+ATF_TEST_CASE_BODY(exec__fail)
+{
+ utils::avoid_coredump_on_crash();
+
+ std::auto_ptr< process::child > child = process::child::fork_files(
+ child_exec(process::exec, fs::path("non-existent"),
+ process::args_vector()),
+ fs::path("stdout"), fs::path("stderr"));
+ const process::status status = child->wait();
+ ATF_REQUIRE(status.signaled());
+ ATF_REQUIRE_EQ(SIGABRT, status.termsig());
+ ATF_REQUIRE(atf::utils::grep_file("Failed to execute non-existent",
+ "stderr"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__no_args);
+ATF_TEST_CASE_BODY(exec_unsafe__no_args)
+{
+ check_exec_no_args(this, process::exec_unsafe);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__some_args);
+ATF_TEST_CASE_BODY(exec_unsafe__some_args)
+{
+ check_exec_some_args(this, process::exec_unsafe);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__fail);
+ATF_TEST_CASE_BODY(exec_unsafe__fail)
+{
+ ATF_REQUIRE_THROW_RE(
+ process::system_error, "Failed to execute missing-program",
+ process::exec_unsafe(fs::path("missing-program"),
+ process::args_vector()));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(terminate_group__setpgrp_executed);
+ATF_TEST_CASE_BODY(terminate_group__setpgrp_executed)
+{
+ int first_fds[2], second_fds[2];
+ ATF_REQUIRE(::pipe(first_fds) != -1);
+ ATF_REQUIRE(::pipe(second_fds) != -1);
+
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ ::setpgid(::getpid(), ::getpid());
+ const pid_t pid2 = ::fork();
+ if (pid2 == -1) {
+ std::exit(EXIT_FAILURE);
+ } else if (pid2 == 0) {
+ ::close(first_fds[0]);
+ ::close(first_fds[1]);
+ ::close(second_fds[0]);
+ write_loop(second_fds[1]);
+ }
+ ::close(first_fds[0]);
+ ::close(second_fds[0]);
+ ::close(second_fds[1]);
+ write_loop(first_fds[1]);
+ }
+ ::close(first_fds[1]);
+ ::close(second_fds[1]);
+
+ int dummy;
+ std::cerr << "Waiting for children to start\n";
+ while (::read(first_fds[0], &dummy, sizeof(dummy)) <= 0 ||
+ ::read(second_fds[0], &dummy, sizeof(dummy)) <= 0) {
+ // Wait for children to come up.
+ }
+
+ process::terminate_group(pid);
+ std::cerr << "Waiting for children to die\n";
+ while (::read(first_fds[0], &dummy, sizeof(dummy)) > 0 ||
+ ::read(second_fds[0], &dummy, sizeof(dummy)) > 0) {
+ // Wait for children to terminate. If they don't, then the test case
+ // will time out.
+ }
+
+ int status;
+ ATF_REQUIRE(::wait(&status) != -1);
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE(WTERMSIG(status) == SIGKILL);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(terminate_group__setpgrp_not_executed);
+ATF_TEST_CASE_BODY(terminate_group__setpgrp_not_executed)
+{
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ // We do not call setgprp() here to simulate the race that happens when
+ // we invoke terminate_group on a process that has not yet had a chance
+ // to run the setpgrp() call.
+ suspend();
+ }
+
+ process::terminate_group(pid);
+
+ int status;
+ ATF_REQUIRE(::wait(&status) != -1);
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE(WTERMSIG(status) == SIGKILL);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__exitstatus);
+ATF_TEST_CASE_BODY(terminate_self_with__exitstatus)
+{
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ const process::status status = process::status::fake_exited(123);
+ process::terminate_self_with(status);
+ }
+
+ int status;
+ ATF_REQUIRE(::wait(&status) != -1);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 123);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__termsig);
+ATF_TEST_CASE_BODY(terminate_self_with__termsig)
+{
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ const process::status status = process::status::fake_signaled(
+ SIGKILL, false);
+ process::terminate_self_with(status);
+ }
+
+ int status;
+ ATF_REQUIRE(::wait(&status) != -1);
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE(WTERMSIG(status) == SIGKILL);
+ ATF_REQUIRE(!WCOREDUMP(status));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__termsig_and_core);
+ATF_TEST_CASE_BODY(terminate_self_with__termsig_and_core)
+{
+ utils::prepare_coredump_test(this);
+
+ const pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ const process::status status = process::status::fake_signaled(
+ SIGABRT, true);
+ process::terminate_self_with(status);
+ }
+
+ int status;
+ ATF_REQUIRE(::wait(&status) != -1);
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE(WTERMSIG(status) == SIGABRT);
+ ATF_REQUIRE(WCOREDUMP(status));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(wait__ok);
+ATF_TEST_CASE_BODY(wait__ok)
+{
+ std::auto_ptr< process::child > child = process::child::fork_capture(
+ child_exit< 15 >);
+ const pid_t pid = child->pid();
+ child.reset(); // Ensure there is no conflict between destructor and wait.
+
+ const process::status status = process::wait(pid);
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(15, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(wait__fail);
+ATF_TEST_CASE_BODY(wait__fail)
+{
+ ATF_REQUIRE_THROW(process::system_error, process::wait(1));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(wait_any__one);
+ATF_TEST_CASE_BODY(wait_any__one)
+{
+ process::child::fork_capture(child_exit< 15 >);
+
+ const process::status status = process::wait_any();
+ ATF_REQUIRE(status.exited());
+ ATF_REQUIRE_EQ(15, status.exitstatus());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(wait_any__many);
+ATF_TEST_CASE_BODY(wait_any__many)
+{
+ process::child::fork_capture(child_exit< 15 >);
+ process::child::fork_capture(child_exit< 30 >);
+ process::child::fork_capture(child_exit< 45 >);
+
+ std::set< int > exit_codes;
+ for (int i = 0; i < 3; i++) {
+ const process::status status = process::wait_any();
+ ATF_REQUIRE(status.exited());
+ exit_codes.insert(status.exitstatus());
+ }
+
+ std::set< int > exp_exit_codes;
+ exp_exit_codes.insert(15);
+ exp_exit_codes.insert(30);
+ exp_exit_codes.insert(45);
+ ATF_REQUIRE_EQ(exp_exit_codes, exit_codes);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(wait_any__none_is_failure);
+ATF_TEST_CASE_BODY(wait_any__none_is_failure)
+{
+ try {
+ const process::status status = process::wait_any();
+ fail("Expected exception but none raised");
+ } catch (const process::system_error& e) {
+ ATF_REQUIRE(atf::utils::grep_string("Failed to wait", e.what()));
+ ATF_REQUIRE_EQ(ECHILD, e.original_errno());
+ }
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, exec__no_args);
+ ATF_ADD_TEST_CASE(tcs, exec__some_args);
+ ATF_ADD_TEST_CASE(tcs, exec__fail);
+
+ ATF_ADD_TEST_CASE(tcs, exec_unsafe__no_args);
+ ATF_ADD_TEST_CASE(tcs, exec_unsafe__some_args);
+ ATF_ADD_TEST_CASE(tcs, exec_unsafe__fail);
+
+ ATF_ADD_TEST_CASE(tcs, terminate_group__setpgrp_executed);
+ ATF_ADD_TEST_CASE(tcs, terminate_group__setpgrp_not_executed);
+
+ ATF_ADD_TEST_CASE(tcs, terminate_self_with__exitstatus);
+ ATF_ADD_TEST_CASE(tcs, terminate_self_with__termsig);
+ ATF_ADD_TEST_CASE(tcs, terminate_self_with__termsig_and_core);
+
+ ATF_ADD_TEST_CASE(tcs, wait__ok);
+ ATF_ADD_TEST_CASE(tcs, wait__fail);
+
+ ATF_ADD_TEST_CASE(tcs, wait_any__one);
+ ATF_ADD_TEST_CASE(tcs, wait_any__many);
+ ATF_ADD_TEST_CASE(tcs, wait_any__none_is_failure);
+}
diff --git a/utils/process/status.cpp b/utils/process/status.cpp
new file mode 100644
index 000000000000..a3cea8e09ebd
--- /dev/null
+++ b/utils/process/status.cpp
@@ -0,0 +1,200 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/status.hpp"
+
+extern "C" {
+#include <sys/wait.h>
+}
+
+#include "utils/format/macros.hpp"
+#include "utils/optional.ipp"
+#include "utils/sanity.hpp"
+
+namespace process = utils::process;
+
+using utils::none;
+using utils::optional;
+
+#if !defined(WCOREDUMP)
+# define WCOREDUMP(x) false
+#endif
+
+
+/// Constructs a new status object based on the status value of waitpid(2).
+///
+/// \param dead_pid_ The PID of the process this status belonged to.
+/// \param stat_loc The status value returnd by waitpid(2).
+process::status::status(const int dead_pid_, int stat_loc) :
+ _dead_pid(dead_pid_),
+ _exited(WIFEXITED(stat_loc) ?
+ optional< int >(WEXITSTATUS(stat_loc)) : none),
+ _signaled(WIFSIGNALED(stat_loc) ?
+ optional< std::pair< int, bool > >(
+ std::make_pair(WTERMSIG(stat_loc), WCOREDUMP(stat_loc))) :
+ none)
+{
+}
+
+
+/// Constructs a new status object based on fake values.
+///
+/// \param exited_ If not none, specifies the exit status of the program.
+/// \param signaled_ If not none, specifies the termination signal and whether
+/// the process dumped core or not.
+process::status::status(const optional< int >& exited_,
+ const optional< std::pair< int, bool > >& signaled_) :
+ _dead_pid(-1),
+ _exited(exited_),
+ _signaled(signaled_)
+{
+}
+
+
+/// Constructs a new status object based on a fake exit status.
+///
+/// \param exitstatus_ The exit code of the process.
+///
+/// \return A status object with fake data.
+process::status
+process::status::fake_exited(const int exitstatus_)
+{
+ return status(utils::make_optional(exitstatus_), none);
+}
+
+
+/// Constructs a new status object based on a fake exit status.
+///
+/// \param termsig_ The termination signal of the process.
+/// \param coredump_ Whether the process dumped core or not.
+///
+/// \return A status object with fake data.
+process::status
+process::status::fake_signaled(const int termsig_, const bool coredump_)
+{
+ return status(none, utils::make_optional(std::make_pair(termsig_,
+ coredump_)));
+}
+
+
+/// Returns the PID of the process this status was taken from.
+///
+/// Please note that the process is already dead and gone from the system. This
+/// PID can only be used for informational reasons and not to address the
+/// process in any way.
+///
+/// \return The PID of the original process.
+int
+process::status::dead_pid(void) const
+{
+ return _dead_pid;
+}
+
+
+/// Returns whether the process exited cleanly or not.
+///
+/// \return True if the process exited cleanly, false otherwise.
+bool
+process::status::exited(void) const
+{
+ return _exited;
+}
+
+
+/// Returns the exit code of the process.
+///
+/// \pre The process must have exited cleanly (i.e. exited() must be true).
+///
+/// \return The exit code.
+int
+process::status::exitstatus(void) const
+{
+ PRE(exited());
+ return _exited.get();
+}
+
+
+/// Returns whether the process terminated due to a signal or not.
+///
+/// \return True if the process terminated due to a signal, false otherwise.
+bool
+process::status::signaled(void) const
+{
+ return _signaled;
+}
+
+
+/// Returns the signal that terminated the process.
+///
+/// \pre The process must have terminated by a signal (i.e. signaled() must be
+/// true.
+///
+/// \return The signal number.
+int
+process::status::termsig(void) const
+{
+ PRE(signaled());
+ return _signaled.get().first;
+}
+
+
+/// Returns whether the process core dumped or not.
+///
+/// This functionality may be unsupported in some platforms. In such cases,
+/// this method returns false unconditionally.
+///
+/// \pre The process must have terminated by a signal (i.e. signaled() must be
+/// true.
+///
+/// \return True if the process dumped core, false otherwise.
+bool
+process::status::coredump(void) const
+{
+ PRE(signaled());
+ return _signaled.get().second;
+}
+
+
+/// Injects the object into a stream.
+///
+/// \param output The stream into which to inject the object.
+/// \param status The object to format.
+///
+/// \return The output stream.
+std::ostream&
+process::operator<<(std::ostream& output, const status& status)
+{
+ if (status.exited()) {
+ output << F("status{exitstatus=%s}") % status.exitstatus();
+ } else {
+ INV(status.signaled());
+ output << F("status{termsig=%s, coredump=%s}") % status.termsig() %
+ status.coredump();
+ }
+ return output;
+}
diff --git a/utils/process/status.hpp b/utils/process/status.hpp
new file mode 100644
index 000000000000..b14ff55c01a2
--- /dev/null
+++ b/utils/process/status.hpp
@@ -0,0 +1,84 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/status.hpp
+/// Provides the utils::process::status class.
+
+#if !defined(UTILS_PROCESS_STATUS_HPP)
+#define UTILS_PROCESS_STATUS_HPP
+
+#include "utils/process/status_fwd.hpp"
+
+#include <ostream>
+#include <utility>
+
+#include "utils/optional.ipp"
+
+namespace utils {
+namespace process {
+
+
+/// Representation of the termination status of a process.
+class status {
+ /// The PID of the process that generated this status.
+ ///
+ /// Note that the process has exited already and been awaited for, so the
+ /// PID cannot be used to address the process.
+ int _dead_pid;
+
+ /// The exit status of the process, if it exited cleanly.
+ optional< int > _exited;
+
+ /// The signal that terminated the program, if any, and if it dumped core.
+ optional< std::pair< int, bool > > _signaled;
+
+ status(const optional< int >&, const optional< std::pair< int, bool > >&);
+
+public:
+ status(const int, int);
+ static status fake_exited(const int);
+ static status fake_signaled(const int, const bool);
+
+ int dead_pid(void) const;
+
+ bool exited(void) const;
+ int exitstatus(void) const;
+
+ bool signaled(void) const;
+ int termsig(void) const;
+ bool coredump(void) const;
+};
+
+
+std::ostream& operator<<(std::ostream&, const status&);
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_STATUS_HPP)
diff --git a/utils/process/status_fwd.hpp b/utils/process/status_fwd.hpp
new file mode 100644
index 000000000000..3a14683dc15c
--- /dev/null
+++ b/utils/process/status_fwd.hpp
@@ -0,0 +1,45 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/status_fwd.hpp
+/// Forward declarations for utils/process/status.hpp
+
+#if !defined(UTILS_PROCESS_STATUS_FWD_HPP)
+#define UTILS_PROCESS_STATUS_FWD_HPP
+
+namespace utils {
+namespace process {
+
+
+class status;
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_STATUS_FWD_HPP)
diff --git a/utils/process/status_test.cpp b/utils/process/status_test.cpp
new file mode 100644
index 000000000000..5a3e19eeaf18
--- /dev/null
+++ b/utils/process/status_test.cpp
@@ -0,0 +1,209 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/status.hpp"
+
+extern "C" {
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#include "utils/test_utils.ipp"
+
+using utils::process::status;
+
+
+namespace {
+
+
+/// Body of a subprocess that exits with a particular exit status.
+///
+/// \tparam ExitStatus The status to exit with.
+template< int ExitStatus >
+void child_exit(void)
+{
+ std::exit(ExitStatus);
+}
+
+
+/// Body of a subprocess that sends a particular signal to itself.
+///
+/// \tparam Signo The signal to send to self.
+template< int Signo >
+void child_signal(void)
+{
+ ::kill(::getpid(), Signo);
+}
+
+
+/// Spawns a process and waits for completion.
+///
+/// \param hook The function to run within the child. Should not return.
+///
+/// \return The termination status of the spawned subprocess.
+status
+fork_and_wait(void (*hook)(void))
+{
+ pid_t pid = ::fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0) {
+ hook();
+ std::abort();
+ } else {
+ int stat_loc;
+ ATF_REQUIRE(::waitpid(pid, &stat_loc, 0) != -1);
+ const status s = status(pid, stat_loc);
+ ATF_REQUIRE_EQ(pid, s.dead_pid());
+ return s;
+ }
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(fake_exited)
+ATF_TEST_CASE_BODY(fake_exited)
+{
+ const status fake = status::fake_exited(123);
+ ATF_REQUIRE_EQ(-1, fake.dead_pid());
+ ATF_REQUIRE(fake.exited());
+ ATF_REQUIRE_EQ(123, fake.exitstatus());
+ ATF_REQUIRE(!fake.signaled());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(fake_signaled)
+ATF_TEST_CASE_BODY(fake_signaled)
+{
+ const status fake = status::fake_signaled(567, true);
+ ATF_REQUIRE_EQ(-1, fake.dead_pid());
+ ATF_REQUIRE(!fake.exited());
+ ATF_REQUIRE(fake.signaled());
+ ATF_REQUIRE_EQ(567, fake.termsig());
+ ATF_REQUIRE(fake.coredump());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__exitstatus);
+ATF_TEST_CASE_BODY(output__exitstatus)
+{
+ const status fake = status::fake_exited(123);
+ std::ostringstream str;
+ str << fake;
+ ATF_REQUIRE_EQ("status{exitstatus=123}", str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__signaled_without_core);
+ATF_TEST_CASE_BODY(output__signaled_without_core)
+{
+ const status fake = status::fake_signaled(8, false);
+ std::ostringstream str;
+ str << fake;
+ ATF_REQUIRE_EQ("status{termsig=8, coredump=false}", str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__signaled_with_core);
+ATF_TEST_CASE_BODY(output__signaled_with_core)
+{
+ const status fake = status::fake_signaled(9, true);
+ std::ostringstream str;
+ str << fake;
+ ATF_REQUIRE_EQ("status{termsig=9, coredump=true}", str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__exited);
+ATF_TEST_CASE_BODY(integration__exited)
+{
+ const status exit_success = fork_and_wait(child_exit< EXIT_SUCCESS >);
+ ATF_REQUIRE(exit_success.exited());
+ ATF_REQUIRE_EQ(EXIT_SUCCESS, exit_success.exitstatus());
+ ATF_REQUIRE(!exit_success.signaled());
+
+ const status exit_failure = fork_and_wait(child_exit< EXIT_FAILURE >);
+ ATF_REQUIRE(exit_failure.exited());
+ ATF_REQUIRE_EQ(EXIT_FAILURE, exit_failure.exitstatus());
+ ATF_REQUIRE(!exit_failure.signaled());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__signaled);
+ATF_TEST_CASE_BODY(integration__signaled)
+{
+ const status sigterm = fork_and_wait(child_signal< SIGTERM >);
+ ATF_REQUIRE(!sigterm.exited());
+ ATF_REQUIRE(sigterm.signaled());
+ ATF_REQUIRE_EQ(SIGTERM, sigterm.termsig());
+ ATF_REQUIRE(!sigterm.coredump());
+
+ const status sigkill = fork_and_wait(child_signal< SIGKILL >);
+ ATF_REQUIRE(!sigkill.exited());
+ ATF_REQUIRE(sigkill.signaled());
+ ATF_REQUIRE_EQ(SIGKILL, sigkill.termsig());
+ ATF_REQUIRE(!sigkill.coredump());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integration__coredump);
+ATF_TEST_CASE_BODY(integration__coredump)
+{
+ utils::prepare_coredump_test(this);
+
+ const status coredump = fork_and_wait(child_signal< SIGQUIT >);
+ ATF_REQUIRE(!coredump.exited());
+ ATF_REQUIRE(coredump.signaled());
+ ATF_REQUIRE_EQ(SIGQUIT, coredump.termsig());
+#if !defined(WCOREDUMP)
+ expect_fail("Platform does not support checking for coredump");
+#endif
+ ATF_REQUIRE(coredump.coredump());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, fake_exited);
+ ATF_ADD_TEST_CASE(tcs, fake_signaled);
+
+ ATF_ADD_TEST_CASE(tcs, output__exitstatus);
+ ATF_ADD_TEST_CASE(tcs, output__signaled_without_core);
+ ATF_ADD_TEST_CASE(tcs, output__signaled_with_core);
+
+ ATF_ADD_TEST_CASE(tcs, integration__exited);
+ ATF_ADD_TEST_CASE(tcs, integration__signaled);
+ ATF_ADD_TEST_CASE(tcs, integration__coredump);
+}
diff --git a/utils/process/system.cpp b/utils/process/system.cpp
new file mode 100644
index 000000000000..ac41ddb7daa7
--- /dev/null
+++ b/utils/process/system.cpp
@@ -0,0 +1,59 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/system.hpp"
+
+extern "C" {
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+namespace detail = utils::process::detail;
+
+
+/// Indirection to execute the dup2(2) system call.
+int (*detail::syscall_dup2)(const int, const int) = ::dup2;
+
+
+/// Indirection to execute the fork(2) system call.
+pid_t (*detail::syscall_fork)(void) = ::fork;
+
+
+/// Indirection to execute the open(2) system call.
+int (*detail::syscall_open)(const char*, const int, ...) = ::open;
+
+
+/// Indirection to execute the pipe(2) system call.
+int (*detail::syscall_pipe)(int[2]) = ::pipe;
+
+
+/// Indirection to execute the waitpid(2) system call.
+pid_t (*detail::syscall_waitpid)(const pid_t, int*, const int) = ::waitpid;
diff --git a/utils/process/system.hpp b/utils/process/system.hpp
new file mode 100644
index 000000000000..a794876f3579
--- /dev/null
+++ b/utils/process/system.hpp
@@ -0,0 +1,66 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/system.hpp
+/// Indirection to perform system calls.
+///
+/// The indirections exposed in this file are provided to allow unit-testing of
+/// particular system behaviors (e.g. failures). The caller of a routine in
+/// this library is allowed, for testing purposes only, to explicitly replace
+/// the pointers in this file with custom functions to inject a particular
+/// behavior into the library code.
+///
+/// Do not include this header from other header files.
+///
+/// It may be nice to go one step further and completely abstract the library
+/// functions in here to provide exception-based error reporting.
+
+#if !defined(UTILS_PROCESS_SYSTEM_HPP)
+#define UTILS_PROCESS_SYSTEM_HPP
+
+extern "C" {
+#include <unistd.h>
+}
+
+namespace utils {
+namespace process {
+namespace detail {
+
+
+extern int (*syscall_dup2)(const int, const int);
+extern pid_t (*syscall_fork)(void);
+extern int (*syscall_open)(const char*, const int, ...);
+extern int (*syscall_pipe)(int[2]);
+extern pid_t (*syscall_waitpid)(const pid_t, int*, const int);
+
+
+} // namespace detail
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_SYSTEM_HPP)
diff --git a/utils/process/systembuf.cpp b/utils/process/systembuf.cpp
new file mode 100644
index 000000000000..661b336221ac
--- /dev/null
+++ b/utils/process/systembuf.cpp
@@ -0,0 +1,152 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/systembuf.hpp"
+
+extern "C" {
+#include <unistd.h>
+}
+
+#include "utils/auto_array.ipp"
+#include "utils/noncopyable.hpp"
+#include "utils/sanity.hpp"
+
+using utils::process::systembuf;
+
+
+/// Private implementation fields for systembuf.
+struct systembuf::impl : utils::noncopyable {
+ /// File descriptor attached to the systembuf.
+ int _fd;
+
+ /// Size of the _read_buf and _write_buf buffers.
+ std::size_t _bufsize;
+
+ /// In-memory buffer for read operations.
+ utils::auto_array< char > _read_buf;
+
+ /// In-memory buffer for write operations.
+ utils::auto_array< char > _write_buf;
+
+ /// Initializes private implementation data.
+ ///
+ /// \param fd The file descriptor.
+ /// \param bufsize The size of the created read and write buffers.
+ impl(const int fd, const std::size_t bufsize) :
+ _fd(fd),
+ _bufsize(bufsize),
+ _read_buf(new char[bufsize]),
+ _write_buf(new char[bufsize])
+ {
+ }
+};
+
+
+/// Constructs a new systembuf based on an open file descriptor.
+///
+/// This grabs ownership of the file descriptor.
+///
+/// \param fd The file descriptor to wrap. Must be open and valid.
+/// \param bufsize The size to use for the internal read/write buffers.
+systembuf::systembuf(const int fd, std::size_t bufsize) :
+ _pimpl(new impl(fd, bufsize))
+{
+ setp(_pimpl->_write_buf.get(), _pimpl->_write_buf.get() + _pimpl->_bufsize);
+}
+
+
+/// Destroys a systembuf object.
+///
+/// \post The file descriptor attached to this systembuf is closed.
+systembuf::~systembuf(void)
+{
+ ::close(_pimpl->_fd);
+}
+
+
+/// Reads new data when the systembuf read buffer underflows.
+///
+/// \return The new character to be read, or EOF if no more.
+systembuf::int_type
+systembuf::underflow(void)
+{
+ PRE(gptr() >= egptr());
+
+ bool ok;
+ ssize_t cnt = ::read(_pimpl->_fd, _pimpl->_read_buf.get(),
+ _pimpl->_bufsize);
+ ok = (cnt != -1 && cnt != 0);
+
+ if (!ok)
+ return traits_type::eof();
+ else {
+ setg(_pimpl->_read_buf.get(), _pimpl->_read_buf.get(),
+ _pimpl->_read_buf.get() + cnt);
+ return traits_type::to_int_type(*gptr());
+ }
+}
+
+
+/// Writes data to the file descriptor when the write buffer overflows.
+///
+/// \param c The character that causes the overflow.
+///
+/// \return EOF if error, some other value for success.
+///
+/// \throw something TODO(jmmv): According to the documentation, it is OK for
+/// this method to throw in case of errors. Revisit this code to see if we
+/// can do better.
+systembuf::int_type
+systembuf::overflow(int c)
+{
+ PRE(pptr() >= epptr());
+ if (sync() == -1)
+ return traits_type::eof();
+ if (!traits_type::eq_int_type(c, traits_type::eof())) {
+ traits_type::assign(*pptr(), c);
+ pbump(1);
+ }
+ return traits_type::not_eof(c);
+}
+
+
+/// Synchronizes the stream with the file descriptor.
+///
+/// \return 0 on success, -1 on error.
+int
+systembuf::sync(void)
+{
+ ssize_t cnt = pptr() - pbase();
+
+ bool ok;
+ ok = ::write(_pimpl->_fd, pbase(), cnt) == cnt;
+
+ if (ok)
+ pbump(-cnt);
+ return ok ? 0 : -1;
+}
diff --git a/utils/process/systembuf.hpp b/utils/process/systembuf.hpp
new file mode 100644
index 000000000000..c89c9108dc4b
--- /dev/null
+++ b/utils/process/systembuf.hpp
@@ -0,0 +1,71 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/systembuf.hpp
+/// Provides the utils::process::systembuf class.
+
+#if !defined(UTILS_PROCESS_SYSTEMBUF_HPP)
+#define UTILS_PROCESS_SYSTEMBUF_HPP
+
+#include "utils/process/systembuf_fwd.hpp"
+
+#include <cstddef>
+#include <memory>
+#include <streambuf>
+
+#include "utils/noncopyable.hpp"
+
+namespace utils {
+namespace process {
+
+
+/// A std::streambuf implementation for raw file descriptors.
+///
+/// This class grabs ownership of the file descriptor. I.e. when the class is
+/// destroyed, the file descriptor is closed unconditionally.
+class systembuf : public std::streambuf, noncopyable {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::auto_ptr< impl > _pimpl;
+
+protected:
+ int_type underflow(void);
+ int_type overflow(int);
+ int sync(void);
+
+public:
+ explicit systembuf(const int, std::size_t = 8192);
+ ~systembuf(void);
+};
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_SYSTEMBUF_HPP)
diff --git a/utils/process/systembuf_fwd.hpp b/utils/process/systembuf_fwd.hpp
new file mode 100644
index 000000000000..b3e341336b1d
--- /dev/null
+++ b/utils/process/systembuf_fwd.hpp
@@ -0,0 +1,45 @@
+// Copyright 2015 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/// \file utils/process/systembuf_fwd.hpp
+/// Forward declarations for utils/process/systembuf.hpp
+
+#if !defined(UTILS_PROCESS_SYSTEMBUF_FWD_HPP)
+#define UTILS_PROCESS_SYSTEMBUF_FWD_HPP
+
+namespace utils {
+namespace process {
+
+
+class systembuf;
+
+
+} // namespace process
+} // namespace utils
+
+#endif // !defined(UTILS_PROCESS_SYSTEMBUF_FWD_HPP)
diff --git a/utils/process/systembuf_test.cpp b/utils/process/systembuf_test.cpp
new file mode 100644
index 000000000000..ef9ff1930cf6
--- /dev/null
+++ b/utils/process/systembuf_test.cpp
@@ -0,0 +1,166 @@
+// Copyright 2010 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "utils/process/systembuf.hpp"
+
+extern "C" {
+#include <sys/stat.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include <fstream>
+
+#include <atf-c++.hpp>
+
+using utils::process::systembuf;
+
+
+static void
+check_data(std::istream& is, std::size_t length)
+{
+ char ch = 'A', chr;
+ std::size_t cnt = 0;
+ while (is >> chr) {
+ ATF_REQUIRE_EQ(ch, chr);
+ if (ch == 'Z')
+ ch = 'A';
+ else
+ ch++;
+ cnt++;
+ }
+ ATF_REQUIRE_EQ(cnt, length);
+}
+
+
+static void
+write_data(std::ostream& os, std::size_t length)
+{
+ char ch = 'A';
+ for (std::size_t i = 0; i < length; i++) {
+ os << ch;
+ if (ch == 'Z')
+ ch = 'A';
+ else
+ ch++;
+ }
+ os.flush();
+}
+
+
+static void
+test_read(std::size_t length, std::size_t bufsize)
+{
+ std::ofstream f("test_read.txt");
+ write_data(f, length);
+ f.close();
+
+ int fd = ::open("test_read.txt", O_RDONLY);
+ ATF_REQUIRE(fd != -1);
+ systembuf sb(fd, bufsize);
+ std::istream is(&sb);
+ check_data(is, length);
+ ::close(fd);
+ ::unlink("test_read.txt");
+}
+
+
+static void
+test_write(std::size_t length, std::size_t bufsize)
+{
+ int fd = ::open("test_write.txt", O_WRONLY | O_CREAT | O_TRUNC,
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+ ATF_REQUIRE(fd != -1);
+ systembuf sb(fd, bufsize);
+ std::ostream os(&sb);
+ write_data(os, length);
+ ::close(fd);
+
+ std::ifstream is("test_write.txt");
+ check_data(is, length);
+ is.close();
+ ::unlink("test_write.txt");
+}
+
+
+ATF_TEST_CASE(short_read);
+ATF_TEST_CASE_HEAD(short_read)
+{
+ set_md_var("descr", "Tests that a short read (one that fits in the "
+ "internal buffer) works when using systembuf");
+}
+ATF_TEST_CASE_BODY(short_read)
+{
+ test_read(64, 1024);
+}
+
+
+ATF_TEST_CASE(long_read);
+ATF_TEST_CASE_HEAD(long_read)
+{
+ set_md_var("descr", "Tests that a long read (one that does not fit in "
+ "the internal buffer) works when using systembuf");
+}
+ATF_TEST_CASE_BODY(long_read)
+{
+ test_read(64 * 1024, 1024);
+}
+
+
+ATF_TEST_CASE(short_write);
+ATF_TEST_CASE_HEAD(short_write)
+{
+ set_md_var("descr", "Tests that a short write (one that fits in the "
+ "internal buffer) works when using systembuf");
+}
+ATF_TEST_CASE_BODY(short_write)
+{
+ test_write(64, 1024);
+}
+
+
+ATF_TEST_CASE(long_write);
+ATF_TEST_CASE_HEAD(long_write)
+{
+ set_md_var("descr", "Tests that a long write (one that does not fit "
+ "in the internal buffer) works when using systembuf");
+}
+ATF_TEST_CASE_BODY(long_write)
+{
+ test_write(64 * 1024, 1024);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, short_read);
+ ATF_ADD_TEST_CASE(tcs, long_read);
+ ATF_ADD_TEST_CASE(tcs, short_write);
+ ATF_ADD_TEST_CASE(tcs, long_write);
+}