aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/zencore/include/zencore/process.h37
-rw-r--r--src/zencore/process.cpp344
-rw-r--r--src/zenserver-test/process-tests.cpp136
-rw-r--r--src/zentest-appstub/zentest-appstub.cpp21
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