|
| 1 | +// mcpp.process — platform-aware process runner. |
| 2 | +// |
| 3 | +// Centralises all popen/system usage into a single module so callers do |
| 4 | +// not need to scatter #if _WIN32 guards or duplicate the popen-read loop. |
| 5 | +// |
| 6 | +// Three entry points: |
| 7 | +// run_capture — run a command, capture stdout (replaces the many inline |
| 8 | +// popen loops in probe.cppm, xlings.cppm, pack.cppm, …) |
| 9 | +// run_with_env — run a command with extra env vars (replaces scattered |
| 10 | +// _putenv_s() calls on Windows) |
| 11 | +// shell_quote — platform-aware shell quoting (delegates to mcpp.xlings::shq; |
| 12 | +// kept here so new code imports mcpp.process, not mcpp.xlings) |
| 13 | +// |
| 14 | +// NOTE on Windows shell_quote: |
| 15 | +// Do NOT use shell_quote() for the FIRST token in a popen/system command |
| 16 | +// string on Windows — cmd.exe strips the leading double-quote pair and the |
| 17 | +// binary name becomes unrecognised. Use the raw path string as the first |
| 18 | +// token and shell_quote() only for arguments. See xlings.cppm::shq() for |
| 19 | +// the full rationale. |
| 20 | + |
| 21 | +module; |
| 22 | +#include <cstdio> |
| 23 | +#include <cstdlib> |
| 24 | +#if defined(_WIN32) |
| 25 | +#include <stdlib.h> // _putenv_s |
| 26 | +#define popen _popen |
| 27 | +#define pclose _pclose |
| 28 | +#endif |
| 29 | + |
| 30 | +export module mcpp.process; |
| 31 | + |
| 32 | +import std; |
| 33 | +import mcpp.xlings; // shq() — the authoritative shell-quoting implementation |
| 34 | + |
| 35 | +export namespace mcpp::process { |
| 36 | + |
| 37 | +// ─── Result type ───────────────────────────────────────────────────────────── |
| 38 | + |
| 39 | +struct RunResult { |
| 40 | + int exit_code = 0; |
| 41 | + std::string output; |
| 42 | +}; |
| 43 | + |
| 44 | +// ─── run_capture ───────────────────────────────────────────────────────────── |
| 45 | +// |
| 46 | +// Run `command` via the platform shell (popen on both POSIX and Windows). |
| 47 | +// Captures stdout. stderr is NOT captured unless the caller redirects it |
| 48 | +// in the command string (e.g. "cmd 2>&1" on POSIX, "cmd 2>&1" on Windows). |
| 49 | +// |
| 50 | +// Returns RunResult with exit_code set and output containing all captured |
| 51 | +// text. On popen failure, exit_code is -1 and output is empty. |
| 52 | +RunResult run_capture(std::string_view command); |
| 53 | + |
| 54 | +// ─── run_with_env ──────────────────────────────────────────────────────────── |
| 55 | +// |
| 56 | +// Run `command` with extra environment variables (additive — existing vars |
| 57 | +// not in `env` are preserved). |
| 58 | +// |
| 59 | +// On Windows: uses _putenv_s() to inject each var into the current process |
| 60 | +// environment before spawning the child via popen(). _putenv_s() changes |
| 61 | +// are inherited by child processes. IMPORTANT: this mutates the calling |
| 62 | +// process's environment; callers should restore vars if needed. |
| 63 | +// |
| 64 | +// On POSIX: prefixes the command with "VAR=val " tokens so the vars are |
| 65 | +// scoped to the child (the calling process's environment is unchanged). |
| 66 | +// |
| 67 | +// Returns the same RunResult as run_capture(). |
| 68 | +RunResult run_with_env(std::string_view command, |
| 69 | + const std::vector<std::pair<std::string, std::string>>& env); |
| 70 | + |
| 71 | +// ─── shell_quote ───────────────────────────────────────────────────────────── |
| 72 | +// |
| 73 | +// Quote `s` for safe embedding in a shell command string. |
| 74 | +// POSIX: wraps in single quotes, escaping embedded single quotes. |
| 75 | +// Windows: wraps in double quotes, escaping embedded double quotes. |
| 76 | +// |
| 77 | +// See the module-level NOTE about cmd.exe's first-token behaviour on Windows. |
| 78 | +std::string shell_quote(std::string_view s); |
| 79 | + |
| 80 | +} // namespace mcpp::process |
| 81 | + |
| 82 | +// ─── Implementation ────────────────────────────────────────────────────────── |
| 83 | + |
| 84 | +namespace mcpp::process { |
| 85 | + |
| 86 | +RunResult run_capture(std::string_view command) { |
| 87 | + std::string cmd_str(command); |
| 88 | + RunResult result; |
| 89 | + |
| 90 | + std::FILE* fp = ::popen(cmd_str.c_str(), "r"); |
| 91 | + if (!fp) { |
| 92 | + result.exit_code = -1; |
| 93 | + return result; |
| 94 | + } |
| 95 | + |
| 96 | + std::array<char, 4096> buf{}; |
| 97 | + while (std::fgets(buf.data(), static_cast<int>(buf.size()), fp) != nullptr) |
| 98 | + result.output += buf.data(); |
| 99 | + |
| 100 | + int rc = ::pclose(fp); |
| 101 | +#if defined(_WIN32) |
| 102 | + // On Windows, pclose() returns the raw exit code from WaitForSingleObject / |
| 103 | + // GetExitCodeProcess — it is already the process exit code, not a wait |
| 104 | + // status word, so no WIFEXITED/WEXITSTATUS unwrapping needed. |
| 105 | + result.exit_code = rc; |
| 106 | +#else |
| 107 | + // On POSIX, pclose() returns a wait-status word; extract the real exit code. |
| 108 | + if (WIFEXITED(rc)) |
| 109 | + result.exit_code = WEXITSTATUS(rc); |
| 110 | + else |
| 111 | + result.exit_code = rc; // signal / abnormal — surface raw value |
| 112 | +#endif |
| 113 | + return result; |
| 114 | +} |
| 115 | + |
| 116 | +RunResult run_with_env(std::string_view command, |
| 117 | + const std::vector<std::pair<std::string, std::string>>& env) |
| 118 | +{ |
| 119 | +#if defined(_WIN32) |
| 120 | + // Inject vars into the current process environment. popen() inherits them. |
| 121 | + for (auto& [k, v] : env) |
| 122 | + _putenv_s(k.c_str(), v.c_str()); |
| 123 | + return run_capture(command); |
| 124 | +#else |
| 125 | + // Build "KEY=val KEY2=val2 <original command>" prefix. |
| 126 | + std::string prefixed; |
| 127 | + for (auto& [k, v] : env) { |
| 128 | + prefixed += k; |
| 129 | + prefixed += '='; |
| 130 | + prefixed += shell_quote(v); |
| 131 | + prefixed += ' '; |
| 132 | + } |
| 133 | + prefixed += command; |
| 134 | + return run_capture(prefixed); |
| 135 | +#endif |
| 136 | +} |
| 137 | + |
| 138 | +std::string shell_quote(std::string_view s) { |
| 139 | + // Delegate to the canonical implementation in mcpp.xlings so the two |
| 140 | + // stay in sync. If xlings.cppm's shq() is ever updated for edge-cases |
| 141 | + // (e.g. NUL bytes, Unicode), this function inherits the fix automatically. |
| 142 | + return mcpp::xlings::shq(s); |
| 143 | +} |
| 144 | + |
| 145 | +} // namespace mcpp::process |
0 commit comments