From: Marcin Siodelski Date: Thu, 29 Jan 2015 16:31:22 +0000 (+0100) Subject: [3669] Created ProcessSpawn class for running background processes. X-Git-Tag: trac3712_base~13^2~18 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=06d26f888ee35dcb66491a05d41ce3a7c278bbe5;p=thirdparty%2Fkea.git [3669] Created ProcessSpawn class for running background processes. --- diff --git a/src/lib/util/Makefile.am b/src/lib/util/Makefile.am index bde5b06432..97f2467968 100644 --- a/src/lib/util/Makefile.am +++ b/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 += optional_value.h 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 += signal_set.cc signal_set.h libkea_util_la_SOURCES += encode/base16_from_binary.h diff --git a/src/lib/util/process_spawn.cc b/src/lib/util/process_spawn.cc new file mode 100644 index 0000000000..a5fb02aac9 --- /dev/null +++ b/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 +#include +#include +#include +#include +#include +#include + +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()); +} + +} +} diff --git a/src/lib/util/process_spawn.h b/src/lib/util/process_spawn.h new file mode 100644 index 0000000000..764311b5ee --- /dev/null +++ b/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 +#include +#include + +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 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 diff --git a/src/lib/util/tests/Makefile.am b/src/lib/util/tests/Makefile.am index 526626a064..e0b2a4f4f2 100644 --- a/src/lib/util/tests/Makefile.am +++ b/src/lib/util/tests/Makefile.am @@ -20,6 +20,8 @@ CLEANFILES += *.csv TESTS_ENVIRONMENT = \ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) +noinst_SCRIPTS = process_spawn_app.sh + TESTS = if HAVE_GTEST 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 += optional_value_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 += random_number_generator_unittest.cc run_unittests_SOURCES += socketsession_unittest.cc diff --git a/src/lib/util/tests/process_spawn_app.sh b/src/lib/util/tests/process_spawn_app.sh new file mode 100755 index 0000000000..053d2327b3 --- /dev/null +++ b/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} diff --git a/src/lib/util/tests/process_spawn_unittest.cc b/src/lib/util/tests/process_spawn_unittest.cc new file mode 100644 index 0000000000..c7ab6f256d --- /dev/null +++ b/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 +#include +#include +#include + +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 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 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