diff options
| author | Leitwolf | 2026-10-24 19:03:40 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-24 19:03:40 +0300 |
| commit | 4f1c759cffcb14bd6511e1d7c5fbace682e24f2d (patch) | |
| tree | 415fe5d48a5b48650bba3747beaf245d727c4f61 /src/Progmasoft/Runner/Process.cpp | |
| parent | 83c6fc3ac13abe83fa39f7b5f8559ccaf4412233 (diff) | |
| parent | 622188f9c08e4af35941c146979883ed4882410e (diff) | |
| download | catch3-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.cpp | 1289 |
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 : environmentStorage) + { + environment.push_back(entry.data()); + } + environment.pu |