Skip to content

Commit a6190dc

Browse files
committed
feat(P3): add mcpp.process — platform-aware process runner module
Create src/process.cppm with three entry points: - run_capture(command): popen-based stdout capture with proper exit-code handling (WIFEXITED/WEXITSTATUS on POSIX, raw rc on Windows). - run_with_env(command, env): runs a command with extra env vars; uses _putenv_s() before popen on Windows, prefix-style on POSIX to avoid mutating the calling process environment. - shell_quote(s): delegates to mcpp::xlings::shq() so the two stay in sync. The module handles the popen/_popen compat #define in its own global fragment, keeping all Windows-vs-POSIX branching in one place. Existing call-sites (probe.cppm, xlings.cppm, pack.cppm) are not changed in this commit; new code should prefer mcpp.process.
1 parent 10bdc89 commit a6190dc

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

src/process.cppm

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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

Comments
 (0)