Skip to content

Commit 8cabce6

Browse files
committed
Replace covert pipe with SIGCHLD handler
For background, see ninja-build#2444 (comment). In short, when running subprocesses that share the terminal, ninja intentionally leaves a pipe open before exec() so that it can use EOF from that pipe to detect when the subprocess has exited. That mechanism is problematic: If the subprocess ends up spawning background processes (e.g. sccache), those would also inherit the pipe by default. In that case, ninja may not detect process termination until all background processes have quitted. This patch makes it so that, instead of propagating a pipe file descriptor to the subprocess without its knowledge, ninja relies on the SIGCHLD signal together with waitpid(WNOHANG) to detect termination of console subprocesses. After this patch, console-sharing subprocesses do no longer have an associated pipe. Fixes ninja-build#2444
1 parent 048a68d commit 8cabce6

File tree

2 files changed

+132
-28
lines changed

2 files changed

+132
-28
lines changed

src/subprocess-posix.cc

+102-25
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ extern char** environ;
3636

3737
using namespace std;
3838

39+
static ExitStatus ParseExitStatus(int status);
40+
3941
Subprocess::Subprocess(bool use_console) : fd_(-1), pid_(-1),
4042
use_console_(use_console) {
4143
}
@@ -49,26 +51,34 @@ Subprocess::~Subprocess() {
4951
}
5052

5153
bool Subprocess::Start(SubprocessSet* set, const string& command) {
52-
int output_pipe[2];
53-
if (pipe(output_pipe) < 0)
54-
Fatal("pipe: %s", strerror(errno));
55-
fd_ = output_pipe[0];
54+
int subproc_stdout_fd = -1;
55+
if (use_console_) {
56+
fd_ = -1;
57+
} else {
58+
int output_pipe[2];
59+
if (pipe(output_pipe) < 0)
60+
Fatal("pipe: %s", strerror(errno));
61+
fd_ = output_pipe[0];
62+
subproc_stdout_fd = output_pipe[1];
5663
#if !defined(USE_PPOLL)
57-
// If available, we use ppoll in DoWork(); otherwise we use pselect
58-
// and so must avoid overly-large FDs.
59-
if (fd_ >= static_cast<int>(FD_SETSIZE))
60-
Fatal("pipe: %s", strerror(EMFILE));
64+
// If available, we use ppoll in DoWork(); otherwise we use pselect
65+
// and so must avoid overly-large FDs.
66+
if (fd_ >= static_cast<int>(FD_SETSIZE))
67+
Fatal("pipe: %s", strerror(EMFILE));
6168
#endif // !USE_PPOLL
62-
SetCloseOnExec(fd_);
69+
SetCloseOnExec(fd_);
70+
}
6371

6472
posix_spawn_file_actions_t action;
6573
int err = posix_spawn_file_actions_init(&action);
6674
if (err != 0)
6775
Fatal("posix_spawn_file_actions_init: %s", strerror(err));
6876

69-
err = posix_spawn_file_actions_addclose(&action, output_pipe[0]);
70-
if (err != 0)
71-
Fatal("posix_spawn_file_actions_addclose: %s", strerror(err));
77+
if (!use_console_) {
78+
err = posix_spawn_file_actions_addclose(&action, fd_);
79+
if (err != 0)
80+
Fatal("posix_spawn_file_actions_addclose: %s", strerror(err));
81+
}
7282

7383
posix_spawnattr_t attr;
7484
err = posix_spawnattr_init(&attr);
@@ -97,18 +107,17 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) {
97107
Fatal("posix_spawn_file_actions_addopen: %s", strerror(err));
98108
}
99109

100-
err = posix_spawn_file_actions_adddup2(&action, output_pipe[1], 1);
110+
err = posix_spawn_file_actions_adddup2(&action, subproc_stdout_fd, 1);
101111
if (err != 0)
102112
Fatal("posix_spawn_file_actions_adddup2: %s", strerror(err));
103-
err = posix_spawn_file_actions_adddup2(&action, output_pipe[1], 2);
113+
err = posix_spawn_file_actions_adddup2(&action, subproc_stdout_fd, 2);
104114
if (err != 0)
105115
Fatal("posix_spawn_file_actions_adddup2: %s", strerror(err));
106-
err = posix_spawn_file_actions_addclose(&action, output_pipe[1]);
116+
err = posix_spawn_file_actions_addclose(&action, subproc_stdout_fd);
107117
if (err != 0)
108118
Fatal("posix_spawn_file_actions_addclose: %s", strerror(err));
109-
// In the console case, output_pipe is still inherited by the child and
110-
// closed when the subprocess finishes, which then notifies ninja.
111119
}
120+
112121
#ifdef POSIX_SPAWN_USEVFORK
113122
flags |= POSIX_SPAWN_USEVFORK;
114123
#endif
@@ -130,7 +139,8 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) {
130139
if (err != 0)
131140
Fatal("posix_spawn_file_actions_destroy: %s", strerror(err));
132141

133-
close(output_pipe[1]);
142+
if (!use_console_)
143+
close(subproc_stdout_fd);
134144
return true;
135145
}
136146

