]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3025] add ability to wait sync in ProcessSpawn
authorAndrei Pavel <andrei@isc.org>
Fri, 16 Feb 2024 06:33:02 +0000 (08:33 +0200)
committerAndrei Pavel <andrei@isc.org>
Thu, 22 Feb 2024 07:57:35 +0000 (09:57 +0200)
src/lib/asiolink/process_spawn.cc
src/lib/asiolink/process_spawn.h
src/lib/asiolink/tests/process_spawn_unittest.cc

index e5f9bdf167e41a98604f01a77088b5f9d7372cae..5ca79fb63f5e9ca28e707590cdcd2fa9460d1784 100644 (file)
@@ -53,7 +53,7 @@ typedef boost::shared_ptr<ProcessState> ProcessStatePtr;
 /// identified by PID.
 typedef std::map<pid_t, ProcessStatePtr> ProcessStates;
 
-class ProcessSpawnImpl;
+class processSpawnImpl;
 
 /// @brief ProcessCollection container which stores all ProcessStates for each
 /// instance of @ref ProcessSpawnImpl.
@@ -87,7 +87,8 @@ public:
                      const std::string& executable,
                      const ProcessArgs& args,
                      const ProcessEnvVars& vars,
-                     const bool inherit_env);
+                     const bool inherit_env,
+                     const bool sync);
 
     /// @brief Destructor.
     ~ProcessSpawnImpl();
@@ -158,8 +159,10 @@ private:
             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_ =
+                boost::make_shared<IOSignalSet>(io_service,
+                                                std::bind(&ProcessSpawnImpl::waitForProcess, ph::_1,
+                                                          /* pid = */ -1, /* sync = */ false));
             io_signal_set_->add(SIGCHLD);
         }
 
@@ -200,12 +203,13 @@ private:
 
     /// @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);
+    /// @param signum the signal number received,
+    /// populated by the signal set in async mode
+    /// @param pid the pid to wait for, -1 by default meaning wait
+    /// for any child process
+    /// @param sync whether this function is called immediately after spawning
+    /// (synchronous) or not (asynchronous, default).:w
+    static void waitForProcess(int signum, pid_t const pid = -1, bool const sync = false);
 
     /// @brief A map holding the status codes of executed processes.
     static ProcessCollection process_collection_;
@@ -233,6 +237,10 @@ private:
 
     /// @brief The IOService which handles IO operations.
     IOServicePtr io_service_;
+
+    /// @brief Whether the process is waited immediately after spawning
+    /// (synchronous) or not (asynchronous, default).
+    bool sync_;
 };
 
 ProcessCollection ProcessSpawnImpl::process_collection_;
@@ -246,9 +254,10 @@ ProcessSpawnImpl::ProcessSpawnImpl(IOServicePtr io_service,
                                    const std::string& executable,
                                    const ProcessArgs& args,
                                    const ProcessEnvVars& vars,
-                                   const bool inherit_env)
+                                   const bool inherit_env,
+                                   const bool sync)
     : executable_(executable), args_(new char*[args.size() + 2]),
-      store_(false), io_service_(io_service) {
+      store_(false), io_service_(io_service), sync_(sync) {
 
     // Size of vars except the trailing null
     size_t vars_size;
@@ -346,6 +355,11 @@ ProcessSpawnImpl::spawn(bool dismiss) {
         store_ = true;
         process_collection_[this].insert(std::pair<pid_t, ProcessStatePtr>(pid, ProcessStatePtr(new ProcessState())));
     }
+
+    if (sync_) {
+        waitForProcess(SIGCHLD, pid, /* sync = */ true);
+    }
+
     return (pid);
 }
 
@@ -355,9 +369,7 @@ ProcessSpawnImpl::isRunning(const pid_t pid) const {
     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");
+        isc_throw(InvalidOperation, "the status of process with pid '" << pid << "' cannot be returned");
     }
     return (proc->second->running_);
 }
@@ -381,9 +393,7 @@ ProcessSpawnImpl::getExitStatus(const pid_t pid) const {
     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");
+        isc_throw(InvalidOperation, "the status of process with pid '" << pid << "' cannot be returned");
     }
     return (WEXITSTATUS(proc->second->status_));
 }
