diff options
| author | Stefan Boberg <[email protected]> | 2026-04-20 10:59:41 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-20 10:59:41 +0200 |
| commit | 38abebcb6ff417faf431dcaa103bb7f173c4b3f7 (patch) | |
| tree | e03a6f63dafc3e75361f6312621c2699aebbc439 /src | |
| parent | zenhttp: add FollowRedirects option to HttpClient (#982) (diff) | |
| download | archived-zen-38abebcb6ff417faf431dcaa103bb7f173c4b3f7.tar.xz archived-zen-38abebcb6ff417faf431dcaa103bb7f173c4b3f7.zip | |
zencore: CreateProc stdin pipes + BuildArgV quote stripping (#983)
Two related improvements to `CreateProc`:
### 1. Stdin pipe support
- Adds `StdinPipeHandles` + `CreateStdinPipe` alongside the existing `StdoutPipeHandles`, letting callers feed data into a child process's stdin.
- Platform-agnostic RAII (Windows `HANDLE` pair / POSIX `pipe()` fd pair) with the same semantics as the stdout pipe: the inherited end goes to the child, the non-inherited end stays with the parent, destructor closes both.
- `CreateProcOptions` gains a `StdinPipe*` field.
- On Windows, `CreateProcNormal` is reworked so stdin/stdout redirection handles all combinations (stdin + stdout, each alone, neither) uniformly. POSIX already supported arbitrary fd redirection and just needed to honor the new option.
- `zentest-appstub` gains a `-stdin_echo` mode that reads stdin to EOF and echoes it back (switching to binary mode on Windows so CRLF translation doesn't mangle bytes).
- `zenserver-test` gets a `server.process` / `stdin_pipe.*` test group that exercises launching a child with a stdin pipe, writing, closing the write end, and reading back the echoed data.
### 2. Shell-style quote stripping in `BuildArgV`
- Callers that build a single command-line string for `CreateProc` commonly wrap spacey paths in double quotes (e.g. `--tracefile="$path"`). The old `BuildArgV` only used quotes to suppress space-splitting and left the characters in the resulting argv element, so the spawned process saw literal `--tracefile="..."` and the value parser failed to open the quoted path.
- `BuildArgV` now compacts in place, dropping quote chars as it goes, matching shell semantics for paired double quotes.
Diffstat (limited to 'src')
| -rw-r--r-- | src/zencore/include/zencore/process.h | 37 | ||||
| -rw-r--r-- | src/zencore/process.cpp | 344 | ||||
| -rw-r--r-- | src/zenserver-test/process-tests.cpp | 136 | ||||
| -rw-r--r-- | src/zentest-appstub/zentest-appstub.cpp | 21 |
4 files changed, 463 insertions, 75 deletions
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h index fd24a6d7d..eac226683 100644 --- a/src/zencore/include/zencore/process.h +++ b/src/zencore/include/zencore/process.h @@ -160,6 +160,42 @@ struct StdoutPipeHandles // The write end is inheritable; the read end is not. bool CreateStdoutPipe(StdoutPipeHandles& OutPipe); +// Platform-agnostic RAII pipe handles for feeding data into a child's stdin. +// The destructor closes any open handles/fds automatically. +struct StdinPipeHandles +{ + StdinPipeHandles() = default; + ~StdinPipeHandles(); + + StdinPipeHandles(const StdinPipeHandles&) = delete; + StdinPipeHandles& operator=(const StdinPipeHandles&) = delete; + + StdinPipeHandles(StdinPipeHandles&& Other) noexcept; + StdinPipeHandles& operator=(StdinPipeHandles&& Other) noexcept; + + // Close only the read end (call after child is launched so parent doesn't hold it open; + // without this the child sees EOF only after the parent closes too). + void CloseReadEnd(); + + // Close only the write end. Signals EOF to the child once the parent is done writing. + void CloseWriteEnd(); + + // Close both ends of the pipe. + void Close(); + +#if ZEN_PLATFORM_WINDOWS + void* ReadHandle = nullptr; // HANDLE for reading (child side) + void* WriteHandle = nullptr; // HANDLE for writing (parent side) +#else + int ReadFd = -1; + int WriteFd = -1; +#endif +}; + +// Create a pipe suitable for feeding data into child process stdin. +// The read end is inheritable; the write end is not. +bool CreateStdinPipe(StdinPipeHandles& OutPipe); + struct CreateProcOptions { enum @@ -193,6 +229,7 @@ struct CreateProcOptions std::filesystem::path StdoutFile; StdoutPipeHandles* StdoutPipe = nullptr; // Mutually exclusive with StdoutFile. Parent reads from ReadHandle after launch. StdoutPipeHandles* StderrPipe = nullptr; // Optional separate pipe for stderr. When null, stderr shares StdoutPipe. + StdinPipeHandles* StdinPipe = nullptr; // Optional pipe feeding child stdin. Parent writes to WriteHandle after launch. /// Additional environment variables for the child process. These are merged /// with the parent's environment - existing variables are inherited, and diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp index 66062df4d..b95e706d5 100644 --- a/src/zencore/process.cpp +++ b/src/zencore/process.cpp @@ -272,6 +272,79 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe) return true; } +StdinPipeHandles::~StdinPipeHandles() +{ + Close(); +} + +StdinPipeHandles::StdinPipeHandles(StdinPipeHandles&& Other) noexcept +: ReadHandle(std::exchange(Other.ReadHandle, nullptr)) +, WriteHandle(std::exchange(Other.WriteHandle, nullptr)) +{ +} + +StdinPipeHandles& +StdinPipeHandles::operator=(StdinPipeHandles&& Other) noexcept +{ + if (this != &Other) + { + Close(); + ReadHandle = std::exchange(Other.ReadHandle, nullptr); + WriteHandle = std::exchange(Other.WriteHandle, nullptr); + } + return *this; +} + +void +StdinPipeHandles::CloseReadEnd() +{ + if (ReadHandle) + { + CloseHandle(ReadHandle); + ReadHandle = nullptr; + } +} + +void +StdinPipeHandles::CloseWriteEnd() +{ + if (WriteHandle) + { + CloseHandle(WriteHandle); + WriteHandle = nullptr; + } +} + +void +StdinPipeHandles::Close() +{ + CloseReadEnd(); + CloseWriteEnd(); +} + +bool +CreateStdinPipe(StdinPipeHandles& 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 write end should not be inherited by the child + SetHandleInformation(WriteHandle, HANDLE_FLAG_INHERIT, 0); + + OutPipe.ReadHandle = ReadHandle; + OutPipe.WriteHandle = WriteHandle; + return true; +} + #else StdoutPipeHandles::~StdoutPipeHandles() @@ -334,6 +407,72 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe) return true; } +StdinPipeHandles::~StdinPipeHandles() +{ + Close(); +} + +StdinPipeHandles::StdinPipeHandles(StdinPipeHandles&& Other) noexcept +: ReadFd(std::exchange(Other.ReadFd, -1)) +, WriteFd(std::exchange(Other.WriteFd, -1)) +{ +} + +StdinPipeHandles& +StdinPipeHandles::operator=(StdinPipeHandles&& Other) noexcept +{ + if (this != &Other) + { + Close(); + ReadFd = std::exchange(Other.ReadFd, -1); + WriteFd = std::exchange(Other.WriteFd, -1); + } + return *this; +} + +void +StdinPipeHandles::CloseReadEnd() +{ + if (ReadFd >= 0) + { + close(ReadFd); + ReadFd = -1; + } +} + +void +StdinPipeHandles::CloseWriteEnd() +{ + if (WriteFd >= 0) + { + close(WriteFd); + WriteFd = -1; + } +} + +void +StdinPipeHandles::Close() +{ + CloseReadEnd(); + CloseWriteEnd(); +} + +bool +CreateStdinPipe(StdinPipeHandles& OutPipe) +{ + int Fds[2]; + if (pipe(Fds) != 0) + { + return false; + } + OutPipe.ReadFd = Fds[0]; + OutPipe.WriteFd = Fds[1]; + + // Set close-on-exec on the write end so the child doesn't inherit it + fcntl(OutPipe.WriteFd, F_SETFD, FD_CLOEXEC); + return true; +} + #endif ////////////////////////////////////////////////////////////////////////// @@ -715,43 +854,53 @@ ProcessHandle::WaitExitCode() ////////////////////////////////////////////////////////////////////////// #if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS +// Splits an in-process command-line string into argv, in place. Double quotes +// suppress space-splitting and are themselves stripped out (shell-style), so +// `foo "a b" c` -> {"foo", "a b", "c"} and `--k="a b"` -> {"--k=a b"}. +// Quotes do not have to wrap the whole token; pairs anywhere in a token are +// removed. There is no escape mechanism — `\"` is not recognised. static void BuildArgV(std::vector<char*>& Out, char* CommandLine) { - char* Cursor = CommandLine; + char* Read = CommandLine; while (true) { - // Skip leading whitespace - for (; *Cursor == ' '; ++Cursor) + // Skip leading whitespace between tokens + for (; *Read == ' '; ++Read) ; - // Check for nullp terminator - if (*Cursor == '\0') + if (*Read == '\0') { break; } - Out.push_back(Cursor); + // Compact in place: Write trails Read, omitting quote chars + char* Write = Read; + Out.push_back(Write); - // Extract word - int QuoteCount = 0; - do + bool InQuotes = false; + while (*Read != '\0') { - QuoteCount += (*Cursor == '\"'); - if (*Cursor == ' ' && !(QuoteCount & 1)) + if (*Read == '\"') + { + InQuotes = !InQuotes; + ++Read; + continue; + } + if (*Read == ' ' && !InQuotes) { break; } - ++Cursor; - } while (*Cursor != '\0'); + *Write++ = *Read++; + } - if (*Cursor == '\0') + const bool AtEnd = (*Read == '\0'); + *Write = '\0'; + if (AtEnd) { break; } - - *Cursor = '\0'; - ++Cursor; + ++Read; } } @@ -852,19 +1001,25 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma ExtendableWideStringBuilder<256> CommandLineZ; CommandLineZ << CommandLine; - bool DuplicatedStdErr = false; + bool DuplicatedStdErr = false; + bool CreatedStdOutFile = false; + bool UseStdHandles = false; + + if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadHandle != nullptr) + { + StartupInfo.hStdInput = (HANDLE)Options.StdinPipe->ReadHandle; + UseStdHandles = true; + } if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteHandle != nullptr) { - StartupInfo.hStdInput = nullptr; StartupInfo.hStdOutput = (HANDLE)Options.StdoutPipe->WriteHandle; + UseStdHandles = true; 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 { @@ -880,8 +1035,6 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma if (DupSuccess) { DuplicatedStdErr = true; - StartupInfo.dwFlags |= STARTF_USESTDHANDLES; - InheritHandles = true; } } } @@ -892,7 +1045,6 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma sa.lpSecurityDescriptor = nullptr; sa.bInheritHandle = TRUE; - StartupInfo.hStdInput = nullptr; StartupInfo.hStdOutput = CreateFileW(Options.StdoutFile.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, @@ -901,25 +1053,54 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma FILE_ATTRIBUTE_NORMAL, nullptr); - const BOOL Success = DuplicateHandle(GetCurrentProcess(), - StartupInfo.hStdOutput, - GetCurrentProcess(), - &StartupInfo.hStdError, - 0, - TRUE, - DUPLICATE_SAME_ACCESS); + if (StartupInfo.hStdOutput != INVALID_HANDLE_VALUE) + { + CreatedStdOutFile = true; + UseStdHandles = true; + + const BOOL Success = DuplicateHandle(GetCurrentProcess(), + StartupInfo.hStdOutput, + GetCurrentProcess(), + &StartupInfo.hStdError, + 0, + TRUE, + DUPLICATE_SAME_ACCESS); + + if (Success) + { + DuplicatedStdErr = true; + } + else + { + CloseHandle(StartupInfo.hStdOutput); + StartupInfo.hStdOutput = 0; + CreatedStdOutFile = false; + UseStdHandles = (Options.StdinPipe != nullptr && Options.StdinPipe->ReadHandle != nullptr); + } + } + } - if (Success) + if (UseStdHandles) + { + // When STARTF_USESTDHANDLES is set, Windows requires all three handles to be + // specified. Fall back to the parent's current std handles for any that the + // caller didn't supply. This is best-effort: GetStdHandle may return handles + // that are not inheritable in a headless parent, in which case the child will + // see closed handles on those streams. + if (StartupInfo.hStdInput == nullptr) { - DuplicatedStdErr = true; - StartupInfo.dwFlags |= STARTF_USESTDHANDLES; - InheritHandles = true; + StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE); } - else + if (StartupInfo.hStdOutput == nullptr) { - CloseHandle(StartupInfo.hStdOutput); - StartupInfo.hStdOutput = 0; + StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + } + if (StartupInfo.hStdError == nullptr) + { + StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); } + StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + InheritHandles = true; } BOOL Success = CreateProcessW(Executable.c_str(), @@ -941,7 +1122,7 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma 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) + if (CreatedStdOutFile) { CloseHandle(StartupInfo.hStdOutput); } @@ -1168,6 +1349,13 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine chdir(Options.WorkingDirectory->c_str()); } + if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadFd >= 0) + { + dup2(Options.StdinPipe->ReadFd, STDIN_FILENO); + close(Options.StdinPipe->ReadFd); + // WriteFd has FD_CLOEXEC so it's auto-closed on exec + } + if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0) { dup2(Options.StdoutPipe->WriteFd, STDOUT_FILENO); @@ -1248,6 +1436,15 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine } } + if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadFd >= 0) + { + const int StdinReadFd = Options.StdinPipe->ReadFd; + ZEN_ASSERT(StdinReadFd > STDERR_FILENO); + posix_spawn_file_actions_adddup2(&FileActions, StdinReadFd, STDIN_FILENO); + posix_spawn_file_actions_addclose(&FileActions, StdinReadFd); + // WriteFd has FD_CLOEXEC so it's auto-closed on exec + } + if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0) { const int StdoutWriteFd = Options.StdoutPipe->WriteFd; @@ -2263,52 +2460,49 @@ TEST_CASE("GetProcessMetrics") TEST_CASE("BuildArgV") { - const char* Words[] = {"one", "two", "three", "four", "five"}; - struct - { - int WordCount; - const char* Input; - } Cases[] = { - {0, ""}, - {0, " "}, - {1, "one"}, - {1, " one"}, - {1, "one "}, - {2, "one two"}, - {2, " one two"}, - {2, "one two "}, - {2, " one two"}, - {2, "one two "}, - {2, "one two "}, - {3, "one two three"}, - {3, "\"one\" two \"three\""}, - {5, "one two three four five"}, + struct Case + { + std::initializer_list<const char*> Expected; + const char* Input; + }; + const Case Cases[] = { + {{}, ""}, + {{}, " "}, + {{"one"}, "one"}, + {{"one"}, " one"}, + {{"one"}, "one "}, + {{"one", "two"}, "one two"}, + {{"one", "two"}, " one two"}, + {{"one", "two"}, "one two "}, + {{"one", "two", "three"}, "one two three"}, + {{"one", "two", "three", "four", "five"}, "one two three four five"}, + + // Quotes are stripped (shell-style) and suppress space-splitting + {{"one", "two", "three"}, "\"one\" two \"three\""}, + {{"hello world"}, "\"hello world\""}, + {{"--key=hello world"}, "--key=\"hello world\""}, + {{"--key=hello world"}, "\"--key=hello world\""}, + {{"a b", "c d"}, "\"a b\" \"c d\""}, + {{"abc"}, "a\"b\"c"}, + {{""}, "\"\""}, + {{"foo", "bar baz", "qux"}, "foo \"bar baz\" qux"}, }; for (const auto& Case : Cases) { std::vector<char*> OutArgs; - StringBuilder<64> Mutable; + StringBuilder<128> Mutable; Mutable << Case.Input; BuildArgV(OutArgs, Mutable.Data()); - CHECK_EQ(OutArgs.size(), Case.WordCount); + REQUIRE_EQ(OutArgs.size(), Case.Expected.size()); - for (int i = 0, n = int(OutArgs.size()); i < n; ++i) + size_t i = 0; + for (const char* Truth : Case.Expected) { - const char* Truth = Words[i]; - size_t TruthLen = strlen(Truth); - - const char* Candidate = OutArgs[i]; - bool bQuoted = (Candidate[0] == '\"'); - Candidate += bQuoted; - - CHECK(strncmp(Truth, Candidate, TruthLen) == 0); - - if (bQuoted) - { - CHECK_EQ(Candidate[TruthLen], '\"'); - } + CHECK_MESSAGE(std::string_view(OutArgs[i]) == std::string_view(Truth), + fmt::format("input='{}' arg[{}]='{}' expected='{}'", Case.Input, i, OutArgs[i], Truth)); + ++i; } } } diff --git a/src/zenserver-test/process-tests.cpp b/src/zenserver-test/process-tests.cpp index 3f6476810..16af1879a 100644 --- a/src/zenserver-test/process-tests.cpp +++ b/src/zenserver-test/process-tests.cpp @@ -13,6 +13,7 @@ # if ZEN_PLATFORM_WINDOWS # include <zencore/windows.h> # else +# include <cerrno> # include <unistd.h> # endif @@ -51,6 +52,45 @@ ReadAllFromPipe(StdoutPipeHandles& Pipe) return Result; } +// Write all of @p Data to the write end of a StdinPipeHandles. Returns true on success. +static bool +WriteAllToPipe(StdinPipeHandles& Pipe, std::string_view Data) +{ +# if ZEN_PLATFORM_WINDOWS + const char* Ptr = Data.data(); + size_t Remaining = Data.size(); + while (Remaining > 0) + { + DWORD BytesWritten = 0; + if (!::WriteFile(Pipe.WriteHandle, Ptr, static_cast<DWORD>(Remaining), &BytesWritten, nullptr) || BytesWritten == 0) + { + return false; + } + Ptr += BytesWritten; + Remaining -= BytesWritten; + } + return true; +# else + const char* Ptr = Data.data(); + size_t Remaining = Data.size(); + while (Remaining > 0) + { + ssize_t BytesWritten = write(Pipe.WriteFd, Ptr, Remaining); + if (BytesWritten <= 0) + { + if (BytesWritten < 0 && errno == EINTR) + { + continue; + } + return false; + } + Ptr += BytesWritten; + Remaining -= static_cast<size_t>(BytesWritten); + } + return true; +# endif +} + TEST_SUITE_BEGIN("server.process"); ////////////////////////////////////////////////////////////////////////// @@ -276,6 +316,102 @@ TEST_CASE("pipe.separate_stderr") ////////////////////////////////////////////////////////////////////////// +TEST_CASE("stdin_pipe.echo_round_trip") +{ + StdinPipeHandles StdinPipe; + StdoutPipeHandles StdoutPipe; + REQUIRE(CreateStdinPipe(StdinPipe)); + REQUIRE(CreateStdoutPipe(StdoutPipe)); + + const std::string Input = "hello-from-stdin\nsecond-line\n"; + std::filesystem::path AppStub = GetAppStubPath(); + std::string CommandLine = "zentest-appstub -stdin_echo"; + + CreateProcOptions Options; + Options.StdinPipe = &StdinPipe; + Options.StdoutPipe = &StdoutPipe; + + ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); + + // Close the read end (child side) so that when we finish writing and close the write end, + // the child sees EOF. Also close the stdout write end so our reads see EOF after the child exits. + StdinPipe.CloseReadEnd(); + StdoutPipe.CloseWriteEnd(); + + REQUIRE(WriteAllToPipe(StdinPipe, Input)); + StdinPipe.CloseWriteEnd(); // signals EOF to the child + + std::string Output = ReadAllFromPipe(StdoutPipe); + + Process.Wait(); + + CHECK(Output.find(Input) != std::string::npos); + CHECK_EQ(Process.GetExitCode(), 0); +} + +TEST_CASE("stdin_pipe.raii_cleanup") +{ + for (int i = 0; i < 100; ++i) + { + StdinPipeHandles Pipe; + REQUIRE(CreateStdinPipe(Pipe)); + } +} + +TEST_CASE("stdin_pipe.move_semantics") +{ + StdinPipeHandles Original; + REQUIRE(CreateStdinPipe(Original)); + + StdinPipeHandles 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 + + StdinPipeHandles 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 +} + +TEST_CASE("stdin_pipe.close_is_idempotent") +{ + StdinPipeHandles Pipe; + REQUIRE(CreateStdinPipe(Pipe)); + + Pipe.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_SUITE_END(); } // namespace zen::tests diff --git a/src/zentest-appstub/zentest-appstub.cpp b/src/zentest-appstub/zentest-appstub.cpp index 73cb7ff2d..8f7c2b166 100644 --- a/src/zentest-appstub/zentest-appstub.cpp +++ b/src/zentest-appstub/zentest-appstub.cpp @@ -24,6 +24,11 @@ #include <system_error> #include <thread> +#if ZEN_PLATFORM_WINDOWS +# include <fcntl.h> +# include <io.h> +#endif + using namespace std::literals; using namespace zen; @@ -354,6 +359,22 @@ main(int argc, char* argv[]) fprintf(stderr, "%.*s", static_cast<int>(Message.size()), Message.data()); fflush(stderr); } + else if (std::strcmp(argv[i], "-stdin_echo") == 0) + { + // Read stdin to EOF and echo it to stdout. Useful for testing stdin pipe wiring. + // Switch to binary mode on Windows so CRLF translation doesn't mangle the bytes. +#if ZEN_PLATFORM_WINDOWS + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif + char Buffer[4096]; + size_t Bytes = 0; + while ((Bytes = fread(Buffer, 1, sizeof(Buffer), stdin)) > 0) + { + fwrite(Buffer, 1, Bytes, stdout); + } + fflush(stdout); + } else if ((_strnicmp(argv[i], "-input=", 7) == 0) || (_strnicmp(argv[i], "-i=", 3) == 0)) { /* mimic DDC2 |