From 53d39cc943934086e8148435e108d798f3d5bb34 Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Fri, 27 Sep 2024 17:49:12 +0200 Subject: [PATCH] feat(thread): add Process --- CHANGELOG.md | 1 + core/CMakeLists.txt | 1 + core/include/cubos/core/thread/process.hpp | 58 ++++++ core/src/thread/process.cpp | 196 +++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 core/include/cubos/core/thread/process.hpp create mode 100644 core/src/thread/process.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index fee7ea5af..4e0337f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Generic Camera component to hold projection matrix (#1331, **@mkuritsu**). - Initial application debugging through Tesseratos (#1303, **@RiscadoA**). - Print stacktrace with *cpptrace* on calls to CUBOS_FAIL (#1172, **@RiscadoA**). +- Simple cross-platform multi-process utilities (**@RiscadoA**). ### Changed diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 9cd7af533..4acb58c20 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -31,6 +31,7 @@ set(CUBOS_CORE_SOURCE "src/metrics.cpp" "src/thread/pool.cpp" + "src/thread/process.cpp" "src/thread/task.cpp" "src/memory/stream.cpp" diff --git a/core/include/cubos/core/thread/process.hpp b/core/include/cubos/core/thread/process.hpp new file mode 100644 index 000000000..fe006d201 --- /dev/null +++ b/core/include/cubos/core/thread/process.hpp @@ -0,0 +1,58 @@ +/// @file +/// @brief Class @ref cubos::core::thread::Process. +/// @ingroup core-thread + +#pragma once + +#include +#include + +#include + +namespace cubos::core::thread +{ + /// @brief Provides a cross-platform way to spawn child processes. + /// @ingroup core-thread + class Process final + { + public: + CUBOS_REFLECT; + + ~Process(); + + /// @brief Default constructor. + Process() = default; + + /// @brief Move constructor. + Process(Process&& other) noexcept; + + /// @brief Move assignment operator. + Process& operator=(Process&& other) noexcept; + + /// @brief Starts a new process. + /// @param command Command to execute. + /// @param args Arguments to pass to the command. + /// @param cwd Working directory for the new process. + /// @return Whether the process was started successfully. + bool start(const std::string& command, const std::vector& args = {}, const std::string& cwd = ""); + + /// @brief Kills the process. + void kill(); + + /// @brief Waits for the process to finish. + /// @return Whether the process exited normally. + bool wait(); + + /// @brief Waits for the process to finish. + /// @param status Exit code of the process, if it exited normally. + /// @return Whether the process exited normally. + bool wait(int& status); + + /// @brief Checks whether the process has been started. + /// @return Whether the process has been started. + bool started() const; + + private: + void* mHandle{nullptr}; + }; +} // namespace cubos::core::thread diff --git a/core/src/thread/process.cpp b/core/src/thread/process.cpp new file mode 100644 index 000000000..c1a02922f --- /dev/null +++ b/core/src/thread/process.cpp @@ -0,0 +1,196 @@ +#include +#include +#include +#include +#include +#include +#include + +using cubos::core::thread::Process; + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include + +#include +#include +#include +#include +#endif + +CUBOS_REFLECT_IMPL(Process) +{ + return reflection::Type::create("cubos::core::thread::Process") + .with(reflection::ConstructibleTrait::typed().withDefaultConstructor().withMoveConstructor().build()); +} + +Process::~Process() +{ + this->kill(); + this->wait(); +} + +Process::Process(Process&& other) noexcept + : mHandle{other.mHandle} +{ + other.mHandle = nullptr; +} + +Process& Process::operator=(Process&& other) noexcept +{ + if (this == &other) + { + return *this; + } + + this->wait(); + mHandle = other.mHandle; + other.mHandle = nullptr; + return *this; +} + +bool Process::start(const std::string& command, const std::vector& args, const std::string& cwd) +{ + this->wait(); + +#ifdef _WIN32 + // Launch the binary with appropriate arguments. + std::string finalCommand = command; + for (const auto& arg : args) + { + finalCommand += " " + arg; + } + STARTUPINFOA si = {sizeof(si)}; + PROCESS_INFORMATION pi; + if (!CreateProcessA(nullptr, const_cast(finalCommand.c_str()), nullptr, nullptr, FALSE, 0, nullptr, + cwd.empty() ? nullptr : cwd.c_str(), &si, &pi)) + { + CUBOS_ERROR("Failed to start process {} with error {}", command, GetLastError()); + return false; + } + + mHandle = new PROCESS_INFORMATION{pi}; + CUBOS_INFO("Started process {} with PID {}", command, pi.dwProcessId); +#else + auto pid = fork(); + if (pid == -1) + { + CUBOS_ERROR("Failed to fork process"); + return false; + } + + if (pid == 0) // Are we the child process? + { + if (!cwd.empty()) + { + // Change the working directory. + if (chdir(cwd.c_str()) == -1) + { + CUBOS_CRITICAL("Failed to change working directory to {}", cwd); + exit(1); + } + } + + // Launch the binary with appropriate arguments. + std::vector argsCopy = args; + argsCopy.insert(argsCopy.begin(), command); + std::vector argv{}; + for (const auto& arg : argsCopy) + { + argv.push_back(const_cast(arg.c_str())); + } + argv.push_back(nullptr); + execvp(command.c_str(), argv.data()); + + // If we reach this point, execv failed. Get the error message and log it. + CUBOS_CRITICAL("Failed to start process {} with error {}", command, strerror(errno)); + exit(1); + } + + // We are the parent process. + mHandle = new pid_t{pid}; + CUBOS_INFO("Started process {} with PID {}", command, pid); +#endif + + return true; +} + +void Process::kill() +{ + if (mHandle == nullptr) + { + return; + } + +#ifdef _WIN32 + auto* pi = static_cast(mHandle); + ::TerminateProcess(pi->hProcess, 1); + CUBOS_DEBUG("Process {} killed", pi->hProcess); +#else + auto* pid = static_cast(mHandle); + ::kill(*pid, SIGKILL); + CUBOS_DEBUG("Process {} killed", *pid); +#endif +} + +bool Process::wait() +{ + int status; + return this->wait(status); +} + +bool Process::wait(int& status) +{ + if (mHandle == nullptr) + { + return false; + } + +#ifdef _WIN32 + auto* pi = static_cast(mHandle); + mHandle = nullptr; + + ::WaitForSingleObject(pi->hProcess, INFINITE); + + DWORD exitCode; + GetExitCodeProcess(pi->hProcess, &exitCode); + status = static_cast(exitCode); + CUBOS_DEBUG("Process {} exited with status {}", pi->hProcess, status); + + ::CloseHandle(pi->hProcess); + ::CloseHandle(pi->hThread); + delete pi; + + return true; +#else + auto pid = *static_cast(mHandle); + delete static_cast(mHandle); + mHandle = nullptr; + + int waitStatus; + ::waitpid(pid, &waitStatus, 0); + if (WIFEXITED(waitStatus)) + { + status = WEXITSTATUS(waitStatus); + CUBOS_DEBUG("Process {} exited with status {}", pid, status); + return true; + } + else if (WIFSIGNALED(waitStatus)) + { + CUBOS_WARN("Process {} terminated by signal {}", pid, WTERMSIG(waitStatus)); + return false; + } + else + { + CUBOS_WARN("Process {} terminated abnormally", pid); + return false; + } +#endif +} + +bool Process::started() const +{ + return mHandle != nullptr; +}