aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/process.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-21 21:43:22 +0100
committerGitHub Enterprise <[email protected]>2026-03-21 21:43:22 +0100
commit14ca5b35d0fc477ba30f10b80f937b523fd7e930 (patch)
tree8aab2acfec8be1af4bf0dffdb4badc3b64bf8385 /src/zencore/process.cpp
parentfix null stats provider crash when build store is not configured (#875) (diff)
downloadzen-14ca5b35d0fc477ba30f10b80f937b523fd7e930.tar.xz
zen-14ca5b35d0fc477ba30f10b80f937b523fd7e930.zip
Interprocess pipe support (for stdout/stderr capture) (#866)
- **RAII pipe handles for child process stdout/stderr capture**: `StdoutPipeHandles` is now a proper RAII type with automatic cleanup, move semantics, and partial close support. This makes it safe to use pipes for capturing child process output without risking handle/fd leaks. - **Optional separate stderr pipe**: `CreateProcOptions` now accepts a `StderrPipe` field so callers can capture stdout and stderr independently. When null (default), stderr shares the stdout pipe as before. - **LogStreamListener with pluggable handler**: The TCP log stream listener accepts connections from remote processes and delivers parsed log lines through a `LogStreamHandler` interface, set dynamically via `SetHandler()`. This allows any client to receive log messages without depending on a specific console implementation. - **TcpLogStreamSink for zen::logging**: A logging sink that forwards log messages to a `LogStreamListener` over TCP, using the native `zen::logging::Sink` infrastructure with proper thread-safe synchronization. - **Reliable child process exit codes on Linux**: `waitpid` result handling is fixed so `ProcessHandle::GetExitCode()` returns the real exit code. `ProcessHandle::Reset()` reaps zombies directly, replacing the global `IgnoreChildSignals()` which prevented exit code collection entirely. Also fixes a TOCTOU race in `ProcessHandle::Wait()` on Linux/Mac. - **Pipe capture test suite**: Tests covering stdout/stderr capture via pipes (both shared and separate modes), RAII cleanup, move semantics, and exit code propagation using `zentest-appstub` as the child process. - **Service command integration tests**: Shell-based integration tests for `zen service` covering the full lifecycle (install, status, start, stop, uninstall) on all three platforms — Linux (systemd), macOS (launchd), and Windows (SCM via PowerShell). - **Test script reorganization**: Platform-specific test scripts moved from `scripts/test_scripts/` into `scripts/test_linux/`, `test_mac/`, and `test_windows/`.
Diffstat (limited to 'src/zencore/process.cpp')
-rw-r--r--src/zencore/process.cpp237
1 files changed, 227 insertions, 10 deletions
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index 29de107bd..8a91ab287 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -137,6 +137,144 @@ IsZombieProcess(int pid, std::error_code& OutEc)
}
#endif // ZEN_PLATFORM_MAC
+//////////////////////////////////////////////////////////////////////////
+// Pipe creation for child process stdout capture
+
+#if ZEN_PLATFORM_WINDOWS
+
+StdoutPipeHandles::~StdoutPipeHandles()
+{
+ Close();
+}
+
+StdoutPipeHandles::StdoutPipeHandles(StdoutPipeHandles&& Other) noexcept
+: ReadHandle(std::exchange(Other.ReadHandle, nullptr))
+, WriteHandle(std::exchange(Other.WriteHandle, nullptr))
+{
+}
+
+StdoutPipeHandles&
+StdoutPipeHandles::operator=(StdoutPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadHandle = std::exchange(Other.ReadHandle, nullptr);
+ WriteHandle = std::exchange(Other.WriteHandle, nullptr);
+ }
+ return *this;
+}
+
+void
+StdoutPipeHandles::CloseWriteEnd()
+{
+ if (WriteHandle)
+ {
+ CloseHandle(WriteHandle);
+ WriteHandle = nullptr;
+ }
+}
+
+void
+StdoutPipeHandles::Close()
+{
+ if (ReadHandle)
+ {
+ CloseHandle(ReadHandle);
+ ReadHandle = nullptr;
+ }
+ CloseWriteEnd();
+}
+
+bool
+CreateStdoutPipe(StdoutPipeHandles& OutPipe)
+{
+ SECURITY_ATTRIBUTES Sa;
+ Sa.nLength = sizeof(Sa);
+ Sa.lpSecurityDescriptor = nullptr;
+ Sa.bInheritHandle = TRUE;
+
+ HANDLE ReadHandle = nullptr;
+ HANDLE WriteHandle = nullptr;
+ if (!::CreatePipe(&ReadHandle, &WriteHandle, &Sa, 0))
+ {
+ return false;
+ }
+
+ // The read end should not be inherited by the child
+ SetHandleInformation(ReadHandle, HANDLE_FLAG_INHERIT, 0);
+
+ OutPipe.ReadHandle = ReadHandle;
+ OutPipe.WriteHandle = WriteHandle;
+ return true;
+}
+
+#else
+
+StdoutPipeHandles::~StdoutPipeHandles()
+{
+ Close();
+}
+
+StdoutPipeHandles::StdoutPipeHandles(StdoutPipeHandles&& Other) noexcept
+: ReadFd(std::exchange(Other.ReadFd, -1))
+, WriteFd(std::exchange(Other.WriteFd, -1))
+{
+}
+
+StdoutPipeHandles&
+StdoutPipeHandles::operator=(StdoutPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadFd = std::exchange(Other.ReadFd, -1);
+ WriteFd = std::exchange(Other.WriteFd, -1);
+ }
+ return *this;
+}
+
+void
+StdoutPipeHandles::CloseWriteEnd()
+{
+ if (WriteFd >= 0)
+ {
+ close(WriteFd);
+ WriteFd = -1;
+ }
+}
+
+void
+StdoutPipeHandles::Close()
+{
+ if (ReadFd >= 0)
+ {
+ close(ReadFd);
+ ReadFd = -1;
+ }
+ CloseWriteEnd();
+}
+
+bool
+CreateStdoutPipe(StdoutPipeHandles& OutPipe)
+{
+ int Fds[2];
+ if (pipe(Fds) != 0)
+ {
+ return false;
+ }
+ OutPipe.ReadFd = Fds[0];
+ OutPipe.WriteFd = Fds[1];
+
+ // Set close-on-exec on the read end so the child doesn't inherit it
+ fcntl(OutPipe.ReadFd, F_SETFD, FD_CLOEXEC);
+ return true;
+}
+
+#endif
+
+//////////////////////////////////////////////////////////////////////////
+
ProcessHandle::ProcessHandle() = default;
#if ZEN_PLATFORM_WINDOWS
@@ -309,6 +447,10 @@ ProcessHandle::Reset()
{
#if ZEN_PLATFORM_WINDOWS
CloseHandle(m_ProcessHandle);
+#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+ // Reap the child if it has already exited to prevent zombies.
+ // If still running, it will be reparented to init on our exit.
+ waitpid(m_Pid, nullptr, WNOHANG);
#endif
m_ProcessHandle = nullptr;
m_Pid = 0;
@@ -350,17 +492,26 @@ ProcessHandle::Wait(int TimeoutMs, std::error_code& OutEc)
timespec SleepTime = {0, SleepMs * 1000 * 1000};
for (int SleepedTimeMS = 0;; SleepedTimeMS += SleepMs)
{
- int WaitState = 0;
- if (waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED) != -1)
+ int WaitState = 0;
+ pid_t WaitResult = waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED);
+ if (WaitResult > 0 && WIFEXITED(WaitState))
{
- if (WIFEXITED(WaitState))
- {
- m_ExitCode = WEXITSTATUS(WaitState);
- }
+ m_ExitCode = WEXITSTATUS(WaitState);
}
if (!IsProcessRunning(m_Pid, OutEc))
{
+ // Process is gone but waitpid(WNOHANG) may have missed the exit status
+ // due to a TOCTOU race (process became a zombie between waitpid and
+ // IsProcessRunning). Do a blocking reap now to capture the exit code.
+ if (WaitResult <= 0)
+ {
+ WaitState = 0;
+ if (waitpid(m_Pid, &WaitState, 0) > 0 && WIFEXITED(WaitState))
+ {
+ m_ExitCode = WEXITSTATUS(WaitState);
+ }
+ }
return true;
}
else if (OutEc)
@@ -381,6 +532,12 @@ ProcessHandle::Wait(int TimeoutMs, std::error_code& OutEc)
else if (IsZombieProcess(m_Pid, OutEc))
{
ZEN_INFO("Found process {} in zombie state, treating as not running", m_Pid);
+ // Reap the zombie to capture its exit code.
+ WaitState = 0;
+ if (waitpid(m_Pid, &WaitState, 0) > 0 && WIFEXITED(WaitState))
+ {
+ m_ExitCode = WEXITSTATUS(WaitState);
+ }
return true;
}
@@ -567,7 +724,40 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
ExtendableWideStringBuilder<256> CommandLineZ;
CommandLineZ << CommandLine;
- if (!Options.StdoutFile.empty())
+ bool DuplicatedStdErr = false;
+
+ if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteHandle != nullptr)
+ {
+ StartupInfo.hStdInput = nullptr;
+ StartupInfo.hStdOutput = (HANDLE)Options.StdoutPipe->WriteHandle;
+
+ if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteHandle != nullptr)
+ {
+ // Use separate pipe for stderr
+ StartupInfo.hStdError = (HANDLE)Options.StderrPipe->WriteHandle;
+ StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ InheritHandles = true;
+ }
+ else
+ {
+ // Duplicate stdout handle for stderr (both go to same pipe)
+ const BOOL DupSuccess = DuplicateHandle(GetCurrentProcess(),
+ StartupInfo.hStdOutput,
+ GetCurrentProcess(),
+ &StartupInfo.hStdError,
+ 0,
+ TRUE,
+ DUPLICATE_SAME_ACCESS);
+
+ if (DupSuccess)
+ {
+ DuplicatedStdErr = true;
+ StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ InheritHandles = true;
+ }
+ }
+ }
+ else if (!Options.StdoutFile.empty())
{
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof sa;
@@ -593,6 +783,7 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
if (Success)
{
+ DuplicatedStdErr = true;
StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
InheritHandles = true;
}
@@ -616,8 +807,16 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
if (StartupInfo.dwFlags & STARTF_USESTDHANDLES)
{
- CloseHandle(StartupInfo.hStdError);
- CloseHandle(StartupInfo.hStdOutput);
+ // Only close hStdError if we duplicated it (caller-owned pipe handles are not ours to close)
+ if (DuplicatedStdErr)
+ {
+ CloseHandle(StartupInfo.hStdError);
+ }
+ // Only close hStdOutput if it was a file handle we created (not a pipe handle owned by caller)
+ if (Options.StdoutPipe == nullptr || Options.StdoutPipe->WriteHandle == nullptr)
+ {
+ CloseHandle(StartupInfo.hStdOutput);
+ }
}
if (!Success)
@@ -826,7 +1025,25 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
ZEN_UNUSED(Result);
}
- if (!Options.StdoutFile.empty())
+ if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0)
+ {
+ dup2(Options.StdoutPipe->WriteFd, STDOUT_FILENO);
+
+ if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteFd >= 0)
+ {
+ dup2(Options.StderrPipe->WriteFd, STDERR_FILENO);
+ close(Options.StderrPipe->WriteFd);
+ // StderrPipe ReadFd has FD_CLOEXEC so it's auto-closed on exec
+ }
+ else
+ {
+ dup2(Options.StdoutPipe->WriteFd, STDERR_FILENO);
+ }
+
+ close(Options.StdoutPipe->WriteFd);
+ // ReadFd has FD_CLOEXEC so it's auto-closed on exec
+ }
+ else if (!Options.StdoutFile.empty())
{
int Fd = open(Options.StdoutFile.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (Fd >= 0)