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/zenserver-test/process-tests.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/zenserver-test/process-tests.cpp')
| -rw-r--r-- | src/zenserver-test/process-tests.cpp | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/zenserver-test/process-tests.cpp b/src/zenserver-test/process-tests.cpp new file mode 100644 index 000000000..649f24f54 --- /dev/null +++ b/src/zenserver-test/process-tests.cpp @@ -0,0 +1,298 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/zencore.h> + +#if ZEN_WITH_TESTS + +# include "zenserver-test.h" + +# include <zencore/filesystem.h> +# include <zencore/process.h> +# include <zencore/testing.h> + +# if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> +# else +# include <unistd.h> +# endif + +namespace zen::tests { + +using namespace std::literals; + +static std::filesystem::path +GetAppStubPath() +{ + return TestEnv.ProgramBaseDir() / ("zentest-appstub" ZEN_EXE_SUFFIX_LITERAL); +} + +// Read all available data from the read end of a StdoutPipeHandles. +// Must be called after CloseWriteEnd() so that the read will see EOF. +static std::string +ReadAllFromPipe(StdoutPipeHandles& Pipe) +{ + std::string Result; + char Buffer[4096]; + +# if ZEN_PLATFORM_WINDOWS + DWORD BytesRead = 0; + while (::ReadFile(Pipe.ReadHandle, Buffer, sizeof(Buffer), &BytesRead, nullptr) && BytesRead > 0) + { + Result.append(Buffer, BytesRead); + } +# else + ssize_t BytesRead = 0; + while ((BytesRead = read(Pipe.ReadFd, Buffer, sizeof(Buffer))) > 0) + { + Result.append(Buffer, static_cast<size_t>(BytesRead)); + } +# endif + + return Result; +} + +TEST_SUITE_BEGIN("server.process"); + +////////////////////////////////////////////////////////////////////////// + +TEST_CASE("pipe.capture_stdout") +{ + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + + const std::string ExpectedOutput = "hello_from_pipe_test"; + std::filesystem::path AppStub = GetAppStubPath(); + std::string CommandLine = fmt::format("zentest-appstub -echo={}", ExpectedOutput); + + CreateProcOptions Options; + Options.StdoutPipe = &Pipe; + + CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); + + ProcessHandle Process; + Process.Initialize(ProcResult); + + // Close the write end, then drain before Wait() to avoid deadlock if output fills the pipe buffer. + Pipe.CloseWriteEnd(); + + std::string Output = ReadAllFromPipe(Pipe); + + Process.Wait(); + + // The appstub also prints "[zentest] exiting with exit code: 0\n" + CHECK(Output.find(ExpectedOutput) != std::string::npos); + CHECK_EQ(Process.GetExitCode(), 0); +} + +TEST_CASE("pipe.capture_multiline") +{ + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + + std::filesystem::path AppStub = GetAppStubPath(); + std::string CommandLine = "zentest-appstub -echo=line1 -echo=line2 -echo=line3"; + + CreateProcOptions Options; + Options.StdoutPipe = &Pipe; + + CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); + + ProcessHandle Process; + Process.Initialize(ProcResult); + + Pipe.CloseWriteEnd(); + + std::string Output = ReadAllFromPipe(Pipe); + + Process.Wait(); + + CHECK(Output.find("line1") != std::string::npos); + CHECK(Output.find("line2") != std::string::npos); + CHECK(Output.find("line3") != std::string::npos); + CHECK_EQ(Process.GetExitCode(), 0); +} + +TEST_CASE("pipe.raii_cleanup") +{ + // Verify that StdoutPipeHandles cleans up handles when it goes out of scope + // (no leaked handles). We can't directly assert on handle counts, but we can + // verify that creating and destroying many pipes doesn't fail. + for (int i = 0; i < 100; ++i) + { + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + // Pipe goes out of scope here — destructor should close both ends + } +} + +TEST_CASE("pipe.move_semantics") +{ + StdoutPipeHandles Original; + REQUIRE(CreateStdoutPipe(Original)); + + // Move-construct a new pipe from Original + StdoutPipeHandles Moved(std::move(Original)); + +# if ZEN_PLATFORM_WINDOWS + CHECK(Moved.ReadHandle != nullptr); + CHECK(Moved.WriteHandle != nullptr); + CHECK(Original.ReadHandle == nullptr); + CHECK(Original.WriteHandle == nullptr); +# else + CHECK(Moved.ReadFd >= 0); + CHECK(Moved.WriteFd >= 0); + CHECK(Original.ReadFd == -1); + CHECK(Original.WriteFd == -1); +# endif + + // Move-assign + StdoutPipeHandles Assigned; + Assigned = std::move(Moved); + +# if ZEN_PLATFORM_WINDOWS + CHECK(Assigned.ReadHandle != nullptr); + CHECK(Assigned.WriteHandle != nullptr); + CHECK(Moved.ReadHandle == nullptr); + CHECK(Moved.WriteHandle == nullptr); +# else + CHECK(Assigned.ReadFd >= 0); + CHECK(Assigned.WriteFd >= 0); + CHECK(Moved.ReadFd == -1); + CHECK(Moved.WriteFd == -1); +# endif + + // Assigned goes out of scope — destructor closes handles +} + +TEST_CASE("pipe.close_is_idempotent") +{ + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + + Pipe.Close(); + // Calling Close again should be safe (no double-close) + Pipe.Close(); + +# if ZEN_PLATFORM_WINDOWS + CHECK(Pipe.ReadHandle == nullptr); + CHECK(Pipe.WriteHandle == nullptr); +# else + CHECK(Pipe.ReadFd == -1); + CHECK(Pipe.WriteFd == -1); +# endif +} + +TEST_CASE("pipe.close_write_end_only") +{ + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + + Pipe.CloseWriteEnd(); + +# if ZEN_PLATFORM_WINDOWS + CHECK(Pipe.ReadHandle != nullptr); + CHECK(Pipe.WriteHandle == nullptr); +# else + CHECK(Pipe.ReadFd >= 0); + CHECK(Pipe.WriteFd == -1); +# endif + + // Remaining read handle cleaned up by destructor +} + +TEST_CASE("pipe.capture_with_nonzero_exit") +{ + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + + std::filesystem::path AppStub = GetAppStubPath(); + std::string CommandLine = "zentest-appstub -echo=before_exit -f=42"; + + CreateProcOptions Options; + Options.StdoutPipe = &Pipe; + + CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); + + ProcessHandle Process; + Process.Initialize(ProcResult); + + Pipe.CloseWriteEnd(); + + std::string Output = ReadAllFromPipe(Pipe); + + Process.Wait(); + + CHECK(Output.find("before_exit") != std::string::npos); + CHECK_EQ(Process.GetExitCode(), 42); +} + +TEST_CASE("pipe.stderr_on_shared_pipe") +{ + StdoutPipeHandles Pipe; + REQUIRE(CreateStdoutPipe(Pipe)); + + std::filesystem::path AppStub = GetAppStubPath(); + std::string CommandLine = "zentest-appstub -echo=from_stdout -echoerr=from_stderr"; + + CreateProcOptions Options; + Options.StdoutPipe = &Pipe; + + CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); + + ProcessHandle Process; + Process.Initialize(ProcResult); + + Pipe.CloseWriteEnd(); + + std::string Output = ReadAllFromPipe(Pipe); + + Process.Wait(); + + // Both stdout and stderr content should appear on the shared pipe + CHECK(Output.find("from_stdout") != std::string::npos); + CHECK(Output.find("from_stderr") != std::string::npos); + CHECK_EQ(Process.GetExitCode(), 0); +} + +TEST_CASE("pipe.separate_stderr") +{ + StdoutPipeHandles StdoutPipe; + StdoutPipeHandles StderrPipe; + REQUIRE(CreateStdoutPipe(StdoutPipe)); + REQUIRE(CreateStdoutPipe(StderrPipe)); + + std::filesystem::path AppStub = GetAppStubPath(); + std::string CommandLine = "zentest-appstub -echo=on_stdout -echoerr=on_stderr"; + + CreateProcOptions Options; + Options.StdoutPipe = &StdoutPipe; + Options.StderrPipe = &StderrPipe; + + CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); + + ProcessHandle Process; + Process.Initialize(ProcResult); + + StdoutPipe.CloseWriteEnd(); + StderrPipe.CloseWriteEnd(); + + std::string StdoutOutput = ReadAllFromPipe(StdoutPipe); + std::string StderrOutput = ReadAllFromPipe(StderrPipe); + + Process.Wait(); + + CHECK(StdoutOutput.find("on_stdout") != std::string::npos); + CHECK(StderrOutput.find("on_stderr") != std::string::npos); + // Verify separation: stderr content should NOT appear in stdout pipe + CHECK(StdoutOutput.find("on_stderr") == std::string::npos); + CHECK(StderrOutput.find("on_stdout") == std::string::npos); + CHECK_EQ(Process.GetExitCode(), 0); +} + +////////////////////////////////////////////////////////////////////////// + +TEST_SUITE_END(); + +} // namespace zen::tests + +#endif |