LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
subprocess.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_SUBPROCESS_H
6#define LINE_UTIL_SUBPROCESS_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Running an external command and capturing its output, with a deadline.
12 *
13 * The port needs this for the Docker orchestration behind the symbolic backend,
14 * where the JAR uses ProcessBuilder. popen() is not enough: it offers no timeout
15 * and runs the command through a shell, so an image tag would be word-split and
16 * glob-expanded. This forks and execs an argv vector directly, so no shell ever
17 * sees the arguments, and enforces the deadline by polling the pipe and killing
18 * the child when it expires.
19 *
20 * A TIMED-OUT CHILD IS KILLED AND REAPED, never merely abandoned: an orphaned
21 * `docker run` would keep a container alive that nobody holds the name of.
22 */
23
24#include <cerrno>
25#include <csignal>
26#include <cstddef>
27#include <cstring>
28#include <ctime>
29#include <string>
30#include <vector>
31
32#include <fcntl.h>
33#include <poll.h>
34#include <sys/types.h>
35#include <sys/wait.h>
36#include <unistd.h>
37
38namespace line {
39namespace util {
40
41/** Outcome of a captured command. */
42struct ProcResult {
43 int exitCode = -1; ///< Exit status, or -1 when the command could not run
44 std::string out; ///< Everything the command wrote to stdout
45 bool timedOut = false; ///< True when the deadline expired and the child was killed
46};
47
48namespace detail {
49
50/** Milliseconds on the monotonic clock, for deadlines that survive a clock step. */
51inline long monotonic_millis() {
52 struct timespec ts;
53 clock_gettime(CLOCK_MONOTONIC, &ts);
54 return static_cast<long>(ts.tv_sec) * 1000L + ts.tv_nsec / 1000000L;
55}
56
57inline void exec_never_returns(const std::vector<std::string>& argv) {
58 std::vector<char*> raw;
59 raw.reserve(argv.size() + 1);
60 for (std::size_t i = 0; i < argv.size(); ++i)
61 raw.push_back(const_cast<char*>(argv[i].c_str()));
62 raw.push_back(nullptr);
63 ::execvp(raw[0], &raw[0]);
64 ::_exit(127);
65}
66
67} // namespace detail
68
69/**
70 * Runs a command, capturing stdout and discarding stderr.
71 *
72 * `mergeStderr` sends the child's stderr down the SAME pipe instead, which is
73 * what MATLAB's `[status, cmdout] = system(cmd)` does and what a wrapper needs
74 * on the failure path: an external engine reports why it refused on stderr, and
75 * dropping it leaves the wrapper reporting an exit code and nothing else.
76 *
77 * @param argv the command and its arguments, argv[0] resolved on PATH
78 * @param timeoutSeconds deadline; not positive waits indefinitely
79 * @param mergeStderr capture stderr too, interleaved with stdout
80 * @return the exit code, the captured output and whether the deadline expired
81 */
82inline ProcResult capture(const std::vector<std::string>& argv, int timeoutSeconds,
83 bool mergeStderr = false) {
84 ProcResult r;
85 if (argv.empty()) return r;
86
87 int pipefd[2];
88 if (::pipe(pipefd) != 0) return r;
89
90 const pid_t pid = ::fork();
91 if (pid < 0) {
92 ::close(pipefd[0]);
93 ::close(pipefd[1]);
94 return r;
95 }
96 if (pid == 0) {
97 ::close(pipefd[0]);
98 ::dup2(pipefd[1], STDOUT_FILENO);
99 if (mergeStderr) {
100 ::dup2(pipefd[1], STDERR_FILENO);
101 } else {
102 const int devnull = ::open("/dev/null", O_WRONLY);
103 if (devnull >= 0) {
104 ::dup2(devnull, STDERR_FILENO);
105 ::close(devnull);
106 }
107 }
108 ::close(pipefd[1]);
109 detail::exec_never_returns(argv);
110 }
111
112 ::close(pipefd[1]);
113 const long deadline =
114 timeoutSeconds > 0 ? detail::monotonic_millis() + timeoutSeconds * 1000L : -1;
115 char buf[4096];
116 while (true) {
117 int wait = -1;
118 if (deadline >= 0) {
119 const long left = deadline - detail::monotonic_millis();
120 if (left <= 0) {
121 r.timedOut = true;
122 break;
123 }
124 wait = static_cast<int>(left);
125 }
126 struct pollfd pfd;
127 pfd.fd = pipefd[0];
128 pfd.events = POLLIN;
129 pfd.revents = 0;
130 const int pr = ::poll(&pfd, 1, wait);
131 if (pr == 0) {
132 r.timedOut = true;
133 break;
134 }
135 if (pr < 0) {
136 if (errno == EINTR) continue;
137 break;
138 }
139 const ssize_t k = ::read(pipefd[0], buf, sizeof(buf));
140 if (k > 0) {
141 r.out.append(buf, static_cast<std::size_t>(k));
142 continue;
143 }
144 if (k < 0 && errno == EINTR) continue;
145 break; // end of stream
146 }
147 ::close(pipefd[0]);
148
149 if (r.timedOut) {
150 ::kill(pid, SIGKILL);
151 int status = 0;
152 ::waitpid(pid, &status, 0);
153 r.exitCode = -1;
154 return r;
155 }
156 int status = 0;
157 if (::waitpid(pid, &status, 0) == pid && WIFEXITED(status))
158 r.exitCode = WEXITSTATUS(status);
159 return r;
160}
161
162/**
163 * Runs a command with the parent's stdout and stderr, e.g. so a `docker pull`
164 * streams its progress. There is no deadline: a pull is long by nature and the
165 * user can see it working.
166 *
167 * @param argv the command and its arguments
168 * @return the exit code, or -1 if the command could not run
169 */
170inline int run_inherit(const std::vector<std::string>& argv) {
171 if (argv.empty()) return -1;
172 const pid_t pid = ::fork();
173 if (pid < 0) return -1;
174 if (pid == 0) detail::exec_never_returns(argv);
175 int status = 0;
176 if (::waitpid(pid, &status, 0) == pid && WIFEXITED(status)) return WEXITSTATUS(status);
177 return -1;
178}
179
180/** Trims ASCII whitespace from both ends, as Java's String.trim() does. */
181inline std::string trim(const std::string& s) {
182 const std::size_t b = s.find_first_not_of(" \t\r\n");
183 if (b == std::string::npos) return std::string();
184 const std::size_t e = s.find_last_not_of(" \t\r\n");
185 return s.substr(b, e - b + 1);
186}
187
188} // namespace util
189} // namespace line
190
191#endif // LINE_UTIL_SUBPROCESS_H
int run_inherit(const std::vector< std::string > &argv)
Runs a command with the parent's stdout and stderr, e.g.
Definition subprocess.h:170
ProcResult capture(const std::vector< std::string > &argv, int timeoutSeconds, bool mergeStderr=false)
Runs a command, capturing stdout and discarding stderr.
Definition subprocess.h:82
std::string trim(const std::string &s)
Trims ASCII whitespace from both ends, as Java's String.trim() does.
Definition subprocess.h:181
Outcome of a captured command.
Definition subprocess.h:42
int exitCode
Exit status, or -1 when the command could not run.
Definition subprocess.h:43
bool timedOut
True when the deadline expired and the child was killed.
Definition subprocess.h:45
std::string out
Everything the command wrote to stdout.
Definition subprocess.h:44