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;