Browse Source

[3669] Created ProcessSpawn class for running background processes.

Marcin Siodelski 10 years ago
parent
commit
06d26f888e

+ 1 - 0
src/lib/util/Makefile.am

@@ -19,6 +19,7 @@ libkea_util_la_SOURCES += memory_segment.h
 libkea_util_la_SOURCES += memory_segment_local.h memory_segment_local.cc
 libkea_util_la_SOURCES += memory_segment_local.h memory_segment_local.cc
 libkea_util_la_SOURCES += optional_value.h
 libkea_util_la_SOURCES += optional_value.h
 libkea_util_la_SOURCES += pid_file.h pid_file.cc
 libkea_util_la_SOURCES += pid_file.h pid_file.cc
+libkea_util_la_SOURCES += process_spawn.h process_spawn.cc
 libkea_util_la_SOURCES += range_utilities.h
 libkea_util_la_SOURCES += range_utilities.h
 libkea_util_la_SOURCES += signal_set.cc signal_set.h
 libkea_util_la_SOURCES += signal_set.cc signal_set.h
 libkea_util_la_SOURCES += encode/base16_from_binary.h
 libkea_util_la_SOURCES += encode/base16_from_binary.h

+ 223 - 0
src/lib/util/process_spawn.cc

@@ -0,0 +1,223 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <util/process_spawn.h>
+#include <util/signal_set.h>
+#include <boost/bind.hpp>
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+
+namespace isc {
+namespace util {
+
+/// @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.
+class ProcessSpawnImpl {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param executable A path to the program to be executed.
+    /// @param args Arguments for the program to be executed.
+    ProcessSpawnImpl(const std::string& executable,
+                     const ProcessArgs& args);
+
+    /// @brief Destructor.
+    ~ProcessSpawnImpl();
+
+    /// @brief Spawn the new process.
+    ///
+    /// This method forks the current process and execues the specified
+    /// binary with arguments within the child process.
+    ///
+    /// The child process will return EXIT_FAILURE if the method was unable
+    /// to start the exuctable, 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.
+    ///
+    /// @throw ProcessSpawnError if forking a current process failed.
+    void spawn();
+
+    /// @brief Checks if the process is still running.
+    ///
+    /// @return true if the child process is running, false otherwise.
+    bool isRunning() 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.
+    ///
+    /// @return Exit code of the process.
+    int getExitStatus() const;
+
+private:
+
+    /// @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 execvp function in the @c ProcessSpawnImpl::spawn. It allocates a
+    /// new C string and copies the contents of the @c src to it.
+    ///
+    /// @param src A source string.
+    ///
+    /// @return Allocated C string holding the data from @c src.
+    char* allocateArg(const std::string& src) const;
+
+    /// @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.
+    bool waitForProcess(int signum);
+
+    /// @brief A signal set installing a handler for SIGCHLD.
+    SignalSetPtr signals_;
+
+    /// @brief PID of the child process.
+    pid_t pid_;
+
+    /// @brief Last returned status code by the child process.
+    int status_;
+
+    /// @brief Path to an executable.
+    std::string executable_;
+
+    /// @brief An array holding arguments for the executable.
+    char** args_;
+};
+
+ProcessSpawnImpl::ProcessSpawnImpl(const std::string& executable,
+                                   const ProcessArgs& args)
+    : signals_(new SignalSet(SIGCHLD)), pid_(0), status_(0),
+      executable_(executable), args_(new char*[args.size() + 2]) {
+    // Set the handler which is invoked immediatelly when the signal
+    // is received.
+    signals_->setOnReceiptHandler(boost::bind(&ProcessSpawnImpl::waitForProcess,
+                                              this, _1));
+    // Convertion 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_, 0, (args.size() + 2) * sizeof(char*));
+    // By convention, the first argument points to an executable name.
+    args_[0] = allocateArg(executable_);
+    // Copy arguments to the array.
+    for (int i = 1; i <= args.size(); ++i) {
+        args_[i] = allocateArg(args[i-1]);
+    }
+}
+
+
+ProcessSpawnImpl::~ProcessSpawnImpl() {
+    int i = 0;
+    // Deallocate strings in the array of arguments.
+    while (args_[i] != NULL) {
+        delete[] args_[i];
+        ++i;
+    }
+    // Deallocate the array.
+    delete[] args_;
+}
+
+void
+ProcessSpawnImpl::spawn() {
+    pid_ = fork();
+    if (pid_ < 0) {
+        isc_throw(ProcessSpawnError, "unable to fork current process");
+
+    } else if (pid_ == 0) {
+        // We're in the child process. Run the executable.
+        if (execvp(executable_.c_str(), args_) != 0) {
+            // We may end up here if the execvp failed, e.g. as a result
+            // of issue with permissions or invalid executable name.
+            exit(EXIT_FAILURE);
+        }
+        // Process finished, exit the child process.
+        exit(EXIT_SUCCESS);
+    }
+}
+
+bool
+ProcessSpawnImpl::isRunning() const {
+    return ((pid_ != 0) && (kill(pid_, 0) == 0));
+}
+
+int
+ProcessSpawnImpl::getExitStatus() const {
+    return (WEXITSTATUS(status_));
+}
+
+char*
+ProcessSpawnImpl::allocateArg(const std::string& src) const {
+    const size_t src_len = src.length();
+    // Allocate the C-string with one byte more for the null termination.
+    char* dest = new char[src_len + 1];
+    // 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 signum) {
+    // We're only interested in SIGCHLD.
+    if (signum == SIGCHLD) {
+        status_ = 0;
+        while (wait4(pid_, &status_, WNOHANG, NULL) > 0) {
+            // continue
+        }
+        pid_ = 0;
+        return (true);
+    }
+    return (false);
+}
+
+
+ProcessSpawn::ProcessSpawn(const std::string& executable,
+                           const ProcessArgs& args)
+    : impl_(new ProcessSpawnImpl(executable, args)) {
+}
+
+ProcessSpawn::~ProcessSpawn() {
+    delete impl_;
+}
+
+void
+ProcessSpawn::spawn() {
+    impl_->spawn();
+}
+
+bool
+ProcessSpawn::isRunning() const {
+    return (impl_->isRunning());
+}
+
+int
+ProcessSpawn::getExitStatus() const {
+    return (impl_->getExitStatus());
+}
+
+}
+}

