Skip to content

Commit 765adfb

Browse files
committed
exec: Create wrapper to capture stderr output separately from stdout
Current implementation of the exec function merges stdout and stderr, which may lead to problems when the output is meant to be parsed, but some error message is printed as well. Properly separate both outputs from the subprocess, allowing stderr to be ignored if the execution is successful, or logged in case of errors. Signed-off-by: Andre Detsch <andre.detsch@foundries.io>
1 parent a5f4f5a commit 765adfb

File tree

1 file changed

+184
-34
lines changed

1 file changed

+184
-34
lines changed

src/exec.cc

Lines changed: 184 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,177 @@
55

66
#include "logging/logging.h"
77

8+
#include <array>
9+
#include <iostream>
10+
#include <string>
11+
#include <vector>
12+
#include <unistd.h>
13+
#include <sys/wait.h>
14+
#include <spawn.h>
15+
#include <cstring>
16+
#include <unistd.h>
17+
#include <fcntl.h>
18+
19+
struct ProcessResult {
20+
std::string stdout_output;
21+
std::string stderr_output;
22+
int exit_code;
23+
};
24+
25+
class Process {
26+
private:
27+
int stdout_pipe[2];
28+
int stderr_pipe[2];
29+
pid_t pid;
30+
31+
// Set file descriptor to non-blocking mode
32+
bool setNonBlocking(int fd) {
33+
int flags = fcntl(fd, F_GETFL, 0);
34+
if (flags == -1) return false;
35+
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
36+
}
37+
38+
// Read from both pipes simultaneously to avoid deadlock
39+
void readFromBothPipes(std::string& stdout_data, std::string& stderr_data) {
40+
std::array<char, 4096> buffer;
41+
fd_set read_fds;
42+
int max_fd = std::max(stdout_pipe[0], stderr_pipe[0]) + 1;
43+
bool stdout_open = true;
44+
bool stderr_open = true;
45+
46+
// Set both pipes to non-blocking mode
47+
setNonBlocking(stdout_pipe[0]);
48+
setNonBlocking(stderr_pipe[0]);
49+
50+
while (stdout_open || stderr_open) {
51+
FD_ZERO(&read_fds);
52+
53+
if (stdout_open) FD_SET(stdout_pipe[0], &read_fds);
54+
if (stderr_open) FD_SET(stderr_pipe[0], &read_fds);
55+
56+
int result = select(max_fd, &read_fds, nullptr, nullptr, NULL);
57+
if (result == -1) {
58+
LOG_ERROR << "Error in select: " << strerror(errno);
59+
throw std::runtime_error("exec: Error creating pipes");
60+
}
61+
62+
// Read from stdout if data is available
63+
if (stdout_open && FD_ISSET(stdout_pipe[0], &read_fds)) {
64+
ssize_t bytes_read = read(stdout_pipe[0], buffer.data(), buffer.size());
65+
if (bytes_read > 0) {
66+
stdout_data.append(buffer.data(), bytes_read);
67+
} else if (bytes_read == 0) {
68+
stdout_open = false;
69+
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
70+
stdout_open = false;
71+
}
72+
}
73+
74+
// Read from stderr if data is available
75+
if (stderr_open && FD_ISSET(stderr_pipe[0], &read_fds)) {
76+
ssize_t bytes_read = read(stderr_pipe[0], buffer.data(), buffer.size());
77+
if (bytes_read > 0) {
78+
stderr_data.append(buffer.data(), bytes_read);
79+
} else if (bytes_read == 0) {
80+
stderr_open = false;
81+
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
82+
stderr_open = false;
83+
}
84+
}
85+
}
86+
}
87+
88+
public:
89+
Process() : pid(-1) {
90+
stdout_pipe[0] = stdout_pipe[1] = -1;
91+
stderr_pipe[0] = stderr_pipe[1] = -1;
92+
}
93+
94+
~Process() {
95+
closeAllPipes();
96+
}
97+
98+
void closeAllPipes() {
99+
if (stdout_pipe[0] != -1) close(stdout_pipe[0]);
100+
if (stdout_pipe[1] != -1) close(stdout_pipe[1]);
101+
if (stderr_pipe[0] != -1) close(stderr_pipe[0]);
102+
if (stderr_pipe[1] != -1) close(stderr_pipe[1]);
103+
104+
stdout_pipe[0] = stdout_pipe[1] = -1;
105+
stderr_pipe[0] = stderr_pipe[1] = -1;
106+
}
107+
108+
ProcessResult execute(const std::string& command) {
109+
ProcessResult result;
110+
result.exit_code = -1;
111+
112+
// Create pipes for stdout and stderr
113+
if (pipe(stdout_pipe) == -1 || pipe(stderr_pipe) == -1) {
114+
LOG_ERROR << "Error creating pipes: " << strerror(errno);
115+
throw std::runtime_error("exec: Error creating pipes");
116+
}
117+
118+
// Setup file actions for posix_spawn
119+
posix_spawn_file_actions_t file_actions;
120+
posix_spawn_file_actions_init(&file_actions);
121+
122+
// Redirect stdout to pipe
123+
posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[0]);
124+
posix_spawn_file_actions_adddup2(&file_actions, stdout_pipe[1], STDOUT_FILENO);
125+
posix_spawn_file_actions_addclose(&file_actions, stdout_pipe[1]);
126+
127+
// Redirect stderr to pipe
128+
posix_spawn_file_actions_addclose(&file_actions, stderr_pipe[0]);
129+
posix_spawn_file_actions_adddup2(&file_actions, stderr_pipe[1], STDERR_FILENO);
130+
posix_spawn_file_actions_addclose(&file_actions, stderr_pipe[1]);
131+
132+
// Prepare arguments for shell execution
133+
char *argv[] = {
134+
const_cast<char*>("sh"),
135+
const_cast<char*>("-c"),
136+
const_cast<char*>(command.c_str()),
137+
nullptr
138+
};
139+
140+
// Spawn the process
141+
int spawn_result = posix_spawn(&pid, "/bin/sh", &file_actions, nullptr, argv, environ);
142+
143+
// Clean up file actions
144+
posix_spawn_file_actions_destroy(&file_actions);
145+
146+
if (spawn_result != 0) {
147+
LOG_ERROR << "Error spawning process: " << strerror(spawn_result);
148+
closeAllPipes();
149+
throw std::runtime_error("exec: Error spawning process");
150+
}
151+
152+
// Parent process - close write ends of pipes
153+
close(stdout_pipe[1]);
154+
stdout_pipe[1] = -1;
155+
close(stderr_pipe[1]);
156+
stderr_pipe[1] = -1;
157+
158+
// Read from both pipes simultaneously to avoid deadlock
159+
readFromBothPipes(result.stdout_output, result.stderr_output);
160+
161+
// Close read ends
162+
close(stdout_pipe[0]);
163+
stdout_pipe[0] = -1;
164+
close(stderr_pipe[0]);
165+
stderr_pipe[0] = -1;
166+
167+
// Wait for child process to finish
168+
int status;
169+
waitpid(pid, &status, 0);
170+
171+
if (WIFEXITED(status)) {
172+
result.exit_code = WEXITSTATUS(status);
173+
}
174+
175+
return result;
176+
}
177+
};
178+
8179
void exec(const std::string& cmd, const std::string& err_msg_prefix, const boost::filesystem::path& start_dir,
9180
std::string* output, const std::string& timeout, bool print_output) {
10181
std::string command;
@@ -20,50 +191,29 @@ void exec(const std::string& cmd, const std::string& err_msg_prefix, const boost
20191
if (!timeout.empty()) {
21192
command += "timeout " + timeout + " ";
22193
}
23-
command += cmd + " 2>&1";
194+
command += cmd;
24195
if (!start_dir.empty()) {
25196
command = "cd " + start_dir.string() + " && " + command;
26197
}
27198

28199
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-
}
200+
Process proc;
201+
auto result = proc.execute(command.c_str());
43202

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());
203+
if (result.exit_code == 124) {
204+
// `timeout` command return code indicating that a timeout has occurred
205+
throw std::runtime_error("Timeout occurred while waiting for a child process completion");
50206
}
207+
LOG_DEBUG << "Command exited with code " << result.exit_code;
51208

52-
int status = pclose(pipe);
53-
if (status == -1) {
54-
throw std::runtime_error("exec: pclose() failed!");
209+
if (result.exit_code != EXIT_SUCCESS) {
210+
throw ExecError(err_msg_prefix, cmd, result.stderr_output, result.exit_code);
55211
}
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");
212+
if (output != nullptr) {
213+
*output = result.stdout_output;
62214
}
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);
215+
if (result.stderr_output.size() > 0) {
216+
LOG_DEBUG << "Command stderr: " << result.stderr_output;
67217
}
68218
}
69219

0 commit comments

Comments
 (0)