11#include " exec.h"
22
3+ #include < boost/chrono.hpp>
34#include < boost/filesystem.hpp>
45#include < boost/format.hpp>
56
67#include " logging/logging.h"
78
9+ #include < fcntl.h>
10+ #include < spawn.h>
11+ #include < sys/wait.h>
12+ #include < unistd.h>
13+ #include < array>
14+ #include < cstring>
15+ #include < iostream>
16+ #include < string>
17+ #include < vector>
18+
19+ struct ProcessResult {
20+ std::string stdout_output;
21+ std::string stderr_output;
22+ int exit_code;
23+ };
24+
25+ static long long convertToSeconds (std::string input);
26+ class Process {
27+ private:
28+ int stdout_pipe[2 ];
29+ int stderr_pipe[2 ];
30+ pid_t pid;
31+
32+ // Set file descriptor to non-blocking mode
33+ bool setNonBlocking (int fd) {
34+ int flags = fcntl (fd, F_GETFL, 0 );
35+ if (flags == -1 ) return false ;
36+ return fcntl (fd, F_SETFL, flags | O_NONBLOCK) != -1 ;
37+ }
38+
39+ // Read from both pipes simultaneously to avoid deadlock
40+ void readFromBothPipes (std::string& stdout_data, std::string& stderr_data, bool print_output,
41+ const std::string& timeout) {
42+ std::array<char , 4096 > buffer;
43+ fd_set read_fds;
44+ int max_fd = std::max (stdout_pipe[0 ], stderr_pipe[0 ]) + 1 ;
45+ bool stdout_open = true ;
46+ bool stderr_open = true ;
47+
48+ // Set both pipes to non-blocking mode
49+ setNonBlocking (stdout_pipe[0 ]);
50+ setNonBlocking (stderr_pipe[0 ]);
51+
52+ auto start_time = boost::chrono::steady_clock::now ();
53+ auto end_time = start_time;
54+ if (!timeout.empty ()) {
55+ end_time = start_time + boost::chrono::seconds (convertToSeconds (timeout));
56+ }
57+ while (stdout_open || stderr_open) {
58+ FD_ZERO (&read_fds);
59+
60+ if (stdout_open) FD_SET (stdout_pipe[0 ], &read_fds);
61+ if (stderr_open) FD_SET (stderr_pipe[0 ], &read_fds);
62+
63+ // Wait for data on either pipe (with timeout)
64+ struct timeval select_timeout;
65+ select_timeout.tv_sec = 1 ;
66+ select_timeout.tv_usec = 0 ;
67+
68+ int result = select (max_fd, &read_fds, nullptr , nullptr , &select_timeout);
69+ if (result == -1 ) {
70+ LOG_ERROR << " Error in select: " << strerror (errno);
71+ throw std::runtime_error (" exec: Error creating pipes" );
72+ }
73+
74+ // Read from stdout if data is available
75+ if (stdout_open && FD_ISSET (stdout_pipe[0 ], &read_fds)) {
76+ ssize_t bytes_read = read (stdout_pipe[0 ], buffer.data (), buffer.size ());
77+ if (bytes_read > 0 ) {
78+ stdout_data.append (buffer.data (), bytes_read);
79+ if (print_output) {
80+ std::cout.write (buffer.data (), bytes_read);
81+ std::cout.flush ();
82+ }
83+ } else if (bytes_read == 0 ) {
84+ stdout_open = false ;
85+ } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
86+ stdout_open = false ;
87+ }
88+ }
89+
90+ // Read from stderr if data is available
91+ if (stderr_open && FD_ISSET (stderr_pipe[0 ], &read_fds)) {
92+ ssize_t bytes_read = read (stderr_pipe[0 ], buffer.data (), buffer.size ());
93+ if (bytes_read > 0 ) {
94+ stderr_data.append (buffer.data (), bytes_read);
95+ } else if (bytes_read == 0 ) {
96+ stderr_open = false ;
97+ } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
98+ stderr_open = false ;
99+ }
100+ }
101+ if (!timeout.empty () && boost::chrono::steady_clock::now () > end_time) {
102+ throw std::runtime_error (" Timeout occurred while waiting for a child process completion" );
103+ }
104+ }
105+ }
106+
107+ public:
108+ Process () : pid(-1 ) {
109+ stdout_pipe[0 ] = stdout_pipe[1 ] = -1 ;
110+ stderr_pipe[0 ] = stderr_pipe[1 ] = -1 ;
111+ }
112+
113+ ~Process () { closeAllPipes (); }
114+
115+ void closeAllPipes () {
116+ if (stdout_pipe[0 ] != -1 ) close (stdout_pipe[0 ]);
117+ if (stdout_pipe[1 ] != -1 ) close (stdout_pipe[1 ]);
118+ if (stderr_pipe[0 ] != -1 ) close (stderr_pipe[0 ]);
119+ if (stderr_pipe[1 ] != -1 ) close (stderr_pipe[1 ]);
120+
121+ stdout_pipe[0 ] = stdout_pipe[1 ] = -1 ;
122+ stderr_pipe[0 ] = stderr_pipe[1 ] = -1 ;
123+ }
124+
125+ ProcessResult execute (const std::string& command, bool print_output, const std::string& timeout) {
126+ ProcessResult result;
127+ result.exit_code = -1 ;
128+
129+ // Create pipes for stdout and stderr
130+ if (pipe (stdout_pipe) == -1 || pipe (stderr_pipe) == -1 ) {
131+ LOG_ERROR << " Error creating pipes: " << strerror (errno);
132+ throw std::runtime_error (" exec: Error creating pipes" );
133+ }
134+
135+ // Setup file actions for posix_spawn
136+ posix_spawn_file_actions_t file_actions;
137+ posix_spawn_file_actions_init (&file_actions);
138+
139+ // Redirect stdout to pipe
140+ posix_spawn_file_actions_addclose (&file_actions, stdout_pipe[0 ]);
141+ posix_spawn_file_actions_adddup2 (&file_actions, stdout_pipe[1 ], STDOUT_FILENO);
142+ posix_spawn_file_actions_addclose (&file_actions, stdout_pipe[1 ]);
143+
144+ // Redirect stderr to pipe
145+ posix_spawn_file_actions_addclose (&file_actions, stderr_pipe[0 ]);
146+ posix_spawn_file_actions_adddup2 (&file_actions, stderr_pipe[1 ], STDERR_FILENO);
147+ posix_spawn_file_actions_addclose (&file_actions, stderr_pipe[1 ]);
148+
149+ // Prepare arguments for shell execution
150+ char * argv[] = {const_cast <char *>(" sh" ), const_cast <char *>(" -c" ), const_cast <char *>(command.c_str ()), nullptr };
151+
152+ // Spawn the process
153+ int spawn_result = posix_spawn (&pid, " /bin/sh" , &file_actions, nullptr , argv, environ);
154+
155+ // Clean up file actions
156+ posix_spawn_file_actions_destroy (&file_actions);
157+
158+ if (spawn_result != 0 ) {
159+ LOG_ERROR << " Error spawning process: " << strerror (spawn_result);
160+ closeAllPipes ();
161+ throw std::runtime_error (" exec: Error spawning process" );
162+ }
163+
164+ // Parent process - close write ends of pipes
165+ close (stdout_pipe[1 ]);
166+ stdout_pipe[1 ] = -1 ;
167+ close (stderr_pipe[1 ]);
168+ stderr_pipe[1 ] = -1 ;
169+
170+ // Read from both pipes simultaneously to avoid deadlock
171+ readFromBothPipes (result.stdout_output , result.stderr_output , print_output, timeout);
172+
173+ // Close read ends
174+ close (stdout_pipe[0 ]);
175+ stdout_pipe[0 ] = -1 ;
176+ close (stderr_pipe[0 ]);
177+ stderr_pipe[0 ] = -1 ;
178+
179+ // Wait for child process to finish
180+ int status;
181+ waitpid (pid, &status, 0 );
182+
183+ if (WIFEXITED (status)) {
184+ result.exit_code = WEXITSTATUS (status);
185+ }
186+
187+ return result;
188+ }
189+ };
190+
191+ static long long convertToSeconds (std::string input) {
192+ if (input.empty ()) return 0 ;
193+
194+ char unit = input.back ();
195+ long long value = std::stoll (input.substr (0 , input.size () - 1 ));
196+
197+ switch (unit) {
198+ case ' h' :
199+ return value * 3600 ;
200+ case ' m' :
201+ return value * 60 ;
202+ case ' s' :
203+ return value;
204+ default :
205+ throw std::invalid_argument (" Invalid time interval " + input);
206+ }
207+ }
208+
8209void exec (const std::string& cmd, const std::string& err_msg_prefix, const boost::filesystem::path& start_dir,
9210 std::string* output, const std::string& timeout, bool print_output) {
10211 std::string command;
@@ -17,53 +218,25 @@ void exec(const std::string& cmd, const std::string& err_msg_prefix, const boost
17218 command = " PARENT_HAS_TTY=1 " ;
18219 }
19220
20- if (!timeout.empty ()) {
21- command += " timeout " + timeout + " " ;
22- }
23- command += cmd + " 2>&1" ;
221+ command += cmd;
24222 if (!start_dir.empty ()) {
25223 command = " cd " + start_dir.string () + " && " + command;
26224 }
27225
28226 LOG_DEBUG << " Running: `" << command << " `" ;
29- FILE* pipe = popen (command.c_str (), " r" );
30- if (!pipe) {
31- throw std::runtime_error (" exec: popen() failed!" );
32- }
33- std::string result;
34- try {
35- std::array<char , 128 > buffer_array = {};
36- char * buffer = buffer_array.data ();
37- while (std::fgets (buffer, sizeof (buffer_array), pipe) != nullptr ) {
38- if (print_output) {
39- fputs (buffer, stdout);
40- }
41- result += buffer;
42- }
227+ Process proc;
228+ auto result = proc.execute (command.c_str (), print_output, timeout);
43229
44- if (output != nullptr ) {
45- *output = result;
46- }
47- } catch (const std::exception& e) {
48- pclose (pipe);
49- throw std::runtime_error (std::string (" exec: " ) + e.what ());
50- }
230+ LOG_DEBUG << " Command exited with code " << result.exit_code ;
51231
52- int status = pclose (pipe);
53- if (status == -1 ) {
54- throw std::runtime_error (" exec: pclose() failed!" );
232+ if (result.exit_code != EXIT_SUCCESS) {
233+ throw ExecError (err_msg_prefix, cmd, result.stderr_output , result.exit_code );
55234 }
56-
57- int exit_code = WEXITSTATUS (status);
58-
59- if (exit_code == 124 ) {
60- // `timeout` command return code indicating that a timeout has occured
61- throw std::runtime_error (" Timeout occurred while waiting for a child process completion" );
235+ if (output != nullptr ) {
236+ *output = result.stdout_output ;
62237 }
63- LOG_DEBUG << " Command exited with code " << exit_code;
64-
65- if (exit_code != EXIT_SUCCESS) {
66- throw ExecError (err_msg_prefix, cmd, result, exit_code);
238+ if (result.stderr_output .size () > 0 ) {
239+ LOG_DEBUG << " Command stderr: " << result.stderr_output ;
67240 }
68241}
69242
0 commit comments