diff --git a/CMakeLists.txt b/CMakeLists.txt index fe6d9f5..d1520b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,11 @@ cmake_minimum_required(VERSION 3.10) project(CatalystCX) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Compiler flags -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic -Wall -Wextra -O2") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -pedantic -Wall -Wextra -O2") # Test suite executable add_executable(cxtest Evaluate.cpp) diff --git a/CatalystCX.hpp b/CatalystCX.hpp index a049f70..66a6acd 100644 --- a/CatalystCX.hpp +++ b/CatalystCX.hpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -27,8 +29,8 @@ #include #include #include +#include #include -#include #ifdef _WIN32 #include @@ -48,6 +50,11 @@ extern char **environ; #endif #endif +#define CatalystCX_VERSION "0.0.1" +#define CatalystCX_VERSION_MAJOR 0 +#define CatalystCX_VERSION_MINOR 0 +#define CatalystCX_VERSION_PATCH 1 + namespace fs = std::filesystem; // Constants @@ -70,6 +77,252 @@ namespace Concepts { }; } +// Error Handling System +namespace Errors { + enum class ErrorCategory : uint8_t { + None = 0, + Validation, // Invalid arguments, missing executable, etc. + System, // System call failures (pipe, fork, etc.) + Process, // Process-related errors (spawn failure, etc.) + Timeout, // Timeout-related errors + Permission, // Permission-related errors + Resource, // Resource exhaustion (memory, file descriptors, etc.) + Platform // Platform-specific errors + }; + + enum class ErrorCode : uint16_t { + // Success + Success = 0, + + // Validation Errors (100-199) + EmptyCommand = 100, + ExecutableNotFound = 101, + InvalidArguments = 102, + InvalidWorkingDirectory = 103, + + // System Errors (200-299) + PipeCreationFailed = 200, + ForkFailed = 201, + ExecFailed = 202, + EnvironmentSetupFailed = 203, + FileDescriptorError = 204, + + // Process Errors (300-399) + SpawnFailed = 300, + ProcessAlreadyFinished = 301, + ProcessNotFound = 302, + KillFailed = 303, + WaitFailed = 304, + + // Timeout Errors (400-499) + ExecutionTimeout = 400, + WaitTimeout = 401, + + // Permission Errors (500-599) + ExecutePermissionDenied = 500, + DirectoryAccessDenied = 501, + + // Resource Errors (600-699) + OutOfMemory = 600, + TooManyOpenFiles = 601, + ProcessLimitReached = 602, + + // Platform Errors (700-799) + WindowsApiError = 700, + PosixError = 701, + MacOSError = 702, + + // Unknown + Unknown = 999 + }; + + struct ErrorInfo { + ErrorCode Code = ErrorCode::Success; + ErrorCategory Category = ErrorCategory::None; + std::string Message; + std::string Details; // Platform-specific details + std::string Suggestion; // Recovery suggestion + int SystemErrorCode = 0; // Platform errno/GetLastError() + + [[nodiscard]] constexpr bool IsSuccess() const noexcept { + return Code == ErrorCode::Success; + } + + [[nodiscard]] constexpr bool IsFailure() const noexcept { + return !IsSuccess(); + } + + [[nodiscard]] std::string FullMessage() const { + std::string full = Message; + if (!Details.empty()) { + full += " (Details: " + Details + ")"; + } + if (!Suggestion.empty()) { + full += " Suggestion: " + Suggestion; + } + if (SystemErrorCode != 0) { + full += " [System Error: " + std::to_string(SystemErrorCode) + "]"; + } + return full; + } + }; + + // Result-like type for better error handling + template + class Result { + std::variant data_; + public: + constexpr explicit Result(T&& value) noexcept : data_(std::move(value)) {} + constexpr explicit Result(const T& value) : data_(value) {} + constexpr explicit Result(ErrorInfo error) noexcept : data_(std::move(error)) {} + + [[nodiscard]] constexpr bool IsOk() const noexcept { + return std::holds_alternative(data_); + } + + [[nodiscard]] constexpr bool IsError() const noexcept { + return !IsOk(); + } + + [[nodiscard]] constexpr const T& Value() const& { + if (IsError()) { + throw std::runtime_error("Attempted to access value of error result: " + Error().FullMessage()); + } + return std::get(data_); + } + + [[nodiscard]] constexpr T&& Value() && { + if (IsError()) { + throw std::runtime_error("Attempted to access value of error result: " + Error().FullMessage()); + } + return std::get(std::move(data_)); + } + + [[nodiscard]] constexpr const ErrorInfo& Error() const& { + if (IsOk()) { + static const ErrorInfo success{}; + return success; + } + return std::get(data_); + } + + [[nodiscard]] constexpr T ValueOr(T&& default_value) const& { + return IsOk() ? Value() : std::move(default_value); + } + + // Monadic operations + template + [[nodiscard]] constexpr auto Map(F&& func) const& -> Result> { + if (IsError()) { + return Error(); + } + return func(Value()); + } + + template + [[nodiscard]] constexpr auto AndThen(F&& func) const& -> std::invoke_result_t { + if (IsError()) { + return Error(); + } + return func(Value()); + } + }; + + // Specialization for void operations + template<> + class Result { + std::optional error_; + + public: + constexpr Result() noexcept : error_(std::nullopt) {} + constexpr explicit Result(ErrorInfo error) noexcept : error_(std::move(error)) {} + + [[nodiscard]] constexpr bool IsOk() const noexcept { + return !error_.has_value(); + } + + [[nodiscard]] constexpr bool IsError() const noexcept { + return error_.has_value(); + } + + [[nodiscard]] constexpr const ErrorInfo& Error() const& { + if (IsOk()) { + static const ErrorInfo success{}; + return success; + } + return *error_; + } + + constexpr void Value() const { + if (IsError()) throw std::runtime_error("Attempted to access value of error result: " + Error().FullMessage()); + + } + + template + [[nodiscard]] constexpr auto AndThen(F&& func) const -> std::invoke_result_t { + if (IsError()) return Error(); + return func(); + } + }; + + // Error creation helpers + [[nodiscard]] inline ErrorInfo MakeError(ErrorCode code, std::string message, + std::string details = "", std::string suggestion = "") { + ErrorInfo error; + error.Code = code; + error.Message = std::move(message); + error.Details = std::move(details); + error.Suggestion = std::move(suggestion); + + // Determine category from code + if (const auto code_val = static_cast(code); code_val >= 100 && code_val < 200) error.Category = ErrorCategory::Validation; + else if (code_val >= 200 && code_val < 300) error.Category = ErrorCategory::System; + else if (code_val >= 300 && code_val < 400) error.Category = ErrorCategory::Process; + else if (code_val >= 400 && code_val < 500) error.Category = ErrorCategory::Timeout; + else if (code_val >= 500 && code_val < 600) error.Category = ErrorCategory::Permission; + else if (code_val >= 600 && code_val < 700) error.Category = ErrorCategory::Resource; + else if (code_val >= 700 && code_val < 800) error.Category = ErrorCategory::Platform; + +#ifdef _WIN32 + error.SystemErrorCode = static_cast(GetLastError()); +#else + error.SystemErrorCode = errno; +#endif + + return error; + } + + [[nodiscard]] inline ErrorInfo MakeSystemError(const ErrorCode code, std::string message) { + std::string details; + std::string suggestion; + +#ifdef _WIN32 + const DWORD error_code = GetLastError(); + LPSTR message_buffer = nullptr; + const size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message_buffer), 0, nullptr); + if (message_buffer) { + details = std::string(message_buffer, size); + LocalFree(message_buffer); + } +#else + const int err = errno; // snapshot + details = std::strerror(err); + switch (err) { + case EACCES: suggestion = "Check file permissions and executable bit"; break; + case ENOENT: suggestion = "Verify the executable path exists and is in PATH"; break; + case ENOMEM: suggestion = "Free up system memory or increase limits"; break; + case EMFILE: suggestion = "Close unused file descriptors or increase ulimits"; break; + case EAGAIN: suggestion = "Retry the operation or check system process limits"; break; + default: break; + } +#endif + + return MakeError(code, std::move(message), std::move(details), std::move(suggestion)); + } +} + struct CommandResult { int ExitCode{}; std::string Stdout; @@ -83,6 +336,9 @@ struct CommandResult { bool Stopped = false; int StopSignal = 0; + // Enhanced error information + Errors::ErrorInfo ExecutionError{}; // Details about any execution issues + struct ResourceUsage { #if defined(__linux__) long UserCpuTime = 0; @@ -101,12 +357,32 @@ struct CommandResult { } Usage{}; [[nodiscard]] constexpr bool IsSuccessful() const noexcept { - return ExitCode == 0 && !TimedOut && !KilledBySignal; + return ExitCode == 0 && !TimedOut && !KilledBySignal && ExecutionError.IsSuccess(); } [[nodiscard]] constexpr bool HasOutput() const noexcept { return !Stdout.empty() || !Stderr.empty(); } + + [[nodiscard]] constexpr bool HasExecutionError() const noexcept { + return ExecutionError.IsFailure(); + } + + [[nodiscard]] std::string GetErrorSummary() const { + if (ExecutionError.IsFailure()) { + return ExecutionError.FullMessage(); + } + if (TimedOut) { + return "Process execution timed out"; + } + if (KilledBySignal) { + return "Process was killed by signal " + std::to_string(TerminatingSignal); + } + if (ExitCode != 0) { + return "Process exited with non-zero code: " + std::to_string(ExitCode); + } + return "No errors"; + } }; class Child { @@ -130,13 +406,13 @@ class Child { : ProcessId(pid), StdoutFd(stdout_fd), StderrFd(stderr_fd) {} #endif - [[nodiscard]] CommandResult Wait(std::optional> timeout = std::nullopt) const; + [[nodiscard]] Errors::Result Wait(std::optional> timeout = std::nullopt) const; [[nodiscard]] pid_t GetPid() const { return ProcessId; } #ifdef _WIN32 - void Kill(int signal = 0) const; + [[nodiscard]] Errors::Result Kill(int signal = 0) const; #else - void Kill(int signal = SIGTERM) const; + [[nodiscard]] Errors::Result Kill(int signal = SIGTERM) const; #endif private: @@ -195,8 +471,8 @@ class Command { return *this; } - [[nodiscard]] CommandResult Execute(); - [[nodiscard]] std::optional Spawn(); + [[nodiscard]] Errors::Result Execute(); + [[nodiscard]] Errors::Result Spawn(); private: std::string Executable; @@ -351,8 +627,8 @@ class ExecutionValidator { [cmd_view](const auto& dir_range) { const std::string_view dir{dir_range.begin(), dir_range.end()}; if (dir.empty()) return false; - const auto full_path = (std::filesystem::path(dir) / std::string(cmd_view)); - return ::access(full_path.c_str(), X_OK) == 0; + const auto full_path = std::filesystem::path(dir) / std::string(cmd_view); + return access(full_path.c_str(), X_OK) == 0; }); #endif @@ -367,7 +643,7 @@ class ExecutionValidator { // Windows implementations #ifdef _WIN32 -inline CommandResult Child::Wait(std::optional> timeout) const { +inline Errors::Result Child::Wait(std::optional> timeout) const { const auto start_time = std::chrono::steady_clock::now(); CommandResult result; @@ -412,32 +688,55 @@ inline CommandResult Child::Wait(std::optional> ti const auto end_time = std::chrono::steady_clock::now(); result.ExecutionTime = end_time - start_time; - return result; + return Errors::Result(result); } -inline void Child::Kill(int) const { - TerminateProcess(ProcessHandle, 1); +inline Errors::Result Child::Kill(int) const { + if (!TerminateProcess(ProcessHandle, 1)) { + return Errors::Result( + Errors::MakeSystemError(Errors::ErrorCode::KillFailed, "Failed to terminate process")); + } + return {}; } -inline std::optional Command::Spawn() { +inline Errors::Result Command::Spawn() { + // Validate command + if (Executable.empty()) { + return Errors::Result(Errors::MakeError(Errors::ErrorCode::EmptyCommand, + "Command executable cannot be empty", + "", "Provide a valid executable name or path")); + } + 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; + return Errors::Result(Errors::MakeError(Errors::ErrorCode::ExecutableNotFound, + "Executable not found: " + Executable, "", + "Verify the executable exists and is in PATH")); + } + + // Validate working directory if specified + if (WorkDir && !fs::exists(*WorkDir)) { + return Errors::Result(Errors::MakeError(Errors::ErrorCode::InvalidWorkingDirectory, + "Working directory does not exist: " + *WorkDir, "", + "Create the directory or specify a valid path")); } 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)) { - return std::nullopt; + return Errors::Result( + Errors::MakeSystemError(Errors::ErrorCode::PipeCreationFailed, "Failed to create stdout pipe")); } + if (!CreatePipe(&stderr_read, &stderr_write, &sa, 0)) { CloseHandle(stdout_read); CloseHandle(stdout_write); - return std::nullopt; + return Errors::Result( + Errors::MakeSystemError(Errors::ErrorCode::PipeCreationFailed, "Failed to create stderr pipe")); } SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0); @@ -514,10 +813,31 @@ inline std::optional Command::Spawn() { if (!success) { CloseHandle(stdout_read); CloseHandle(stderr_read); - return std::nullopt; + + const DWORD error_code = GetLastError(); + std::string suggestion; + switch (error_code) { + case ERROR_FILE_NOT_FOUND: + suggestion = "Verify the executable path exists"; + break; + case ERROR_ACCESS_DENIED: + suggestion = "Check file permissions and security settings"; + break; + case ERROR_NOT_ENOUGH_MEMORY: + suggestion = "Free up system memory"; + break; + default: + suggestion = "Check Windows event logs for more details"; + break; + } + + return Errors::Result(Errors::MakeError(Errors::ErrorCode::SpawnFailed, + "Failed to create process: " + Executable, + "CreateProcessA failed with error " + std::to_string(error_code), + suggestion)); } - return Child(pi.hProcess, pi.hThread, stdout_read, stderr_read); + return Errors::Result(Child(pi.hProcess, pi.hThread, stdout_read, stderr_read)); } inline std::pair AsyncPipeReader::ReadPipes(HANDLE stdout_handle, HANDLE stderr_handle) { @@ -559,7 +879,7 @@ inline bool AsyncPipeReader::ReadFromPipe(PipeData& pipe_data, Buffer& buffer) n #else // Unix implementations -inline CommandResult Child::Wait(std::optional> timeout) const { +inline Errors::Result Child::Wait(std::optional> timeout) const { const auto start_time = std::chrono::steady_clock::now(); CommandResult result; @@ -574,7 +894,8 @@ inline CommandResult Child::Wait(std::optional> ti 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 + // Child finished; collect final status + waitpid(ProcessId, &status, 0); break; // Process finished } @@ -590,15 +911,18 @@ inline CommandResult Child::Wait(std::optional> ti // Check if we timed out if (std::chrono::steady_clock::now() >= timeout_time) { if (const int wait_result = waitpid(ProcessId, &status, WNOHANG); wait_result == 0) { // Still running - Kill(); + static_cast(Kill()); result.TimedOut = true; - wait4(ProcessId, &status, 0, &usage); + // Collect final status after terminating + waitpid(ProcessId, &status, 0); } else if (wait_result == ProcessId) { - wait4(ProcessId, &status, 0, &usage); + // Already finished; ensure status is reaped + waitpid(ProcessId, &status, 0); } } } else { - wait4(ProcessId, &status, 0, &usage); + // Blocking wait for child process + waitpid(ProcessId, &status, 0); } // Collect outputs (reader finishes when pipes close) @@ -609,6 +933,8 @@ inline CommandResult Child::Wait(std::optional> ti close(StdoutFd); close(StderrFd); + // Populate resource usage after child has terminated + getrusage(RUSAGE_CHILDREN, &usage); #ifdef __linux__ constexpr long MICROSECONDS_PER_SECOND = 1000000; result.Usage.UserCpuTime = usage.ru_utime.tv_sec * MICROSECONDS_PER_SECOND + usage.ru_utime.tv_usec; @@ -628,8 +954,12 @@ inline CommandResult Child::Wait(std::optional> ti result.KilledBySignal = true; result.TerminatingSignal = WTERMSIG(status); result.ExitCode = 128 + result.TerminatingSignal; -#ifdef WCOREDUMP +#if defined(WCOREFLAG) + result.CoreDumped = (status & WCOREFLAG) != 0; +#elif defined(WCOREDUMP) result.CoreDumped = WCOREDUMP(status); +#else + result.CoreDumped = false; #endif } else if (WIFSTOPPED(status)) { result.Stopped = true; @@ -640,25 +970,39 @@ inline CommandResult Child::Wait(std::optional> ti const auto end_time = std::chrono::steady_clock::now(); result.ExecutionTime = end_time - start_time; - return result; + return Errors::Result(std::move(result)); } -inline void Child::Kill(const int signal) const { - kill(ProcessId, signal); +inline Errors::Result Child::Kill(const int signal) const { + if (kill(ProcessId, signal) == -1) { + return Errors::Result( + Errors::MakeSystemError(Errors::ErrorCode::KillFailed, + "Failed to send signal " + std::to_string(signal) + + " to process " + std::to_string(ProcessId))); + } + return {}; } -inline std::optional Command::Spawn() { +inline Errors::Result Command::Spawn() { + if (Executable.empty()) { + return Errors::Result(Errors::MakeError(Errors::ErrorCode::EmptyCommand, + "Command executable cannot be empty", "", + "Provide a valid executable name or path")); + } 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; + return Errors::Result(Errors::MakeError(Errors::ErrorCode::ExecutableNotFound, + "Executable not found: " + Executable, "", + "Verify the executable exists and is in PATH")); } int stdout_pipe[2], stderr_pipe[2]; if (pipe(stdout_pipe) == -1 || pipe(stderr_pipe) == -1) { - return std::nullopt; + return Errors::Result( + Errors::MakeSystemError(Errors::ErrorCode::PipeCreationFailed, "Failed to create stdout pipe")); } #ifdef __APPLE__ @@ -669,7 +1013,8 @@ inline std::optional Command::Spawn() { posix_spawnattr_init(&attr) != 0) { close(stdout_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[0]); close(stderr_pipe[1]); - return std::nullopt; + return Errors::MakeSystemError(Errors::ErrorCode::SpawnFailed, + "Failed to initialize posix_spawn attributes/actions"); } posix_spawn_file_actions_adddup2(&file_actions, stdout_pipe[1], STDOUT_FILENO); @@ -718,14 +1063,18 @@ inline std::optional Command::Spawn() { if (result != 0) { close(stdout_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[0]); close(stderr_pipe[1]); - return std::nullopt; + return Errors::MakeError(Errors::ErrorCode::SpawnFailed, + "posix_spawnp failed for: " + Executable, + std::string(strerror(result)), + "Verify the executable, PATH, and permissions"); } #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]); - return std::nullopt; + return Errors::Result( + Errors::MakeSystemError(Errors::ErrorCode::ForkFailed, "fork() failed")); } if (pid == 0) { @@ -759,7 +1108,7 @@ inline std::optional Command::Spawn() { close(stdout_pipe[1]); close(stderr_pipe[1]); - return Child(pid, stdout_pipe[0], stderr_pipe[0]); + return Errors::Result(Child(pid, stdout_pipe[0], stderr_pipe[0])); } // Implementation of AsyncPipeReader @@ -819,14 +1168,33 @@ inline bool AsyncPipeReader::IsPipeOpen(const int fd) noexcept { #endif -inline CommandResult Command::Execute() { - if (const auto child = Spawn()) { - return child->Wait(TimeoutDuration); +inline Errors::Result Command::Execute() { + auto spawn_result = Spawn(); + if (spawn_result.IsError()) { + // Convert spawn error to CommandResult with error info + CommandResult result; + result.ExitCode = Constants::EXIT_FAIL_EC; + result.ExecutionError = spawn_result.Error(); + result.Stderr = spawn_result.Error().FullMessage(); + return Errors::Result(std::move(result)); } - CommandResult result; - result.ExitCode = Constants::EXIT_FAIL_EC; - result.Stderr = "Failed to spawn process"; - return result; + + auto wait_result = spawn_result.Value().Wait(TimeoutDuration); + if (wait_result.IsError()) { + return Errors::Result(wait_result.Error()); // Propagate wait error + } + + auto result = wait_result.Value(); + if (result.TimedOut) { + result.ExecutionError = Errors::MakeError( + Errors::ErrorCode::ExecutionTimeout, + "Process execution timed out", + "Process exceeded the specified timeout duration", + "Consider increasing the timeout or optimizing the command" + ); + } + + return Errors::Result(std::move(result)); } class SignalInfo { diff --git a/Evaluate.cpp b/Evaluate.cpp index f50d3c9..c7135e9 100644 --- a/Evaluate.cpp +++ b/Evaluate.cpp @@ -40,34 +40,50 @@ void TestBasicExecution(TestRunner& runner) { std::cout << "\n=== Basic Execution Tests ===" << std::endl; // Test simple command - auto result = Command("echo").Arg("hello").Execute(); - runner.Assert(result.ExitCode == 0, "Echo command success"); - runner.Assert(result.Stdout.find("hello") != std::string::npos, "Echo output correct"); - + auto execute_result = Command("echo").Arg("hello").Execute(); + runner.Assert(execute_result.IsOk(), "Echo command executed successfully"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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(Utils::Expand({"hello", "world"})).Execute(); - runner.Assert(result.ExitCode == 0, "Multiple args success"); - runner.Assert(result.Stdout.find("hello world") != std::string::npos, "Multiple args output"); - + execute_result = Command("echo").Args(Utils::Expand({"hello", "world"})).Execute(); + runner.Assert(execute_result.IsOk(), "Multiple args command executed"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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").Execute(); - runner.Assert(result.ExitCode == 1, "False command exit code"); + execute_result = Command("false").Execute(); + runner.Assert(execute_result.IsOk(), "False command spawned (even though it fails)"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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 - const auto child = Command("sleep").Arg("1").Spawn(); - runner.Assert(child.has_value(), "Sleep spawn success"); - - if (child) { - const pid_t pid = child->GetPid(); + const auto spawn_result = Command("sleep").Arg("1").Spawn(); + runner.Assert(spawn_result.IsOk(), "Sleep spawn success"); + + if (spawn_result.IsOk()) { + const auto& child = spawn_result.Value(); + const pid_t pid = child.GetPid(); runner.Assert(pid > 0, "Valid PID returned"); - const auto result = child->Wait(); - runner.Assert(result.ExitCode == 0, "Sleep completed successfully"); - runner.Assert(result.ExecutionTime.count() >= 1.0, "Sleep duration correct"); + const auto result = child.Wait(); + runner.Assert(result.Value().ExitCode == 0, "Sleep completed successfully"); + runner.Assert(result.Value().ExecutionTime.count() >= 1.0, "Sleep duration correct"); } } @@ -76,73 +92,103 @@ void TestTimeout(TestRunner& runner) { // Test timeout functionality const auto start = std::chrono::steady_clock::now(); - const auto result = Command("sleep").Arg("5").Timeout(std::chrono::seconds(1)).Execute(); + const auto execute_result = Command("sleep").Arg("5").Timeout(std::chrono::seconds(1)).Execute(); const 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"); + + runner.Assert(execute_result.IsOk(), "Timeout command executed"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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 - if (const auto child = Command("sleep").Arg("10").Spawn()) { + if (const auto spawn_result = Command("sleep").Arg("10").Spawn(); spawn_result.IsOk()) { + const auto& child = spawn_result.Value(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - child->Kill(SIGTERM); - const 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"); + static_cast(child.Kill(SIGTERM)); + const auto result = child.Wait(); + + runner.Assert(result.Value().KilledBySignal, "Process killed by signal"); + runner.Assert(result.Value().TerminatingSignal == SIGTERM, "Correct terminating signal"); + runner.Assert(result.Value().ExitCode == 128 + SIGTERM, "Correct exit code for signal"); + } else { + runner.Assert(false, "Failed to spawn sleep for signal test"); } - +#ifdef __linux__ // 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"); +#endif } void TestEnvironmentVariables(TestRunner& runner) { std::cout << "\n=== Environment Variable Tests ===" << std::endl; - const auto result = Command("printenv").Arg("TEST_VAR") + const auto execute_result = Command("printenv").Arg("TEST_VAR") .Environment("TEST_VAR", "test_value") .Execute(); - - runner.Assert(result.ExitCode == 0, "Environment variable set"); - runner.Assert(result.Stdout.find("test_value") != std::string::npos, "Environment variable value"); + + runner.Assert(execute_result.IsOk(), "Environment variable command executed"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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; - const auto result = Command("pwd").WorkingDirectory("/tmp").Execute(); - runner.Assert(result.ExitCode == 0, "Working directory command success"); - runner.Assert(result.Stdout.find("/tmp") != std::string::npos, "Working directory set correctly"); + const auto execute_result = Command("pwd").WorkingDirectory("/tmp").Execute(); + runner.Assert(execute_result.IsOk(), "Working directory command executed"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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 - const auto child = Command("nonexistentcommand12345").Spawn(); - runner.Assert(!child.has_value(), "Non-existent command fails to spawn"); - + const auto spawn_result = Command("nonexistentcommand12345").Spawn(); + runner.Assert(spawn_result.IsError(), "Non-existent command fails to spawn"); + + if (spawn_result.IsError()) { + std::cout << "Expected error: " << spawn_result.Error().FullMessage() << std::endl; + } + // Test command that writes to stderr - const auto result = Command("sh").Args(Utils::Expand({"-c", "echo error >&2; exit 42"})).Execute(); - runner.Assert(result.ExitCode == 42, "Custom exit code preserved"); - runner.Assert(result.Stderr.find("error") != std::string::npos, "Stderr captured"); + const auto execute_result = Command("sh").Args(Utils::Expand({"-c", "echo error >&2; exit 42"})).Execute(); + runner.Assert(execute_result.IsOk(), "Stderr command executed"); + + if (execute_result.IsOk()) { + const auto& result = execute_result.Value(); + 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__ - const auto result = Command("dd").Args(Utils::Expand({"if=/dev/zero", "of=/dev/null", "bs=1M", "count=10"})).Execute(); - 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"); + if (const auto result = Command("dd").Args(Utils::Expand({"if=/dev/zero", "of=/dev/null", "bs=1M", "count=10"})).Execute(); result.IsOk()) { + const auto& r = result.Value(); + runner.Assert(r.ExitCode == 0, "DD command success"); + runner.Assert(r.Usage.UserCpuTime >= 0, "User CPU time recorded"); + runner.Assert(r.Usage.SystemCpuTime >= 0, "System CPU time recorded"); + runner.Assert(r.Usage.MaxResidentSetSize > 0, "Memory usage recorded"); + } #else std::cout << "[SKIP] Resource usage tests (Linux only)" << std::endl; #endif @@ -150,16 +196,22 @@ void TestResourceUsage(TestRunner& runner) { void TestPipeHandling(TestRunner& runner) { std::cout << "\n=== Pipe Handling Tests ===" << std::endl; - - // Test large output auto result = Command("seq").Args(Utils::Expand({"1", "1000"})).Execute(); - runner.Assert(result.ExitCode == 0, "Large output command success"); - runner.Assert(result.Stdout.find("1000") != std::string::npos, "Large output captured"); - + // Test large output + if (result.IsOk()) { + const auto& r = result.Value(); + runner.Assert(r.ExitCode == 0, "Seq command success"); + runner.Assert(r.Stdout.find("1000") != std::string::npos, "Large output captured"); + } + // Test mixed stdout/stderr result = Command("sh").Args(Utils::Expand({"-c", "echo stdout; echo stderr >&2"})).Execute(); - runner.Assert(result.Stdout.find("stdout") != std::string::npos, "Stdout separated"); - runner.Assert(result.Stderr.find("stderr") != std::string::npos, "Stderr separated"); + if (result.IsOk()) { + const auto& r = result.Value(); + runner.Assert(r.Stdout.find("stdout") != std::string::npos, "Stdout captured"); + runner.Assert(r.Stderr.find("stderr") != std::string::npos, "Stderr captured"); + } + } void TestExecutionValidator(TestRunner& runner) { @@ -181,12 +233,12 @@ void TestProcessInfo(TestRunner& runner) { // Test normal exit auto result = Command("true").Execute(); - std::string info = SignalInfo::GetProcessInfo(result); + std::string info = SignalInfo::GetProcessInfo(result.Value()); 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)).Execute(); - info = SignalInfo::GetProcessInfo(result); + info = SignalInfo::GetProcessInfo(result.Value()); runner.Assert(info.find("timed out") != std::string::npos, "Timeout info"); } @@ -199,11 +251,11 @@ void TestEdgeCases(TestRunner& runner) { // Command with spaces in args auto result = Command("echo").Arg("hello world").Execute(); - runner.Assert(result.Stdout.find("hello world") != std::string::npos, "Spaces in args handled"); + runner.Assert(result.Value().Stdout.find("hello world") != std::string::npos, "Spaces in args handled"); // Very short timeout result = Command("sleep").Arg("1").Timeout(std::chrono::milliseconds(1)).Execute(); - runner.Assert(result.TimedOut, "Very short timeout works"); + runner.Assert(result.Value().TimedOut, "Very short timeout works"); } #ifndef _WIN32 @@ -275,20 +327,35 @@ void TestLargeStdoutStderr(TestRunner& runner) { std::cout << "\n=== Large Stdout/Stderr Tests ===" << std::endl; // Large stdout (~5MB) - auto res = Command("sh").Args(Utils::Expand({"-c", "dd if=/dev/zero bs=1M count=5 2>/dev/null"})).Execute(); - runner.Assert(res.ExitCode == 0, "Large stdout command success"); - runner.Assert(res.Stdout.size() >= 5 * 1024 * 1024, "Large stdout captured without deadlock"); + auto execute_result = Command("sh").Args(Utils::Expand({"-c", "dd if=/dev/zero bs=1M count=5 2>/dev/null"})).Execute(); + runner.Assert(execute_result.IsOk(), "Large stdout command executed"); + + if (execute_result.IsOk()) { + const auto& res = execute_result.Value(); + runner.Assert(res.ExitCode == 0, "Large stdout command success"); + runner.Assert(res.Stdout.size() >= 5 * 1024 * 1024, "Large stdout captured without deadlock"); + } // Large stderr (~5MB zeros) - res = Command("sh").Args(Utils::Expand({"-c", "dd if=/dev/zero of=/dev/stderr bs=1M count=5 1>/dev/null"})).Execute(); - runner.Assert(res.ExitCode == 0, "Large stderr command success"); - runner.Assert(res.Stderr.size() >= 5 * 1024 * 1024, "Large stderr captured without deadlock"); + execute_result = Command("sh").Args(Utils::Expand({"-c", "dd if=/dev/zero of=/dev/stderr bs=1M count=5 1>/dev/null"})).Execute(); + runner.Assert(execute_result.IsOk(), "Large stderr command executed"); + + if (execute_result.IsOk()) { + const auto& res = execute_result.Value(); + runner.Assert(res.ExitCode == 0, "Large stderr command success"); + runner.Assert(res.Stderr.size() >= 5 * 1024 * 1024, "Large stderr captured without deadlock"); + } // Interleaved stdout and stderr - res = Command("sh").Args(Utils::Expand({"-c", "for i in $(seq 1 2000); do echo outline; echo errline >&2; done"})).Execute(); - runner.Assert(res.ExitCode == 0, "Interleaved out/err success"); - runner.Assert(res.Stdout.find("outline") != std::string::npos, "Interleaved stdout captured"); - runner.Assert(res.Stderr.find("errline") != std::string::npos, "Interleaved stderr captured"); + execute_result = Command("sh").Args(Utils::Expand({"-c", "for i in $(seq 1 2000); do echo outline; echo errline >&2; done"})).Execute(); + runner.Assert(execute_result.IsOk(), "Interleaved command executed"); + + if (execute_result.IsOk()) { + const auto& res = execute_result.Value(); + runner.Assert(res.ExitCode == 0, "Interleaved out/err success"); + runner.Assert(res.Stdout.find("outline") != std::string::npos, "Interleaved stdout captured"); + runner.Assert(res.Stderr.find("errline") != std::string::npos, "Interleaved stderr captured"); + } } void TestExecutionValidatorFilePermissions(TestRunner& runner) { @@ -319,16 +386,26 @@ void TestEnvMergingAndOverride(TestRunner& runner) { std::cout << "\n=== Environment Merge/Override Tests ===" << std::endl; // New variable should be visible - auto res = Command("sh").Args(Utils::Expand({"-c", "printf '%s' \"$NEW_VAR\""})) + auto execute_result = Command("sh").Args(Utils::Expand({"-c", "printf '%s' \"$NEW_VAR\""})) .Environment("NEW_VAR", "new_value").Execute(); - runner.Assert(res.ExitCode == 0, "New env var set"); - runner.Assert(res.Stdout == "new_value", "New env var value visible"); + runner.Assert(execute_result.IsOk(), "New env var command executed"); + + if (execute_result.IsOk()) { + const auto& res = execute_result.Value(); + runner.Assert(res.ExitCode == 0, "New env var set"); + runner.Assert(res.Stdout == "new_value", "New env var value visible"); + } // Override an env var for the child only // Use HOME which should exist; don't leak to parent - res = Command("sh").Args(Utils::Expand({"-c", "printf '%s' \"$HOME\""})) + execute_result = Command("sh").Args(Utils::Expand({"-c", "printf '%s' \"$HOME\""})) .Environment("HOME", "/tmp/testhome").Execute(); - runner.Assert(res.ExitCode == 0, "Override env var success"); - runner.Assert(res.Stdout == "/tmp/testhome", "Override value applied in child"); + runner.Assert(execute_result.IsOk(), "Override env var command executed"); + + if (execute_result.IsOk()) { + const auto& res = execute_result.Value(); + runner.Assert(res.ExitCode == 0, "Override env var success"); + runner.Assert(res.Stdout == "/tmp/testhome", "Override value applied in child"); + } } #endif // _WIN32 diff --git a/README.md b/README.md index 48b0210..580e685 100644 --- a/README.md +++ b/README.md @@ -93,15 +93,29 @@ int main() { ### Asynchronous Execution ```cpp -if (auto child = Command("sleep").Arg("5").Spawn()) { - std::cout << "Process spawned with PID: " << child->GetPid() << std::endl; +#include +#include + +int main() { + auto spawn = Command("sleep").Arg("5").Spawn(); // Errors::Result + if (spawn.IsError()) { + std::cerr << "Failed to spawn process: " << spawn.Error().FullMessage() << std::endl; + return 1; + } + + const Child& child = spawn.Value(); + std::cout << "Process spawned with PID: " << child.GetPid() << std::endl; // ... do other work ... - CommandResult result = child->Wait(); - std::cout << "Sleep command finished." << std::endl; -} else { - std::cerr << "Failed to spawn process." << std::endl; + auto wait = child.Wait(); // Errors::Result + if (wait.IsError()) { + std::cerr << "Wait failed: " << wait.Error().FullMessage() << std::endl; + return 1; + } + + const CommandResult& result = wait.Value(); + std::cout << "Sleep command finished. Exit: " << result.ExitCode << std::endl; } ``` @@ -132,18 +146,46 @@ std::cout << result.Stdout; // "Hello\n" ### Signal Handling and Process Information ```cpp -auto child = Command("sleep").Arg("10").Spawn(); -if (child) { +#include +#include +#include + +int main() { + auto spawn = Command("sleep").Arg("10").Spawn(); + if (spawn.IsError()) { + std::cerr << "Spawn failed: " << spawn.Error().FullMessage() << std::endl; + return 1; + } + Child child = spawn.Value(); + std::this_thread::sleep_for(std::chrono::seconds(1)); - child->Kill(SIGTERM); - - auto result = child->Wait(); + +#ifndef _WIN32 + auto killRes = child.Kill(SIGTERM); +#else + auto killRes = child.Kill(); // Windows ignores POSIX signals +#endif + if (killRes.IsError()) { + std::cerr << "Kill failed: " << killRes.Error().FullMessage() << std::endl; + } + + auto wait = child.Wait(); + if (wait.IsError()) { + std::cerr << "Wait failed: " << wait.Error().FullMessage() << std::endl; + return 1; + } + + const auto& result = wait.Value(); if (result.KilledBySignal) { - std::cout << "Process killed by signal: " - << SignalInfo::GetSignalName(result.TerminatingSignal) + std::cout << "Process killed by signal: " +#ifndef _WIN32 + << SignalInfo::GetSignalName(result.TerminatingSignal) +#else + << result.TerminatingSignal +#endif << std::endl; } - + // Get human-readable process info std::cout << SignalInfo::GetProcessInfo(result) << std::endl; } @@ -174,23 +216,28 @@ std::cout << "Page Faults: " << result.Usage.PageFaultCount << std::endl; ### Multiple Arguments ```cpp -std::vector args = {"arg1", "arg2"}; -cmd.Args(args); +Command cmd("myprog"); + +// std::vector +std::vector v = {"arg1", "arg2"}; +cmd.Args(v); -std::array args = {"arg1", "arg2", "arg3"}; -cmd.Args(args); +// std::array +std::array a = {"arg1", "arg2", "arg3"}; +cmd.Args(a); -std::initializer_list args = {"arg1", "arg2"}; -cmd.Args(args); +// initializer_list +cmd.Args({"arg1", "arg2"}); +// std::vector cmd.Args(std::vector{"arg1", "arg2"}); -// Or C-style arrays: -const char* args[] = {"arg1", "arg2"}; -cmd.Args(args); +// C-style array +const char* cargs[] = {"arg1", "arg2"}; +cmd.Args(cargs); -// Or use builtin expansion: -cmd.Args(utils::Expand({"arg1", "arg2"})); +// Built-in expansion helper +cmd.Args(Utils::Expand({"arg1", "arg2"})); ``` ### Batch Processing @@ -229,6 +276,70 @@ while (retries-- > 0) { } ``` +## Error Handling + +CatalystCX uses a lightweight result type for robust, explicit error handling. + +- Errors::Result: holds either a value T or an Errors::ErrorInfo. +- Errors::Result: holds success or an error. +- CommandResult also includes ExecutionError for issues that occurred during execution (e.g., timeouts). + +Examples: + +```cpp +// Spawning a process +auto spawn = Command("does-not-exist").Spawn(); +if (spawn.IsError()) { + const auto& e = spawn.Error(); + std::cerr << "Spawn failed: " << e.Message << "\nDetails: " << e.Details + << "\nSuggestion: " << e.Suggestion << std::endl; +} + +// Waiting on a child +auto spawn2 = Command("echo").Arg("hi").Spawn(); +if (spawn2.IsOk()) { + auto wait = spawn2.Value().Wait(); + if (wait.IsOk()) { + const auto& res = wait.Value(); + if (res.HasExecutionError()) { + std::cerr << res.ExecutionError.FullMessage() << std::endl; + } + } else { + std::cerr << wait.Error().FullMessage() << std::endl; + } +} + +// Inspecting CommandResult for summary +auto res = Command("sh").Arg("-c").Arg("exit 7").Execute(); +if (!res.IsSuccessful()) { + std::cerr << res.GetErrorSummary() << std::endl; +} +``` + +### Error Codes and Categories + +ErrorInfo contains: +- Code (ErrorCode): e.g., ExecutableNotFound, SpawnFailed, ExecutionTimeout +- Category (ErrorCategory): Validation, System, Process, Timeout, Permission, Resource, Platform +- Message, Details, Suggestion and SystemErrorCode + +## API Reference (Quick) + +- Command + - Arg(string), Args(range), Environment(key, value), WorkingDirectory(path), Timeout(duration) + - Execute() -> Errors::Result + - Spawn() -> Errors::Result +- Child + - GetPid() + - Wait([timeout]) -> Errors::Result + - Kill([signal]) -> Errors::Result +- CommandResult + - ExitCode, Stdout, Stderr, ExecutionTime, TimedOut + - Signal info: KilledBySignal, TerminatingSignal, CoreDumped, Stopped, StopSignal + - Usage: platform-specific resource usage fields + - ExecutionError: Errors::ErrorInfo + - helpers: IsSuccessful(), HasOutput(), HasExecutionError(), GetErrorSummary() + ## Testing ```bash @@ -264,6 +375,6 @@ Contributions are welcome! Please feel free to submit a pull request, open an is ## License -This project is licensed under the [GPLv3 License—](LICENSE)see the LICENSE file for details. +This project is licensed under the GPLv3. See the [LICENSE](LICENSE) file for details. ---