Skip to content

Commit

Permalink
feat(thread): add Process
Browse files Browse the repository at this point in the history
  • Loading branch information
RiscadoA committed Sep 29, 2024
1 parent 0a6e088 commit 53d39cc
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
58 changes: 58 additions & 0 deletions core/include/cubos/core/thread/process.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// @file
/// @brief Class @ref cubos::core::thread::Process.
/// @ingroup core-thread

#pragma once

#include <string>
#include <vector>

#include <cubos/core/reflection/reflect.hpp>

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<std::string>& 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
196 changes: 196 additions & 0 deletions core/src/thread/process.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#include <cubos/core/log.hpp>
#include <cubos/core/reflection/external/cstring.hpp>
#include <cubos/core/reflection/external/primitives.hpp>
#include <cubos/core/reflection/external/string.hpp>
#include <cubos/core/reflection/traits/constructible.hpp>
#include <cubos/core/reflection/type.hpp>
#include <cubos/core/thread/process.hpp>

using cubos::core::thread::Process;

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#else
#include <cstring>

#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif

CUBOS_REFLECT_IMPL(Process)
{
return reflection::Type::create("cubos::core::thread::Process")
.with(reflection::ConstructibleTrait::typed<Process>().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<std::string>& 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<char*>(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<std::string> argsCopy = args;
argsCopy.insert(argsCopy.begin(), command);
std::vector<char*> argv{};
for (const auto& arg : argsCopy)
{
argv.push_back(const_cast<char*>(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<PROCESS_INFORMATION*>(mHandle);
::TerminateProcess(pi->hProcess, 1);
CUBOS_DEBUG("Process {} killed", pi->hProcess);
#else
auto* pid = static_cast<pid_t*>(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<PROCESS_INFORMATION*>(mHandle);
mHandle = nullptr;

::WaitForSingleObject(pi->hProcess, INFINITE);

DWORD exitCode;
GetExitCodeProcess(pi->hProcess, &exitCode);
status = static_cast<int>(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<pid_t*>(mHandle);
delete static_cast<pid_t*>(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;
}

0 comments on commit 53d39cc

Please sign in to comment.