@@ -401,17 +411,26 @@ ProcessSpawnImpl::allocateInternal(const std::string& src) {
     return (dest);
 }
 
-bool
-ProcessSpawnImpl::waitForProcess(int) {
-    lock_guard<std::mutex> lk(mutex_);
+void
+ProcessSpawnImpl::waitForProcess(int /* signum */,
+                                 pid_t const pid /* = -1 */,
+                                 bool sync /* = false */) {
+    unique_lock<std::mutex> lk{mutex_, std::defer_lock};
+    if (!sync) {
+        lk.lock();
+    }
     for (;;) {
         int status = 0;
-        pid_t pid = waitpid(-1, &status, WNOHANG);
-        if (pid <= 0) {
+        // When called synchronously, this function needs to be blocking.
+        // When called asynchronously, the first IO context event is for
+        // receiving the SIGCHLD signal which itself is blocking,
+        // hence no need to block here too.
+        pid_t wpid = waitpid(pid, &status, sync ? 0 : WNOHANG);
+        if (wpid <= 0) {
             break;
         }
         for (auto const& instance : process_collection_) {
-            auto const& proc = instance.second.find(pid);
+            auto const& proc = instance.second.find(wpid);
             /// Check that the terminating process was started
             /// by our instance of ProcessSpawn
             if (proc != instance.second.end()) {
@@ -421,7 +440,6 @@ ProcessSpawnImpl::waitForProcess(int) {
             }
         }
     }
-    return (true);
 }
 
 void
@@ -441,7 +459,20 @@ ProcessSpawn::ProcessSpawn(IOServicePtr io_service,
                            const ProcessArgs& args,
                            const ProcessEnvVars& vars,
                            const bool inherit_env /* = false */)
-    : impl_(new ProcessSpawnImpl(io_service, executable, args, vars, inherit_env)) {
+    : impl_(new ProcessSpawnImpl(
+          io_service, executable, args, vars, inherit_env, /* sync = */ false)) {
+}
+
+ProcessSpawn::ProcessSpawn(const std::string& executable,
+                           const ProcessArgs& args,
+                           const ProcessEnvVars& vars,
+                           const bool inherit_env /* = false */)
+    : impl_(new ProcessSpawnImpl(IOServicePtr(new IOService()),
+                                 executable,
+                                 args,
+                                 vars,
+                                 inherit_env,
+                                 /* sync = */ true)) {
 }
 
 std::string
index 7d6e6623011bad72b5f2393d9ab58883163ef449..43d9b3a5487e76228b3a179a63a6598e2b5fc44d 100644 (file)
@@ -75,6 +75,21 @@ public:
                  const ProcessEnvVars& vars = ProcessEnvVars(),
                  const bool inherit_env = false);
 
+    /// @brief Constructor for synchronous spawn and wait.
+    ///
+    /// Abstracts away the IO service detail since the caller is not
+    /// required to interact with it in sync mode.
+    ///
+    /// @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.
+    /// @param inherit_env whether the spawned process will inherit the
+    ///        environment before adding 'vars' on top.
+    ProcessSpawn(const std::string& executable,
+                 const ProcessArgs& args = ProcessArgs(),
+                 const ProcessEnvVars& vars = ProcessEnvVars(),
+                 const bool inherit_env = false);
+
     /// @brief Destructor.
     ~ProcessSpawn() = default;
 
index aa9a28cb0cd0878aa70af37777d741b8b610a806..af3597e1c6518707e737132db3a56f46dcc004c9 100644 (file)
@@ -115,14 +115,16 @@ TEST_F(ProcessSpawnTest, spawnWithArgs) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // ProcessSpawnTest::processSignal handler.
+    io_service_->runOne();
+
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(1, processed_signals_.size());
@@ -149,14 +151,16 @@ TEST_F(ProcessSpawnTest, spawnWithArgsAndEnvVars) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // ProcessSpawnTest::processSignal handler.
+    io_service_->runOne();
+
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(1, processed_signals_.size());
@@ -181,14 +185,16 @@ TEST_F(ProcessSpawnTest, spawnTwoProcesses) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
+    io_service_->runOne();
+
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // ProcessSpawnTest::processSignal handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // Poll to be sure.
     io_service_->poll();
 
     pid_t pid2 = 0;
@@ -197,14 +203,16 @@ TEST_F(ProcessSpawnTest, spawnTwoProcesses) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
+    io_service_->runOne();
+
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // ProcessSpawnTest::processSignal handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(2, processed_signals_.size());
@@ -234,14 +242,16 @@ TEST_F(ProcessSpawnTest, spawnNoArgs) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
+    io_service_->runOne();
+
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // ProcessSpawnTest::processSignal handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(1, processed_signals_.size());
@@ -255,14 +265,16 @@ TEST_F(ProcessSpawnTest, spawnNoArgs) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // ProcessSpawnTest::processSignal handler.
+    io_service_->runOne();
+
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(2, processed_signals_.size());
@@ -336,14 +348,16 @@ TEST_F(ProcessSpawnTest, isRunning) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // ProcessSpawnTest::processSignal handler.
+    io_service_->runOne();
+
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(1, processed_signals_.size());
@@ -366,14 +380,16 @@ TEST_F(ProcessSpawnTest, inheritEnv) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
+    io_service_->runOne();
+
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // ProcessSpawnTest::processSignal handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(1, processed_signals_.size());
@@ -400,14 +416,16 @@ TEST_F(ProcessSpawnTest, inheritEnvWithParentVar) {
     // Set test fail safe.
     setTestTime(1000);
 
-    // The next handler executed is IOSignal's handler.
+    // SIGCHLD signal.
+    io_service_->runOne();
+
+    // waitForProcess handler.
     io_service_->runOne();
 
-    // The first handler executed is the IOSignal's internal timer expire
-    // callback.
+    // ProcessSpawnTest::processSignal handler.
     io_service_->runOne();
 
-    // Polling once to be sure.
+    // Poll to be sure.
     io_service_->poll();
 
     ASSERT_EQ(1, processed_signals_.size());
@@ -417,4 +435,177 @@ TEST_F(ProcessSpawnTest, inheritEnvWithParentVar) {
     EXPECT_EQ(34, process.getExitStatus(pid));
 }
 
+// This test verifies that the external application can be ran synchronously
+// with arguments and that the exit code is gathered.
+TEST_F(ProcessSpawnTest, spawnWithArgsSync) {
+    vector<string> args;
+    args.push_back("-e");
+    args.push_back("64");
+
+    ProcessSpawn process(TEST_SCRIPT_SH, args);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    // Exit code 64 as requested.
+    EXPECT_EQ(64, process.getExitStatus(pid));
+}
+
+// This test verifies that the external application can be ran synchronously
+// with arguments and environment variables that the exit code is gathered.
+TEST_F(ProcessSpawnTest, spawnWithArgsAndEnvVarsSync) {
+    vector<string> args;
+    vector<string> vars;
+    args.push_back("-v");
+    args.push_back("TEST_VARIABLE_NAME");
+    args.push_back("TEST_VARIABLE_VALUE");
+    vars.push_back("TEST_VARIABLE_NAME=TEST_VARIABLE_VALUE");
+
+    ProcessSpawn process(TEST_SCRIPT_SH, args, vars);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    // 56 means successful comparison of env vars.
+    EXPECT_EQ(56, process.getExitStatus(pid));
+}
+
+// This test verifies that the single ProcessSpawn object can be used
+// to start two processes synchronously and that their status codes can be
+// gathered. It also checks that it is possible to clear the status of the
+// process.
+TEST_F(ProcessSpawnTest, spawnTwoProcessesSync) {
+    vector<string> args;
+    args.push_back("-p");
+
+    ProcessSpawn process(TEST_SCRIPT_SH, args);
+    pid_t pid1 = 0;
+    ASSERT_NO_THROW(pid1 = process.spawn());
+
+    pid_t pid2 = 0;
+    ASSERT_NO_THROW(pid2 = process.spawn());
+
+    EXPECT_NE(process.getExitStatus(pid1), process.getExitStatus(pid2));
+
+    // Clear the status of the first process. An attempt to get the status
+    // for the cleared process should result in exception. But, there should
+    // be no exception for the second process.
+    process.clearState(pid1);
+    EXPECT_THROW(process.getExitStatus(pid1), InvalidOperation);
+    EXPECT_NO_THROW(process.getExitStatus(pid2));
+
+    process.clearState(pid2);
+    EXPECT_THROW(process.getExitStatus(pid2), InvalidOperation);
+}
+
+// This test verifies that the external application can be ran synchronously
+// without arguments and that the exit code is gathered.
+TEST_F(ProcessSpawnTest, spawnNoArgsSync) {
+    ProcessSpawn process(TEST_SCRIPT_SH);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    // 32 means no args.
+    EXPECT_EQ(32, process.getExitStatus(pid));
+
+    ASSERT_NO_THROW(pid = process.spawn(true));
+
+    EXPECT_THROW(process.getExitStatus(pid), InvalidOperation);
+}
+
+// This test verifies that the EXIT_FAILURE code is returned when
+// application can't be executed synchronously.
+TEST_F(ProcessSpawnTest, invalidExecutableSync) {
+    std::string expected = "File not found: foo";
+    ASSERT_THROW_MSG(ProcessSpawn process("foo"),
+                     ProcessSpawnError, expected);
+
+    std::string name = INVALID_TEST_SCRIPT_SH;
+
+    expected = "File not executable: ";
+    expected += name;
+    ASSERT_THROW_MSG(ProcessSpawn process(name),
+                     ProcessSpawnError, expected);
+}
+
+// This test verifies that the full command line for the synchronous process is
+// returned.
+TEST_F(ProcessSpawnTest, getCommandLineSync) {
+    // Note that cases below are enclosed in separate scopes to make
+    // sure that the ProcessSpawn object is destroyed before a new
+    // object is created. Current implementation doesn't allow for
+    // having two ProcessSpawn objects simultaneously as they will
+    // both try to allocate a signal handler for SIGCHLD.
+    {
+        // Case 1: arguments present.
+        ProcessArgs args;
+        args.push_back("-x");
+        args.push_back("-y");
+        args.push_back("foo");
+        args.push_back("bar");
+        ProcessSpawn process(TEST_SCRIPT_SH, args);
+        std::string expected = TEST_SCRIPT_SH;
+        expected += " -x -y foo bar";
+        EXPECT_EQ(expected, process.getCommandLine());
+    }
+
+    {
+        // Case 2: no arguments.
+        ProcessSpawn process(TEST_SCRIPT_SH);
+        EXPECT_EQ(TEST_SCRIPT_SH, process.getCommandLine());
+    }
+}
+
+// This test verifies that it is possible to check if the synchronous process is
+// running.
+TEST_F(ProcessSpawnTest, DISABLED_isRunningSync) {
+    // Run the process which sleeps for 10 seconds, so as we have
+    // enough time to check if it is running.
+    vector<string> args;
+    args.push_back("-s");
+    args.push_back("10");
+
+    ProcessSpawn process(TEST_SCRIPT_SH, args);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    EXPECT_TRUE(process.isRunning(pid));
+
+    // Kill the process.
+    ASSERT_EQ(0, kill(pid, SIGKILL));
+}
+
+// This test verifies inheritance of environment in a synchronous context.
+TEST_F(ProcessSpawnTest, inheritEnvSync) {
+    // Run the process which sleeps for 10 seconds, so as we have
+    // enough time to check if it is running.
+    vector<string> args{"-v", "VAR", "value"};
+
+    ProcessEnvVars vars{"VAR=value"};
+
+    ProcessSpawn process(TEST_SCRIPT_SH, args, vars,
+                         /* inherit_env = */ true);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    // 56 means successful comparison of env vars.
+    EXPECT_EQ(56, process.getExitStatus(pid));
+}
+
+// This test verifies inheritance of environment when a variable is inherited
+// from parent in a synchronous context. It assumes PATH is set.
+TEST_F(ProcessSpawnTest, inheritEnvWithParentVarSync) {
+    // Run the process which sleeps for 10 seconds, so as we have
+    // enough time to check if it is running.
+    vector<string> args{"-v", "PATH", "value"};
+
+    ProcessEnvVars vars{"VAR=value"};
+
+    ProcessSpawn process(TEST_SCRIPT_SH, args, vars,
+                         /* inherit_env = */ true);
+    pid_t pid = 0;
+    ASSERT_NO_THROW(pid = process.spawn());
+
+    // 34 means failed comparison of env vars.
+    EXPECT_EQ(34, process.getExitStatus(pid));
+}
+
 } // end of anonymous namespace