| // Copyright (C) 2015 The Android Open Source Project | |
| // | |
| // 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. | |
| // makeparallel communicates with the GNU make jobserver | |
| // (http://make.mad-scientist.net/papers/jobserver-implementation/) | |
| // in order claim all available jobs, and then passes the number of jobs | |
| // claimed to a subprocess with -j<jobs>. | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <getopt.h> | |
| #include <poll.h> | |
| #include <signal.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <unistd.h> | |
| #include <sys/resource.h> | |
| #include <sys/time.h> | |
| #include <sys/types.h> | |
| #include <sys/wait.h> | |
| #include <string> | |
| #include <vector> | |
| #ifdef __linux__ | |
| #include <error.h> | |
| #endif | |
| #ifdef __APPLE__ | |
| #include <err.h> | |
| #define error(code, eval, fmt, ...) errc(eval, code, fmt, ##__VA_ARGS__) | |
| // Darwin does not interrupt syscalls by default. | |
| #define TEMP_FAILURE_RETRY(exp) (exp) | |
| #endif | |
| // Throw an error if fd is not valid. | |
| static void CheckFd(int fd) { | |
| int ret = fcntl(fd, F_GETFD); | |
| if (ret < 0) { | |
| if (errno == EBADF) { | |
| error(errno, 0, "no jobserver pipe, prefix recipe command with '+'"); | |
| } else { | |
| error(errno, errno, "fnctl failed"); | |
| } | |
| } | |
| } | |
| // Extract flags from MAKEFLAGS that need to be propagated to subproccess | |
| static std::vector<std::string> ReadMakeflags() { | |
| std::vector<std::string> args; | |
| const char* makeflags_env = getenv("MAKEFLAGS"); | |
| if (makeflags_env == nullptr) { | |
| return args; | |
| } | |
| // The MAKEFLAGS format is pretty useless. The first argument might be empty | |
| // (starts with a leading space), or it might be a set of one-character flags | |
| // merged together with no leading space, or it might be a variable | |
| // definition. | |
| std::string makeflags = makeflags_env; | |
| // Split makeflags into individual args on spaces. Multiple spaces are | |
| // elided, but an initial space will result in a blank arg. | |
| size_t base = 0; | |
| size_t found; | |
| do { | |
| found = makeflags.find_first_of(" ", base); | |
| args.push_back(makeflags.substr(base, found - base)); | |
| base = found + 1; | |
| } while (found != makeflags.npos); | |
| // Drop the first argument if it is empty | |
| while (args.size() > 0 && args[0].size() == 0) { | |
| args.erase(args.begin()); | |
| } | |
| // Prepend a - to the first argument if it does not have one and is not a | |
| // variable definition | |
| if (args.size() > 0 && args[0][0] != '-') { | |
| if (args[0].find('=') == makeflags.npos) { | |
| args[0] = '-' + args[0]; | |
| } | |
| } | |
| return args; | |
| } | |
| static bool ParseMakeflags(std::vector<std::string>& args, | |
| int* in_fd, int* out_fd, bool* parallel, bool* keep_going) { | |
| std::vector<char*> getopt_argv; | |
| // getopt starts reading at argv[1] | |
| getopt_argv.reserve(args.size() + 1); | |
| getopt_argv.push_back(strdup("")); | |
| for (std::string& v : args) { | |
| getopt_argv.push_back(strdup(v.c_str())); | |
| } | |
| opterr = 0; | |
| optind = 1; | |
| while (1) { | |
| const static option longopts[] = { | |
| {"jobserver-fds", required_argument, 0, 0}, | |
| {0, 0, 0, 0}, | |
| }; | |
| int longopt_index = 0; | |
| int c = getopt_long(getopt_argv.size(), getopt_argv.data(), "kj", | |
| longopts, &longopt_index); | |
| if (c == -1) { | |
| break; | |
| } | |
| switch (c) { | |
| case 0: | |
| switch (longopt_index) { | |
| case 0: | |
| { | |
| // jobserver-fds | |
| if (sscanf(optarg, "%d,%d", in_fd, out_fd) != 2) { | |
| error(EXIT_FAILURE, 0, "incorrect format for --jobserver-fds: %s", optarg); | |
| } | |
| // TODO: propagate in_fd, out_fd | |
| break; | |
| } | |
| default: | |
| abort(); | |
| } | |
| break; | |
| case 'j': | |
| *parallel = true; | |
| break; | |
| case 'k': | |
| *keep_going = true; | |
| break; | |
| case '?': | |
| // ignore unknown arguments | |
| break; | |
| default: | |
| abort(); | |
| } | |
| } | |
| for (char *v : getopt_argv) { | |
| free(v); | |
| } | |
| return true; | |
| } | |
| // Read a single byte from fd, with timeout in milliseconds. Returns true if | |
| // a byte was read, false on timeout. Throws away the read value. | |
| // Non-reentrant, uses timer and signal handler global state, plus static | |
| // variable to communicate with signal handler. | |
| // | |
| // Uses a SIGALRM timer to fire a signal after timeout_ms that will interrupt | |
| // the read syscall if it hasn't yet completed. If the timer fires before the | |
| // read the read could block forever, so read from a dup'd fd and close it from | |
| // the signal handler, which will cause the read to return EBADF if it occurs | |
| // after the signal. | |
| // The dup/read/close combo is very similar to the system described to avoid | |
| // a deadlock between SIGCHLD and read at | |
| // http://make.mad-scientist.net/papers/jobserver-implementation/ | |
| static bool ReadByteTimeout(int fd, int timeout_ms) { | |
| // global variable to communicate with the signal handler | |
| static int dup_fd = -1; | |
| // dup the fd so the signal handler can close it without losing the real one | |
| dup_fd = dup(fd); | |
| if (dup_fd < 0) { | |
| error(errno, errno, "dup failed"); | |
| } | |
| // set up a signal handler that closes dup_fd on SIGALRM | |
| struct sigaction action = {}; | |
| action.sa_flags = SA_SIGINFO, | |
| action.sa_sigaction = [](int, siginfo_t*, void*) { | |
| close(dup_fd); | |
| }; | |
| struct sigaction oldaction = {}; | |
| int ret = sigaction(SIGALRM, &action, &oldaction); | |
| if (ret < 0) { | |
| error(errno, errno, "sigaction failed"); | |
| } | |
| // queue a SIGALRM after timeout_ms | |
| const struct itimerval timeout = {{}, {0, timeout_ms * 1000}}; | |
| ret = setitimer(ITIMER_REAL, &timeout, NULL); | |
| if (ret < 0) { | |
| error(errno, errno, "setitimer failed"); | |
| } | |
| // start the blocking read | |
| char buf; | |
| int read_ret = read(dup_fd, &buf, 1); | |
| int read_errno = errno; | |
| // cancel the alarm in case it hasn't fired yet | |
| const struct itimerval cancel = {}; | |
| ret = setitimer(ITIMER_REAL, &cancel, NULL); | |
| if (ret < 0) { | |
| error(errno, errno, "reset setitimer failed"); | |
| } | |
| // remove the signal handler | |
| ret = sigaction(SIGALRM, &oldaction, NULL); | |
| if (ret < 0) { | |
| error(errno, errno, "reset sigaction failed"); | |
| } | |
| // clean up the dup'd fd in case the signal never fired | |
| close(dup_fd); | |
| dup_fd = -1; | |
| if (read_ret == 0) { | |
| error(EXIT_FAILURE, 0, "EOF on jobserver pipe"); | |
| } else if (read_ret > 0) { | |
| return true; | |
| } else if (read_errno == EINTR || read_errno == EBADF) { | |
| return false; | |
| } else { | |
| error(read_errno, read_errno, "read failed"); | |
| } | |
| abort(); | |
| } | |
| // Measure the size of the jobserver pool by reading from in_fd until it blocks | |
| static int GetJobserverTokens(int in_fd) { | |
| int tokens = 0; | |
| pollfd pollfds[] = {{in_fd, POLLIN, 0}}; | |
| int ret; | |
| while ((ret = TEMP_FAILURE_RETRY(poll(pollfds, 1, 0))) != 0) { | |
| if (ret < 0) { | |
| error(errno, errno, "poll failed"); | |
| } else if (pollfds[0].revents != POLLIN) { | |
| error(EXIT_FAILURE, 0, "unexpected event %d\n", pollfds[0].revents); | |
| } | |
| // There is probably a job token in the jobserver pipe. There is a chance | |
| // another process reads it first, which would cause a blocking read to | |
| // block forever (or until another process put a token back in the pipe). | |
| // The file descriptor can't be set to O_NONBLOCK as that would affect | |
| // all users of the pipe, including the parent make process. | |
| // ReadByteTimeout emulates a non-blocking read on a !O_NONBLOCK socket | |
| // using a SIGALRM that fires after a short timeout. | |
| bool got_token = ReadByteTimeout(in_fd, 10); | |
| if (!got_token) { | |
| // No more tokens | |
| break; | |
| } else { | |
| tokens++; | |
| } | |
| } | |
| // This process implicitly gets a token, so pool size is measured size + 1 | |
| return tokens; | |
| } | |
| // Return tokens to the jobserver pool. | |
| static void PutJobserverTokens(int out_fd, int tokens) { | |
| // Return all the tokens to the pipe | |
| char buf = '+'; | |
| for (int i = 0; i < tokens; i++) { | |
| int ret = TEMP_FAILURE_RETRY(write(out_fd, &buf, 1)); | |
| if (ret < 0) { | |
| error(errno, errno, "write failed"); | |
| } else if (ret == 0) { | |
| error(EXIT_FAILURE, 0, "EOF on jobserver pipe"); | |
| } | |
| } | |
| } | |
| int main(int argc, char* argv[]) { | |
| int in_fd = -1; | |
| int out_fd = -1; | |
| bool parallel = false; | |
| bool keep_going = false; | |
| bool ninja = false; | |
| int tokens = 0; | |
| if (argc > 1 && strcmp(argv[1], "--ninja") == 0) { | |
| ninja = true; | |
| argv++; | |
| argc--; | |
| } | |
| if (argc < 2) { | |
| error(EXIT_FAILURE, 0, "expected command to run"); | |
| } | |
| const char* path = argv[1]; | |
| std::vector<char*> args({argv[1]}); | |
| std::vector<std::string> makeflags = ReadMakeflags(); | |
| if (ParseMakeflags(makeflags, &in_fd, &out_fd, ¶llel, &keep_going)) { | |
| if (in_fd >= 0 && out_fd >= 0) { | |
| CheckFd(in_fd); | |
| CheckFd(out_fd); | |
| fcntl(in_fd, F_SETFD, FD_CLOEXEC); | |
| fcntl(out_fd, F_SETFD, FD_CLOEXEC); | |
| tokens = GetJobserverTokens(in_fd); | |
| } | |
| } | |
| std::string jarg; | |
| if (parallel) { | |
| if (tokens == 0) { | |
| if (ninja) { | |
| // ninja is parallel by default | |
| jarg = ""; | |
| } else { | |
| // make -j with no argument, guess a reasonable parallelism like ninja does | |
| jarg = "-j" + std::to_string(sysconf(_SC_NPROCESSORS_ONLN) + 2); | |
| } | |
| } else { | |
| jarg = "-j" + std::to_string(tokens + 1); | |
| } | |
| } | |
| if (ninja) { | |
| if (!parallel) { | |
| // ninja is parallel by default, pass -j1 to disable parallelism if make wasn't parallel | |
| args.push_back(strdup("-j1")); | |
| } else { | |
| if (jarg != "") { | |
| args.push_back(strdup(jarg.c_str())); | |
| } | |
| } | |
| if (keep_going) { | |
| args.push_back(strdup("-k0")); | |
| } | |
| } else { | |
| if (jarg != "") { | |
| args.push_back(strdup(jarg.c_str())); | |
| } | |
| } | |
| args.insert(args.end(), &argv[2], &argv[argc]); | |
| args.push_back(nullptr); | |
| static pid_t pid; | |
| // Set up signal handlers to forward SIGTERM to child. | |
| // Assume that all other signals are sent to the entire process group, | |
| // and that we'll wait for our child to exit instead of handling them. | |
| struct sigaction action = {}; | |
| action.sa_flags = SA_RESTART; | |
| action.sa_handler = [](int signal) { | |
| if (signal == SIGTERM && pid > 0) { | |
| kill(pid, signal); | |
| } | |
| }; | |
| int ret = 0; | |
| if (!ret) ret = sigaction(SIGHUP, &action, NULL); | |
| if (!ret) ret = sigaction(SIGINT, &action, NULL); | |
| if (!ret) ret = sigaction(SIGQUIT, &action, NULL); | |
| if (!ret) ret = sigaction(SIGTERM, &action, NULL); | |
| if (!ret) ret = sigaction(SIGALRM, &action, NULL); | |
| if (ret < 0) { | |
| error(errno, errno, "sigaction failed"); | |
| } | |
| pid = fork(); | |
| if (pid < 0) { | |
| error(errno, errno, "fork failed"); | |
| } else if (pid == 0) { | |
| // child | |
| unsetenv("MAKEFLAGS"); | |
| unsetenv("MAKELEVEL"); | |
| // make 3.81 sets the stack ulimit to unlimited, which may cause problems | |
| // for child processes | |
| struct rlimit rlim{}; | |
| if (getrlimit(RLIMIT_STACK, &rlim) == 0 && rlim.rlim_cur == RLIM_INFINITY) { | |
| rlim.rlim_cur = 8*1024*1024; | |
| setrlimit(RLIMIT_STACK, &rlim); | |
| } | |
| int ret = execvp(path, args.data()); | |
| if (ret < 0) { | |
| error(errno, errno, "exec %s failed", path); | |
| } | |
| abort(); | |
| } | |
| // parent | |
| siginfo_t status = {}; | |
| int exit_status = 0; | |
| ret = waitid(P_PID, pid, &status, WEXITED); | |
| if (ret < 0) { | |
| error(errno, errno, "waitpid failed"); | |
| } else if (status.si_code == CLD_EXITED) { | |
| exit_status = status.si_status; | |
| } else { | |
| exit_status = -(status.si_status); | |
| } | |
| if (tokens > 0) { | |
| PutJobserverTokens(out_fd, tokens); | |
| } | |
| exit(exit_status); | |
| } |