// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "subprocess.h" #include #include #include #include #include #include #include #include #include extern char** environ; #include "util.h" Subprocess::Subprocess(bool use_console) : fd_(-1), pid_(-1), use_console_(use_console) { } Subprocess::~Subprocess() { if (fd_ >= 0) close(fd_); // Reap child if forgotten. if (pid_ != -1) Finish(); } bool Subprocess::Start(SubprocessSet* set, const string& command) { int output_pipe[2]; if (pipe(output_pipe) < 0) Fatal("pipe: %s", strerror(errno)); fd_ = output_pipe[0]; #if !defined(USE_PPOLL) // If available, we use ppoll in DoWork(); otherwise we use pselect // and so must avoid overly-large FDs. if (fd_ >= static_cast(FD_SETSIZE)) Fatal("pipe: %s", strerror(EMFILE)); #endif // !USE_PPOLL SetCloseOnExec(fd_); posix_spawn_file_actions_t action; if (posix_spawn_file_actions_init(&action) != 0) Fatal("posix_spawn_file_actions_init: %s", strerror(errno)); if (posix_spawn_file_actions_addclose(&action, output_pipe[0]) != 0) Fatal("posix_spawn_file_actions_addclose: %s", strerror(errno)); posix_spawnattr_t attr; if (posix_spawnattr_init(&attr) != 0) Fatal("posix_spawnattr_init: %s", strerror(errno)); short flags = 0; flags |= POSIX_SPAWN_SETSIGMASK; if (posix_spawnattr_setsigmask(&attr, &set->old_mask_) != 0) Fatal("posix_spawnattr_setsigmask: %s", strerror(errno)); // Signals which are set to be caught in the calling process image are set to // default action in the new process image, so no explicit // POSIX_SPAWN_SETSIGDEF parameter is needed. // TODO: Consider using POSIX_SPAWN_USEVFORK on Linux with glibc? if (!use_console_) { // Put the child in its own process group, so ctrl-c won't reach it. flags |= POSIX_SPAWN_SETPGROUP; // No need to posix_spawnattr_setpgroup(&attr, 0), it's the default. // Open /dev/null over stdin. if (posix_spawn_file_actions_addopen(&action, 0, "/dev/null", O_RDONLY, 0) != 0) { Fatal("posix_spawn_file_actions_addopen: %s", strerror(errno)); } if (posix_spawn_file_actions_adddup2(&action, output_pipe[1], 1) != 0) Fatal("posix_spawn_file_actions_adddup2: %s", strerror(errno)); if (posix_spawn_file_actions_adddup2(&action, output_pipe[1], 2) != 0) Fatal("posix_spawn_file_actions_adddup2: %s", strerror(errno)); // In the console case, output_pipe is still inherited by the child and // closed when the subprocess finishes, which then notifies ninja. } if (posix_spawnattr_setflags(&attr, flags) != 0) Fatal("posix_spawnattr_setflags: %s", strerror(errno)); const char* spawned_args[] = { "/bin/sh", "-c", command.c_str(), NULL }; if (posix_spawn(&pid_, "/bin/sh", &action, &attr, const_cast(spawned_args), environ) != 0) Fatal("posix_spawn: %s", strerror(errno)); if (posix_spawnattr_destroy(&attr) != 0) Fatal("posix_spawnattr_destroy: %s", strerror(errno)); if (posix_spawn_file_actions_destroy(&action) != 0) Fatal("posix_spawn_file_actions_destroy: %s", strerror(errno)); close(output_pipe[1]); return true; } void Subprocess::OnPipeReady() { char buf[4 << 10]; ssize_t len = read(fd_, buf, sizeof(buf)); if (len > 0) { buf_.append(buf, len); } else { if (len < 0) Fatal("read: %s", strerror(errno)); close(fd_); fd_ = -1; } } ExitStatus Subprocess::Finish() { assert(pid_ != -1); int status; if (waitpid(pid_, &status, 0) < 0) Fatal("waitpid(%d): %s", pid_, strerror(errno)); pid_ = -1; if (WIFEXITED(status)) { int exit = WEXITSTATUS(status); if (exit == 0) return ExitSuccess; } else if (WIFSIGNALED(status)) { if (WTERMSIG(status) == SIGINT || WTERMSIG(status) == SIGTERM || WTERMSIG(status) == SIGHUP) return ExitInterrupted; } return ExitFailure; } bool Subprocess::Done() const { return fd_ == -1; } const string& Subprocess::GetOutput() const { return buf_; } int SubprocessSet::interrupted_; void SubprocessSet::SetInterruptedFlag(int signum) { interrupted_ = signum; } void SubprocessSet::HandlePendingInterruption() { sigset_t pending; sigemptyset(&pending); if (sigpending(&pending) == -1) { perror("ninja: sigpending"); return; } if (sigismember(&pending, SIGINT)) interrupted_ = SIGINT; else if (sigismember(&pending, SIGTERM)) interrupted_ = SIGTERM; else if (sigismember(&pending, SIGHUP)) interrupted_ = SIGHUP; } SubprocessSet::SubprocessSet() { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGTERM); sigaddset(&set, SIGHUP); if (sigprocmask(SIG_BLOCK, &set, &old_mask_) < 0) Fatal("sigprocmask: %s", strerror(errno)); struct sigaction act; memset(&act, 0, sizeof(act)); act.sa_handler = SetInterruptedFlag; if (sigaction(SIGINT, &act, &old_int_act_) < 0) Fatal("sigaction: %s", strerror(errno)); if (sigaction(SIGTERM, &act, &old_term_act_) < 0) Fatal("sigaction: %s", strerror(errno)); if (sigaction(SIGHUP, &act, &old_hup_act_) < 0) Fatal("sigaction: %s", strerror(errno)); } SubprocessSet::~SubprocessSet() { Clear(); if (sigaction(SIGINT, &old_int_act_, 0) < 0) Fatal("sigaction: %s", strerror(errno)); if (sigaction(SIGTERM, &old_term_act_, 0) < 0) Fatal("sigaction: %s", strerror(errno)); if (sigaction(SIGHUP, &old_hup_act_, 0) < 0) Fatal("sigaction: %s", strerror(errno)); if (sigprocmask(SIG_SETMASK, &old_mask_, 0) < 0) Fatal("sigprocmask: %s", strerror(errno)); } Subprocess *SubprocessSet::Add(const string& command, bool use_console) { Subprocess *subprocess = new Subprocess(use_console); if (!subprocess->Start(this, command)) { delete subprocess; return 0; } running_.push_back(subprocess); return subprocess; } #ifdef USE_PPOLL bool SubprocessSet::DoWork() { vector fds; nfds_t nfds = 0; for (vector::iterator i = running_.begin(); i != running_.end(); ++i) { int fd = (*i)->fd_; if (fd < 0) continue; pollfd pfd = { fd, POLLIN | POLLPRI, 0 }; fds.push_back(pfd); ++nfds; } interrupted_ = 0; int ret = ppoll(&fds.front(), nfds, NULL, &old_mask_); if (ret == -1) { if (errno != EINTR) { perror("ninja: ppoll"); return false; } return IsInterrupted(); } HandlePendingInterruption(); if (IsInterrupted()) return true; nfds_t cur_nfd = 0; for (vector::iterator i = running_.begin(); i != running_.end(); ) { int fd = (*i)->fd_; if (fd < 0) continue; assert(fd == fds[cur_nfd].fd); if (fds[cur_nfd++].revents) { (*i)->OnPipeReady(); if ((*i)->Done()) { finished_.push(*i); i = running_.erase(i); continue; } } ++i; } return IsInterrupted(); } #else // !defined(USE_PPOLL) bool SubprocessSet::DoWork() { fd_set set; int nfds = 0; FD_ZERO(&set); for (vector::iterator i = running_.begin(); i != running_.end(); ++i) { int fd = (*i)->fd_; if (fd >= 0) { FD_SET(fd, &set); if (nfds < fd+1) nfds = fd+1; } } interrupted_ = 0; int ret = pselect(nfds, &set, 0, 0, 0, &old_mask_); if (ret == -1) { if (errno != EINTR) { perror("ninja: pselect"); return false; } return IsInterrupted(); } HandlePendingInterruption(); if (IsInterrupted()) return true; for (vector::iterator i = running_.begin(); i != running_.end(); ) { int fd = (*i)->fd_; if (fd >= 0 && FD_ISSET(fd, &set)) { (*i)->OnPipeReady(); if ((*i)->Done()) { finished_.push(*i); i = running_.erase(i); continue; } } ++i; } return IsInterrupted(); } #endif // !defined(USE_PPOLL) Subprocess* SubprocessSet::NextFinished() { if (finished_.empty()) return NULL; Subprocess* subproc = finished_.front(); finished_.pop(); return subproc; } void SubprocessSet::Clear() { for (vector::iterator i = running_.begin(); i != running_.end(); ++i) // Since the foreground process is in our process group, it will receive // the interruption signal (i.e. SIGINT or SIGTERM) at the same time as us. if (!(*i)->use_console_) kill(-(*i)->pid_, interrupted_); for (vector::iterator i = running_.begin(); i != running_.end(); ++i) delete *i; running_.clear(); }