// 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();
}
