aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/process.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zencore/process.cpp')
-rw-r--r--src/zencore/process.cpp344
1 files changed, 269 insertions, 75 deletions
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;
}
}
}