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(&currentTime, nullptr);
373           starttime = currentTime;
374         }
375 
376         bool killed = false;
377         if (_timelimit != 0) {
378           timeval currentTime;
379           gettimeofday(&currentTime, 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