From d24c7f324685bfb44e343cd0c47f4f51b4d91328 Mon Sep 17 00:00:00 2001 From: dev0 Date: Wed, 26 Nov 2025 22:12:49 +0530 Subject: [PATCH] ProcessOps --- .gitignore | 2 +- CMakeLists.txt | 7 + Src/IACore/CMakeLists.txt | 4 + Src/IACore/imp/cpp/AsyncOps.cpp | 22 ++ Src/IACore/imp/cpp/FileOps.cpp | 0 Src/IACore/imp/cpp/Logger.cpp | 2 +- Src/IACore/imp/cpp/ProcessOps.cpp | 308 ++++++++++++++++++ Src/IACore/imp/cpp/SocketOps.cpp | 22 ++ Src/IACore/inc/IACore/AsyncOps.hpp | 24 ++ .../inc/IACore/{File.hpp => FileOps.hpp} | 0 Src/IACore/inc/IACore/PCH.hpp | 9 +- Src/IACore/inc/IACore/Process.hpp | 292 ----------------- Src/IACore/inc/IACore/ProcessOps.hpp | 64 ++++ Tests/CMakeLists.txt | 1 + Tests/Subjects/CMakeLists.txt | 1 + Tests/Subjects/LongProcess/Main.cpp | 12 + Tests/Unit/CMakeLists.txt | 20 +- Tests/Unit/{File.cpp => FileOps.cpp} | 10 +- Tests/Unit/{Process.cpp => ProcessOps.cpp} | 14 +- 19 files changed, 497 insertions(+), 317 deletions(-) create mode 100644 Src/IACore/imp/cpp/AsyncOps.cpp create mode 100644 Src/IACore/imp/cpp/FileOps.cpp create mode 100644 Src/IACore/imp/cpp/ProcessOps.cpp create mode 100644 Src/IACore/imp/cpp/SocketOps.cpp create mode 100644 Src/IACore/inc/IACore/AsyncOps.hpp rename Src/IACore/inc/IACore/{File.hpp => FileOps.hpp} (100%) delete mode 100644 Src/IACore/inc/IACore/Process.hpp create mode 100644 Src/IACore/inc/IACore/ProcessOps.hpp create mode 100644 Tests/Subjects/CMakeLists.txt create mode 100644 Tests/Subjects/LongProcess/Main.cpp rename Tests/Unit/{File.cpp => FileOps.cpp} (96%) rename Tests/Unit/{Process.cpp => ProcessOps.cpp} (93%) diff --git a/.gitignore b/.gitignore index 25674ec..29010d6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,6 @@ .vscode/* !.vscode/settings.json !.vscode/tasks.json -!.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets @@ -47,4 +46,5 @@ *.vsix .cache/ +.local/ Build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 10cdf18..fb297d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,3 +50,10 @@ add_subdirectory(Src/) if(IACore_BUILD_TESTS) add_subdirectory(Tests) endif() + +# ------------------------------------------------- +# Local Development Sandboxes (not included in source control) +# ------------------------------------------------- +if (EXISTS ".local") + add_subdirectory(.local) +endif() diff --git a/Src/IACore/CMakeLists.txt b/Src/IACore/CMakeLists.txt index 8be49bf..a00cbe6 100644 --- a/Src/IACore/CMakeLists.txt +++ b/Src/IACore/CMakeLists.txt @@ -1,6 +1,10 @@ set(SRC_FILES "imp/cpp/IACore.cpp" "imp/cpp/Logger.cpp" + "imp/cpp/FileOps.cpp" + "imp/cpp/AsyncOps.cpp" + "imp/cpp/SocketOps.cpp" + "imp/cpp/ProcessOps.cpp" ) add_library(IACore STATIC ${SRC_FILES}) diff --git a/Src/IACore/imp/cpp/AsyncOps.cpp b/Src/IACore/imp/cpp/AsyncOps.cpp new file mode 100644 index 0000000..770ac97 --- /dev/null +++ b/Src/IACore/imp/cpp/AsyncOps.cpp @@ -0,0 +1,22 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +namespace IACore +{ + +} \ No newline at end of file diff --git a/Src/IACore/imp/cpp/FileOps.cpp b/Src/IACore/imp/cpp/FileOps.cpp new file mode 100644 index 0000000..e69de29 diff --git a/Src/IACore/imp/cpp/Logger.cpp b/Src/IACore/imp/cpp/Logger.cpp index 1827e54..9d68aed 100644 --- a/Src/IACore/imp/cpp/Logger.cpp +++ b/Src/IACore/imp/cpp/Logger.cpp @@ -15,7 +15,7 @@ // along with this program. If not, see . #include -#include +#include namespace IACore { diff --git a/Src/IACore/imp/cpp/ProcessOps.cpp b/Src/IACore/imp/cpp/ProcessOps.cpp new file mode 100644 index 0000000..a80b272 --- /dev/null +++ b/Src/IACore/imp/cpp/ProcessOps.cpp @@ -0,0 +1,308 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +namespace IACore +{ + // --------------------------------------------------------------------- + // Output Buffering Helper + // Splits raw chunks into lines, preserving partial lines across chunks + // --------------------------------------------------------------------- + struct LineBuffer + { + String Accumulator; + Function &Callback; + + VOID Append(IN PCCHAR data, IN SIZE_T size); + VOID Flush(); + }; +} // namespace IACore + +namespace IACore +{ + Expected ProcessOps::SpawnProcessSync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback) + { + Atomic id; +#if IA_PLATFORM_WINDOWS + return SpawnProcessWindows(command, args, onOutputLineCallback, id); +#else + return SpawnProcessPosix(command, args, onOutputLineCallback, id); +#endif + } + + SharedPtr ProcessOps::SpawnProcessAsync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + IN Function)> onFinishCallback) + { + SharedPtr handle = std::make_shared(); + handle->IsRunning = true; + + handle->ThreadHandle = JoiningThread([&, cmd = IA_MOVE(command), args = std::move(args)]() mutable { + +#if IA_PLATFORM_WINDOWS + auto result = SpawnProcessWindows(cmd, args, onOutputLineCallback, handle->ID); +#else + auto result = SpawnProcessPosix(cmd, args, onOutputLineCallback, handle->ID); +#endif + + handle->IsRunning = false; + if (onFinishCallback) + onFinishCallback(IA_MOVE(result)); + }); + + return handle; + } + + VOID ProcessOps::TerminateProcess(IN CONST SharedPtr &handle) + { + if (!handle || !handle->IsActive()) + return; + + NativeProcessID pid = handle->ID.load(); + if (pid == 0) + return; + +#if IA_PLATFORM_WINDOWS + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (hProcess != NULL) + { + TerminateProcess(hProcess, 9); + CloseHandle(hProcess); + } +#else + kill(pid, SIGKILL); +#endif + } +} // namespace IACore + +namespace IACore +{ +#if IA_PLATFORM_WINDOWS + Expected ProcessOps::SpawnProcessWindows(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + OUT Atomic &id) + { + SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE}; // Allow inheritance + HANDLE hRead = NULL, hWrite = NULL; + + if (!CreatePipe(&hRead, &hWrite, &saAttr, 0)) + return tl::make_unexpected("Failed to create pipe"); + + // Ensure the read handle to the pipe for STDOUT is NOT inherited + if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0)) + return tl::make_unexpected("Failed to secure pipe handles"); + + STARTUPINFOA si = {sizeof(STARTUPINFOA)}; + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdOutput = hWrite; + si.hStdError = hWrite; // Merge stderr + si.hStdInput = NULL; // No input + + PROCESS_INFORMATION pi = {0}; + + // Windows command line needs to be mutable and concatenated + String commandLine = BuildString("\"", command, "\" ", args); + + BOOL success = CreateProcessA(NULL, commandLine.data(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); + + // Important: Close write end in parent, otherwise ReadFile never returns EOF! + CloseHandle(hWrite); + + if (!success) + { + CloseHandle(hRead); + return tl::make_unexpected(String("CreateProcess failed: ") + std::to_string(GetLastError())); + } + + id.store(pi.dwProcessId); + + // Read Loop + LineBuffer lineBuf{"", onOutputLineCallback}; + DWORD bytesRead; + CHAR buffer[4096]; + + while (ReadFile(hRead, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead != 0) + { + lineBuf.Append(buffer, bytesRead); + } + lineBuf.Flush(); + + // NOW we wait for exit code + DWORD exitCode = 0; + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exitCode); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + CloseHandle(hRead); + id.store(0); + + return static_cast(exitCode); + } +#endif + +#if IA_PLATFORM_UNIX + Expected ProcessOps::SpawnProcessPosix(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + OUT Atomic &id) + { + int pipefd[2]; + if (pipe(pipefd) == -1) + return tl::make_unexpected("Failed to create pipe"); + + pid_t pid = fork(); + + if (pid == -1) + { + return tl::make_unexpected("Failed to fork process"); + } + else if (pid == 0) + { + // --- Child Process --- + close(pipefd[0]); + + dup2(pipefd[1], STDOUT_FILENO); + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + + // --- ARGUMENT PARSING START --- + std::vector argStorage; // To keep strings alive + std::vector argv; + + std::string cmdStr = command; + argv.push_back(cmdStr.data()); + + // Manual Quote-Aware Splitter + std::string currentToken; + bool inQuotes = false; + + for (char c : args) + { + if (c == '\"') + { + inQuotes = !inQuotes; + // Determine if you want to keep the quotes or strip them. + // Usually for execvp, you strip them so the shell receives the raw content. + continue; + } + + if (c == ' ' && !inQuotes) + { + if (!currentToken.empty()) + { + argStorage.push_back(currentToken); + currentToken.clear(); + } + } + else + { + currentToken += c; + } + } + if (!currentToken.empty()) + { + argStorage.push_back(currentToken); + } + + // Build char* array from the std::string storage + for (auto &s : argStorage) + { + argv.push_back(s.data()); + } + argv.push_back(nullptr); + // --- ARGUMENT PARSING END --- + + execvp(argv[0], argv.data()); + _exit(127); + } + else + { + // --- Parent Process --- + id.store(pid); + + close(pipefd[1]); + + LineBuffer lineBuf{"", onOutputLineCallback}; + char buffer[4096]; + ssize_t count; + + while ((count = read(pipefd[0], buffer, sizeof(buffer))) > 0) + { + lineBuf.Append(buffer, count); + } + lineBuf.Flush(); + close(pipefd[0]); + + int status; + waitpid(pid, &status, 0); + + id.store(0); + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; + } + } +#endif +} // namespace IACore + +namespace IACore +{ + VOID LineBuffer::Append(IN PCCHAR data, IN SIZE_T size) + { + SIZE_T start = 0; + for (SIZE_T i = 0; i < size; ++i) + { + if (data[i] == '\n' || data[i] == '\r') + { + // Flush Accumulator + current chunk + if (!Accumulator.empty()) + { + Accumulator.append(data + start, i - start); + if (!Accumulator.empty()) + Callback(Accumulator); + Accumulator.clear(); + } + else + { + // Zero copy optimization for pure lines in one chunk + if (i > start) + Callback(StringView(data + start, i - start)); + } + + // Skip \r\n sequence if needed, or just start next + if (data[i] == '\r' && i + 1 < size && data[i + 1] == '\n') + i++; + start = i + 1; + } + } + // Save remaining partial line + if (start < size) + { + Accumulator.append(data + start, size - start); + } + } + + VOID LineBuffer::Flush() + { + if (!Accumulator.empty()) + { + Callback(Accumulator); + Accumulator.clear(); + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/SocketOps.cpp b/Src/IACore/imp/cpp/SocketOps.cpp new file mode 100644 index 0000000..7d1cf70 --- /dev/null +++ b/Src/IACore/imp/cpp/SocketOps.cpp @@ -0,0 +1,22 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +namespace IACore +{ + +} \ No newline at end of file diff --git a/Src/IACore/inc/IACore/AsyncOps.hpp b/Src/IACore/inc/IACore/AsyncOps.hpp new file mode 100644 index 0000000..822a5f6 --- /dev/null +++ b/Src/IACore/inc/IACore/AsyncOps.hpp @@ -0,0 +1,24 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +namespace IACore +{ + +} \ No newline at end of file diff --git a/Src/IACore/inc/IACore/File.hpp b/Src/IACore/inc/IACore/FileOps.hpp similarity index 100% rename from Src/IACore/inc/IACore/File.hpp rename to Src/IACore/inc/IACore/FileOps.hpp diff --git a/Src/IACore/inc/IACore/PCH.hpp b/Src/IACore/inc/IACore/PCH.hpp index af67102..9a73c4c 100644 --- a/Src/IACore/inc/IACore/PCH.hpp +++ b/Src/IACore/inc/IACore/PCH.hpp @@ -27,6 +27,7 @@ # include # include # include +# include # include # include # include @@ -501,6 +502,7 @@ STATIC CONST FLOAT64 FLOAT64_EPSILON = DBL_EPSILON; # include # include # include +# include #endif #if IA_CHECK(IA_PLATFORM_WIN64) || IA_CHECK(IA_PLATFORM_UNIX) @@ -524,6 +526,9 @@ template using UnorderedSet = ankerl::unordered_dense::set<_ template using Span = std::span<_value_type>; template using UnorderedMap = ankerl::unordered_dense::map<_key_type, _value_type>; +template using Atomic = std::atomic<_value_type>; +template using SharedPtr = std::shared_ptr<_value_type>; +template using UniquePtr = std::unique_ptr<_value_type>; template using Expected = tl::expected<_expected_type, _unexpected_type>; @@ -537,7 +542,9 @@ using HRClock = std::chrono::high_resolution_clock; using HRTimePoint = std::chrono::time_point; using Mutex = std::mutex; -using LockGuard = std::lock_guard; +using ScopedLock = std::scoped_lock; +using UniqueLock = std::unique_lock; +using JoiningThread = std::jthread; template using FormatterString = std::format_string; diff --git a/Src/IACore/inc/IACore/Process.hpp b/Src/IACore/inc/IACore/Process.hpp deleted file mode 100644 index a03b0e5..0000000 --- a/Src/IACore/inc/IACore/Process.hpp +++ /dev/null @@ -1,292 +0,0 @@ -// IACore-OSS; The Core Library for All IA Open Source Projects -// Copyright (C) 2025 IAS (ias@iasoft.dev) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#pragma once - -#include - -namespace IACore -{ - - class Process - { - public: - // --------------------------------------------------------------------- - // Static One-Shot Execution - // --------------------------------------------------------------------- - - // Returns Exit Code or Error String - // callback receives distinct lines of output (stdout + stderr merged) - STATIC tl::expected Run(CONST String &cmd, CONST String &args, - Function onOutputLine) - { -#if IA_PLATFORM_WINDOWS - return RunWindows(cmd, args, onOutputLine); -#else - return RunPosix(cmd, args, onOutputLine); -#endif - } - - // --------------------------------------------------------------------- - // Async Execution - // --------------------------------------------------------------------- - - // Returns a jthread. - // - Store it if you want to wait for it later (join). - // - If you destroy the returned jthread immediately, it will BLOCK (join) by design. - // - If you want "fire and forget", call .detach() on the returned thread. - STATIC std::jthread RunAsync(String cmd, String args, Function onOutputLine, - Function)> onComplete) - { - // We capture arguments by VALUE (=) to ensure the thread owns its own copies of the strings. - return std::jthread([=, cmd = std::move(cmd), args = std::move(args)]() mutable { - tl::expected result = tl::make_unexpected("Not started"); - -#if IA_PLATFORM_WINDOWS - result = RunWindows(cmd, args, onOutputLine); -#else - result = RunPosix(cmd, args, onOutputLine); -#endif - - // Report final result to the callback - if (onComplete) - { - onComplete(std::move(result)); - } - }); - } - - private: - // --------------------------------------------------------------------- - // Output Buffering Helper - // Splits raw chunks into lines, preserving partial lines across chunks - // --------------------------------------------------------------------- - struct LineBuffer - { - String accumulator; - Function &callback; - - VOID Append(const char *data, SIZE_T size) - { - SIZE_T start = 0; - for (SIZE_T i = 0; i < size; ++i) - { - if (data[i] == '\n' || data[i] == '\r') - { - // Flush accumulator + current chunk - if (!accumulator.empty()) - { - accumulator.append(data + start, i - start); - if (!accumulator.empty()) - callback(accumulator); - accumulator.clear(); - } - else - { - // Zero copy optimization for pure lines in one chunk - if (i > start) - callback(StringView(data + start, i - start)); - } - - // Skip \r\n sequence if needed, or just start next - if (data[i] == '\r' && i + 1 < size && data[i + 1] == '\n') - i++; - start = i + 1; - } - } - // Save remaining partial line - if (start < size) - { - accumulator.append(data + start, size - start); - } - } - - VOID Flush() - { - if (!accumulator.empty()) - { - callback(accumulator); - accumulator.clear(); - } - } - }; - -// --------------------------------------------------------------------- -// Windows Implementation -// --------------------------------------------------------------------- -#if IA_PLATFORM_WINDOWS - STATIC tl::expected RunWindows(CONST String &cmd, CONST String &args, - Function cb) - { - SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE}; // Allow inheritance - HANDLE hRead = NULL, hWrite = NULL; - - if (!CreatePipe(&hRead, &hWrite, &saAttr, 0)) - return tl::make_unexpected("Failed to create pipe"); - - // Ensure the read handle to the pipe for STDOUT is NOT inherited - if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0)) - return tl::make_unexpected("Failed to secure pipe handles"); - - STARTUPINFOA si = {sizeof(STARTUPINFOA)}; - si.dwFlags |= STARTF_USESTDHANDLES; - si.hStdOutput = hWrite; - si.hStdError = hWrite; // Merge stderr - si.hStdInput = NULL; // No input - - PROCESS_INFORMATION pi = {0}; - - // Windows command line needs to be mutable and concatenated - String commandLine = BuildString("\"", cmd, "\" ", args); - - BOOL success = CreateProcessA(NULL, commandLine.data(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); - - // Important: Close write end in parent, otherwise ReadFile never returns EOF! - CloseHandle(hWrite); - - if (!success) - { - CloseHandle(hRead); - return tl::make_unexpected(String("CreateProcess failed: ") + std::to_string(GetLastError())); - } - - // Read Loop - LineBuffer lineBuf{"", cb}; - DWORD bytesRead; - CHAR buffer[4096]; - - while (ReadFile(hRead, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead != 0) - { - lineBuf.Append(buffer, bytesRead); - } - lineBuf.Flush(); - - // NOW we wait for exit code - DWORD exitCode = 0; - WaitForSingleObject(pi.hProcess, INFINITE); - GetExitCodeProcess(pi.hProcess, &exitCode); - - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - CloseHandle(hRead); - - return static_cast(exitCode); - } -#endif - -// --------------------------------------------------------------------- -// POSIX (Linux/Mac) Implementation -// --------------------------------------------------------------------- -#if IA_PLATFORM_UNIX - STATIC tl::expected RunPosix(CONST String &cmd, CONST String &args, - Function cb) - { - int pipefd[2]; - if (pipe(pipefd) == -1) - return tl::make_unexpected("Failed to create pipe"); - - pid_t pid = fork(); - - if (pid == -1) - { - return tl::make_unexpected("Failed to fork process"); - } - else if (pid == 0) - { - // --- Child Process --- - close(pipefd[0]); - - dup2(pipefd[1], STDOUT_FILENO); - dup2(pipefd[1], STDERR_FILENO); - close(pipefd[1]); - - // --- ARGUMENT PARSING START --- - std::vector argStorage; // To keep strings alive - std::vector argv; - - std::string cmdStr = cmd; - argv.push_back(cmdStr.data()); - - // Manual Quote-Aware Splitter - std::string currentToken; - bool inQuotes = false; - - for (char c : args) - { - if (c == '\"') - { - inQuotes = !inQuotes; - // Determine if you want to keep the quotes or strip them. - // Usually for execvp, you strip them so the shell receives the raw content. - continue; - } - - if (c == ' ' && !inQuotes) - { - if (!currentToken.empty()) - { - argStorage.push_back(currentToken); - currentToken.clear(); - } - } - else - { - currentToken += c; - } - } - if (!currentToken.empty()) - { - argStorage.push_back(currentToken); - } - - // Build char* array from the std::string storage - for (auto &s : argStorage) - { - argv.push_back(s.data()); - } - argv.push_back(nullptr); - // --- ARGUMENT PARSING END --- - - execvp(argv[0], argv.data()); - _exit(127); - } - else - { - // --- Parent Process --- - close(pipefd[1]); - - LineBuffer lineBuf{"", cb}; - char buffer[4096]; - ssize_t count; - - while ((count = read(pipefd[0], buffer, sizeof(buffer))) > 0) - { - lineBuf.Append(buffer, count); - } - lineBuf.Flush(); - close(pipefd[0]); - - int status; - waitpid(pid, &status, 0); - - if (WIFEXITED(status)) - return WEXITSTATUS(status); - return -1; - } - } -#endif - }; -} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/ProcessOps.hpp b/Src/IACore/inc/IACore/ProcessOps.hpp new file mode 100644 index 0000000..9d8104a --- /dev/null +++ b/Src/IACore/inc/IACore/ProcessOps.hpp @@ -0,0 +1,64 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +#if IA_PLATFORM_WINDOWS +using NativeProcessID = DWORD; +#elif IA_PLATFORM_UNIX +using NativeProcessID = pid_t; +#else +# error "This platform does not support IACore ProcessOps" +#endif + +namespace IACore +{ + struct ProcessHandle + { + Atomic ID{0}; + Atomic IsRunning{false}; + + BOOL IsActive() CONST + { + return IsRunning && ID != 0; + } + + private: + JoiningThread ThreadHandle; + + friend class ProcessOps; + }; + + class ProcessOps + { + public: + STATIC Expected SpawnProcessSync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback); + STATIC SharedPtr SpawnProcessAsync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + IN Function)> onFinishCallback); + + STATIC VOID TerminateProcess(IN CONST SharedPtr &handle); + + private: + STATIC Expected SpawnProcessWindows(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, OUT Atomic& id); + STATIC Expected SpawnProcessPosix(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, OUT Atomic& id); + }; +} // namespace IACore \ No newline at end of file diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index c18a472..a4bccdc 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(Subjects/) add_subdirectory(Unit/) add_subdirectory(Regression/) diff --git a/Tests/Subjects/CMakeLists.txt b/Tests/Subjects/CMakeLists.txt new file mode 100644 index 0000000..6248438 --- /dev/null +++ b/Tests/Subjects/CMakeLists.txt @@ -0,0 +1 @@ +add_executable(LongProcess LongProcess/Main.cpp) diff --git a/Tests/Subjects/LongProcess/Main.cpp b/Tests/Subjects/LongProcess/Main.cpp new file mode 100644 index 0000000..803803b --- /dev/null +++ b/Tests/Subjects/LongProcess/Main.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int, char **) +{ + std::cout << "Started!\n"; + std::cout.flush(); + std::this_thread::sleep_for(std::chrono::seconds(5)); + std::cout << "Ended!\n"; + std::cout.flush(); + return 100; +} \ No newline at end of file diff --git a/Tests/Unit/CMakeLists.txt b/Tests/Unit/CMakeLists.txt index 0944a37..5801492 100644 --- a/Tests/Unit/CMakeLists.txt +++ b/Tests/Unit/CMakeLists.txt @@ -38,20 +38,20 @@ target_compile_options(${TEST_NAME_PREFIX}Environment PRIVATE -fexceptions) set_target_properties(${TEST_NAME_PREFIX}Environment PROPERTIES USE_EXCEPTIONS ON) # ------------------------------------------------ -# Unit: File +# Unit: FileOps # ------------------------------------------------ -add_executable(${TEST_NAME_PREFIX}File "File.cpp") -target_link_libraries(${TEST_NAME_PREFIX}File PRIVATE IACore) -target_compile_options(${TEST_NAME_PREFIX}File PRIVATE -fexceptions) -set_target_properties(${TEST_NAME_PREFIX}File PROPERTIES USE_EXCEPTIONS ON) +add_executable(${TEST_NAME_PREFIX}FileOps "FileOps.cpp") +target_link_libraries(${TEST_NAME_PREFIX}FileOps PRIVATE IACore) +target_compile_options(${TEST_NAME_PREFIX}FileOps PRIVATE -fexceptions) +set_target_properties(${TEST_NAME_PREFIX}FileOps PROPERTIES USE_EXCEPTIONS ON) # ------------------------------------------------ -# Unit: Process +# Unit: ProcessOps # ------------------------------------------------ -add_executable(${TEST_NAME_PREFIX}Process "Process.cpp") -target_link_libraries(${TEST_NAME_PREFIX}Process PRIVATE IACore) -target_compile_options(${TEST_NAME_PREFIX}Process PRIVATE -fexceptions) -set_target_properties(${TEST_NAME_PREFIX}Process PROPERTIES USE_EXCEPTIONS ON) +add_executable(${TEST_NAME_PREFIX}ProcessOps "ProcessOps.cpp") +target_link_libraries(${TEST_NAME_PREFIX}ProcessOps PRIVATE IACore) +target_compile_options(${TEST_NAME_PREFIX}ProcessOps PRIVATE -fexceptions) +set_target_properties(${TEST_NAME_PREFIX}ProcessOps PROPERTIES USE_EXCEPTIONS ON) # ------------------------------------------------ # Unit: Utils diff --git a/Tests/Unit/File.cpp b/Tests/Unit/FileOps.cpp similarity index 96% rename from Tests/Unit/File.cpp rename to Tests/Unit/FileOps.cpp index 223822d..070e034 100644 --- a/Tests/Unit/File.cpp +++ b/Tests/Unit/FileOps.cpp @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include +#include #include @@ -129,7 +129,7 @@ IAT_BEGIN_BLOCK(Core, File) // 1. Write { File f; - auto res = f.Open(path, flagsWrite); + auto res = f.Open(path, (File::EOpenFlags)flagsWrite); IAT_CHECK(res.has_value()); IAT_CHECK(f.IsOpen()); @@ -145,7 +145,7 @@ IAT_BEGIN_BLOCK(Core, File) UINT32 flagsRead = (UINT32)File::EOpenFlags::Read | (UINT32)File::EOpenFlags::Binary; - File f(path, flagsRead); // Test RAII constructor + File f(path, (File::EOpenFlags)flagsRead); // Test RAII constructor IAT_CHECK(f.IsOpen()); UINT32 magicRead = 0; @@ -172,7 +172,7 @@ IAT_BEGIN_BLOCK(Core, File) // Open in append mode File f; - const auto openResult = f.Open(path, flagsAppend); + const auto openResult = f.Open(path, (File::EOpenFlags)flagsAppend); if(!openResult) { IA_PANIC(openResult.error().c_str()) @@ -209,7 +209,7 @@ IAT_BEGIN_BLOCK(Core, File) // Test Instance File f; - auto resInstance = f.Open(ghostPath, (UINT32)File::EOpenFlags::Read); + auto resInstance = f.Open(ghostPath, File::EOpenFlags::Read); IAT_CHECK_NOT(resInstance.has_value()); IAT_CHECK_NOT(f.IsOpen()); diff --git a/Tests/Unit/Process.cpp b/Tests/Unit/ProcessOps.cpp similarity index 93% rename from Tests/Unit/Process.cpp rename to Tests/Unit/ProcessOps.cpp index eafc257..563e64a 100644 --- a/Tests/Unit/Process.cpp +++ b/Tests/Unit/ProcessOps.cpp @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include +#include #include @@ -44,7 +44,7 @@ IAT_BEGIN_BLOCK(Core, Process) // Simple "echo hello" String captured; - auto result = Process::Run(CMD_ECHO_EXE, CMD_ARG_PREFIX " HelloIA", + auto result = ProcessOps::SpawnProcessSync(CMD_ECHO_EXE, CMD_ARG_PREFIX " HelloIA", [&](StringView line) { captured = line; } @@ -73,7 +73,7 @@ IAT_BEGIN_BLOCK(Core, Process) String args = String(CMD_ARG_PREFIX) + " one two"; if(args[0] == ' ') args.erase(0, 1); // cleanup space if prefix empty - auto result = Process::Run(CMD_ECHO_EXE, args, + auto result = ProcessOps::SpawnProcessSync(CMD_ECHO_EXE, args, [&](StringView line) { lines.push_back(String(line)); } @@ -107,7 +107,7 @@ IAT_BEGIN_BLOCK(Core, Process) arg = "-c \"exit 42\""; // quotes needed for sh -c #endif - auto result = Process::Run(cmd, arg, [](StringView){}); + auto result = ProcessOps::SpawnProcessSync(cmd, arg, [](StringView){}); IAT_CHECK(result.has_value()); IAT_CHECK_EQ(*result, 42); @@ -121,7 +121,7 @@ IAT_BEGIN_BLOCK(Core, Process) BOOL TestMissingExe() { // Try to run a random string - auto result = Process::Run("sdflkjghsdflkjg", "", [](StringView){}); + auto result = ProcessOps::SpawnProcessSync("sdflkjghsdflkjg", "", [](StringView){}); // Windows: CreateProcess usually fails -> returns unexpected // Linux: execvp fails inside child, returns 127 via waitpid @@ -166,7 +166,7 @@ IAT_BEGIN_BLOCK(Core, Process) #endif String captured; - auto result = Process::Run(cmd, arg, + auto result = ProcessOps::SpawnProcessSync(cmd, arg, [&](StringView line) { captured += line; } @@ -203,7 +203,7 @@ IAT_BEGIN_BLOCK(Core, Process) bool foundA = false; bool foundB = false; - UNUSED(Process::Run(cmd, arg, [&](StringView line) { + UNUSED(ProcessOps::SpawnProcessSync(cmd, arg, [&](StringView line) { lineCount++; if (line.find("LineA") != String::npos) foundA = true; if (line.find("LineB") != String::npos) foundB = true;