freebsd-nq/contrib/kyua/utils/process/executor.cpp
Brooks Davis b0d29bc47d Import the kyua test framework.
Having kyua in the base system will simplify automated testing in CI and
eliminates bootstrapping issues on new platforms.

The build of kyua is controlled by WITH(OUT)_TESTS_SUPPORT.

Reviewed by:	emaste
Obtained from:	CheriBSD
Sponsored by:	DARPA
Differential Revision:	https://reviews.freebsd.org/D24103
2020-03-23 19:01:23 +00:00

870 lines
28 KiB
C++

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