+ 105 - 0
src/lib/util/process_spawn.h

@@ -0,0 +1,105 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PROCESS_SPAWN_H
+#define PROCESS_SPAWN_H
+
+#include <exceptions/exceptions.h>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace util {
+
+/// @brief Exception thrown when error occurs during spawning a process.
+class ProcessSpawnError : public Exception {
+public:
+    ProcessSpawnError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
+/// @brief Forward declaration to the implementation of the @c ProcessSpawn
+/// class.
+class ProcessSpawnImpl;
+
+/// @brief Type of the container holding arguments of the executable
+/// being run as a background process.
+typedef std::vector<std::string> ProcessArgs;
+
+/// @brief Utility class for spawning new processes.
+///
+/// This class is used to spawn new process by Kea. It forks the current
+/// process and then uses the @c execvp function to execute the specified
+/// binary with parameters. The @c ProcessSpawn installs the handler for
+/// the SIGCHLD signal, which is executed when the child process ends.
+/// The handler checks the exit code returned by the process and records
+/// it. The exit code can be retrieved by the caller using the
+/// @c ProcessSpawn::getExitStatus method.
+///
+/// @warning It is not recommended to use multiple instances of the
+/// @c ProcessSpawn classes at the same time, because each newly
+/// created instance would replace the SIGCHLD handler. This means that
+/// it would not be possible to gather the exit code of all but one
+/// (newly created) process, because the SIGCHLD handler is responsible
+/// for checking the exit code of the process.
+class ProcessSpawn {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param executable A path to the program to be executed.
+    /// @param args Arguments for the program to be executed.
+    ProcessSpawn(const std::string& executable,
+                 const ProcessArgs& args = ProcessArgs());
+
+    /// @brief Destructor.
+    ~ProcessSpawn();
+
+    /// @brief Spawn the new process.
+    ///
+    /// This method forks the current process and execues the specified
+    /// binary with arguments within the child process.
+    ///
+    /// The child process will return EXIT_FAILURE if the method was unable
+    /// to start the exuctable, 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.
+    ///
+    /// @throw ProcessSpawnError if forking a current process failed.
+    void spawn();
+
+    /// @brief Checks if the process is still running.
+    ///
+    /// @return true if the child process is running, false otherwise.
+    bool isRunning() 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.
+    ///
+    /// @return Exit code of the process.
+    int getExitStatus() const;
+
+private:
+
+    /// @brief A pointer to the implementation of this class.
+    ProcessSpawnImpl* impl_;
+
+};
+
+}
+}
+
+#endif // PROCESS_SPAWN_H

