]> git.localhorst.tv Git - blank.git/blob - src/app/proc.cpp
timeouts reading from spawned processes
[blank.git] / src / app / proc.cpp
1 #include "Process.hpp"
2
3 #include "error.hpp"
4
5 #ifdef _WIN32
6 #  include <tchar.h>
7 #  include <windows.h>
8 #else
9 #  include <fcntl.h>
10 #  include <signal.h>
11 #  include <unistd.h>
12 #  include <sys/select.h>
13 #  include <sys/wait.h>
14 #endif
15
16 #include <cstdio>
17 #include <stdexcept>
18
19 using namespace std;
20
21
22 namespace blank {
23
24 struct Process::Impl {
25
26         Impl(
27                 const string &path,
28                 const Arguments &args,
29                 char *const env[]);
30         ~Impl();
31
32         static Impl *EnvHelper(
33                 const string &path,
34                 const Arguments &args,
35                 const Environment &env);
36
37         size_t WriteIn(const void *buffer, size_t max_len);
38         void CloseIn();
39
40         size_t ReadOut(void *buffer, size_t max_len, int timeout);
41         void CloseOut();
42
43         size_t ReadErr(void *buffer, size_t max_len, int timeout);
44         void CloseErr();
45
46         void Terminate();
47         bool Terminated();
48         int Join();
49
50         bool joined;
51         int status;
52
53         bool in_closed;
54         bool out_closed;
55         bool err_closed;
56
57 #ifdef _WIN32
58         PROCESS_INFORMATION pi;
59         HANDLE fd_in[2];
60         HANDLE fd_out[2];
61         HANDLE fd_err[2];
62 #else
63         int pid;
64         int fd_in[2];
65         int fd_out[2];
66         int fd_err[2];
67 #endif
68
69 };
70
71
72 Process::Process(
73         const string &path,
74         const Arguments &args)
75 : impl(new Impl(path, args, nullptr)) {
76
77 }
78
79 Process::Process(
80         const string &path,
81         const Arguments &args,
82         const Environment &env)
83 : impl(Impl::EnvHelper(path, args, env)) {
84
85 }
86
87 Process::~Process() {
88         Join();
89 }
90
91
92 size_t Process::WriteIn(const void *buffer, size_t max_len) {
93         return impl->WriteIn(buffer, max_len);
94 }
95
96 void Process::CloseIn() {
97         impl->CloseIn();
98 }
99
100 size_t Process::ReadOut(void *buffer, size_t max_len, int timeout) {
101         return impl->ReadOut(buffer, max_len, timeout);
102 }
103
104 void Process::CloseOut() {
105         impl->CloseOut();
106 }
107
108 size_t Process::ReadErr(void *buffer, size_t max_len, int timeout) {
109         return impl->ReadErr(buffer, max_len, timeout);
110 }
111
112 void Process::CloseErr() {
113         impl->CloseErr();
114 }
115
116 void Process::Terminate() {
117         impl->Terminate();
118 }
119
120 bool Process::Terminated() {
121         return impl->Terminated();
122 }
123
124 int Process::Join() {
125         return impl->Join();
126 }
127
128 Process::Impl *Process::Impl::EnvHelper(
129         const string &path,
130         const Arguments &args,
131         const Environment &env
132 ) {
133         char *envp[env.size() + 1];
134         for (size_t i = 0; i < env.size(); ++i) {
135                 envp[i] = const_cast<char *>(env[i].c_str());
136         }
137         envp[env.size()] = nullptr;
138         return new Impl(path, args, envp);
139 }
140
141 Process::Impl::Impl(
142         const string &path_in,
143         const Arguments &args,
144         char *const envp[])
145 : joined(false)
146 , status(0)
147 , in_closed(false)
148 , out_closed(false)
149 , err_closed(false) {
150         const char *path = path_in.c_str();
151 #ifdef _WIN32
152         string cmdline;
153         for (const auto &arg : args) {
154                 cmdline += '"';
155                 cmdline += arg;
156                 cmdline += '"';
157         }
158
159         SECURITY_ATTRIBUTES sa;
160         sa.nLength = sizeof(SECURITY_ATTRIBUTES);
161         sa.bInheritHandle = true;
162         sa.lpSecurityDescriptor = nullptr;
163         if (!CreatePipe(&fd_in[0], &fd_in[1], &sa, 0)) {
164                 throw runtime_error("failed to open pipe for child process' stdin");
165         }
166         if (!SetHandleInformation(fd_in[1], HANDLE_FLAG_INHERIT, 0)) {
167                 throw runtime_error("failed to stop child process from inheriting stdin write handle");
168         }
169         if (!CreatePipe(&fd_out[0], &fd_out[1], &sa, 0)) {
170                 throw runtime_error("failed to open pipe for child process' stdout");
171         }
172         if (!SetHandleInformation(fd_out[0], HANDLE_FLAG_INHERIT, 0)) {
173                 throw runtime_error("failed to stop child process from inheriting stdout read handle");
174         }
175         if (!CreatePipe(&fd_err[0], &fd_err[1], &sa, 0)) {
176                 throw runtime_error("failed to open pipe for child process' stderr");
177         }
178         if (!SetHandleInformation(fd_err[0], HANDLE_FLAG_INHERIT, 0)) {
179                 throw runtime_error("failed to stop child process from inheriting stderr read handle");
180         }
181
182         STARTUPINFO si;
183         ZeroMemory(&si, sizeof(STARTUPINFO));
184         si.cb = sizeof(STARTUPINFO);
185         si.hStdError = fd_err[1];
186         si.hStdOutput = fd_out[1];
187         si.hStdInput = fd_in[0];
188         si.dwFlags |= STARTF_USESTDHANDLES;
189         ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
190
191         if (!CreateProcess(
192                 path,
193                 cmdline.c_str(),
194                 nullptr,
195                 nullptr,
196                 true,
197                 0,
198                 envp,
199                 nullptr,
200                 &si,
201                 &pi
202         )) {
203                 throw runtime_error("CreateProcess");
204         }
205 #else
206         char *argv[args.size() + 1];
207         for (size_t i = 0; i < args.size(); ++i) {
208                 // I was promised exec won't modify my characters
209                 argv[i] = const_cast<char *>(args[i].c_str());
210         }
211         argv[args.size()] = nullptr;
212
213         if (pipe(fd_in) != 0) {
214                 throw SysError("failed to open pipe for child process' stdin");
215         }
216         if (pipe(fd_out) != 0) {
217                 throw SysError("failed to open pipe for child process' stdout");
218         }
219         if (pipe(fd_err) != 0) {
220                 throw SysError("failed to open pipe for child process' stderr");
221         }
222
223         pid = fork();
224         if (pid == -1) {
225                 throw SysError("fork");
226         } else if (pid == 0) {
227
228                 if (dup2(fd_in[0], STDIN_FILENO) == -1) {
229                         exit(EXIT_FAILURE);
230                 }
231                 if (dup2(fd_out[1], STDOUT_FILENO) == -1) {
232                         exit(EXIT_FAILURE);
233                 }
234                 if (dup2(fd_err[1], STDERR_FILENO) == -1) {
235                         exit(EXIT_FAILURE);
236                 }
237
238                 close(fd_in[0]);
239                 close(fd_in[1]);
240                 close(fd_out[0]);
241                 close(fd_out[1]);
242                 close(fd_err[0]);
243                 close(fd_err[1]);
244
245                 if (envp) {
246                         execve(path, argv, envp);
247                 } else {
248                         execv(path, argv);
249                 }
250                 // if execve returns, something bad happened
251                 exit(EXIT_FAILURE);
252
253         } else {
254
255                 close(fd_in[0]);
256                 close(fd_out[1]);
257                 close(fd_err[1]);
258
259         }
260 }
261 #endif
262
263 Process::Impl::~Impl() {
264         CloseIn();
265         CloseOut();
266         CloseErr();
267 }
268
269 size_t Process::Impl::WriteIn(const void *buffer, size_t max_len) {
270 #ifdef _WIN32
271         DWORD written;
272         if (!WriteFile(fd_in[1], buffer, max_len, &written, nullptr)) {
273                 throw runtime_error("failed to write to child process' input stream");
274         }
275         return written;
276 #else
277         int written = write(fd_in[1], buffer, max_len);
278         if (written < 0) {
279                 if (errno == EAGAIN) {
280                         return 0;
281                 } else {
282                         throw SysError("failed to write to child process' input stream");
283                 }
284         }
285         return written;
286 #endif
287 }
288
289 void Process::Impl::CloseIn() {
290         if (in_closed) {
291                 return;
292         }
293 #ifdef _WIN32
294         CloseHandle(fd_in[1]);
295 #else
296         close(fd_in[1]);
297 #endif
298         in_closed = true;
299 }
300
301 size_t Process::Impl::ReadOut(void *buffer, size_t max_len, int timeout) {
302 #ifdef _WIN32
303         // TODO: timeout implementation for windows child process I/O
304         DWORD ret;
305         if (!ReadFile(fd_out[0], buffer, max_len, &ret, nullptr)) {
306                 throw runtime_error("failed to read from child process' output stream");
307         }
308         return ret;
309 #else
310         if (timeout >= 0) {
311                 fd_set read_set;
312                 fd_set error_set;
313                 FD_ZERO(&read_set);
314                 FD_ZERO(&error_set);
315                 FD_SET(fd_out[0], &read_set);
316                 FD_SET(fd_out[0], &error_set);
317                 timeval timer;
318                 timer.tv_sec = timeout / 1000;
319                 timer.tv_usec = (timeout % 1000) * 1000;
320                 if (select(fd_out[0] + 1, &read_set, nullptr, &error_set, &timer) == -1) {
321                         throw SysError("error waiting on child process' output stream");
322                 }
323                 if (FD_ISSET(fd_out[0], &error_set)) {
324                         throw runtime_error("error condition on child process' output stream");
325                 }
326                 if (!FD_ISSET(fd_out[0], &read_set)) {
327                         throw runtime_error("timeout while waiting on child process' output stream");
328                 }
329         }
330         int ret = read(fd_out[0], buffer, max_len);
331         if (ret < 0) {
332                 if (errno == EAGAIN) {
333                         return 0;
334                 } else {
335                         throw SysError("failed to read from child process' output stream");
336                 }
337         }
338         return ret;
339 #endif
340 }
341
342 void Process::Impl::CloseOut() {
343         if (out_closed) {
344                 return;
345         }
346 #ifdef _WIN32
347         CloseHandle(fd_out[0]);
348 #else
349         close(fd_out[0]);
350 #endif
351         out_closed = true;
352 }
353
354 size_t Process::Impl::ReadErr(void *buffer, size_t max_len, int timeout) {
355 #ifdef _WIN32
356         // TODO: timeout implementation for windows child process I/O
357         DWORD ret;
358         if (!ReadFile(fd_err[0], buffer, max_len, &ret, nullptr)) {
359                 throw runtime_error("failed to read from child process' error stream");
360         }
361         return ret;
362 #else
363         if (timeout >= 0) {
364                 fd_set read_set;
365                 fd_set error_set;
366                 FD_ZERO(&read_set);
367                 FD_ZERO(&error_set);
368                 FD_SET(fd_err[0], &read_set);
369                 FD_SET(fd_err[0], &error_set);
370                 timeval timer;
371                 timer.tv_sec = timeout / 1000;
372                 timer.tv_usec = (timeout % 1000) * 1000;
373                 if (select(fd_err[0] + 1, &read_set, nullptr, &error_set, &timer) == -1) {
374                         throw SysError("error waiting on child process' error stream");
375                 }
376                 if (FD_ISSET(fd_err[0], &error_set)) {
377                         throw runtime_error("error condition on child process' error stream");
378                 }
379                 if (!FD_ISSET(fd_err[0], &read_set)) {
380                         throw runtime_error("timeout while waiting on child process' error stream");
381                 }
382         }
383         int ret = read(fd_err[0], buffer, max_len);
384         if (ret < 0) {
385                 if (errno == EAGAIN) {
386                         return 0;
387                 } else {
388                         throw SysError("failed to read from child process' error stream");
389                 }
390         }
391         return ret;
392 #endif
393 }
394
395 void Process::Impl::CloseErr() {
396         if (err_closed) {
397                 return;
398         }
399 #ifdef _WIN32
400         CloseHandle(fd_err[0]);
401 #else
402         close(fd_err[0]);
403 #endif
404         err_closed = true;
405 }
406
407 void Process::Impl::Terminate() {
408         if (joined) {
409                 // can only terminate once
410                 return;
411         }
412 #ifdef _WIN32
413         if (!TerminateProcess(pi.hProcess, -1)) {
414 #else
415         if (kill(pid, SIGTERM) == -1) {
416 #endif
417                 throw SysError("failed to terminate child process");
418         }
419 }
420
421 bool Process::Impl::Terminated() {
422         if (joined) {
423                 return true;
424         }
425 #ifdef _WIN32
426         DWORD exit_code;
427         GetExitCodeProcess(pi.hProcess, &exit_code);
428         if (exit_code == STILL_ACTIVE) {
429                 return false;
430         } else {
431                 CloseHandle(pi.hProcess);
432                 CloseHandle(pi.hThread);
433                 status = exit_code;
434                 joined = true;
435                 return true;
436         }
437 #else
438         int stat;
439         int result = waitpid(pid, &stat, WNOHANG);
440         if (result == -1) {
441                 throw SysError("error polling child process");
442         } else if (result == 0) {
443                 return false;
444         } else if (result == pid) {
445                 // child just exited, reap
446                 if (WIFEXITED(stat)) {
447                         // autonomous termination
448                         status = WEXITSTATUS(stat);
449                 } else if (WIFSIGNALED(stat)) {
450                         // signalled termination
451                         status = WTERMSIG(stat);
452                 }
453                 joined = true;
454                 return true;
455         } else {
456                 throw runtime_error("bogus return value of waitpid");
457         }
458 #endif
459 }
460
461 int Process::Impl::Join() {
462         if (joined) {
463                 // can only join once
464                 return status;
465         }
466 #ifdef _WIN32
467         DWORD exit_code;
468         WaitForSingleObject(pi.hProcess, INFINITE);
469         GetExitCodeProcess(pi.hProcess, &exit_code);
470         CloseHandle(pi.hProcess);
471         CloseHandle(pi.hThread);
472         status = exit_code;
473         joined = true;
474         return status;
475 #else
476         while (true) {
477                 int stat;
478                 int result = waitpid(pid, &stat, 0);
479                 if (result == -1) {
480                         throw SysError("error waiting on child process");
481                 }
482                 if (result != pid) {
483                         // should in theory only happen with WNOHANG set
484                         continue;
485                 }
486                 if (WIFEXITED(stat)) {
487                         // autonomous termination
488                         status = WEXITSTATUS(stat);
489                         joined = true;
490                         return status;
491                 }
492                 if (WIFSIGNALED(stat)) {
493                         // signalled termination
494                         status = WTERMSIG(stat);
495                         joined = true;
496                         return status;
497                 }
498                 // otherwise, child probably signalled stop/continue, which we
499                 // don't care about (please don't tell youth welfare), so try again
500         }
501 #endif
502 }
503
504 }