diff options
Diffstat (limited to 'utils/process')
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); +} |