diff options
| author | Stefan Boberg <[email protected]> | 2026-03-21 21:43:22 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-21 21:43:22 +0100 |
| commit | 14ca5b35d0fc477ba30f10b80f937b523fd7e930 (patch) | |
| tree | 8aab2acfec8be1af4bf0dffdb4badc3b64bf8385 /src/zencore/process.cpp | |
| parent | fix null stats provider crash when build store is not configured (#875) (diff) | |
| download | zen-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.cpp | 237 |
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) |