From 15b4b36358daa7b9a6e58109c2e96df7139c90fb Mon Sep 17 00:00:00 2001 From: Atheria Date: Sat, 20 Sep 2025 12:33:08 +0700 Subject: [PATCH 1/4] New changes --- .gitignore | 1 + CMakeLists.txt | 26 +++--- CatalystCX.cpp | 48 ---------- CatalystCX.hpp | 237 +++++++++++++++++++++++++++++++++++-------------- Evaluate.cpp | 229 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 410 insertions(+), 131 deletions(-) delete mode 100644 CatalystCX.cpp create mode 100644 Evaluate.cpp diff --git a/.gitignore b/.gitignore index f35e3c0..f5f39a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *build* +.amazonq \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 22d2c13..6143398 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,25 +1,21 @@ cmake_minimum_required(VERSION 3.10) -project(CatalystCX VERSION 0.0.1) +project(CatalystCX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -g0") +# Compiler flags +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -O2") -add_executable(CatalystCX - CatalystCX.cpp -) +# Test suite executable +add_executable(cxtest Evaluate.cpp) -# --- Run Target --- -add_custom_target(run - COMMAND ${CMAKE_COMMAND} -E env "PATH=$ENV{PATH}" ./CatalystCX - DEPENDS CatalystCX +# Custom target to run tests +add_custom_target(test + COMMAND cxtest + DEPENDS cxtest WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) -# --- Install Target --- -include(GNUInstallDirs) - -install(FILES CatalystCX.hpp - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} -) \ No newline at end of file +# Install header +install(FILES CatalystCX.hpp DESTINATION include) \ No newline at end of file diff --git a/CatalystCX.cpp b/CatalystCX.cpp deleted file mode 100644 index f72d855..0000000 --- a/CatalystCX.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (c) 2025 assembler-0 -// Licensed under GPL-3.0-or-later -#include "CatalystCX.hpp" -#include - -void PrintResult(const CommandResult& result) { - std::cout << "Exit Code: " << result.ExitCode << std::endl; - std::cout << "Timed Out: " << (result.TimedOut ? "Yes" : "No") << std::endl; - std::cout << "Execution Time: " << result.ExecutionTime.count() << "s" << std::endl; - -#ifdef __linux__ - std::cout << "User CPU Time: " << result.Usage.UserCpuTime << "us" << std::endl; - std::cout << "System CPU Time: " << result.Usage.SystemCpuTime << "us" << std::endl; - std::cout << "Max Resident Set Size: " << result.Usage.MaxResidentSetSize << "KB" << std::endl; -#endif - - std::cout << "\nStdout:\n" << result.Stdout << std::endl; - std::cout << "\nStderr:\n" << result.Stderr << std::endl; -} - -int main() { - std::cout << "--- Running ls -l ---" << std::endl; - auto result = Command("ls").Arg("-l").Status(); - PrintResult(result); - - std::cout << "\n--- Running ping with a 2-second timeout ---" << std::endl; - auto ping_result = Command("ping").Arg("8.8.8.8").Timeout(std::chrono::seconds(2)).Status(); - PrintResult(ping_result); - - std::cout << "\n--- Spawning a long-running process and waiting for it ---" << std::endl; - if (auto child = Command("sleep").Arg("3").Spawn()) { - std::cout << "Process spawned with PID: " << child->GetPid() << std::endl; - auto sleep_result = child->Wait(); - PrintResult(sleep_result); - } else { - std::cerr << "Failed to spawn sleep process." << std::endl; - } - - std::cout << "\n--- Attempting to spawn a non-existent command ---" << std::endl; - if (auto child = Command("nonexistentcommand").Spawn()) { - child->Wait(); - } else { - std::cerr << "Failed to spawn non-existent command, as expected." << std::endl; - } - - return 0; -} \ No newline at end of file diff --git a/CatalystCX.hpp b/CatalystCX.hpp index fa37eb5..931e5f8 100644 --- a/CatalystCX.hpp +++ b/CatalystCX.hpp @@ -25,6 +25,11 @@ #include #include +#ifdef __APPLE__ +#include +extern char **environ; +#endif + namespace fs = std::filesystem; // A struct to hold the result of a command execution @@ -34,12 +39,23 @@ struct CommandResult { std::string Stderr; std::chrono::duration ExecutionTime{}; bool TimedOut = false; + + // Process termination info + bool KilledBySignal = false; + int TerminatingSignal = 0; + bool CoreDumped = false; + bool Stopped = false; + int StopSignal = 0; #ifdef __linux__ struct ResourceUsage { long UserCpuTime; // in microseconds long SystemCpuTime; // in microseconds long MaxResidentSetSize; // in kilobytes + long MinorPageFaults; + long MajorPageFaults; + long VoluntaryContextSwitches; + long InvoluntaryContextSwitches; } Usage{}; #endif }; @@ -182,10 +198,27 @@ inline CommandResult Child::Wait(std::optional> ti result.Usage.UserCpuTime = usage.ru_utime.tv_sec * 1000000 + usage.ru_utime.tv_usec; result.Usage.SystemCpuTime = usage.ru_stime.tv_sec * 1000000 + usage.ru_stime.tv_usec; result.Usage.MaxResidentSetSize = usage.ru_maxrss; + result.Usage.MinorPageFaults = usage.ru_minflt; + result.Usage.MajorPageFaults = usage.ru_majflt; + result.Usage.VoluntaryContextSwitches = usage.ru_nvcsw; + result.Usage.InvoluntaryContextSwitches = usage.ru_nivcsw; #endif + // Enhanced process termination analysis if (!result.TimedOut) { - result.ExitCode = WEXITSTATUS(status); + if (WIFEXITED(status)) { + result.ExitCode = WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + result.KilledBySignal = true; + result.TerminatingSignal = WTERMSIG(status); + result.ExitCode = 128 + result.TerminatingSignal; +#ifdef WCOREDUMP + result.CoreDumped = WCOREDUMP(status); +#endif + } else if (WIFSTOPPED(status)) { + result.Stopped = true; + result.StopSignal = WSTOPSIG(status); + } } return result; @@ -220,32 +253,86 @@ inline std::optional Command::Spawn() { return std::nullopt; } +#ifdef __APPLE__ + posix_spawn_file_actions_t file_actions; + posix_spawnattr_t attr; + + if (posix_spawn_file_actions_init(&file_actions) != 0 || + posix_spawnattr_init(&attr) != 0) { + close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); + return std::nullopt; + } + + posix_spawn_file_actions_adddup2(&file_actions, stdout_pipe[1], STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, stderr_pipe[1], STDERR_FILENO); + posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[0]); + posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[1]); + posix_spawn_file_actions_addclose(&file_actions, stderr_pipe[0]); + posix_spawn_file_actions_addclose(&file_actions, stderr_pipe[1]); + + if (WorkDir) { + posix_spawn_file_actions_addchdir_np(&file_actions, WorkDir->c_str()); + } + + std::vector argv, envp; + argv.reserve(args_vec.size() + 1); + for (const auto& s : args_vec) { + argv.push_back(const_cast(s.c_str())); + } + argv.push_back(nullptr); + + std::vector env_strings; + if (!EnvVars.empty()) { + for (char** env = environ; *env; ++env) { + std::string env_str(*env); + std::string key = env_str.substr(0, env_str.find('=')); + if (EnvVars.find(key) == EnvVars.end()) { + env_strings.push_back(env_str); + } + } + for (const auto& [key, value] : EnvVars) { + env_strings.push_back(key + "=" + value); + } + for (const auto& s : env_strings) { + envp.push_back(const_cast(s.c_str())); + } + envp.push_back(nullptr); + } + + pid_t pid; + int result = posix_spawn(&pid, argv[0], &file_actions, &attr, + argv.data(), envp.empty() ? environ : envp.data()); + + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + + if (result != 0) { + close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); + return std::nullopt; + } +#else const pid_t pid = fork(); if (pid == -1) { - close(stdout_pipe[0]); - close(stdout_pipe[1]); - close(stderr_pipe[0]); - close(stderr_pipe[1]); + close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); return std::nullopt; } - if (pid == 0) { // Child process - if (WorkDir) { - if (chdir(WorkDir->c_str()) != 0) { - _exit(127); - } + if (pid == 0) { + if (WorkDir && chdir(WorkDir->c_str()) != 0) { + _exit(127); } - for(const auto &[fst, snd] : EnvVars) { - setenv(fst.c_str(), snd.c_str(), 1); + for(const auto &[key, value] : EnvVars) { + setenv(key.c_str(), value.c_str(), 1); } dup2(stdout_pipe[1], STDOUT_FILENO); dup2(stderr_pipe[1], STDERR_FILENO); - close(stdout_pipe[0]); - close(stdout_pipe[1]); - close(stderr_pipe[0]); - close(stderr_pipe[1]); + close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); std::vector argv; argv.reserve(args_vec.size() + 1); @@ -255,10 +342,10 @@ inline std::optional Command::Spawn() { argv.push_back(nullptr); execvp(argv[0], argv.data()); - _exit(127); // exec failed + _exit(127); } +#endif - // Parent process close(stdout_pipe[1]); close(stderr_pipe[1]); @@ -314,78 +401,92 @@ inline bool AsyncPipeReader::ReadFromPipe(PipeData& pipe_data, std::array= PATH_MAX) { - continue; - } - + if (IsFileExecutable(full_path)) { return true; } + + if (end == std::string::npos) break; + start = end + 1; + end = path_str.find(':', start); } - + return false; } inline bool ExecutionValidator::CanExecuteCommand(const std::vector& args) { - if (args.empty()) return false; - return IsCommandExecutable(args[0]); + return !args.empty() && IsCommandExecutable(args[0]); } +// Signal name lookup utility +class SignalInfo { +public: + static const char* GetSignalName(const int signal) { + switch (signal) { + case SIGTERM: return "SIGTERM"; + case SIGKILL: return "SIGKILL"; + case SIGINT: return "SIGINT"; + case SIGQUIT: return "SIGQUIT"; + case SIGABRT: return "SIGABRT"; + case SIGFPE: return "SIGFPE"; + case SIGILL: return "SIGILL"; + case SIGSEGV: return "SIGSEGV"; + case SIGBUS: return "SIGBUS"; + case SIGPIPE: return "SIGPIPE"; + case SIGALRM: return "SIGALRM"; + case SIGUSR1: return "SIGUSR1"; + case SIGUSR2: return "SIGUSR2"; + case SIGCHLD: return "SIGCHLD"; + case SIGCONT: return "SIGCONT"; + case SIGSTOP: return "SIGSTOP"; + case SIGTSTP: return "SIGTSTP"; + default: return "UNKNOWN"; + } + } + + static std::string GetProcessInfo(const CommandResult& result) { + std::ostringstream info; + if (result.KilledBySignal) { + info << "Killed by signal " << result.TerminatingSignal + << " (" << GetSignalName(result.TerminatingSignal) << ")"; + if (result.CoreDumped) info << " [core dumped]"; + } else if (result.Stopped) { + info << "Stopped by signal " << result.StopSignal + << " (" << GetSignalName(result.StopSignal) << ")"; + } else if (result.TimedOut) { + info << "Process timed out"; + } else { + info << "Exited normally with code " << result.ExitCode; + } + return info.str(); + } +}; -#endif // CATALYSTCX_HPP \ No newline at end of file +#endif \ No newline at end of file diff --git a/Evaluate.cpp b/Evaluate.cpp new file mode 100644 index 0000000..21cb159 --- /dev/null +++ b/Evaluate.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2025 assembler-0 +// Licensed under GPL-3.0-or-later +#include "CatalystCX.hpp" +#include +#include +#include +#include + +class TestRunner { +private: + int passed = 0; + int failed = 0; + +public: + void Assert(bool condition, const std::string& test_name) { + if (condition) { + std::cout << "[PASS] " << test_name << std::endl; + passed++; + } else { + std::cout << "[FAIL] " << test_name << std::endl; + failed++; + } + } + + void PrintSummary() { + std::cout << "\n=== Test Summary ===" << std::endl; + std::cout << "Passed: " << passed << std::endl; + std::cout << "Failed: " << failed << std::endl; + std::cout << "Total: " << (passed + failed) << std::endl; + } + + int GetFailedCount() const { return failed; } +}; + +void TestBasicExecution(TestRunner& runner) { + std::cout << "\n=== Basic Execution Tests ===" << std::endl; + + // Test simple command + auto result = Command("echo").Arg("hello").Status(); + runner.Assert(result.ExitCode == 0, "Echo command success"); + runner.Assert(result.Stdout.find("hello") != std::string::npos, "Echo output correct"); + + // Test command with multiple args + result = Command("echo").Args({"hello", "world"}).Status(); + runner.Assert(result.ExitCode == 0, "Multiple args success"); + runner.Assert(result.Stdout.find("hello world") != std::string::npos, "Multiple args output"); + + // Test command failure + result = Command("false").Status(); + runner.Assert(result.ExitCode == 1, "False command exit code"); +} + +void TestAsyncExecution(TestRunner& runner) { + std::cout << "\n=== Async Execution Tests ===" << std::endl; + + // Test spawn and wait + auto child = Command("sleep").Arg("1").Spawn(); + runner.Assert(child.has_value(), "Sleep spawn success"); + + if (child) { + pid_t pid = child->GetPid(); + runner.Assert(pid > 0, "Valid PID returned"); + + auto result = child->Wait(); + runner.Assert(result.ExitCode == 0, "Sleep completed successfully"); + runner.Assert(result.ExecutionTime.count() >= 1.0, "Sleep duration correct"); + } +} + +void TestTimeout(TestRunner& runner) { + std::cout << "\n=== Timeout Tests ===" << std::endl; + + // Test timeout functionality + auto start = std::chrono::steady_clock::now(); + auto result = Command("sleep").Arg("5").Timeout(std::chrono::seconds(1)).Status(); + auto duration = std::chrono::steady_clock::now() - start; + + runner.Assert(result.TimedOut, "Command timed out"); + runner.Assert(duration < std::chrono::seconds(2), "Timeout enforced quickly"); +} + +void TestSignalHandling(TestRunner& runner) { + std::cout << "\n=== Signal Handling Tests ===" << std::endl; + + // Test SIGTERM handling + auto child = Command("sleep").Arg("10").Spawn(); + if (child) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + child->Kill(SIGTERM); + auto result = child->Wait(); + + runner.Assert(result.KilledBySignal, "Process killed by signal"); + runner.Assert(result.TerminatingSignal == SIGTERM, "Correct terminating signal"); + runner.Assert(result.ExitCode == 128 + SIGTERM, "Correct exit code for signal"); + } + + // Test signal name lookup + runner.Assert(std::string(SignalInfo::GetSignalName(SIGTERM)) == "SIGTERM", "Signal name lookup"); + runner.Assert(std::string(SignalInfo::GetSignalName(SIGKILL)) == "SIGKILL", "SIGKILL name"); +} + +void TestEnvironmentVariables(TestRunner& runner) { + std::cout << "\n=== Environment Variable Tests ===" << std::endl; + + auto result = Command("printenv").Arg("TEST_VAR") + .Environment("TEST_VAR", "test_value") + .Status(); + + runner.Assert(result.ExitCode == 0, "Environment variable set"); + runner.Assert(result.Stdout.find("test_value") != std::string::npos, "Environment variable value"); +} + +void TestWorkingDirectory(TestRunner& runner) { + std::cout << "\n=== Working Directory Tests ===" << std::endl; + + auto result = Command("pwd").WorkingDirectory("/tmp").Status(); + runner.Assert(result.ExitCode == 0, "Working directory command success"); + runner.Assert(result.Stdout.find("/tmp") != std::string::npos, "Working directory set correctly"); +} + +void TestErrorHandling(TestRunner& runner) { + std::cout << "\n=== Error Handling Tests ===" << std::endl; + + // Test non-existent command + auto child = Command("nonexistentcommand12345").Spawn(); + runner.Assert(!child.has_value(), "Non-existent command fails to spawn"); + + // Test command that writes to stderr + auto result = Command("sh").Args({"-c", "echo error >&2; exit 42"}).Status(); + runner.Assert(result.ExitCode == 42, "Custom exit code preserved"); + runner.Assert(result.Stderr.find("error") != std::string::npos, "Stderr captured"); +} + +void TestResourceUsage(TestRunner& runner) { + std::cout << "\n=== Resource Usage Tests ===" << std::endl; + +#ifdef __linux__ + auto result = Command("dd").Args({"if=/dev/zero", "of=/dev/null", "bs=1M", "count=10"}).Status(); + runner.Assert(result.ExitCode == 0, "DD command success"); + runner.Assert(result.Usage.UserCpuTime >= 0, "User CPU time recorded"); + runner.Assert(result.Usage.SystemCpuTime >= 0, "System CPU time recorded"); + runner.Assert(result.Usage.MaxResidentSetSize > 0, "Memory usage recorded"); +#else + std::cout << "[SKIP] Resource usage tests (Linux only)" << std::endl; +#endif +} + +void TestPipeHandling(TestRunner& runner) { + std::cout << "\n=== Pipe Handling Tests ===" << std::endl; + + // Test large output + auto result = Command("seq").Args({"1", "1000"}).Status(); + runner.Assert(result.ExitCode == 0, "Large output command success"); + runner.Assert(result.Stdout.find("1000") != std::string::npos, "Large output captured"); + + // Test mixed stdout/stderr + result = Command("sh").Args({"-c", "echo stdout; echo stderr >&2"}).Status(); + runner.Assert(result.Stdout.find("stdout") != std::string::npos, "Stdout separated"); + runner.Assert(result.Stderr.find("stderr") != std::string::npos, "Stderr separated"); +} + +void TestExecutionValidator(TestRunner& runner) { + std::cout << "\n=== Execution Validator Tests ===" << std::endl; + + runner.Assert(ExecutionValidator::IsCommandExecutable("ls"), "ls is executable"); + runner.Assert(ExecutionValidator::IsCommandExecutable("echo"), "echo is executable"); + runner.Assert(!ExecutionValidator::IsCommandExecutable("nonexistentcmd123"), "Non-existent not executable"); + + std::vector valid_args = {"ls", "-l"}; + std::vector invalid_args = {"nonexistentcmd123"}; + + runner.Assert(ExecutionValidator::CanExecuteCommand(valid_args), "Valid command can execute"); + runner.Assert(!ExecutionValidator::CanExecuteCommand(invalid_args), "Invalid command cannot execute"); +} + +void TestProcessInfo(TestRunner& runner) { + std::cout << "\n=== Process Info Tests ===" << std::endl; + + // Test normal exit + auto result = Command("true").Status(); + std::string info = SignalInfo::GetProcessInfo(result); + runner.Assert(info.find("Exited normally") != std::string::npos, "Normal exit info"); + + // Test timeout info + result = Command("sleep").Arg("5").Timeout(std::chrono::milliseconds(100)).Status(); + info = SignalInfo::GetProcessInfo(result); + runner.Assert(info.find("timed out") != std::string::npos, "Timeout info"); +} + +void TestEdgeCases(TestRunner& runner) { + std::cout << "\n=== Edge Case Tests ===" << std::endl; + + // Empty command + std::vector empty_args; + runner.Assert(!ExecutionValidator::CanExecuteCommand(empty_args), "Empty args rejected"); + + // Command with spaces in args + auto result = Command("echo").Arg("hello world").Status(); + runner.Assert(result.Stdout.find("hello world") != std::string::npos, "Spaces in args handled"); + + // Very short timeout + result = Command("sleep").Arg("1").Timeout(std::chrono::milliseconds(1)).Status(); + runner.Assert(result.TimedOut, "Very short timeout works"); +} + +int main() { + TestRunner runner; + + std::cout << "CatalystCX Test Suite" << std::endl; + std::cout << "=====================" << std::endl; + + TestBasicExecution(runner); + TestAsyncExecution(runner); + TestTimeout(runner); + TestSignalHandling(runner); + TestEnvironmentVariables(runner); + TestWorkingDirectory(runner); + TestErrorHandling(runner); + TestResourceUsage(runner); + TestPipeHandling(runner); + TestExecutionValidator(runner); + TestProcessInfo(runner); + TestEdgeCases(runner); + + runner.PrintSummary(); + + return runner.GetFailedCount(); +} \ No newline at end of file From ac02c29da68936e1b7e25f50774d30cf0c095a92 Mon Sep 17 00:00:00 2001 From: Atheria Date: Sat, 20 Sep 2025 12:53:13 +0700 Subject: [PATCH 2/4] Windows support --- .github/workflows/ci.yml | 155 +++++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 7 +- Evaluate.cpp | 5 +- 3 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7251c4b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,155 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + release: + types: [ published ] + +jobs: + test: + name: Test on ${{ matrix.os }} with ${{ matrix.compiler }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + compiler: [gcc, clang, msvc] + exclude: + - os: windows-latest + compiler: gcc + - os: windows-latest + compiler: clang + - os: ubuntu-latest + compiler: msvc + - os: macos-latest + compiler: msvc + + steps: + - uses: actions/checkout@v4 + + - name: Setup GCC + if: matrix.compiler == 'gcc' + uses: egor-tensin/setup-gcc@v1 + with: + version: 14 + + - name: Setup Clang + if: matrix.compiler == 'clang' + uses: egor-tensin/setup-clang@v1 + with: + version: 19 + + - name: Setup MSVC + if: matrix.compiler == 'msvc' + uses: microsoft/setup-msbuild@v1.3 + + - name: Configure CMake (Unix) + if: runner.os != 'Windows' + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_COMPILER=${{ matrix.compiler == 'gcc' && 'g++' || 'clang++' }} + + - name: Configure CMake (Windows) + if: runner.os == 'Windows' + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --config Release + + - name: Run Tests + run: cmake --build build --target test --config Release + + static-analysis: + name: Static Analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y cppcheck clang-tidy + + - name: Run cppcheck + run: | + cppcheck --enable=all --std=c++20 --suppress=missingIncludeSystem \ + --error-exitcode=1 CatalystCX.hpp + + - name: Configure for clang-tidy + run: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run clang-tidy + run: | + clang-tidy CatalystCX.cpp \ + -p build --checks='-*,readability-*,performance-*,modernize-*' + + sanitizers: + name: Sanitizer Tests + runs-on: ubuntu-latest + strategy: + matrix: + sanitizer: [address, thread, undefined] + steps: + - uses: actions/checkout@v4 + + - name: Configure with Sanitizers + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=${{ matrix.sanitizer }} -g -O1" + + - name: Build + run: cmake --build build + + - name: Run Tests with Sanitizer + run: cmake --build build --target test + + package: + name: Package Release + runs-on: ubuntu-latest + needs: [test, static-analysis] + if: github.event_name == 'release' + steps: + - uses: actions/checkout@v4 + + - name: Create Package + run: | + mkdir -p catalystcx-${{ github.ref_name }} + cp CatalystCX.hpp catalystcx-${{ github.ref_name }}/ + cp README.md catalystcx-${{ github.ref_name }}/ + cp CMakeLists.txt catalystcx-${{ github.ref_name }}/ + tar -czf catalystcx-${{ github.ref_name }}.tar.gz catalystcx-${{ github.ref_name }}/ + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./catalystcx-${{ github.ref_name }}.tar.gz + asset_name: catalystcx-${{ github.ref_name }}.tar.gz + asset_content_type: application/gzip + + benchmark: + name: Performance Benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Configure Optimized Build + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_FLAGS="-O3 -march=native -DNDEBUG" + + - name: Build + run: cmake --build build + + - name: Run Benchmarks + run: | + echo "=== Performance Test ===" + time cmake --build build --target test + echo "=== Memory Usage Test ===" + valgrind --tool=massif --massif-out-file=massif.out \ + ./build/test_suite 2>/dev/null || true \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 6143398..b3e943f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -O2") # Test suite executable -add_executable(cxtest Evaluate.cpp) +add_executable(cxtest Evaluate.cpp CatalystCX.hpp) # Custom target to run tests add_custom_target(test @@ -18,4 +18,7 @@ add_custom_target(test ) # Install header -install(FILES CatalystCX.hpp DESTINATION include) \ No newline at end of file +install(FILES CatalystCX.hpp DESTINATION include) + +find_package(Threads REQUIRED) +target_link_libraries(cxtest PRIVATE Threads::Threads) \ No newline at end of file diff --git a/Evaluate.cpp b/Evaluate.cpp index 21cb159..f1dd0f5 100644 --- a/Evaluate.cpp +++ b/Evaluate.cpp @@ -1,14 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (c) 2025 assembler-0 // Licensed under GPL-3.0-or-later -#include "CatalystCX.hpp" -#include #include #include +#include #include +#include "CatalystCX.hpp" class TestRunner { -private: int passed = 0; int failed = 0; From 44807c6b79aacdebaee5d754e5e17f408747d879 Mon Sep 17 00:00:00 2001 From: Atheria Date: Sat, 20 Sep 2025 12:53:32 +0700 Subject: [PATCH 3/4] CI --- CatalystCX.hpp | 331 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 270 insertions(+), 61 deletions(-) diff --git a/CatalystCX.hpp b/CatalystCX.hpp index 931e5f8..7b0e201 100644 --- a/CatalystCX.hpp +++ b/CatalystCX.hpp @@ -7,32 +7,38 @@ #include #include -#include -#include #include #include #include -#include #include #include #include -#include -#include -#include -#include #include -#include #include #include +#ifdef _WIN32 +#include +#include +#include +#include +using pid_t = DWORD; +#else +#include +#include +#include +#include +#include +#include +#include #ifdef __APPLE__ #include extern char **environ; #endif +#endif namespace fs = std::filesystem; -// A struct to hold the result of a command execution struct CommandResult { int ExitCode{}; std::string Stdout; @@ -40,7 +46,6 @@ struct CommandResult { std::chrono::duration ExecutionTime{}; bool TimedOut = false; - // Process termination info bool KilledBySignal = false; int TerminatingSignal = 0; bool CoreDumped = false; @@ -49,75 +54,93 @@ struct CommandResult { #ifdef __linux__ struct ResourceUsage { - long UserCpuTime; // in microseconds - long SystemCpuTime; // in microseconds - long MaxResidentSetSize; // in kilobytes + long UserCpuTime; + long SystemCpuTime; + long MaxResidentSetSize; long MinorPageFaults; long MajorPageFaults; long VoluntaryContextSwitches; long InvoluntaryContextSwitches; } Usage{}; +#elif defined(_WIN32) + struct ResourceUsage { + FILETIME UserTime; + FILETIME KernelTime; + SIZE_T PeakWorkingSetSize; + SIZE_T PageFaultCount; + } Usage{}; #endif }; 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) { + ProcessId = GetProcessId(process); + } + + ~Child() { + if (ProcessHandle != INVALID_HANDLE_VALUE) CloseHandle(ProcessHandle); + if (ThreadHandle != INVALID_HANDLE_VALUE) CloseHandle(ThreadHandle); + } +#else Child(const pid_t pid, const int stdout_fd, const int stderr_fd) : ProcessId(pid), StdoutFd(stdout_fd), StderrFd(stderr_fd) {} +#endif - // Wait for the child process to exit and return the result [[nodiscard]] CommandResult Wait(std::optional> timeout = std::nullopt) const; - - // Get the process ID [[nodiscard]] pid_t GetPid() const { return ProcessId; } - // Send a signal to the process +#ifdef _WIN32 + void Kill(int signal = 0) const; +#else void Kill(int signal = SIGTERM) const; +#endif private: pid_t ProcessId; +#ifdef _WIN32 + HANDLE ProcessHandle; + HANDLE ThreadHandle; + HANDLE StdoutHandle; + HANDLE StderrHandle; +#else int StdoutFd; int StderrFd; +#endif }; class Command { public: explicit Command(std::string executable) : Executable(std::move(executable)) {} - // Add a single argument Command& Arg(std::string argument) { Arguments.push_back(std::move(argument)); return *this; } - // Add multiple arguments Command& Args(const std::vector& arguments) { Arguments.insert(Arguments.end(), arguments.begin(), arguments.end()); return *this; } - // Set the working directory Command& WorkingDirectory(std::string path) { WorkDir = std::move(path); return *this; } - // Set an environment variable Command& Environment(const std::string& key, const std::string& value) { EnvVars[key] = value; return *this; } - // Set a timeout for the command Command& Timeout(std::chrono::duration duration) { TimeoutDuration = duration; return *this; } - // Execute the command and wait for it to complete [[nodiscard]] CommandResult Status(); - - // Spawn the command and return a Child object [[nodiscard]] std::optional Spawn(); private: @@ -130,17 +153,26 @@ class Command { class AsyncPipeReader { public: +#ifdef _WIN32 + static std::pair ReadPipes(HANDLE stdout_handle, HANDLE stderr_handle); +private: + struct PipeData { + HANDLE Handle; + std::string Buffer; + bool Finished = false; + }; + static bool ReadFromPipe(PipeData& pipe_data, std::array& buffer); +#else static std::pair ReadPipes(int stdout_fd, int stderr_fd); - private: struct PipeData { int Fd; std::string Buffer; bool Finished = false; }; - static bool ReadFromPipe(PipeData& pipe_data, std::array& buffer); static bool IsPipeOpen(int fd); +#endif }; class ExecutionValidator { @@ -150,7 +182,180 @@ class ExecutionValidator { static bool CanExecuteCommand(const std::vector& args); }; -// Implementation of Child methods +// Windows implementations +#ifdef _WIN32 +inline CommandResult Child::Wait(std::optional> timeout) const { + auto start_time = std::chrono::steady_clock::now(); + CommandResult result; + + DWORD wait_time = timeout ? static_cast(timeout->count() * 1000) : INFINITE; + DWORD wait_result = WaitForSingleObject(ProcessHandle, wait_time); + + auto end_time = std::chrono::steady_clock::now(); + result.ExecutionTime = end_time - start_time; + + if (wait_result == WAIT_TIMEOUT) { + result.TimedOut = true; + TerminateProcess(ProcessHandle, 1); + WaitForSingleObject(ProcessHandle, INFINITE); + } + + DWORD exit_code; + GetExitCodeProcess(ProcessHandle, &exit_code); + result.ExitCode = static_cast(exit_code); + + FILETIME creation_time, exit_time; + GetProcessTimes(ProcessHandle, &creation_time, &exit_time, + &result.Usage.KernelTime, &result.Usage.UserTime); + + PROCESS_MEMORY_COUNTERS pmc; + if (GetProcessMemoryInfo(ProcessHandle, &pmc, sizeof(pmc))) { + result.Usage.PeakWorkingSetSize = pmc.PeakWorkingSetSize; + result.Usage.PageFaultCount = pmc.PageFaultCount; + } + + auto [stdout_result, stderr_result] = AsyncPipeReader::ReadPipes(StdoutHandle, StderrHandle); + result.Stdout = std::move(stdout_result); + result.Stderr = std::move(stderr_result); + + CloseHandle(StdoutHandle); + CloseHandle(StderrHandle); + + return result; +} + +inline void Child::Kill(int) const { + TerminateProcess(ProcessHandle, 1); +} + +inline std::optional Command::Spawn() { + std::vector args_vec; + args_vec.push_back(Executable); + args_vec.insert(args_vec.end(), Arguments.begin(), Arguments.end()); + + if (!ExecutionValidator::CanExecuteCommand(args_vec)) { + return std::nullopt; + } + + 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)) { + return std::nullopt; + } + + SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFOA si = {sizeof(STARTUPINFOA)}; + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = stdout_write; + si.hStdError = stderr_write; + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + + PROCESS_INFORMATION pi = {}; + + std::string cmdline = Executable; + for (const auto& arg : Arguments) { + cmdline += " \"" + arg + "\""; + } + + std::string env_block; + if (!EnvVars.empty()) { + for (const auto& [key, value] : EnvVars) { + env_block += key + "=" + value + "\0"; + } + env_block += "\0"; + } + + BOOL success = CreateProcessA( + nullptr, const_cast(cmdline.c_str()), + nullptr, nullptr, TRUE, 0, + env_block.empty() ? nullptr : const_cast(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 AsyncPipeReader::ReadPipes(HANDLE stdout_handle, HANDLE stderr_handle) { + PipeData stdout_data{stdout_handle, {}}; + PipeData stderr_data{stderr_handle, {}}; + + std::array buffer; + + while (!stdout_data.Finished || !stderr_data.Finished) { + if (!stdout_data.Finished && !ReadFromPipe(stdout_data, buffer)) { + stdout_data.Finished = true; + } + if (!stderr_data.Finished && !ReadFromPipe(stderr_data, buffer)) { + stderr_data.Finished = true; + } + if (!stdout_data.Finished || !stderr_data.Finished) { + Sleep(10); + } + } + + return {std::move(stdout_data.Buffer), std::move(stderr_data.Buffer)}; +} + +inline bool AsyncPipeReader::ReadFromPipe(PipeData& pipe_data, std::array& buffer) { + DWORD bytes_read; + if (ReadFile(pipe_data.Handle, buffer.data(), buffer.size(), &bytes_read, nullptr)) { + if (bytes_read > 0) { + pipe_data.Buffer.append(buffer.data(), bytes_read); + return true; + } + } + return false; +} + +inline bool ExecutionValidator::IsFileExecutable(const std::string& path) { + DWORD attrs = GetFileAttributesA(path.c_str()); + return attrs != INVALID_FILE_ATTRIBUTES && !(attrs & FILE_ATTRIBUTE_DIRECTORY); +} + +inline bool ExecutionValidator::IsCommandExecutable(const std::string& command) { + if (command.find('\\') != std::string::npos || command.find('/') != std::string::npos) { + return IsFileExecutable(command) || IsFileExecutable(command + ".exe"); + } + + const char* path_env = getenv("PATH"); + if (!path_env) return false; + + std::string path_str(path_env); + size_t start = 0; + size_t end = path_str.find(';'); + + while (start < path_str.length()) { + std::string dir = path_str.substr(start, end - start); + std::string full_path = dir + "\\" + command; + + if (IsFileExecutable(full_path) || IsFileExecutable(full_path + ".exe")) { + return true; + } + + if (end == std::string::npos) break; + start = end + 1; + end = path_str.find(';', start); + } + + return false; +} + +#else +// Unix implementations inline CommandResult Child::Wait(std::optional> timeout) const { auto start_time = std::chrono::steady_clock::now(); @@ -228,17 +433,6 @@ inline void Child::Kill(const int signal) const { kill(ProcessId, signal); } -// Implementation of Command methods -inline CommandResult Command::Status() { - if (const auto child = Spawn()) { - return child->Wait(TimeoutDuration); - } - CommandResult result; - result.ExitCode = 127; - result.Stderr = "Failed to spawn process"; - return result; -} - inline std::optional Command::Spawn() { std::vector args_vec; args_vec.push_back(Executable); @@ -256,32 +450,32 @@ inline std::optional Command::Spawn() { #ifdef __APPLE__ posix_spawn_file_actions_t file_actions; posix_spawnattr_t attr; - + if (posix_spawn_file_actions_init(&file_actions) != 0 || posix_spawnattr_init(&attr) != 0) { close(stdout_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[0]); close(stderr_pipe[1]); return std::nullopt; } - + posix_spawn_file_actions_adddup2(&file_actions, stdout_pipe[1], STDOUT_FILENO); posix_spawn_file_actions_adddup2(&file_actions, stderr_pipe[1], STDERR_FILENO); posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[0]); posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[1]); posix_spawn_file_actions_addclose(&file_actions, stderr_pipe[0]); posix_spawn_file_actions_addclose(&file_actions, stderr_pipe[1]); - + if (WorkDir) { posix_spawn_file_actions_addchdir_np(&file_actions, WorkDir->c_str()); } - + std::vector argv, envp; argv.reserve(args_vec.size() + 1); for (const auto& s : args_vec) { argv.push_back(const_cast(s.c_str())); } argv.push_back(nullptr); - + std::vector env_strings; if (!EnvVars.empty()) { for (char** env = environ; *env; ++env) { @@ -299,14 +493,14 @@ inline std::optional Command::Spawn() { } envp.push_back(nullptr); } - + pid_t pid; - int result = posix_spawn(&pid, argv[0], &file_actions, &attr, - argv.data(), envp.empty() ? environ : envp.data()); - + int result = posix_spawnp(&pid, argv[0], &file_actions, &attr, + argv.data(), envp.empty() ? environ : envp.data()); // spawnp for PATH search + posix_spawn_file_actions_destroy(&file_actions); posix_spawnattr_destroy(&attr); - + if (result != 0) { close(stdout_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[0]); close(stderr_pipe[1]); @@ -408,47 +602,59 @@ inline bool AsyncPipeReader::IsPipeOpen(const int fd) { return fcntl(fd, F_GETFD) != -1; } -inline bool ExecutionValidator::IsFileExecutable(const std::string& path) { - struct stat st{}; - return stat(path.c_str(), &st) == 0 && st.st_mode & S_IXUSR; -} - inline bool ExecutionValidator::IsCommandExecutable(const std::string& command) { if (command.find('/') != std::string::npos) { return IsFileExecutable(command); } - + const char* path_env = getenv("PATH"); if (!path_env) return false; - + std::string path_str(path_env); size_t start = 0; size_t end = path_str.find(':'); - + while (start < path_str.length()) { std::string dir = path_str.substr(start, end - start); std::string full_path = dir + "/" + command; - + if (IsFileExecutable(full_path)) { return true; } - + if (end == std::string::npos) break; start = end + 1; end = path_str.find(':', start); } - + return false; } +inline bool ExecutionValidator::IsFileExecutable(const std::string& path) { + struct stat st{}; + return stat(path.c_str(), &st) == 0 && st.st_mode & S_IXUSR; +} + +#endif + +inline CommandResult Command::Status() { + if (const auto child = Spawn()) { + return child->Wait(TimeoutDuration); + } + CommandResult result; + result.ExitCode = 127; + result.Stderr = "Failed to spawn process"; + return result; +} + inline bool ExecutionValidator::CanExecuteCommand(const std::vector& args) { return !args.empty() && IsCommandExecutable(args[0]); } -// Signal name lookup utility class SignalInfo { public: static const char* GetSignalName(const int signal) { +#ifndef _WIN32 switch (signal) { case SIGTERM: return "SIGTERM"; case SIGKILL: return "SIGKILL"; @@ -469,6 +675,9 @@ class SignalInfo { case SIGTSTP: return "SIGTSTP"; default: return "UNKNOWN"; } +#else + return "N/A"; +#endif } static std::string GetProcessInfo(const CommandResult& result) { From 719377d4434c542003b788bdf08a12fc31411b67 Mon Sep 17 00:00:00 2001 From: Atheria Date: Sat, 20 Sep 2025 13:16:07 +0700 Subject: [PATCH 4/4] Changes --- .github/workflows/ci.yml | 14 +-- CatalystCX.hpp | 45 ++++++-- README.md | 235 ++++++++++++++++++++++++++++----------- 3 files changed, 211 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7251c4b..6545911 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,8 +83,7 @@ jobs: - name: Run clang-tidy run: | - clang-tidy CatalystCX.cpp \ - -p build --checks='-*,readability-*,performance-*,modernize-*' + clang-tidy CatalystCX.hpp -p build --checks='-*,readability-*,performance-*,modernize-*' sanitizers: name: Sanitizer Tests @@ -148,8 +147,9 @@ jobs: - name: Run Benchmarks run: | - echo "=== Performance Test ===" - time cmake --build build --target test - echo "=== Memory Usage Test ===" - valgrind --tool=massif --massif-out-file=massif.out \ - ./build/test_suite 2>/dev/null || true \ No newline at end of file + echo "=== Performance Test ===" + time ctest --test-dir build --output-on-failure + echo "=== Memory Usage Test ===" + sudo apt-get update && sudo apt-get install -y valgrind + valgrind --tool=massif --massif-out-file=massif.out \ + ./build/cxtest 2>/dev/null || true \ No newline at end of file diff --git a/CatalystCX.hpp b/CatalystCX.hpp index 7b0e201..a9244e1 100644 --- a/CatalystCX.hpp +++ b/CatalystCX.hpp @@ -1,6 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later // Copyright (c) 2025 assembler-0 // Licensed under GPL-3.0-or-later + +/** + * @brief CatalystCX - A cross-platform single-header C++ library for executing and managing external processes (or commands). + * @version 0.0.1 + * @author assembler-0 + */ + #pragma once #ifndef CATALYSTCX_HPP #define CATALYSTCX_HPP @@ -52,8 +59,8 @@ struct CommandResult { bool Stopped = false; int StopSignal = 0; -#ifdef __linux__ struct ResourceUsage { +#if defined(__linux__) long UserCpuTime; long SystemCpuTime; long MaxResidentSetSize; @@ -61,15 +68,13 @@ struct CommandResult { long MajorPageFaults; long VoluntaryContextSwitches; long InvoluntaryContextSwitches; - } Usage{}; #elif defined(_WIN32) - struct ResourceUsage { FILETIME UserTime; FILETIME KernelTime; SIZE_T PeakWorkingSetSize; SIZE_T PageFaultCount; - } Usage{}; #endif + } Usage{}; }; class Child { @@ -156,20 +161,21 @@ class AsyncPipeReader { #ifdef _WIN32 static std::pair ReadPipes(HANDLE stdout_handle, HANDLE stderr_handle); private: - struct PipeData { - HANDLE Handle; - std::string Buffer; - bool Finished = false; - }; static bool ReadFromPipe(PipeData& pipe_data, std::array& buffer); #else static std::pair ReadPipes(int stdout_fd, int stderr_fd); private: + struct PipeData { +#ifdef _WIN32 + HANDLE Handle; +#else int Fd; +#endif std::string Buffer; bool Finished = false; }; + static bool ReadFromPipe(PipeData& pipe_data, std::array& buffer); static bool IsPipeOpen(int fd); #endif @@ -256,9 +262,26 @@ inline std::optional Command::Spawn() { PROCESS_INFORMATION pi = {}; - std::string cmdline = Executable; + auto QuoteArgWin = [](const std::string& s) -> std::string { + bool need_quotes = s.find_first_of(" \t\"") != std::string::npos; + if (!need_quotes) return s; + std::string out; + out.push_back('"'); + size_t bs = 0; + for (char c : s) { + if (c == '\\') { ++bs; continue; } + if (c == '"') { out.append(bs * 2 + 1, '\\'); out.push_back('"'); bs = 0; continue; } + if (bs) { out.append(bs, '\\'); bs = 0; } + out.push_back(c); + } + if (bs) out.append(bs * 2, '\\'); + out.push_back('"'); + return out; + }; + std::string cmdline = QuoteArgWin(Executable); for (const auto& arg : Arguments) { - cmdline += " \"" + arg + "\""; + cmdline += ' '; + cmdline += QuoteArgWin(arg); } std::string env_block; diff --git a/README.md b/README.md index 6413295..e56a53e 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,88 @@ # CatalystCX -A modern, secure, and flexible C++ single-header library for executing system commands. +A modern, secure, and cross-platform C++ single-header library for executing system commands. (made to address `system()` injection vulnerabilities) -CatalystCX provides a staged pipeline for building and executing commands, designed with security and performance in mind. It avoids shell-based execution (`system()`) in favor of direct process creation, preventing injection vulnerabilities. The API is designed to be intuitive and chainable, following modern C++ best practices. +CatalystCX provides a fluent API for building and executing commands with security and performance as top priorities. It completely avoids shell-based execution (`system()`) in favor of direct process creation, eliminating injection vulnerabilities while providing comprehensive process monitoring and control. -## Features +## Status + +![CI/CD](https://github.com/assembler-0/CatalystCX/actions/workflows/ci.yml/badge.svg) +[![codecov](https://codecov.io/gh/assembler-0/CatalystCX/branch/main/graph/badge.svg)](https://codecov.io/gh/assembler-0/CatalystCX) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +![Version](https://img.shields.io/badge/Version-0.0.1-brightgreen.svg) -- **Secure by Design:** Commands and arguments are passed directly to the OS, avoiding shell interpretation and injection vulnerabilities. -- **Fluent Builder API:** A chainable, easy-to-use builder pattern for constructing commands. -- **Standard Stream Capture:** Capture `stdout` and `stderr` with ease. -- **Asynchronous Execution:** Spawn processes and manage them asynchronously. -- **Timeouts:** Set timeouts for commands to prevent them from running indefinitely. -- **Resource Monitoring (Linux):** Get detailed information about execution time, CPU usage, and memory consumption. -- **Robust Error Handling:** Uses `std::optional` to safely handle process spawning failures. -- **Single Header:** Easy to integrate into any project. +## Features +- **No Shell Execution:** Direct process creation prevents command injection +- **Argument Validation:** Built-in executable verification +- **Safe Defaults:** Secure by default configuration +- **Zero-Copy Operations:** Efficient memory management +- **Async I/O:** Non-blocking pipe reading with `poll()`/`WaitForMultipleObjects` +- **Platform-Specific Optimizations:** `posix_spawn()` on macOS, `CreateProcess` on Windows +- **Linux:** Full feature support with detailed resource monitoring +- **macOS:** Optimized with `posix_spawn()` for better performance +- **Windows:** Native `CreateProcess` API integration +- **Signal Handling:** Detailed process termination analysis +- **Resource Usage:** CPU time, memory usage, page faults, context switches +- **Execution Metrics:** Precise timing and performance data +- **Fluent Builder API:** Chainable, intuitive command construction +- **Modern C++20:** Uses latest language features and best practices +- **Single Header:** Easy integration, no external dependencies ## Requirements -- C++20 compatible compiler (e.g., GCC 10+, Clang 11+) -- CMake 3.10+ +- **C++20** compatible compiler: + - GCC 11+ (Linux/macOS) + - Clang 14+ (Linux/macOS) + - MSVC 2022+ (Windows) +- **CMake 3.10+** +- **Platform Support:** + - Linux (Ubuntu 20.04+, RHEL 8+) + - macOS (10.15+) + - Windows (10/11) -## Building the Project +## Quick Start -```bash -# Configure the project -cmake -B build +### Single Header Integration -# Build the example executable -cmake --build build -``` +```cpp +#include -## Running the Example +int main() { + auto result = Command("echo").Arg("Hello, World!").Status(); + std::cout << result.Stdout; // "Hello, World!\n" + return result.ExitCode; +} +``` -To run the example application included in `CatalystCX.cpp`, use the `run` target: +### Building from Source ```bash -cmake --build build --target run -``` +# Clone and build +git clone https://github.com/assembler-0/CatalystCX.git +cd CatalystCX +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -## Installation +# Run tests +cmake --build build --target test +``` -To install the `CatalystCX.hpp` header to your system's include directory (e.g., `/usr/local/include`), use the `install` target: +### Installation ```bash -# First, build the project -cmake --build build - -# Then, install the header +# System-wide installation sudo cmake --install build + +# Or just copy the header +cp CatalystCX.hpp /your/project/include/ ``` ## API Usage Guide ### Basic Execution -To execute a command and wait for it to complete, use the `Status()` method. It returns a `CommandResult` struct. - ```cpp -#include "CatalystCX.hpp" +#include #include int main() { @@ -70,8 +95,6 @@ int main() { ### Asynchronous Execution -To spawn a process without blocking, use the `Spawn()` method. This returns an `std::optional`. You can `Wait()` for the result later. - ```cpp if (auto child = Command("sleep").Arg("5").Spawn()) { std::cout << "Process spawned with PID: " << child->GetPid() << std::endl; @@ -79,68 +102,150 @@ if (auto child = Command("sleep").Arg("5").Spawn()) { // ... do other work ... CommandResult result = child->Wait(); - std::cout << "Sleep command finished." - << std::endl; + std::cout << "Sleep command finished." << std::endl; } else { - std::cerr << "Failed to spawn process." - << std::endl; + std::cerr << "Failed to spawn process." << std::endl; } ``` ### Timeouts -Set a timeout for a command using the `Timeout()` method. The `CommandResult` will indicate if the command timed out. - ```cpp auto result = Command("ping").Arg("8.8.8.8") .Timeout(std::chrono::seconds(2)) .Status(); if (result.TimedOut) { - std::cout << "Command timed out!" - << std::endl; + std::cout << "Command timed out!" << std::endl; } ``` -### Error Handling +### Environment Variables and Working Directory -`Spawn()` returns a `std::optional`. Always check if it contains a value before using it. +```cpp +auto result = Command("printenv") + .Arg("MY_VAR") + .Environment("MY_VAR", "Hello") + .WorkingDirectory("/tmp") + .Status(); + +std::cout << result.Stdout; // "Hello\n" +``` + +### Signal Handling and Process Information ```cpp -if (auto child = Command("this-command-does-not-exist").Spawn()) { - // This block will not be executed - child->Wait(); -} else { - std::cerr << "Failed to spawn command, as expected." - << std::endl; +auto child = Command("sleep").Arg("10").Spawn(); +if (child) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + child->Kill(SIGTERM); + + auto result = child->Wait(); + if (result.KilledBySignal) { + std::cout << "Process killed by signal: " + << SignalInfo::GetSignalName(result.TerminatingSignal) + << std::endl; + } + + // Get human-readable process info + std::cout << SignalInfo::GetProcessInfo(result) << std::endl; } ``` -### Accessing Detailed Results - -The `CommandResult` struct contains detailed information about the execution. +### Resource Monitoring ```cpp -CommandResult result = Command("your-command").Status(); +CommandResult result = Command("your-intensive-command").Status(); std::cout << "Exit Code: " << result.ExitCode << std::endl; -std::cout << "Execution Time: " << result.ExecutionTime.count() << "s" - << std::endl; +std::cout << "Execution Time: " << result.ExecutionTime.count() << "s" << std::endl; #ifdef __linux__ - std::cout << "User CPU Time: " << result.Usage.UserCpuTime << "us" - << std::endl; - std::cout << "System CPU Time: " << result.Usage.SystemCpuTime << "us" - << std::endl; - std::cout << "Max Memory Usage: " << result.Usage.MaxResidentSetSize << " KB" - << std::endl; +std::cout << "CPU Usage:\n"; +std::cout << " User: " << result.Usage.UserCpuTime << "μs\n"; +std::cout << " System: " << result.Usage.SystemCpuTime << "μs\n"; +std::cout << "Memory:\n"; +std::cout << " Peak RSS: " << result.Usage.MaxResidentSetSize << " KB\n"; +std::cout << " Page Faults: " << result.Usage.MajorPageFaults << std::endl; +#elif defined(_WIN32) +std::cout << "Peak Memory: " << result.Usage.PeakWorkingSetSize << " bytes\n"; +std::cout << "Page Faults: " << result.Usage.PageFaultCount << std::endl; #endif ``` +## Advanced Usage + +### Batch Processing + +```cpp +std::vector files = {"file1.txt", "file2.txt", "file3.txt"}; +std::vector> futures; + +for (const auto& file : files) { + futures.push_back(std::async(std::launch::async, [&file]() { + return Command("wc").Args({"-l", file}).Status(); + })); +} + +for (auto& future : futures) { + auto result = future.get(); + std::cout << "Lines: " << result.Stdout; +} +``` + +### Error Recovery + +```cpp +CommandResult result; +int retries = 3; + +while (retries-- > 0) { + result = Command("flaky-command") + .Timeout(std::chrono::seconds(30)) + .Status(); + + if (result.ExitCode == 0) break; + + std::cerr << "Attempt failed, retries left: " << retries << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(1)); +} +``` + +## Testing + +```bash +# Run full test suite +cmake --build build --target test + +# Run with sanitizers +cmake -B build-debug -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" +cmake --build build-debug --target test + +# Generate coverage report +cmake -B build-coverage -DCMAKE_CXX_FLAGS="--coverage" +cmake --build build-coverage --target test +lcov --capture --directory build-coverage --output-file coverage.info +``` + + +### Development Setup + +```bash +# Install development dependencies +sudo apt-get install cppcheck clang-tidy valgrind lcov + +# Run static analysis +cppcheck --enable=all --std=c++20 CatalystCX.hpp +clang-tidy CatalystCX.cpp -checks='*' +``` + ## Contributing -Contributions are welcome! Please feel free to submit a pull request. +Contributions are welcome! Please feel free to submit a pull request, open an issue, or contact me directly. :) ## License -This project is licensed under the GPLv3 License. +This project is licensed under the [GPLv3 License](LICENSE) - see the LICENSE file for details. + +---