@@ -147,13 +157,30 @@ void Subprocess::OnPipeReady() {
147157
}
148158
}
149159

150-
ExitStatus Subprocess::Finish() {
160+
161+
bool Subprocess::TryFinish(int waitpid_options) {
151162
assert(pid_ != -1);
152-
int status;
153-
if (waitpid(pid_, &status, 0) < 0)
154-
Fatal("waitpid(%d): %s", pid_, strerror(errno));
163+
int status, ret;
164+
while ((ret = waitpid(pid_, &status, waitpid_options)) < 0) {
165+
if (errno != EINTR)
166+
Fatal("waitpid(%d): %s", pid_, strerror(errno));
167+
}
168+
if (ret == 0)
169+
return false; // Subprocess is alive (WNOHANG-only).
155170
pid_ = -1;
171+
exit_status_ = ParseExitStatus(status);
172+
return true; // Subprocess has terminated.
173+
}
174+
175+
ExitStatus Subprocess::Finish() {
176+
if (pid_ != -1) {
177+
TryFinish(0);
178+
assert(pid_ == -1);
179+
}
180+
return exit_status_;
181+
}
156182

183+
static ExitStatus ParseExitStatus(int status) {
157184
#ifdef _AIX
158185
if (WIFEXITED(status) && WEXITSTATUS(status) & 0x80) {
159186
// Map the shell's exit code used for signal failure (128 + signal) to the
@@ -177,19 +204,28 @@ ExitStatus Subprocess::Finish() {
177204
}
178205

179206
bool Subprocess::Done() const {
180-
return fd_ == -1;
207+
// Console subprocesses share console with ninja, and we consider them done
208+
// when they exit.
209+
// For other processes, we consider them done when we have consumed all their
210+
// output and closed their associated pipe.
211+
return (use_console_ && pid_ == -1) || (!use_console_ && fd_ == -1);
181212
}
182213

183214
const string& Subprocess::GetOutput() const {
184215
return buf_;
185216
}
186217

187-
int SubprocessSet::interrupted_;
218+
volatile int SubprocessSet::interrupted_;
219+
volatile sig_atomic_t SubprocessSet::s_sigchld_received;
188220

189221
void SubprocessSet::SetInterruptedFlag(int signum) {
190222
interrupted_ = signum;
191223
}
192224

225+
void SubprocessSet::SigChldHandler(int signo, siginfo_t* info, void* context) {
226+
s_sigchld_received = 1;
227+
}
228+
193229
void SubprocessSet::HandlePendingInterruption() {
194230
sigset_t pending;
195231
sigemptyset(&pending);
@@ -206,11 +242,14 @@ void SubprocessSet::HandlePendingInterruption() {
206242
}
207243

208244
SubprocessSet::SubprocessSet() {
245+
// Block all these signals.
246+
// Their handlers will only be enabled during ppoll/pselect().
209247
sigset_t set;
210248
sigemptyset(&set);
211249
sigaddset(&set, SIGINT);
212250
sigaddset(&set, SIGTERM);
213251
sigaddset(&set, SIGHUP);
252+
sigaddset(&set, SIGCHLD);
214253
if (sigprocmask(SIG_BLOCK, &set, &old_mask_) < 0)
215254
Fatal("sigprocmask: %s", strerror(errno));
216255

@@ -223,6 +262,25 @@ SubprocessSet::SubprocessSet() {
223262
Fatal("sigaction: %s", strerror(errno));
224263
if (sigaction(SIGHUP, &act, &old_hup_act_) < 0)
225264
Fatal("sigaction: %s", strerror(errno));
265+
266+
memset(&act, 0, sizeof(act));
267+
act.sa_flags = SA_SIGINFO | SA_NOCLDSTOP;
268+
act.sa_sigaction = SigChldHandler;
269+
if (sigaction(SIGCHLD, &act, &old_chld_act_) < 0)
270+
Fatal("sigaction: %s", strerror(errno));
271+
}
272+
273+
void SubprocessSet::CheckConsoleProcessTerminated() {
274+
if (!s_sigchld_received)
275+
return;
276+
for (auto i = running_.begin(); i != running_.end(); ) {
277+
if ((*i)->use_console_ && (*i)->TryFinish(WNOHANG)) {
278+
finished_.push(*i);
279+
i = running_.erase(i);
280+
} else {
281+
++i;
282+
}
283+
}
226284
}
227285

228286
SubprocessSet::~SubprocessSet() {
@@ -234,6 +292,8 @@ SubprocessSet::~SubprocessSet() {
234292
Fatal("sigaction: %s", strerror(errno));
235293
if (sigaction(SIGHUP, &old_hup_act_, 0) < 0)
236294
Fatal("sigaction: %s", strerror(errno));
295+
if (sigaction(SIGCHLD, &old_chld_act_, 0) < 0)
296+
Fatal("sigaction: %s", strerror(errno));
237297
if (sigprocmask(SIG_SETMASK, &old_mask_, 0) < 0)
238298
Fatal("sigprocmask: %s", strerror(errno));
239299
}
@@ -262,17 +322,29 @@ bool SubprocessSet::DoWork() {
262322
fds.push_back(pfd);
263323
++nfds;
264324
}
325+
if (nfds == 0) {
326+
// Add a dummy entry to prevent using an empty pollfd vector.
327+
// ppoll() allows to do this by setting fd < 0.
328+
pollfd pfd = { -1, 0, 0 };
329+
fds.push_back(pfd);
330+
++nfds;
331+
}
265332

266333
interrupted_ = 0;
334+
s_sigchld_received = 0;
267335
int ret = ppoll(&fds.front(), nfds, NULL, &old_mask_);
268336
if (ret == -1) {
269337
if (errno != EINTR) {
270338
perror("ninja: ppoll");
271339
return false;
272340
}
341+
CheckConsoleProcessTerminated();
273342
return IsInterrupted();
274343
}
275344

345+
// ppoll/pselect prioritizes file descriptor events over a signal delivery.
346+
// However, if the user is trying to quit ninja, we should react as fast as
347+
// possible.
276348
HandlePendingInterruption();
277349
if (IsInterrupted())
278350
return true;
@@ -315,15 +387,20 @@ bool SubprocessSet::DoWork() {
315387
}
316388

317389
interrupted_ = 0;
318-
int ret = pselect(nfds, &set, 0, 0, 0, &old_mask_);
390+
s_sigchld_received = 0;
391+
int ret = pselect(nfds, (nfds > 0 ? &set : nullptr), 0, 0, 0, &old_mask_);
319392
if (ret == -1) {
320393
if (errno != EINTR) {
321394
perror("ninja: pselect");
322395
return false;
323396
}
397+
CheckConsoleProcessTerminated();
324398
return IsInterrupted();
325399
}
326400

401+
// ppoll/pselect prioritizes file descriptor events over a signal delivery.
402+
// However, if the user is trying to quit ninja, we should react as fast as
403+
// possible.
327404
HandlePendingInterruption();
328405
if (IsInterrupted())
329406
return true;

src/subprocess.h

+30-3
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,27 @@ struct Subprocess {
6868
char overlapped_buf_[4 << 10];
6969
bool is_reading_;
7070
#else
71+
/// The file descriptor that will be used in ppoll/pselect() for this process,
72+
/// if any. Otherwise -1.
73+
/// In non-console mode, this is the read-side of a pipe that was created
74+
/// specifically for this subprocess. The write-side of the pipe is given to
75+
/// the subprocess as combined stdout and stderr.
76+
/// In console mode no pipe is created: fd_ is -1, and process termination is
77+
/// detected using the SIGCHLD signal and waitpid(WNOHANG).
7178
int fd_;
79+
/// PID of the subprocess. Set to -1 when the subprocess is reaped.
7280
pid_t pid_;
81+
/// In POSIX platforms it is necessary to use waitpid(WNOHANG) to know whether
82+
/// a certain subprocess has finished. This is done for terminal subprocesses.
83+
/// However, this also causes the subprocess to be reaped before Finish() is
84+
/// called, so we need to store the ExitStatus so that a later Finish()
85+
/// invocation can return it.
86+
ExitStatus exit_status_;
87+
88+
/// Call waitpid() on the subprocess with the provided options and update the
89+
/// pid_ and exit_status_ fields.
90+
/// Return a boolean indicating whether the subprocess has indeed terminated.
91+
bool TryFinish(int waitpid_options);
7392
#endif
7493
bool use_console_;
7594

@@ -96,16 +115,24 @@ struct SubprocessSet {
96115
static HANDLE ioport_;
97116
#else
98117
static void SetInterruptedFlag(int signum);
99-
static void HandlePendingInterruption();
118+
static void SigChldHandler(int signo, siginfo_t* info, void* context);
119+
100120
/// Store the signal number that causes the interruption.
101121
/// 0 if not interruption.
102-
static int interrupted_;
103-
122+
static volatile int interrupted_;
123+
/// Whether ninja should quit. Set on SIGINT, SIGTERM or SIGHUP reception.
104124
static bool IsInterrupted() { return interrupted_ != 0; }
125+
static void HandlePendingInterruption();
126+
127+
/// Initialized to 0 before ppoll/pselect().
128+
/// Filled to 1 by SIGCHLD handler when a child process terminates.
129+
static volatile sig_atomic_t s_sigchld_received;
130+
void CheckConsoleProcessTerminated();
105131

106132
struct sigaction old_int_act_;
107133
struct sigaction old_term_act_;
108134
struct sigaction old_hup_act_;
135+
struct sigaction old_chld_act_;
109136
sigset_t old_mask_;
110137
#endif
111138
};

0 commit comments

Comments
 (0)