// 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);
}