1 /* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */
2
3 /*
4 * Main authors:
5 * Guido Tack <guido.tack@monash.edu>
6 */
7
8 /* This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this
10 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
11
12 #pragma once
13
14 #include <minizinc/solver.hh>
15
16 #ifdef _WIN32
17 #define NOMINMAX
18 #include <Windows.h>
19 #include <tchar.h>
20 #undef ERROR
21 //#include <atlstr.h>
22 #else
23 #include <sys/select.h>
24 #include <sys/time.h>
25 #include <sys/wait.h>
26 #include <unistd.h>
27 #endif
28 #include <condition_variable>
29 #include <csignal>
30 #include <deque>
31 #include <mutex>
32 #include <string>
33 #include <sys/types.h>
34 #include <thread>
35 #include <vector>
36
37 namespace MiniZinc {
38
39 #ifdef _WIN32
40
41 template <class S2O>
ReadPipePrint(HANDLE g_hCh,bool * _done,std::ostream * pOs,std::deque<std::string> * outputQueue,std::mutex * mtx,std::mutex * cv_mutex,std::condition_variable * cv)42 void ReadPipePrint(HANDLE g_hCh, bool* _done, std::ostream* pOs,
43 std::deque<std::string>* outputQueue, std::mutex* mtx, std::mutex* cv_mutex,
44 std::condition_variable* cv) {
45 bool& done = *_done;
46 assert(pOs != 0 || outputQueue != 0);
47 while (!done) {
48 char buffer[5255];
49 char nl_buffer[5255];
50 DWORD count = 0;
51 BOOL bSuccess = ReadFile(g_hCh, buffer, sizeof(buffer) - 1, &count, NULL);
52 if (bSuccess && count > 0) {
53 int nl_count = 0;
54 for (int i = 0; i < count; i++) {
55 if (buffer[i] != 13) {
56 nl_buffer[nl_count++] = buffer[i];
57 }
58 }
59 nl_buffer[nl_count] = 0;
60 std::lock_guard<std::mutex> lck(*mtx);
61 if (outputQueue) {
62 std::unique_lock<std::mutex> lk(*cv_mutex);
63 bool wasEmpty = outputQueue->empty();
64 outputQueue->push_back(nl_buffer);
65 lk.unlock();
66 if (wasEmpty) {
67 cv->notify_one();
68 }
69 }
70 if (pOs) (*pOs) << nl_buffer << std::flush;
71 } else {
72 if (outputQueue) {
73 std::unique_lock<std::mutex> lk(*cv_mutex);
74 bool wasEmpty = outputQueue->empty();
75 outputQueue->push_back("\n");
76 done = true;
77 lk.unlock();
78 if (wasEmpty) {
79 cv->notify_one();
80 }
81 } else {
82 done = true;
83 }
84 }
85 }
86 }
87 #endif
88
89 template <class S2O>
90 class Process {
91 protected:
92 std::vector<std::string> _fzncmd;
93 S2O* _pS2Out;
94 int _timelimit;
95 bool _sigint;
96 #ifdef _WIN32
handleInterrupt(DWORD fdwCtrlType)97 static BOOL WINAPI handleInterrupt(DWORD fdwCtrlType) {
98 switch (fdwCtrlType) {
99 case CTRL_C_EVENT: {
100 std::unique_lock<std::mutex> lck(_interruptMutex);
101 hadInterrupt = true;
102 _interruptCondition.notify_all();
103 return TRUE;
104 }
105 default:
106 return FALSE;
107 }
108 }
109 static std::mutex _interruptMutex;
110 static std::condition_variable _interruptCondition;
111 #else
handleInterrupt(int signal)112 static void handleInterrupt(int signal) {
113 if (signal == SIGINT) {
114 hadInterrupt = true;
115 } else {
116 hadTerm = true;
117 }
118 }
119 static bool hadTerm;
120 #endif
121 static bool hadInterrupt;
122
123 public:
Process(std::vector<std::string> & fzncmd,S2O * pso,int tl,bool si)124 Process(std::vector<std::string>& fzncmd, S2O* pso, int tl, bool si)
125 : _fzncmd(fzncmd), _pS2Out(pso), _timelimit(tl), _sigint(si) {
126 assert(nullptr != _pS2Out);
127 }
run()128 int run() {
129 #ifdef _WIN32
130 SetConsoleCtrlHandler(handleInterrupt, TRUE);
131
132 SECURITY_ATTRIBUTES saAttr;
133 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
134 saAttr.bInheritHandle = TRUE;
135 saAttr.lpSecurityDescriptor = NULL;
136
137 HANDLE g_hChildStd_IN_Rd = NULL;
138 HANDLE g_hChildStd_IN_Wr = NULL;
139 HANDLE g_hChildStd_OUT_Rd = NULL;
140 HANDLE g_hChildStd_OUT_Wr = NULL;
141 HANDLE g_hChildStd_ERR_Rd = NULL;
142 HANDLE g_hChildStd_ERR_Wr = NULL;
143
144 // Create a pipe for the child process's STDOUT.
145 if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0))
146 std::cerr << "Stdout CreatePipe" << std::endl;
147 // Ensure the read handle to the pipe for STDOUT is not inherited.
148 if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0))
149 std::cerr << "Stdout SetHandleInformation" << std::endl;
150
151 // Create a pipe for the child process's STDERR.
152 if (!CreatePipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr, 0))
153 std::cerr << "Stderr CreatePipe" << std::endl;
154 // Ensure the read handle to the pipe for STDERR is not inherited.
155 if (!SetHandleInformation(g_hChildStd_ERR_Rd, HANDLE_FLAG_INHERIT, 0))
156 std::cerr << "Stderr SetHandleInformation" << std::endl;
157
158 // Create a pipe for the child process's STDIN
159 if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0))
160 std::cerr << "Stdin CreatePipe" << std::endl;
161 // Ensure the write handle to the pipe for STDIN is not inherited.
162 if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0))
163 std::cerr << "Stdin SetHandleInformation" << std::endl;
164
165 PROCESS_INFORMATION piProcInfo;
166 STARTUPINFOW siStartInfo;
167 BOOL bSuccess = FALSE;
168
169 // Set up members of the PROCESS_INFORMATION structure.
170 ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
171
172 // Set up members of the STARTUPINFO structure.
173 // This structure specifies the STDIN and STDOUT handles for redirection.
174 ZeroMemory(&siStartInfo, sizeof(STARTUPINFOW));
175 siStartInfo.cb = sizeof(STARTUPINFOW);
176 siStartInfo.hStdError = g_hChildStd_ERR_Wr;
177 siStartInfo.hStdOutput = g_hChildStd_OUT_Wr;
178 siStartInfo.hStdInput = g_hChildStd_IN_Rd;
179 siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
180
181 std::string cmdline = FileUtils::combine_cmd_line(_fzncmd);
182 wchar_t* cmdstr = _wcsdup(FileUtils::utf8_to_wide(cmdline).c_str());
183
184 HANDLE hJobObject = CreateJobObject(NULL, NULL);
185
186 BOOL processStarted = CreateProcessW(NULL,
187 cmdstr, // command line
188 NULL, // process security attributes
189 NULL, // primary thread security attributes
190 TRUE, // handles are inherited
191 0, // creation flags
192 NULL, // use parent's environment
193 NULL, // use parent's current directory
194 &siStartInfo, // STARTUPINFO pointer
195 &piProcInfo); // receives PROCESS_INFORMATION
196
197 if (!processStarted) {
198 std::stringstream ssm;
199 ssm << "Error occurred when executing FZN solver with command \""
200 << FileUtils::wide_to_utf8(cmdstr) << "\".";
201 throw InternalError(ssm.str());
202 }
203
204 BOOL assignedToJob = AssignProcessToJobObject(hJobObject, piProcInfo.hProcess);
205 if (!assignedToJob) {
206 throw InternalError("Failed to assign process to job.");
207 }
208
209 CloseHandle(piProcInfo.hThread);
210 delete cmdstr;
211
212 // Stop ReadFile from blocking
213 CloseHandle(g_hChildStd_OUT_Wr);
214 CloseHandle(g_hChildStd_ERR_Wr);
215 // Just close the child's in pipe here
216 CloseHandle(g_hChildStd_IN_Rd);
217 bool doneStdout = false;
218 bool doneStderr = false;
219
220 // Threaded solution seems simpler than asyncronous pipe reading
221 std::mutex pipeMutex;
222
223 std::mutex cv_mutex;
224 std::condition_variable cv;
225
226 std::deque<std::string> outputQueue;
227 thread thrStdout(&ReadPipePrint<S2O>, g_hChildStd_OUT_Rd, &doneStdout, nullptr, &outputQueue,
228 &pipeMutex, &cv_mutex, &cv);
229 thread thrStderr(&ReadPipePrint<S2O>, g_hChildStd_ERR_Rd, &doneStderr, &_pS2Out->getLog(),
230 nullptr, &pipeMutex, nullptr, nullptr);
231 thread thrTimeout([&] {
232 auto shouldStop = [&] { return hadInterrupt || (doneStderr && doneStdout); };
233 std::unique_lock<std::mutex> lck(_interruptMutex);
234 if (_timelimit != 0) {
235 if (!_interruptCondition.wait_for(lck, std::chrono::milliseconds(_timelimit), shouldStop)) {
236 // If we timed out, generate an interrupt but ignore it ourselves
237 bool oldHadInterrupt = hadInterrupt;
238 GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
239 _interruptCondition.wait(lck, [&] { return hadInterrupt; });
240 hadInterrupt = oldHadInterrupt;
241 }
242 } else {
243 _interruptCondition.wait(lck, shouldStop);
244 }
245 // At this point the child should be stopped/stopping
246 if (!doneStderr || !doneStdout) {
247 if (!_interruptCondition.wait_for(lck, std::chrono::milliseconds(200),
248 [&] { return doneStderr && doneStdout; })) {
249 // Force terminate the child after 200ms
250 TerminateJobObject(hJobObject, 0);
251 };
252 }
253 });
254
255 while (true) {
256 std::unique_lock<std::mutex> lk(cv_mutex);
257 cv.wait(lk, [&] { return !outputQueue.empty(); });
258 while (!outputQueue.empty()) {
259 try {
260 _pS2Out->feedRawDataChunk(outputQueue.front().c_str());
261 outputQueue.pop_front();
262 } catch (...) {
263 TerminateJobObject(hJobObject, 0);
264 doneStdout = true;
265 doneStderr = true;
266 lk.unlock();
267 thrStdout.join();
268 thrStderr.join();
269 {
270 // Make sure thrTimeout terminates
271 std::unique_lock<std::mutex> lck(_interruptMutex);
272 _interruptCondition.notify_all();
273 }
274 thrTimeout.join();
275 SetConsoleCtrlHandler(handleInterrupt, FALSE);
276 std::rethrow_exception(std::current_exception());
277 }
278 }
279 if (doneStdout) break;
280 }
281
282 thrStdout.join();
283 thrStderr.join();
284 {
285 // Make sure thrTimeout terminates
286 std::unique_lock<std::mutex> lck(_interruptMutex);
287 _interruptCondition.notify_all();
288 }
289 thrTimeout.join();
290 DWORD exitCode = 0;
291 if (GetExitCodeProcess(piProcInfo.hProcess, &exitCode) == FALSE) {
292 exitCode = 1;
293 }
294 CloseHandle(piProcInfo.hProcess);
295
296 SetConsoleCtrlHandler(handleInterrupt, FALSE);
297 if (hadInterrupt) {
298 // Re-trigger signal if it was not caused by our own timeout
299 GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
300 }
301 return exitCode;
302 }
303 #else
304 int pipes[3][2];
305 pipe(pipes[0]);
306 pipe(pipes[1]);
307 pipe(pipes[2]);
308
309 if (int childPID = fork()) {
310 close(pipes[0][0]);
311 close(pipes[1][1]);
312 close(pipes[2][1]);
313 close(pipes[0][1]);
314
315 fd_set fdset;
316 FD_ZERO(&fdset); // NOLINT(readability-isolate-declaration)
317
318 struct timeval starttime;
319 gettimeofday(&starttime, nullptr);
320
321 struct timeval timeout_orig;
322 timeout_orig.tv_sec = _timelimit / 1000;
323 timeout_orig.tv_usec = (_timelimit % 1000) * 1000;
324 struct timeval timeout = timeout_orig;
325
326 hadInterrupt = false;
327 hadTerm = false;
328 struct sigaction sa;
329 struct sigaction old_sa_int;
330 struct sigaction old_sa_term;
331 sa.sa_handler = &handleInterrupt;
332 sa.sa_flags = 0;
333 sigfillset(&sa.sa_mask);
334 sigaction(SIGINT, &sa, &old_sa_int);
335 sigaction(SIGTERM, &sa, &old_sa_term);
336 int signal = _sigint ? SIGINT : SIGTERM;
337 bool handledInterrupt = false;
338 bool handledTerm = false;
339
340 bool done = hadTerm || hadInterrupt;
341 bool timed_out = false;
342 while (!done) {
343 FD_SET(pipes[1][0], &fdset);
344 FD_SET(pipes[2][0], &fdset);
345 int sel =
346 select(FD_SETSIZE, &fdset, nullptr, nullptr, _timelimit == 0 ? nullptr : &timeout);
347 if (sel == -1) {
348 if (errno != EINTR) {
349 // some error has happened
350 throw InternalError(std::string("Error in communication with solver: ") +
351 strerror(errno));
352 }
353 }
354 bool timeoutImmediately = false;
355 if (hadInterrupt && !handledInterrupt) {
356 signal = SIGINT;
357 handledInterrupt = true;
358 timeoutImmediately = true;
359 }
360 if (hadTerm && !handledTerm) {
361 signal = SIGTERM;
362 handledTerm = true;
363 timeoutImmediately = true;
364 }
365 if (timeoutImmediately) {
366 // Set timeout to immediately expire
367 _timelimit = -1;
368 timeout.tv_sec = 0;
369 timeout.tv_usec = 0;
370 timeout_orig = timeout;
371 timeval currentTime;
372 gettimeofday(¤tTime, nullptr);
373 starttime = currentTime;
374 }
375
376 bool killed = false;
377 if (_timelimit != 0) {
378 timeval currentTime;
379 gettimeofday(¤tTime, nullptr);
380 if (sel != 0) {
381 timeval elapsed;
382 elapsed.tv_sec = currentTime.tv_sec - starttime.tv_sec;
383 elapsed.tv_usec = currentTime.tv_usec - starttime.tv_usec;
384 if (elapsed.tv_usec < 0) {
385 elapsed.tv_sec--;
386 elapsed.tv_usec += 1000000;
387 }
388 // Reset timeout to original limit
389 timeout = timeout_orig;
390 // Subtract elapsed time
391 timeout.tv_usec = timeout.tv_usec - elapsed.tv_usec;
392 if (timeout.tv_usec < 0) {
393 timeout.tv_sec--;
394 timeout.tv_usec += 1000000;
395 }
396 timeout.tv_sec = timeout.tv_sec - elapsed.tv_sec;
397 } else {
398 timeout.tv_usec = 0;
399 timeout.tv_sec = 0;
400 }
401 if (timeout.tv_sec < 0 || (timeout.tv_sec == 0 && timeout.tv_usec == 0)) {
402 timed_out = true;
403 if (signal == SIGKILL) {
404 killed = true;
405 done = true;
406 }
407 if (killpg(childPID, signal) == -1) {
408 // Fallback to killing the child if killing the process group fails
409 kill(childPID, signal);
410 }
411 timeout.tv_sec = 0;
412 timeout.tv_usec = 200000;
413 timeout_orig = timeout;
414 starttime = currentTime;
415 // Upgrade signal for next attempt
416 signal = signal == SIGINT ? SIGTERM : SIGKILL;
417 }
418 }
419
420 bool addedNl = false;
421 for (int i = 1; i <= 2; ++i) {
422 if (FD_ISSET(pipes[i][0], &fdset)) {
423 char buffer[1000];
424 int count = read(pipes[i][0], buffer, sizeof(buffer) - 1);
425 if (count > 0) {
426 buffer[count] = 0;
427 if (1 == i) {
428 // cerr << "mzn-fzn: raw chunk stdout::: " << flush;
429 // cerr << buffer << flush;
430 try {
431 _pS2Out->feedRawDataChunk(buffer);
432 } catch (...) {
433 // Exception during solns2out, kill process and re-throw
434 if (killpg(childPID, SIGKILL) == -1) {
435 // Fallback to killing the child if killing the process group fails
436 kill(childPID, SIGKILL);
437 }
438 throw;
439 }
440 } else {
441 _pS2Out->getLog() << buffer << std::flush;
442 }
443 } else if (1 == i) {
444 _pS2Out->feedRawDataChunk("\n"); // in case last chunk did not end with \n
445 addedNl = true;
446 done = true;
447 }
448 }
449 }
450 if (killed && !addedNl) {
451 _pS2Out->feedRawDataChunk("\n"); // in case last chunk did not end with \n
452 }
453 }
454
455 close(pipes[1][0]);
456 close(pipes[2][0]);
457 int exitStatus = timed_out ? 0 : 1;
458 int childStatus;
459 int pidStatus = waitpid(childPID, &childStatus, 0);
460 if (!timed_out && pidStatus > 0) {
461 if (WIFEXITED(childStatus)) {
462 exitStatus = WEXITSTATUS(childStatus);
463 }
464 }
465 sigaction(SIGINT, &old_sa_int, nullptr);
466 sigaction(SIGTERM, &old_sa_term, nullptr);
467 if (hadInterrupt) {
468 kill(getpid(), SIGINT);
469 }
470 if (hadTerm) {
471 kill(getpid(), SIGTERM);
472 }
473 return exitStatus;
474 }
475 if (setpgid(0, 0) == -1) {
476 throw InternalError("Failed to set pgid of subprocess");
477 }
478 close(STDOUT_FILENO);
479 close(STDERR_FILENO);
480 close(STDIN_FILENO);
481 dup2(pipes[0][0], STDIN_FILENO);
482 dup2(pipes[1][1], STDOUT_FILENO);
483 dup2(pipes[2][1], STDERR_FILENO);
484 close(pipes[0][0]);
485 close(pipes[0][1]);
486 close(pipes[1][1]);
487 close(pipes[1][0]);
488 close(pipes[2][1]);
489 close(pipes[2][0]);
490
491 std::vector<char*> cmd_line;
492 for (auto& iCmdl : _fzncmd) {
493 cmd_line.push_back(strdup(iCmdl.c_str()));
494 }
495
496 char** argv = new char*[cmd_line.size() + 1];
497 for (unsigned int i = 0; i < cmd_line.size(); i++) {
498 argv[i] = cmd_line[i];
499 }
500 argv[cmd_line.size()] = nullptr;
501
502 int status = execvp(argv[0], argv); // execvp only returns if an error occurs.
503 assert(status == -1); // the returned value will always be -1
504 std::stringstream ssm;
505 ssm << "Error occurred when executing FZN solver with command \"";
506 for (auto& s : cmd_line) {
507 ssm << s << ' ';
508 }
509 ssm << "\".";
510 throw InternalError(ssm.str());
511 }
512 #endif
513 };
514
515 template <class S2O>
516 bool Process<S2O>::hadInterrupt;
517 #ifdef _WIN32
518 template <class S2O>
519 std::mutex Process<S2O>::_interruptMutex;
520 template <class S2O>
521 std::condition_variable Process<S2O>::_interruptCondition;
522 #else
523 template <class S2O>
524 bool Process<S2O>::hadTerm;
525 #endif
526
527 } // namespace MiniZinc
528