@@ -34,6 +34,11 @@ module;
3434#include < unistd.h> // pipe, dup2, close, read
3535#include < sys/wait.h> // waitpid
3636#include < spawn.h> // posix_spawnp, posix_spawn_file_actions_* (incl. addchdir_np)
37+ #include < signal.h> // kill, SIGKILL (deadline runners)
38+ #include < cerrno> // errno, EINTR (deadline wait loop)
39+ #include < poll.h> // poll (deadline capture)
40+ #include < fcntl.h> // fcntl O_NONBLOCK (deadline capture)
41+ #include < time.h> // nanosleep (deadline wait loop)
3742#if defined(__APPLE__)
3843#include < crt_externs.h> // _NSGetEnviron — direct `environ` is only linkable
3944 // from executables on Apple, not from dylibs
@@ -88,6 +93,21 @@ RunResult capture_exec(
8893 const std::vector<std::pair<std::string, std::string>>& extraEnv = {},
8994 std::string_view cwd = {});
9095
96+ // Deadline variants (POSIX): kill the child with SIGKILL once `deadline`
97+ // elapses and set *timed_out. A zero deadline means no limit. On Windows the
98+ // deadline is currently ignored (no supported kill-by-handle path in the
99+ // residual shell launcher) — callers must treat the timeout as best-effort.
100+ int run_exec_deadline (const std::vector<std::string>& argv,
101+ const std::vector<std::pair<std::string, std::string>>& extraEnv,
102+ std::chrono::milliseconds deadline,
103+ bool * timed_out);
104+
105+ RunResult capture_exec_deadline (
106+ const std::vector<std::string>& argv,
107+ const std::vector<std::pair<std::string, std::string>>& extraEnv,
108+ std::chrono::milliseconds deadline,
109+ bool * timed_out);
110+
91111// Run `command` silently (discard stdout/stderr).
92112// On POSIX, stdin is automatically redirected from /dev/null.
93113int run_silent (std::string_view command);
@@ -451,4 +471,117 @@ RunResult capture_exec(
451471#endif
452472}
453473
474+ int run_exec_deadline (const std::vector<std::string>& argv,
475+ const std::vector<std::pair<std::string, std::string>>& extraEnv,
476+ std::chrono::milliseconds deadline,
477+ bool * timed_out)
478+ {
479+ if (timed_out) *timed_out = false ;
480+ if (deadline.count () <= 0 ) return run_exec (argv, extraEnv);
481+ if (argv.empty ()) return 127 ;
482+ #if defined(__linux__) || defined(__APPLE__)
483+ auto envStore = merged_environ (extraEnv);
484+ std::vector<char *> envp;
485+ for (auto & s : envStore) envp.push_back (s.data ());
486+ envp.push_back (nullptr );
487+ std::vector<char *> cargv;
488+ for (auto & a : argv) cargv.push_back (const_cast <char *>(a.c_str ()));
489+ cargv.push_back (nullptr );
490+
491+ pid_t pid = 0 ;
492+ if (::posix_spawnp (&pid, cargv[0 ], nullptr , nullptr , cargv.data (), envp.data ()) != 0 )
493+ return 127 ;
494+
495+ auto until = std::chrono::steady_clock::now () + deadline;
496+ int status = 0 ;
497+ for (;;) {
498+ pid_t r = ::waitpid (pid, &status, WNOHANG );
499+ if (r == pid) return normalize_exit_code (status);
500+ if (r < 0 && errno != EINTR ) return 127 ;
501+ if (std::chrono::steady_clock::now () >= until) {
502+ ::kill (pid, SIGKILL );
503+ while (::waitpid (pid, &status, 0 ) < 0 ) { /* EINTR retry */ }
504+ if (timed_out) *timed_out = true ;
505+ return normalize_exit_code (status);
506+ }
507+ struct timespec ts{0 , 20'000'000 }; // 20ms
508+ ::nanosleep (&ts, nullptr );
509+ }
510+ #else
511+ // Windows: the residual shell launcher has no kill-by-handle path yet —
512+ // run untimed (documented best-effort semantics).
513+ return run_exec (argv, extraEnv);
514+ #endif
515+ }
516+
517+ RunResult capture_exec_deadline (
518+ const std::vector<std::string>& argv,
519+ const std::vector<std::pair<std::string, std::string>>& extraEnv,
520+ std::chrono::milliseconds deadline,
521+ bool * timed_out)
522+ {
523+ if (timed_out) *timed_out = false ;
524+ if (deadline.count () <= 0 ) return capture_exec (argv, extraEnv);
525+ RunResult result;
526+ if (argv.empty ()) { result.exit_code = 127 ; return result; }
527+ #if defined(__linux__) || defined(__APPLE__)
528+ int fds[2 ];
529+ if (::pipe (fds) != 0 ) { result.exit_code = 127 ; return result; }
530+
531+ auto envStore = merged_environ (extraEnv);
532+ std::vector<char *> envp;
533+ for (auto & s : envStore) envp.push_back (s.data ());
534+ envp.push_back (nullptr );
535+ std::vector<char *> cargv;
536+ for (auto & a : argv) cargv.push_back (const_cast <char *>(a.c_str ()));
537+ cargv.push_back (nullptr );
538+
539+ posix_spawn_file_actions_t fa;
540+ ::posix_spawn_file_actions_init (&fa);
541+ ::posix_spawn_file_actions_adddup2 (&fa, fds[1 ], 1 );
542+ ::posix_spawn_file_actions_adddup2 (&fa, fds[1 ], 2 );
543+ ::posix_spawn_file_actions_addclose (&fa, fds[0 ]);
544+ ::posix_spawn_file_actions_addclose (&fa, fds[1 ]);
545+
546+ pid_t pid = 0 ;
547+ int sp = ::posix_spawnp (&pid, cargv[0 ], &fa, nullptr , cargv.data (), envp.data ());
548+ ::posix_spawn_file_actions_destroy (&fa);
549+ ::close (fds[1 ]);
550+ if (sp != 0 ) { ::close (fds[0 ]); result.exit_code = 127 ; return result; }
551+
552+ ::fcntl (fds[0 ], F_SETFL , ::fcntl(fds[0 ], F_GETFL ) | O_NONBLOCK);
553+
554+ auto until = std::chrono::steady_clock::now () + deadline;
555+ bool killed = false ;
556+ std::array<char , 4096 > buf{};
557+ for (;;) {
558+ struct pollfd pfd{fds[0 ], POLLIN , 0 };
559+ ::poll (&pfd, 1 , 50 );
560+ for (;;) {
561+ ssize_t n = ::read (fds[0 ], buf.data (), buf.size ());
562+ if (n > 0 ) { result.output .append (buf.data (), static_cast <size_t >(n)); continue ; }
563+ break ;
564+ }
565+ int status = 0 ;
566+ pid_t r = ::waitpid (pid, &status, WNOHANG );
567+ if (r == pid) {
568+ // Drain whatever is left in the pipe after exit.
569+ ssize_t n;
570+ while ((n = ::read (fds[0 ], buf.data (), buf.size ())) > 0 )
571+ result.output .append (buf.data (), static_cast <size_t >(n));
572+ ::close (fds[0 ]);
573+ result.exit_code = normalize_exit_code (status);
574+ if (timed_out) *timed_out = killed;
575+ return result;
576+ }
577+ if (!killed && std::chrono::steady_clock::now () >= until) {
578+ ::kill (pid, SIGKILL );
579+ killed = true ;
580+ }
581+ }
582+ #else
583+ return capture_exec (argv, extraEnv);
584+ #endif
585+ }
586+
454587} // namespace mcpp::platform::process
0 commit comments