summaryrefslogtreecommitdiffstats
path: root/src/lib/asiolink/process_spawn.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/asiolink/process_spawn.cc')
-rw-r--r--src/lib/asiolink/process_spawn.cc448
1 files changed, 448 insertions, 0 deletions
diff --git a/src/lib/asiolink/process_spawn.cc b/src/lib/asiolink/process_spawn.cc
new file mode 100644
index 0000000..9eb1b06
--- /dev/null
+++ b/src/lib/asiolink/process_spawn.cc
@@ -0,0 +1,448 @@
+// Copyright (C) 2015-2021 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <asiolink/io_service_signal.h>
+#include <asiolink/process_spawn.h>
+#include <exceptions/exceptions.h>
+#include <cstring>
+#include <functional>
+#include <map>
+#include <mutex>
+#include <signal.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <boost/make_shared.hpp>
+
+using namespace std;
+namespace ph = std::placeholders;
+
+namespace isc {
+namespace asiolink {
+
+/// @brief Type for process state
+struct ProcessState {
+
+ /// @brief Constructor
+ ProcessState() : running_(true), status_(0) {
+ }
+
+ /// @brief true until the exit status is collected
+ bool running_;
+
+ /// @brief 0 or the exit status
+ int status_;
+};
+
+/// @brief Defines a pointer to a ProcessState.
+typedef boost::shared_ptr<ProcessState> ProcessStatePtr;
+
+/// @brief ProcessStates container which stores a ProcessState for each process
+/// identified by PID.
+typedef std::map<pid_t, ProcessStatePtr> ProcessStates;
+
+class ProcessSpawnImpl;
+
+/// @brief ProcessCollection container which stores all ProcessStates for each
+/// instance of @ref ProcessSpawnImpl.
+typedef std::map<const ProcessSpawnImpl*, ProcessStates> ProcessCollection;
+
+/// @brief Implementation of the @c ProcessSpawn class.
+///
+/// This pimpl idiom is used by the @c ProcessSpawn in this case to
+/// avoid exposing the internals of the implementation, such as
+/// custom handling of a SIGCHLD signal, and the conversion of the
+/// arguments of the executable from the STL container to the array.
+///
+/// This class is made noncopyable so that we don't have attempts
+/// to make multiple copies of an object. This avoid problems
+/// with multiple copies of objects for a single global resource
+/// such as the SIGCHLD signal handler. In addition making it
+/// noncopyable keeps the static check code from flagging the
+/// lack of a copy constructor as an issue.
+class ProcessSpawnImpl : boost::noncopyable {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// @param io_service The IOService which handles signal handlers.
+ /// @param executable A full path to the program to be executed.
+ /// @param args Arguments for the program to be executed.
+ /// @param vars Environment variables for the program to be executed.
+ ProcessSpawnImpl(IOServicePtr io_service,
+ const std::string& executable,
+ const ProcessArgs& args,
+ const ProcessEnvVars& vars);
+
+ /// @brief Destructor.
+ ~ProcessSpawnImpl();
+
+ /// @brief Returns full command line, including arguments, for the process.
+ std::string getCommandLine() const;
+
+ /// @brief Spawn the new process.
+ ///
+ /// This method forks the current process and executes the specified
+ /// binary with arguments within the child process.
+ ///
+ /// The child process will return EXIT_FAILURE if the method was unable
+ /// to start the executable, e.g. as a result of insufficient permissions
+ /// or when the executable does not exist. If the process ends successfully
+ /// the EXIT_SUCCESS is returned.
+ ///
+ /// @param dismiss The flag which indicated if the process status can be
+ /// disregarded.
+ /// @return PID of the spawned process.
+ /// @throw ProcessSpawnError if forking a current process failed.
+ pid_t spawn(bool dismiss);
+
+ /// @brief Checks if the process is still running.
+ ///
+ /// @param pid ID of the child processes for which state should be checked.
+ /// @return true if the child process is running, false otherwise.
+ bool isRunning(const pid_t pid) const;
+
+ /// @brief Checks if any of the spawned processes is still running.
+ ///
+ /// @return true if at least one child process is still running.
+ bool isAnyRunning() const;
+
+ /// @brief Returns exit status of the process.
+ ///
+ /// If the process is still running, the previous status is returned
+ /// or 0, if the process is being ran for the first time.
+ ///
+ /// @param pid ID of the child process for which exit status should be
+ /// returned.
+ /// @return Exit code of the process.
+ int getExitStatus(const pid_t pid) const;
+
+ /// @brief Removes the status of the process with a specified PID.
+ ///
+ /// This method removes the status of the process with a specified PID.
+ /// If the process is still running, the status is not removed and the
+ /// exception is thrown.
+ ///
+ /// @param pid A process pid.
+ void clearState(const pid_t pid);
+
+private:
+
+ /// @brief Initializer class for the SIGCHLD signal handler.
+ ///
+ /// This is a singleton class used to initialize the SIGCHLD signal handler
+ /// only on the first call of @ref initIOSignalSet which happens on each
+ /// call of @ref ProcessSpawn::spawn.
+ class IOSignalSetInitializer {
+ private:
+
+ /// @brief Constructor
+ ///
+ /// @param io_service The IOService which handles signal handlers.
+ IOSignalSetInitializer(IOServicePtr io_service) {
+ if (!io_service) {
+ isc_throw(ProcessSpawnError, "NULL IOService instance");
+ }
+ io_signal_set_ = boost::make_shared<IOSignalSet>(io_service,
+ std::bind(&ProcessSpawnImpl::waitForProcess, ph::_1));
+ io_signal_set_->add(SIGCHLD);
+ }
+
+ /// @brief Destructor
+ ~IOSignalSetInitializer() {
+ io_signal_set_->remove(SIGCHLD);
+ }
+
+ public:
+
+ /// @brief Initialize the SIGCHLD signal handler.
+ ///
+ /// It creates the single instance of @ref IOSignalSetInitializer.
+ ///
+ /// @param io_service The IOService which handles signal handlers.
+ static void initIOSignalSet(IOServicePtr io_service);
+
+ private:
+
+ /// @brief ASIO signal set.
+ IOSignalSetPtr io_signal_set_;
+ };
+
+ /// @brief Copies the argument specified as a C++ string to the new
+ /// C string.
+ ///
+ /// This method is used to convert arguments specified as an STL container
+ /// holding @c std::string objects to an array of C strings, used by the
+ /// @c execve function in the @c ProcessSpawnImpl::spawn. It allocates a
+ /// new C string and copies the contents of the @c src to it.
+ /// The data is stored in an internal container so that the caller of the
+ /// function can be exception safe.
+ ///
+ /// @param src A source string.
+ ///
+ /// @return Allocated C string holding the data from @c src.
+ char* allocateInternal(const std::string& src);
+
+ /// @brief Signal handler for SIGCHLD.
+ ///
+ /// This handler waits for the child process to finish and retrieves
+ /// its exit code into the @c status_ member.
+ ///
+ /// @return true if the processed signal was SIGCHLD or false if it
+ /// was a different signal.
+ static bool waitForProcess(int signum);
+
+ /// @brief A map holding the status codes of executed processes.
+ static ProcessCollection process_collection_;
+
+ /// @brief Path to an executable.
+ std::string executable_;
+
+ /// @brief An array holding arguments for the executable.
+ boost::shared_ptr<char*[]> args_;
+
+ /// @brief An array holding environment variables for the executable.
+ boost::shared_ptr<char*[]> vars_;
+
+ /// @brief Typedef for CString pointer.
+ typedef boost::shared_ptr<char[]> CStringPtr;
+
+ /// @brief An storage container for all allocated C strings.
+ std::vector<CStringPtr> storage_;
+
+ /// @brief Flag to indicate if process status must be stored.
+ bool store_;
+
+ /// @brief Mutex to protect internal state.
+ static std::mutex mutex_;
+
+ /// @brief The IOService which handles IO operations.
+ IOServicePtr io_service_;
+};
+
+ProcessCollection ProcessSpawnImpl::process_collection_;
+std::mutex ProcessSpawnImpl::mutex_;
+
+void ProcessSpawnImpl::IOSignalSetInitializer::initIOSignalSet(IOServicePtr io_service) {
+ static IOSignalSetInitializer init(io_service);
+}
+
+ProcessSpawnImpl::ProcessSpawnImpl(IOServicePtr io_service,
+ const std::string& executable,
+ const ProcessArgs& args,
+ const ProcessEnvVars& vars)
+ : executable_(executable), args_(new char*[args.size() + 2]),
+ vars_(new char*[vars.size() + 1]), store_(false), io_service_(io_service) {
+
+ struct stat st;
+
+ if (stat(executable_.c_str(), &st)) {
+ isc_throw(ProcessSpawnError, "File not found: " << executable_);
+ }
+
+ if (!(st.st_mode & S_IEXEC)) {
+ isc_throw(ProcessSpawnError, "File not executable: " << executable_);
+ }
+
+ // Conversion of the arguments to the C-style array we start by setting
+ // all pointers within an array to NULL to indicate that they haven't
+ // been allocated yet.
+ memset(args_.get(), 0, (args.size() + 2) * sizeof(char*));
+ memset(vars_.get(), 0, (vars.size() + 1) * sizeof(char*));
+ // By convention, the first argument points to an executable name.
+ args_[0] = allocateInternal(executable_);
+ // Copy arguments to the array.
+ for (int i = 1; i <= args.size(); ++i) {
+ args_[i] = allocateInternal(args[i - 1]);
+ }
+ // Copy environment variables to the array.
+ for (int i = 0; i < vars.size(); ++i) {
+ vars_[i] = allocateInternal(vars[i]);
+ }
+}
+
+ProcessSpawnImpl::~ProcessSpawnImpl() {
+ if (store_) {
+ lock_guard<std::mutex> lk(mutex_);
+ process_collection_.erase(this);
+ }
+}
+
+std::string
+ProcessSpawnImpl::getCommandLine() const {
+ std::ostringstream s;
+ s << executable_;
+ // Start with index 1, because the first argument duplicates the
+ // path to the executable. Note, that even if there are no parameters
+ // the minimum size of the table is 2.
+ int i = 1;
+ while (args_[i] != NULL) {
+ s << " " << args_[i];
+ ++i;
+ }
+ return (s.str());
+}
+
+pid_t
+ProcessSpawnImpl::spawn(bool dismiss) {
+ lock_guard<std::mutex> lk(mutex_);
+ ProcessSpawnImpl::IOSignalSetInitializer::initIOSignalSet(io_service_);
+ // Create the child
+ pid_t pid = fork();
+ if (pid < 0) {
+ isc_throw(ProcessSpawnError, "unable to fork current process");
+
+ } else if (pid == 0) {
+ // Reset masked signals for the child process.
+ sigset_t sset;
+ sigemptyset(&sset);
+ pthread_sigmask(SIG_SETMASK, &sset, 0);
+ // Run the executable.
+ execve(executable_.c_str(), args_.get(), vars_.get());
+ // We may end up here if the execve failed, e.g. as a result
+ // of issue with permissions or invalid executable name.
+ _exit(EXIT_FAILURE);
+ }
+
+ // We're in the parent process.
+ if (!dismiss) {
+ store_ = true;
+ process_collection_[this].insert(std::pair<pid_t, ProcessStatePtr>(pid, ProcessStatePtr(new ProcessState())));
+ }
+ return (pid);
+}
+
+bool
+ProcessSpawnImpl::isRunning(const pid_t pid) const {
+ lock_guard<std::mutex> lk(mutex_);
+ ProcessStates::const_iterator proc;
+ if (process_collection_.find(this) == process_collection_.end() ||
+ (proc = process_collection_[this].find(pid)) == process_collection_[this].end()) {
+ isc_throw(BadValue, "the process with the pid '" << pid
+ << "' hasn't been spawned and it status cannot be"
+ " returned");
+ }
+ return (proc->second->running_);
+}
+
+bool
+ProcessSpawnImpl::isAnyRunning() const {
+ lock_guard<std::mutex> lk(mutex_);
+ if (process_collection_.find(this) != process_collection_.end()) {
+ for (auto const& proc : process_collection_[this]) {
+ if (proc.second->running_) {
+ return (true);
+ }
+ }
+ }
+ return (false);
+}
+
+int
+ProcessSpawnImpl::getExitStatus(const pid_t pid) const {
+ lock_guard<std::mutex> lk(mutex_);
+ ProcessStates::const_iterator proc;
+ if (process_collection_.find(this) == process_collection_.end() ||
+ (proc = process_collection_[this].find(pid)) == process_collection_[this].end()) {
+ isc_throw(InvalidOperation, "the process with the pid '" << pid
+ << "' hasn't been spawned and it status cannot be"
+ " returned");
+ }
+ return (WEXITSTATUS(proc->second->status_));
+}
+
+char*
+ProcessSpawnImpl::allocateInternal(const std::string& src) {
+ const size_t src_len = src.length();
+ storage_.push_back(CStringPtr(new char[src_len + 1]));
+ // Allocate the C-string with one byte more for the null termination.
+ char* dest = storage_[storage_.size() - 1].get();
+ // copy doesn't append the null at the end.
+ src.copy(dest, src_len);
+ // Append null on our own.
+ dest[src_len] = '\0';
+ return (dest);
+}
+
+bool
+ProcessSpawnImpl::waitForProcess(int) {
+ lock_guard<std::mutex> lk(mutex_);
+ for (;;) {
+ int status = 0;
+ pid_t pid = waitpid(-1, &status, WNOHANG);
+ if (pid <= 0) {
+ break;
+ }
+ for (auto const& instance : process_collection_) {
+ auto const& proc = instance.second.find(pid);
+ /// Check that the terminating process was started
+ /// by our instance of ProcessSpawn
+ if (proc != instance.second.end()) {
+ // In this order please
+ proc->second->status_ = status;
+ proc->second->running_ = false;
+ }
+ }
+ }
+ return (true);
+}
+
+void
+ProcessSpawnImpl::clearState(const pid_t pid) {
+ if (isRunning(pid)) {
+ isc_throw(InvalidOperation, "unable to remove the status for the"
+ "process (pid: " << pid << ") which is still running");
+ }
+ lock_guard<std::mutex> lk(mutex_);
+ if (process_collection_.find(this) != process_collection_.end()) {
+ process_collection_[this].erase(pid);
+ }
+}
+
+ProcessSpawn::ProcessSpawn(IOServicePtr io_service,
+ const std::string& executable,
+ const ProcessArgs& args,
+ const ProcessEnvVars& vars)
+ : impl_(new ProcessSpawnImpl(io_service, executable, args, vars)) {
+}
+
+std::string
+ProcessSpawn::getCommandLine() const {
+ return (impl_->getCommandLine());
+}
+
+pid_t
+ProcessSpawn::spawn(bool dismiss) {
+ return (impl_->spawn(dismiss));
+}
+
+bool
+ProcessSpawn::isRunning(const pid_t pid) const {
+ return (impl_->isRunning(pid));
+}
+
+bool
+ProcessSpawn::isAnyRunning() const {
+ return (impl_->isAnyRunning());
+}
+
+int
+ProcessSpawn::getExitStatus(const pid_t pid) const {
+ return (impl_->getExitStatus(pid));
+}
+
+void
+ProcessSpawn::clearState(const pid_t pid) {
+ return (impl_->clearState(pid));
+}
+
+} // namespace asiolink
+} // namespace isc