@@ -32,6 +32,7 @@ module;
3232// Linux and macOS launchers do a direct exec (see run_exec / capture_exec
3333// below); only Windows keeps the std::system shell path (#248).
3434#include < unistd.h> // pipe, dup2, close, read
35+ #include < fcntl.h> // O_RDONLY, O_WRONLY for capture_stdout's /dev/null
3536#include < sys/wait.h> // waitpid
3637#include < spawn.h> // posix_spawnp, posix_spawn_file_actions_* (incl. addchdir_np)
3738// The deadline runners' headers (signal.h, errno, poll.h, fcntl.h, time.h)
@@ -110,6 +111,37 @@ RunResult capture_exec(
110111 std::string_view cwd = {},
111112 int * spawn_error = nullptr );
112113
114+ // A PROGRAM PROBE: run `argv` directly, capture its standard output only, give
115+ // it an empty standard input, and discard its standard error.
116+ //
117+ // This is the argument-vector form of `<program> <args> 2>/dev/null`, and it
118+ // exists because that string form is written in one shell's grammar. On
119+ // Windows every command string reaches cmd.exe, which resolves `/dev/null` to
120+ // `\dev\null` on the current drive, cannot open it, prints "The system cannot
121+ // find the path specified." on its own stderr and does not run the program.
122+ // Measured on windows-2022 with mcpp 2026.9.14.3: the vendored-xlings version
123+ // probe printed that line in every command after the first, and returned an
124+ // empty version, which silently disabled the pin check it existed for. The
125+ // null device and the redirection belong to this layer; a caller states only
126+ // the program and its arguments.
127+ //
128+ // The output is the program's standard output and nothing else. A program
129+ // that cannot be started yields exit code 127 and EMPTY output: a probe parses
130+ // what it captured, so a launcher error message in `output` would be read as
131+ // the program's answer. `spawn_error` has run_exec's contract.
132+ RunResult capture_stdout (
133+ const std::vector<std::string>& argv,
134+ const std::vector<std::pair<std::string, std::string>>& extraEnv = {},
135+ int * spawn_error = nullptr );
136+
137+ // capture_stdout for a host tool: the target runtime library search variable is
138+ // cleared for the child, as capture_host_tool does for a command string.
139+ RunResult capture_host_tool_stdout (const std::vector<std::string>& argv);
140+
141+ // The command line capture_stdout hands to cmd.exe on Windows. Host-independent
142+ // so the Windows rendering is testable from any platform.
143+ std::string windows_stdout_probe_command (const std::vector<std::string>& argv);
144+
113145// Deadline variants: kill the child once `deadline` elapses and set
114146// *timed_out. A zero deadline means no limit.
115147//
@@ -753,6 +785,86 @@ RunResult capture_exec(
753785#endif
754786}
755787
788+ // Host-independent (see the declaration): always the Windows shape. The
789+ // redirect names cmd.exe's own null device, and the argv is quoted by the one
790+ // shaper every Windows launch in this file uses.
791+ std::string windows_stdout_probe_command (const std::vector<std::string>& argv) {
792+ return windows_command_from_argv (argv) + " 2>nul" ;
793+ }
794+
795+ RunResult capture_stdout (
796+ const std::vector<std::string>& argv,
797+ const std::vector<std::pair<std::string, std::string>>& extraEnv,
798+ int * spawn_error)
799+ {
800+ RunResult result;
801+ if (spawn_error) *spawn_error = 0 ;
802+ if (argv.empty ()) { result.exit_code = 127 ; return result; }
803+ #if defined(__linux__) || defined(__APPLE__)
804+ int fds[2 ];
805+ if (::pipe (fds) != 0 ) { result.exit_code = 127 ; return result; }
806+
807+ auto envStore = merged_environ (extraEnv);
808+ std::vector<char *> envp;
809+ for (auto & s : envStore) envp.push_back (s.data ());
810+ envp.push_back (nullptr );
811+ std::vector<char *> cargv;
812+ for (auto & a : argv) cargv.push_back (const_cast <char *>(a.c_str ()));
813+ cargv.push_back (nullptr );
814+
815+ posix_spawn_file_actions_t fa;
816+ ::posix_spawn_file_actions_init (&fa);
817+ ::posix_spawn_file_actions_addopen (&fa, 0 , " /dev/null" , O_RDONLY , 0 );
818+ ::posix_spawn_file_actions_adddup2 (&fa, fds[1 ], 1 ); // stdout -> pipe
819+ ::posix_spawn_file_actions_addopen (&fa, 2 , " /dev/null" , O_WRONLY , 0 );
820+ ::posix_spawn_file_actions_addclose (&fa, fds[0 ]);
821+ ::posix_spawn_file_actions_addclose (&fa, fds[1 ]);
822+
823+ // Owned as capture_exec's child is: a probe that outlives an interrupted
824+ // mcpp is the same orphan, only smaller.
825+ posix_spawnattr_t attr;
826+ ::posix_spawnattr_init (&attr);
827+ ::posix_spawnattr_setpgroup (&attr, 0 );
828+ ::posix_spawnattr_setflags (&attr, POSIX_SPAWN_SETPGROUP );
829+
830+ pid_t pid = 0 ;
831+ int sp = ::posix_spawnp (&pid, cargv[0 ], &fa, &attr, cargv.data (), envp.data ());
832+ ::posix_spawnattr_destroy (&attr);
833+ ::posix_spawn_file_actions_destroy (&fa);
834+ ::close (fds[1 ]);
835+ if (sp != 0 ) {
836+ ::close (fds[0 ]);
837+ result.exit_code = 127 ;
838+ if (spawn_error) *spawn_error = sp;
839+ return result;
840+ }
841+ mcpp::platform::unixproc::guard_group_on_signal (pid);
842+
843+ std::array<char , 4096 > buf{};
844+ ssize_t n;
845+ while ((n = ::read (fds[0 ], buf.data (), buf.size ())) > 0 )
846+ result.output .append (buf.data (), static_cast <size_t >(n));
847+ ::close (fds[0 ]);
848+ int status = 0 ;
849+ while (::waitpid (pid, &status, 0 ) < 0 ) { /* EINTR retry */ }
850+ mcpp::platform::unixproc::unguard_group (pid);
851+ result.exit_code = normalize_exit_code (status);
852+ return result;
853+ #else
854+ // cmd.exe reports a program it cannot find on stderr, which the redirect
855+ // discards, and returns 9009; the output stays empty either way.
856+ return capture_with_env (windows_stdout_probe_command (argv), extraEnv);
857+ #endif
858+ }
859+
860+ RunResult capture_host_tool_stdout (const std::vector<std::string>& argv) {
861+ auto key = mcpp::platform::env::host_tool_runtime_library_path_key ();
862+ std::optional<mcpp::platform::env::ScopedEnv> runtime_env;
863+ if (!key.empty ())
864+ runtime_env.emplace (key, std::nullopt );
865+ return capture_stdout (argv);
866+ }
867+
756868// ─── The ONE place the platform question is asked for a bounded run ────────
757869//
758870// Both launchers answer the same contract behind a `std`-free interface (see
0 commit comments