Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,25 @@ jobs:
run: |
clang-tidy CatalystCX.hpp -p build --checks='-*,readability-*,performance-*,modernize-*'

codecov:
name: Run tests and collect coverage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Set up Python
uses: actions/setup-python@v4

- name: Install dependencies
run: pip install pytest pytest-cov

- name: Run tests
run: pytest --cov --cov-branch --cov-report=xml

- name: Upload results to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
# codecov:
# name: Run tests and collect coverage
# runs-on: ubuntu-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# with:
# fetch-depth: 2
#
# - name: Set up Python
# uses: actions/setup-python@v4
#
# - name: Install dependencies
# run: pip install pytest pytest-cov
#
# - name: Run tests
# run: pytest --cov --cov-branch --cov-report=xml
#
# - name: Upload results to Codecov
# uses: codecov/codecov-action@v5
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
7 changes: 4 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -O2")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic -Wall -Wextra -O2")

# Test suite executable
add_executable(cxtest Evaluate.cpp CatalystCX.hpp)
add_executable(cxtest Evaluate.cpp)

# Custom target to run tests
add_custom_target(test
Expand All @@ -19,10 +19,11 @@ add_custom_target(test

add_compile_definitions(
WCOREDUMP
EXIT_FAIL_EC=127
)

# Install header
install(FILES CatalystCX.hpp DESTINATION include)

find_package(Threads REQUIRED)
target_link_libraries(cxtest PRIVATE Threads::Threads)
target_link_libraries(cxtest PRIVATE Threads::Threads)
132 changes: 114 additions & 18 deletions CatalystCX.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@
// Licensed under GPL-3.0-or-later

/**
* @brief CatalystCX - A cross-platform single-header C++ library for executing and managing external processes (or commands).
* @brief CatalystCX - A cross-platform single-file C++ library/module for executing and managing external processes (or commands).
* @file CatalystCX.hpp
* @version 0.0.1
* @date 20-09-25 (last modified)
* @author assembler-0
*/

#pragma once
#ifndef CATALYSTCX_HPP
#define CATALYSTCX_HPP

#include <algorithm>
#include <array>
#include <chrono>
#include <filesystem>
#include <future>
#include <optional>
#include <shared_mutex>
#include <sstream>
#include <string>
#include <thread>
Expand All @@ -34,7 +36,6 @@ using pid_t = DWORD;
#include <csignal>
#include <fcntl.h>
#include <poll.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/wait.h>
Expand All @@ -46,7 +47,9 @@ extern char **environ;

namespace fs = std::filesystem;

#ifndef EXIT_FAIL_EC
#define EXIT_FAIL_EC 127
#endif

struct CommandResult {
int ExitCode{};
Expand Down Expand Up @@ -83,13 +86,17 @@ class Child {
public:
#ifdef _WIN32
Child(HANDLE process, HANDLE thread, HANDLE stdout_handle, HANDLE stderr_handle)
: ProcessHandle(process), ThreadHandle(thread), StdoutHandle(stdout_handle), StderrHandle(stderr_handle) {
: ProcessHandle(process), ThreadHandle(thread), StdoutHandle(stdout_handle), StderrHandle(stderr_handle), PipesClosed(false) {
ProcessId = GetProcessId(process);
}

~Child() {
if (ProcessHandle != INVALID_HANDLE_VALUE) CloseHandle(ProcessHandle);
if (ThreadHandle != INVALID_HANDLE_VALUE) CloseHandle(ThreadHandle);
if (!PipesClosed) {
if (StdoutHandle != INVALID_HANDLE_VALUE) CloseHandle(StdoutHandle);
if (StderrHandle != INVALID_HANDLE_VALUE) CloseHandle(StderrHandle);
}
}
#else
Child(const pid_t pid, const int stdout_fd, const int stderr_fd)
Expand All @@ -112,6 +119,7 @@ class Child {
HANDLE ThreadHandle;
HANDLE StdoutHandle;
HANDLE StderrHandle;
mutable bool PipesClosed;
#else
int StdoutFd;
int StderrFd;
Expand Down Expand Up @@ -221,13 +229,14 @@ inline CommandResult Child::Wait(std::optional<std::chrono::duration<double>> ti
result.Usage.PageFaultCount = pmc.PageFaultCount;
}

// Gather output after process has exited (pipes should be closed by child)
// Gather output after process has exited (child should close pipes)
auto [stdout_result, stderr_result] = reader_future.get();
result.Stdout = std::move(stdout_result);
result.Stderr = std::move(stderr_result);

CloseHandle(StdoutHandle);
CloseHandle(StderrHandle);
PipesClosed = true;

auto end_time = std::chrono::steady_clock::now();
result.ExecutionTime = end_time - start_time;
Expand All @@ -251,8 +260,12 @@ inline std::optional<Child> Command::Spawn() {
SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};

HANDLE stdout_read, stdout_write, stderr_read, stderr_write;
if (!CreatePipe(&stdout_read, &stdout_write, &sa, 0) ||
!CreatePipe(&stderr_read, &stderr_write, &sa, 0)) {
if (!CreatePipe(&stdout_read, &stdout_write, &sa, 0)) {
return std::nullopt;
}
if (!CreatePipe(&stderr_read, &stderr_write, &sa, 0)) {
CloseHandle(stdout_read);
CloseHandle(stdout_write);
return std::nullopt;
}

Expand Down Expand Up @@ -299,9 +312,83 @@ inline std::optional<Child> Command::Spawn() {
std::unordered_map<std::string, std::string> lower_over;
lower_over.reserve(EnvVars.size());
for (const auto& [k, v] : EnvVars) {
std::string lk = k; for (auto& c : lk) c = static_cast<char>(::CharLowerA(reinterpret_cast<LPSTR>(&c)));
std::string lk = k;
std::transform(lk.begin(), lk.end(), lk.begin(), [](char c) { return std::tolower(c); });
lower_over.emplace(std::move(lk), v);
}
for (LPCSTR p = env_strings; *p; ) {
std::string entry = p;
size_t eq = entry.find('=');
if (eq != std::string::npos) {
std::string key = entry.substr(0, eq);
std::string lk = key;
std::transform(lk.begin(), lk.end(), lk.begin(), [](char c) { return std::tolower(c); });
if (lower_over.find(lk) == lower_over.end()) {
env_block += entry;
env_block.push_back('\0');
}
}
p += entry.size() + 1;
}
FreeEnvironmentStringsA(env_strings);
}
// Add/override with provided variables
for (const auto& [key, value] : EnvVars) {
env_block += key;
env_block += '=';
env_block += value;
env_block.push_back('\0');
}
env_block.push_back('\0');
}

BOOL success = CreateProcessA(
nullptr, const_cast<char*>(cmdline.c_str()),
nullptr, nullptr, TRUE, 0,
env_block.empty() ? nullptr : const_cast<char*>(env_block.c_str()),
WorkDir ? WorkDir->c_str() : nullptr,
&si, &pi
);

CloseHandle(stdout_write);
CloseHandle(stderr_write);

if (!success) {
CloseHandle(stdout_read);
CloseHandle(stderr_read);
return std::nullopt;
}

return Child(pi.hProcess, pi.hThread, stdout_read, stderr_read);
}

inline std::pair<std::string, std::string> AsyncPipeReader::ReadPipes(HANDLE stdout_handle, HANDLE stderr_handle) {
PipeData stdout_data{stdout_handle, {}};
PipeData stderr_data{stderr_handle, {}};

std::array<char, 8192> buffer;

while (!stdout_data.Finished || !stderr_data.Finished) {
bool any_read = false;
if (!stdout_data.Finished && ReadFromPipe(stdout_data, buffer)) {
any_read = true;
} else if (!stdout_data.Finished) {
stdout_data.Finished = true;
}

if (!stderr_data.Finished && ReadFromPipe(stderr_data, buffer)) {
any_read = true;
} else if (!stderr_data.Finished) {
stderr_data.Finished = true;
}

if (!any_read && (!stdout_data.Finished || !stderr_data.Finished)) {
Sleep(1);
}
}

return {std::move(stdout_data.Buffer), std::move(stderr_data.Buffer)};emplace(std::move(lk), v);
}
for (LPCSTR p = env_strings; *p; ) {
std::string entry = p;
size_t eq = entry.find('=');
Expand Down Expand Up @@ -459,9 +546,11 @@ inline CommandResult Child::Wait(std::optional<std::chrono::duration<double>> ti
auto reader_future = std::async(std::launch::async, AsyncPipeReader::ReadPipes, StdoutFd, StderrFd);

if (timeout) {
while (true) {
auto timeout_time = start_time + *timeout;
while (std::chrono::steady_clock::now() < timeout_time) {
const int wait_result = waitpid(ProcessId, &status, WNOHANG);
if (wait_result == ProcessId) {
wait4(ProcessId, &status, 0, &usage); // Get resource usage
break; // Process finished
}

Expand All @@ -471,14 +560,19 @@ inline CommandResult Child::Wait(std::optional<std::chrono::duration<double>> ti
break;
}

if (auto current_time = std::chrono::steady_clock::now(); current_time - start_time > *timeout) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

// Check if we timed out
if (std::chrono::steady_clock::now() >= timeout_time) {
const int wait_result = waitpid(ProcessId, &status, WNOHANG);
if (wait_result == 0) { // Still running
Kill();
result.TimedOut = true;
wait4(ProcessId, &status, 0, &usage); // Clean up the zombie process and get usage
break;
wait4(ProcessId, &status, 0, &usage);
} else if (wait_result == ProcessId) {
wait4(ProcessId, &status, 0, &usage);
}

std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
} else {
wait4(ProcessId, &status, 0, &usage);
Expand All @@ -487,7 +581,7 @@ inline CommandResult Child::Wait(std::optional<std::chrono::duration<double>> ti
// Collect outputs (reader finishes when pipes close)
auto [stdout_result, stderr_result] = reader_future.get();
result.Stdout = std::move(stdout_result);
result.Stderr += stderr_result;
result.Stderr = std::move(stderr_result);

close(StdoutFd);
close(StderrFd);
Expand Down Expand Up @@ -619,8 +713,10 @@ inline std::optional<Child> Command::Spawn() {
setenv(key.c_str(), value.c_str(), 1);
}

dup2(stdout_pipe[1], STDOUT_FILENO);
dup2(stderr_pipe[1], STDERR_FILENO);
if (dup2(stdout_pipe[1], STDOUT_FILENO) == -1 ||
dup2(stderr_pipe[1], STDERR_FILENO) == -1) {
_exit(EXIT_FAIL_EC);
}
close(stdout_pipe[0]); close(stdout_pipe[1]);
close(stderr_pipe[0]); close(stderr_pipe[1]);

Expand Down Expand Up @@ -662,7 +758,7 @@ inline std::pair<std::string, std::string> AsyncPipeReader::ReadPipes(const int
{stderr_fd, POLLIN, 0}}
};

if (const int poll_result = poll(fds.data(), 2, 100); poll_result > 0) {
if (const int poll_result = poll(fds.data(), 2, 50); poll_result > 0) {
if (fds[0].revents & POLLIN) {
if (!ReadFromPipe(stdout_data, read_buffer)) {
stdout_data.Finished = true;
Expand Down
1 change: 1 addition & 0 deletions Evaluate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <fstream>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#include "CatalystCX.hpp"

class TestRunner {
Expand Down
Loading