Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Runner/Process.cpp
diff options
context:
space:
mode:
authorLeitwolf2026-10-24 19:03:40 +0300
committerGitHub2026-10-24 19:03:40 +0300
commit4f1c759cffcb14bd6511e1d7c5fbace682e24f2d (patch)
tree415fe5d48a5b48650bba3747beaf245d727c4f61 /src/Progmasoft/Runner/Process.cpp
parent83c6fc3ac13abe83fa39f7b5f8559ccaf4412233 (diff)
parent622188f9c08e4af35941c146979883ed4882410e (diff)
downloadcatch3-4f1c759cffcb14bd6511e1d7c5fbace682e24f2d.tar.gz
Merge pull request #15 from Progmasoft/feature/catch3-runner
Replace Catch2's runner with the Catch3 runner
Diffstat (limited to 'src/Progmasoft/Runner/Process.cpp')
-rw-r--r--src/Progmasoft/Runner/Process.cpp1289
1 files changed, 1289 insertions, 0 deletions
diff --git a/src/Progmasoft/Runner/Process.cpp b/src/Progmasoft/Runner/Process.cpp
new file mode 100644
index 00000000..8ef02327
--- /dev/null
+++ b/src/Progmasoft/Runner/Process.cpp
@@ -0,0 +1,1289 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+#include <algorithm>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <thread>
+
+#include "Process.hpp"
+
+#if defined(_WIN32)
+# ifndef NOMINMAX
+# define NOMINMAX
+# endif
+# ifndef WIN32_LEAN_AND_MEAN
+# define WIN32_LEAN_AND_MEAN
+# endif
+# include <fcntl.h>
+# include <io.h>
+# include <share.h>
+# include <sys/stat.h>
+# include <windows.h>
+#else
+# include <cerrno>
+# include <csignal>
+# include <fcntl.h>
+# include <spawn.h>
+# include <sys/types.h>
+# include <sys/wait.h>
+# include <unistd.h>
+# if defined(__APPLE__)
+# include <crt_externs.h>
+# include <mach-o/dyld.h>
+# endif
+# if !defined(__APPLE__) && !(defined(__GLIBC__) && defined(_GNU_SOURCE))
+extern char **environ;
+# endif
+#endif
+
+namespace Progmasoft::Catch3::Runner
+{
+ namespace
+ {
+#if defined(_WIN32)
+ HANDLE
+ AsHandle(std::intptr_t value) noexcept
+ {
+ return reinterpret_cast<HANDLE>(value);
+ }
+
+ std::intptr_t
+ FromHandle(HANDLE handle) noexcept
+ {
+ return reinterpret_cast<std::intptr_t>(handle);
+ }
+
+ std::wstring
+ Widen(std::string_view text)
+ {
+ if (text.empty())
+ {
+ return {};
+ }
+ const int size = MultiByteToWideChar(CP_UTF8,
+ 0,
+ text.data(),
+ static_cast<int>(text.size()),
+ nullptr,
+ 0);
+ std::wstring wide(static_cast<std::size_t>(size), L'\0');
+ MultiByteToWideChar(CP_UTF8,
+ 0,
+ text.data(),
+ static_cast<int>(text.size()),
+ wide.data(),
+ size);
+ return wide;
+ }
+
+ std::string
+ Narrow(std::wstring_view text)
+ {
+ if (text.empty())
+ {
+ return {};
+ }
+ const int size = WideCharToMultiByte(CP_UTF8,
+ 0,
+ text.data(),
+ static_cast<int>(text.size()),
+ nullptr,
+ 0,
+ nullptr,
+ nullptr);
+ std::string narrow(static_cast<std::size_t>(size), '\0');
+ WideCharToMultiByte(CP_UTF8,
+ 0,
+ text.data(),
+ static_cast<int>(text.size()),
+ narrow.data(),
+ size,
+ nullptr,
+ nullptr);
+ return narrow;
+ }
+
+ std::string
+ LastErrorMessage(const char *what)
+ {
+ const DWORD code = GetLastError();
+ wchar_t *buffer = nullptr;
+ const DWORD length = FormatMessageW(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
+ | FORMAT_MESSAGE_IGNORE_INSERTS,
+ nullptr,
+ code,
+ 0,
+ reinterpret_cast<wchar_t *>(&buffer),
+ 0,
+ nullptr);
+ std::string message
+ = length > 0 ? Narrow(std::wstring_view{ buffer, length })
+ : std::string{};
+ if (buffer != nullptr)
+ {
+ LocalFree(buffer);
+ }
+ while (!message.empty()
+ && (message.back() == '\n' || message.back() == '\r'
+ || message.back() == ' ' || message.back() == '.'))
+ {
+ message.pop_back();
+ }
+ return std::string{ what } + ": " + message + " (error "
+ + std::to_string(code) + ")";
+ }
+
+ /// Quotes one argument the way the Microsoft C runtime splits a
+ /// command line back into `argv`.
+ void
+ AppendQuotedArgument(std::wstring &commandLine,
+ const std::wstring &argument)
+ {
+ const bool needsQuotes
+ = argument.empty()
+ || argument.find_first_of(L" \t\n\v\"") != std::wstring::npos;
+ if (!needsQuotes)
+ {
+ commandLine += argument;
+ return;
+ }
+ commandLine.push_back(L'"');
+ std::size_t backslashes = 0;
+ for (const wchar_t character : argument)
+ {
+ if (character == L'\\')
+ {
+ ++backslashes;
+ continue;
+ }
+ if (character == L'"')
+ {
+ commandLine.append(backslashes * 2 + 1, L'\\');
+ }
+ else
+ {
+ commandLine.append(backslashes, L'\\');
+ }
+ backslashes = 0;
+ commandLine.push_back(character);
+ }
+ commandLine.append(backslashes * 2, L'\\');
+ commandLine.push_back(L'"');
+ }
+
+ std::wstring
+ EnvironmentBlock(
+ const std::vector<std::pair<std::string, std::string>> &additions)
+ {
+ std::vector<std::wstring> entries;
+ wchar_t *current = GetEnvironmentStringsW();
+ if (current != nullptr)
+ {
+ for (const wchar_t *entry = current; *entry != L'\0';
+ entry += wcslen(entry) + 1)
+ {
+ entries.emplace_back(entry);
+ }
+ FreeEnvironmentStringsW(current);
+ }
+ for (const auto &[name, value] : additions)
+ {
+ const std::wstring wideName = Widen(name);
+ std::erase_if(entries, [&](const std::wstring &entry) {
+ // A leading '=' marks the hidden per-drive variables.
+ const auto equals = entry.find(L'=', 1);
+ return equals == wideName.size()
+ && CompareStringOrdinal(
+ entry.data(),
+ static_cast<int>(equals),
+ wideName.data(),
+ static_cast<int>(wideName.size()),
+ TRUE)
+ == CSTR_EQUAL;
+ });
+ entries.push_back(wideName + L"=" + Widen(value));
+ }
+ std::wstring block;
+ for (const auto &entry : entries)
+ {
+ block += entry;
+ block.push_back(L'\0');
+ }
+ block.push_back(L'\0');
+ return block;
+ }
+#else
+ int
+ Descriptor(std::intptr_t value) noexcept
+ {
+ return static_cast<int>(value);
+ }
+
+ char **
+ CurrentEnvironment() noexcept
+ {
+# if defined(__APPLE__)
+ return *_NSGetEnviron();
+# else
+ return environ;
+# endif
+ }
+
+ std::string
+ ErrnoMessage(const char *what, int error)
+ {
+ return std::string{ what } + ": " + std::strerror(error);
+ }
+
+ bool
+ MakePipe(int (&descriptors)[2])
+ {
+# if defined(__linux__)
+ return pipe2(descriptors, O_CLOEXEC) == 0;
+# else
+ // Only the scheduler thread starts processes, so no other
+ // thread can fork between creating the pipe and marking it.
+ if (pipe(descriptors) != 0)
+ {
+ return false;
+ }
+ fcntl(descriptors[0], F_SETFD, FD_CLOEXEC);
+ fcntl(descriptors[1], F_SETFD, FD_CLOEXEC);
+ return true;
+# endif
+ }
+
+ const char *
+ SignalName(int signal) noexcept
+ {
+ switch (signal)
+ {
+ case SIGSEGV:
+ return "SIGSEGV";
+ case SIGABRT:
+ return "SIGABRT";
+ case SIGBUS:
+ return "SIGBUS";
+ case SIGFPE:
+ return "SIGFPE";
+ case SIGILL:
+ return "SIGILL";
+ case SIGKILL:
+ return "SIGKILL";
+ case SIGTERM:
+ return "SIGTERM";
+ case SIGPIPE:
+ return "SIGPIPE";
+ case SIGINT:
+ return "SIGINT";
+ default:
+ return nullptr;
+ }
+ }
+#endif
+
+#if defined(_WIN32)
+ const char *
+ ExceptionName(std::int64_t code) noexcept
+ {
+ switch (static_cast<DWORD>(code))
+ {
+ case 0xC0000005:
+ return "access violation";
+ case 0xC00000FD:
+ return "stack overflow";
+ case 0xC0000094:
+ return "integer division by zero";
+ case 0xC000008E:
+ return "floating-point division by zero";
+ case 0xC000001D:
+ return "illegal instruction";
+ case 0xC0000409:
+ return "stack buffer overrun or fast fail";
+ case 0xC0000374:
+ return "heap corruption";
+ case 0x80000003:
+ return "breakpoint";
+ case 0xE06D7363:
+ return "unhandled C++ exception";
+ default:
+ return nullptr;
+ }
+ }
+#endif
+ } // namespace
+
+ std::string
+ ExitStatus::Describe() const
+ {
+ switch (kind)
+ {
+ case Kind::kExited:
+ return "exit code " + std::to_string(code);
+ case Kind::kSignaled:
+ {
+ std::string text = "signal " + std::to_string(code);
+#if !defined(_WIN32)
+ if (const char *name = SignalName(static_cast<int>(code)))
+ {
+ text += std::string{ " (" } + name + ")";
+ }
+#endif
+ return text;
+ }
+ case Kind::kException:
+ {
+ char buffer[32];
+ std::snprintf(buffer,
+ sizeof(buffer),
+ "0x%08llX",
+ static_cast<unsigned long long>(code));
+ std::string text = std::string{ "exception " } + buffer;
+#if defined(_WIN32)
+ if (const char *name = ExceptionName(code))
+ {
+ text += std::string{ " (" } + name + ")";
+ }
+#endif
+ return text;
+ }
+ }
+ return "unknown status";
+ }
+
+ PipeReader::PipeReader(PipeReader &&other) noexcept
+ : handle_{ other.handle_ }
+ {
+ other.handle_ = -1;
+ }
+
+ PipeReader &
+ PipeReader::operator=(PipeReader &&other) noexcept
+ {
+ if (this != &other)
+ {
+ Close();
+ handle_ = other.handle_;
+ other.handle_ = -1;
+ }
+ return *this;
+ }
+
+ PipeReader::~PipeReader()
+ {
+ Close();
+ }
+
+ bool
+ PipeReader::IsOpen() const noexcept
+ {
+ return handle_ != -1;
+ }
+
+ void
+ PipeReader::Close() noexcept
+ {
+ if (handle_ == -1)
+ {
+ return;
+ }
+#if defined(_WIN32)
+ CloseHandle(AsHandle(handle_));
+#else
+ close(Descriptor(handle_));
+#endif
+ handle_ = -1;
+ }
+
+ std::int64_t
+ PipeReader::Read(char *buffer, std::size_t size) noexcept
+ {
+ if (handle_ == -1)
+ {
+ return -1;
+ }
+#if defined(_WIN32)
+ DWORD read = 0;
+ const DWORD request
+ = size > 0x10000000 ? 0x10000000 : static_cast<DWORD>(size);
+ if (!::ReadFile(AsHandle(handle_), buffer, request, &read, nullptr))
+ {
+ const DWORD error = GetLastError();
+ return error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA ? 0
+ : -1;
+ }
+ return static_cast<std::int64_t>(read);
+#else
+ while (true)
+ {
+ const auto count = read(Descriptor(handle_), buffer, size);
+ if (count >= 0)
+ {
+ return static_cast<std::int64_t>(count);
+ }
+ if (errno != EINTR)
+ {
+ return -1;
+ }
+ }
+#endif
+ }
+
+ PipeWriter::PipeWriter(PipeWriter &&other) noexcept
+ : handle_{ other.handle_ }
+ {
+ other.handle_ = -1;
+ }
+
+ PipeWriter &
+ PipeWriter::operator=(PipeWriter &&other) noexcept
+ {
+ if (this != &other)
+ {
+ Close();
+ handle_ = other.handle_;
+ other.handle_ = -1;
+ }
+ return *this;
+ }
+
+ PipeWriter::~PipeWriter()
+ {
+ Close();
+ }
+
+ bool
+ PipeWriter::IsOpen() const noexcept
+ {
+ return handle_ != -1;
+ }
+
+ void
+ PipeWriter::Close() noexcept
+ {
+ if (handle_ == -1)
+ {
+ return;
+ }
+#if defined(_WIN32)
+ CloseHandle(AsHandle(handle_));
+#else
+ close(Descriptor(handle_));
+#endif
+ handle_ = -1;
+ }
+
+ Status
+ PipeWriter::Write(std::string_view data) noexcept
+ {
+ if (handle_ == -1)
+ {
+ return Error{ "the pipe is closed" };
+ }
+ while (!data.empty())
+ {
+#if defined(_WIN32)
+ DWORD written = 0;
+ const DWORD request = data.size() > 0x10000000
+ ? 0x10000000
+ : static_cast<DWORD>(data.size());
+ if (!::WriteFile(AsHandle(handle_),
+ data.data(),
+ request,
+ &written,
+ nullptr))
+ {
+ return Error{ "the other end of the pipe is gone" };
+ }
+ data.remove_prefix(written);
+#else
+ const auto count
+ = write(Descriptor(handle_), data.data(), data.size());
+ if (count < 0)
+ {
+ if (errno == EINTR)
+ {
+ continue;
+ }
+ return Error{ ErrnoMessage("cannot write to the pipe", errno) };
+ }
+ data.remove_prefix(static_cast<std::size_t>(count));
+#endif
+ }
+ return Status::Ok();
+ }
+
+ ChildProcess::~ChildProcess()
+ {
+ if (!exit_)
+ {
+ Kill();
+ static_cast<void>(Wait());
+ }
+#if defined(_WIN32)
+ if (process_ != -1)
+ {
+ CloseHandle(AsHandle(process_));
+ }
+ if (group_ != -1)
+ {
+ CloseHandle(AsHandle(group_));
+ }
+#endif
+ }
+
+#if defined(_WIN32)
+ Expected<std::unique_ptr<ChildProcess>>
+ ChildProcess::Start(const ChildOptions &options)
+ {
+ if (options.arguments.empty())
+ {
+ return Error{ "no program to start" };
+ }
+ SECURITY_ATTRIBUTES inheritable{};
+ inheritable.nLength = sizeof(inheritable);
+ inheritable.bInheritHandle = TRUE;
+
+ HANDLE pipes[3][2] = {};
+ for (auto &pipe : pipes)
+ {
+ if (!CreatePipe(&pipe[0], &pipe[1], &inheritable, 0))
+ {
+ const std::string message
+ = LastErrorMessage("cannot create a pipe");
+ for (auto &created : pipes)
+ {
+ for (HANDLE handle : created)
+ {
+ if (handle != nullptr)
+ {
+ CloseHandle(handle);
+ }
+ }
+ }
+ return Error{ message };
+ }
+ }
+ // The child gets the read end of its input and the write ends of its
+ // output; the ends this process keeps are not inheritable.
+ HANDLE childInput = pipes[0][0];
+ HANDLE parentInput = pipes[0][1];
+ HANDLE parentOutput = pipes[1][0];
+ HANDLE childOutput = pipes[1][1];
+ HANDLE parentErrors = pipes[2][0];
+ HANDLE childErrors = pipes[2][1];
+ SetHandleInformation(parentInput, HANDLE_FLAG_INHERIT, 0);
+ SetHandleInformation(parentOutput, HANDLE_FLAG_INHERIT, 0);
+ SetHandleInformation(parentErrors, HANDLE_FLAG_INHERIT, 0);
+
+ auto closeChildEnds = [&] {
+ CloseHandle(childInput);
+ CloseHandle(childOutput);
+ CloseHandle(childErrors);
+ };
+
+ std::unique_ptr<ChildProcess> child{ new ChildProcess{} };
+ child->input_ = PipeWriter{ FromHandle(parentInput) };
+ child->output_ = PipeReader{ FromHandle(parentOutput) };
+ child->errors_ = PipeReader{ FromHandle(parentErrors) };
+
+ // Only these three handles are inherited, so a worker never holds
+ // the pipes of another one open.
+ HANDLE inherited[3] = { childInput, childOutput, childErrors };
+ SIZE_T attributeSize = 0;
+ InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeSize);
+ std::vector<unsigned char> attributeStorage(attributeSize);
+ auto *attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(
+ attributeStorage.data());
+ if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attributeSize)
+ || !UpdateProcThreadAttribute(attributes,
+ 0,
+ PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ inherited,
+ sizeof(inherited),
+ nullptr,
+ nullptr))
+ {
+ const std::string message
+ = LastErrorMessage("cannot prepare the process attributes");
+ closeChildEnds();
+ return Error{ message };
+ }
+
+ STARTUPINFOEXW startup{};
+ startup.StartupInfo.cb = sizeof(startup);
+ startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
+ startup.StartupInfo.hStdInput = childInput;
+ startup.StartupInfo.hStdOutput = childOutput;
+ startup.StartupInfo.hStdError = childErrors;
+ startup.lpAttributeList = attributes;
+
+ std::wstring program = Widen(options.arguments.front());
+ std::wstring commandLine;
+ for (std::size_t index = 0; index < options.arguments.size(); ++index)
+ {
+ if (index > 0)
+ {
+ commandLine.push_back(L' ');
+ }
+ AppendQuotedArgument(commandLine, Widen(options.arguments[index]));
+ }
+ std::wstring environment = EnvironmentBlock(options.environment);
+ const std::wstring directory = options.workingDirectory.wstring();
+
+ PROCESS_INFORMATION information{};
+ const BOOL created
+ = CreateProcessW(options.searchPath ? nullptr : program.c_str(),
+ commandLine.data(),
+ nullptr,
+ nullptr,
+ TRUE,
+ CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED
+ | EXTENDED_STARTUPINFO_PRESENT,
+ environment.data(),
+ directory.empty() ? nullptr : directory.c_str(),
+ &startup.StartupInfo,
+ &information);
+ DeleteProcThreadAttributeList(attributes);
+ if (!created)
+ {
+ const std::string message = LastErrorMessage(
+ ("cannot start '" + options.arguments.front() + "'").c_str());
+ closeChildEnds();
+ return Error{ message };
+ }
+ closeChildEnds();
+
+ // A job ends the whole tree at once, and its limit keeps a crash from
+ // waiting on the error-reporting dialog.
+ HANDLE job = CreateJobObjectW(nullptr, nullptr);
+ if (job != nullptr)
+ {
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
+ limits.BasicLimitInformation.LimitFlags
+ = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
+ | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
+ SetInformationJobObject(job,
+ JobObjectExtendedLimitInformation,
+ &limits,
+ sizeof(limits));
+ if (!AssignProcessToJobObject(job, information.hProcess))
+ {
+ CloseHandle(job);
+ job = nullptr;
+ }
+ }
+ ResumeThread(information.hThread);
+ CloseHandle(information.hThread);
+
+ child->id_ = static_cast<std::int64_t>(information.dwProcessId);
+ child->process_ = FromHandle(information.hProcess);
+ child->group_ = job == nullptr ? -1 : FromHandle(job);
+ return child;
+ }
+
+ void
+ ChildProcess::Kill() noexcept
+ {
+ if (exit_)
+ {
+ return;
+ }
+ if (group_ != -1)
+ {
+ TerminateJobObject(AsHandle(group_), 0xC000013A);
+ }
+ else if (process_ != -1)
+ {
+ TerminateProcess(AsHandle(process_), 0xC000013A);
+ }
+ }
+
+ ExitStatus
+ ChildProcess::Wait() noexcept
+ {
+ if (exit_)
+ {
+ return *exit_;
+ }
+ ExitStatus status;
+ if (process_ != -1)
+ {
+ WaitForSingleObject(AsHandle(process_), INFINITE);
+ DWORD code = 0;
+ GetExitCodeProcess(AsHandle(process_), &code);
+ status.code = static_cast<std::int64_t>(code);
+ // NTSTATUS error codes have both top bits set.
+ status.kind
+ = (code & 0xC0000000U) == 0xC0000000U || code == 0x80000003U
+ ? ExitStatus::Kind::kException
+ : ExitStatus::Kind::kExited;
+ if (status.kind == ExitStatus::Kind::kExited && code > 0x7FFFFFFFU)
+ {
+ status.code = static_cast<std::int32_t>(code);
+ }
+ }
+ exit_ = status;
+ return status;
+ }
+
+ std::optional<ExitStatus>
+ ChildProcess::WaitFor(std::chrono::milliseconds timeout) noexcept
+ {
+ if (exit_ || process_ == -1)
+ {
+ return Wait();
+ }
+ const auto waited = WaitForSingleObject(
+ AsHandle(process_),
+ static_cast<DWORD>(std::max<long long>(0, timeout.count())));
+ if (waited == WAIT_TIMEOUT)
+ {
+ return std::nullopt;
+ }
+ return Wait();
+ }
+
+ Expected<WorkerStreams>
+ TakeStandardStreams()
+ {
+ HANDLE process = GetCurrentProcess();
+ HANDLE input = nullptr;
+ HANDLE output = nullptr;
+ if (!DuplicateHandle(process,
+ GetStdHandle(STD_INPUT_HANDLE),
+ process,
+ &input,
+ 0,
+ FALSE,
+ DUPLICATE_SAME_ACCESS)
+ || !DuplicateHandle(process,
+ GetStdHandle(STD_OUTPUT_HANDLE),
+ process,
+ &output,
+ 0,
+ FALSE,
+ DUPLICATE_SAME_ACCESS))
+ {
+ return Error{ LastErrorMessage(
+ "cannot take the standard streams") };
+ }
+ std::fflush(stdout);
+ int nullDescriptor = -1;
+ _sopen_s(&nullDescriptor,
+ "NUL",
+ _O_RDONLY | _O_BINARY,
+ _SH_DENYNO,
+ _S_IREAD);
+ if (nullDescriptor >= 0)
+ {
+ _dup2(nullDescriptor, 0);
+ _close(nullDescriptor);
+ }
+ _dup2(2, 1);
+ // The runner writes what the worker prints through its own text-mode
+ // streams; translating line ends here as well would double them.
+ _setmode(1, _O_BINARY);
+ _setmode(2, _O_BINARY);
+ SetStdHandle(STD_INPUT_HANDLE,
+ reinterpret_cast<HANDLE>(_get_osfhandle(0)));
+ SetStdHandle(STD_OUTPUT_HANDLE, GetStdHandle(STD_ERROR_HANDLE));
+ WorkerStreams streams;
+ streams.commands = PipeReader{ FromHandle(input) };
+ streams.events = PipeWriter{ FromHandle(output) };
+ return streams;
+ }
+
+ std::filesystem::path
+ CurrentExecutable(const char *argumentZero)
+ {
+ std::wstring buffer(MAX_PATH, L'\0');
+ while (true)
+ {
+ const DWORD length
+ = GetModuleFileNameW(nullptr,
+ buffer.data(),
+ static_cast<DWORD>(buffer.size()));
+ if (length == 0)
+ {
+ return argumentZero == nullptr
+ ? std::filesystem::path{}
+ : std::filesystem::path{ argumentZero };
+ }
+ if (length < buffer.size())
+ {
+ buffer.resize(length);
+ return std::filesystem::path{ buffer };
+ }
+ buffer.resize(buffer.size() * 2);
+ }
+ }
+
+ std::string
+ ReadEnvironment(const char *name)
+ {
+ const std::wstring wideName = Widen(name);
+ const DWORD size
+ = GetEnvironmentVariableW(wideName.c_str(), nullptr, 0);
+ if (size == 0)
+ {
+ return {};
+ }
+ std::wstring value(size, L'\0');
+ const DWORD length
+ = GetEnvironmentVariableW(wideName.c_str(), value.data(), size);
+ value.resize(length);
+ return Narrow(value);
+ }
+
+ bool
+ HasEnvironment(const char *name)
+ {
+ const std::wstring wideName = Widen(name);
+ return GetEnvironmentVariableW(wideName.c_str(), nullptr, 0) > 0
+ || GetLastError() != ERROR_ENVVAR_NOT_FOUND;
+ }
+
+ bool
+ IsStandardOutputTerminal() noexcept
+ {
+ DWORD mode = 0;
+ return GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &mode) != 0;
+ }
+
+ bool
+ EnableTerminalColours() noexcept
+ {
+ HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
+ DWORD mode = 0;
+ if (!GetConsoleMode(output, &mode))
+ {
+ return false;
+ }
+ return SetConsoleMode(output, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+ != 0;
+ }
+
+ void
+ QuietCrashReporting() noexcept
+ {
+ SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX
+ | SEM_NOOPENFILEERRORBOX);
+ _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
+ }
+
+ std::int64_t
+ CurrentProcessId() noexcept
+ {
+ return static_cast<std::int64_t>(GetCurrentProcessId());
+ }
+
+ void
+ WriteStandardError(std::string_view text) noexcept
+ {
+ while (!text.empty())
+ {
+ const unsigned chunk = static_cast<unsigned>(
+ std::min<std::size_t>(text.size(), 1U << 20U));
+ const int written = _write(2, text.data(), chunk);
+ if (written <= 0)
+ {
+ return;
+ }
+ text.remove_prefix(static_cast<std::size_t>(written));
+ }
+ }
+#else
+ Expected<std::unique_ptr<ChildProcess>>
+ ChildProcess::Start(const ChildOptions &options)
+ {
+ if (options.arguments.empty())
+ {
+ return Error{ "no program to start" };
+ }
+ int input[2] = { -1, -1 };
+ int output[2] = { -1, -1 };
+ int errors[2] = { -1, -1 };
+ auto closeAll = [&] {
+ for (int *pipe : { input, output, errors })
+ {
+ for (int index = 0; index < 2; ++index)
+ {
+ if (pipe[index] != -1)
+ {
+ close(pipe[index]);
+ pipe[index] = -1;
+ }
+ }
+ }
+ };
+ if (!MakePipe(input) || !MakePipe(output) || !MakePipe(errors))
+ {
+ const int error = errno;
+ closeAll();
+ return Error{ ErrnoMessage("cannot create a pipe", error) };
+ }
+
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_adddup2(&actions, input[0], 0);
+ posix_spawn_file_actions_adddup2(&actions, output[1], 1);
+ posix_spawn_file_actions_adddup2(&actions, errors[1], 2);
+
+ posix_spawnattr_t attributes;
+ posix_spawnattr_init(&attributes);
+ sigset_t defaults;
+ sigemptyset(&defaults);
+ // The runner ignores SIGPIPE; a test must see the default.
+ sigaddset(&defaults, SIGPIPE);
+ posix_spawnattr_setsigdefault(&attributes, &defaults);
+ sigset_t mask;
+ sigemptyset(&mask);
+ posix_spawnattr_setsigmask(&attributes, &mask);
+ posix_spawnattr_setpgroup(&attributes, 0);
+ posix_spawnattr_setflags(&attributes,
+ static_cast<short>(POSIX_SPAWN_SETPGROUP
+ | POSIX_SPAWN_SETSIGDEF
+ | POSIX_SPAWN_SETSIGMASK));
+
+ std::vector<std::string> environmentStorage;
+ for (char **entry = CurrentEnvironment();
+ entry != nullptr && *entry != nullptr;
+ ++entry)
+ {
+ environmentStorage.emplace_back(*entry);
+ }
+ for (const auto &[name, value] : options.environment)
+ {
+ std::erase_if(environmentStorage, [&](const std::string &entry) {
+ return entry.size() > name.size()
+ && entry.compare(0, name.size(), name) == 0
+ && entry[name.size()] == '=';
+ });
+ environmentStorage.push_back(name + "=" + value);
+ }
+ std::vector<char *> environment;
+ environment.reserve(environmentStorage.size() + 1);
+ for (auto &entry : env