+ 3 - 0
src/lib/util/tests/Makefile.am

@@ -20,6 +20,8 @@ CLEANFILES += *.csv
 TESTS_ENVIRONMENT = \
 TESTS_ENVIRONMENT = \
         $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
         $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
 
 
+noinst_SCRIPTS = process_spawn_app.sh
+
 TESTS =
 TESTS =
 if HAVE_GTEST
 if HAVE_GTEST
 TESTS += run_unittests
 TESTS += run_unittests
@@ -39,6 +41,7 @@ run_unittests_SOURCES += memory_segment_common_unittest.h
 run_unittests_SOURCES += memory_segment_common_unittest.cc
 run_unittests_SOURCES += memory_segment_common_unittest.cc
 run_unittests_SOURCES += optional_value_unittest.cc
 run_unittests_SOURCES += optional_value_unittest.cc
 run_unittests_SOURCES += pid_file_unittest.cc
 run_unittests_SOURCES += pid_file_unittest.cc
+run_unittests_SOURCES += process_spawn_unittest.cc
 run_unittests_SOURCES += qid_gen_unittest.cc
 run_unittests_SOURCES += qid_gen_unittest.cc
 run_unittests_SOURCES += random_number_generator_unittest.cc
 run_unittests_SOURCES += random_number_generator_unittest.cc
 run_unittests_SOURCES += socketsession_unittest.cc
 run_unittests_SOURCES += socketsession_unittest.cc

+ 23 - 0
src/lib/util/tests/process_spawn_app.sh

@@ -0,0 +1,23 @@
+#!/bin/sh
+#
+# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+exit_code=${1}
+
+if [ $# -eq 0 ]; then
+    exit 32
+fi
+
+exit ${exit_code}

+ 91 - 0
src/lib/util/tests/process_spawn_unittest.cc

@@ -0,0 +1,91 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <util/process_spawn.h>
+#include <gtest/gtest.h>
+#include <signal.h>
+#include <unistd.h>
+
+namespace {
+
+using namespace isc;
+using namespace isc::util;
+
+/// @brief Returns a location of the test script.
+///
+/// The test script is no-op and it returns the exit code equal to
+/// the argument passed to it.
+///
+/// @return Absolute location of the test script.
+std::string getApp() {
+    std::ostringstream s;
+    s << TEST_DATA_TOPBUILDDIR << "/src/lib/util/tests/process_spawn_app.sh";
+    return (s.str());
+}
+
+/// @brief Waits for the specified process to finish.
+///
+/// @param process An object which started the process.
+/// @param timeout Timeout in seconds.
+///
+/// @return true if the process ended, false otherwise
+bool waitForProcess(const ProcessSpawn& process, const uint8_t timeout) {
+    uint32_t iterations = 0;
+    const uint32_t iterations_max = timeout * 1000;
+    while (process.isRunning() && (iterations < iterations_max)) {
+        usleep(1000);
+        ++iterations;
+    }
+    return (iterations < iterations_max);
+}
+
+// This test verifies that the external application can be ran with
+// arguments and that the exit code is gathered.
+TEST(ProcessSpawn, spawnWithArgs) {
+    std::vector<std::string> args;
+    args.push_back("64");
+    ProcessSpawn process(getApp(), args);
+    ASSERT_NO_THROW(process.spawn());
+
+    ASSERT_TRUE(waitForProcess(process, 2));
+
+    EXPECT_EQ(64, process.getExitStatus());
+}
+
+// This test verifies that the external application can be ran without
+// arguments and that the exit code is gathered.
+TEST(ProcessSpawn, spawnNoArgs) {
+    std::vector<std::string> args;
+    ProcessSpawn process(getApp());
+    ASSERT_NO_THROW(process.spawn());
+
+    ASSERT_TRUE(waitForProcess(process, 2));
+
+    EXPECT_EQ(32, process.getExitStatus());
+}
+
+
+// This test verifies that the EXIT_FAILURE code is returned when
+// application can't be executed.
+TEST(ProcessSpawn, invalidExecutable) {
+    ProcessSpawn process("foo");
+    ASSERT_NO_THROW(process.spawn());
+
+    ASSERT_TRUE(waitForProcess(process, 2));
+
+    EXPECT_EQ(EXIT_FAILURE, process.getExitStatus());
+
+}
+
+} // end of anonymous namespace