| // Copyright 2019 The Bazel Authors. 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. | |
| // daemonize [-a] -l log_path -p pid_path [-c cgroup] [-s systemd_wrapper_path] | |
| // -- binary_path binary_name [args] | |
| // | |
| // daemonize spawns a program as a daemon, redirecting all of its output to the | |
| // given log_path and writing the daemon's PID to pid_path. binary_path | |
| // specifies the full location of the program to execute and binary_name | |
| // indicates its display name (aka argv[0], so the optional args do not have to | |
| // specify it again). log_path is created/truncated unless the -a (append) flag | |
| // is specified. Also note that pid_path is guaranteed to exists when this | |
| // program terminates successfully. | |
| // | |
| // Some important details about the implementation of this program: | |
| // | |
| // * No threads to ensure the use of fork below does not cause trouble. | |
| // | |
| // * Pure C, no C++. This is intentional to keep the program low overhead | |
| // and to avoid the accidental introduction of heavy dependencies that | |
| // could spawn threads. | |
| // | |
| // * Error handling is extensive but there is no error propagation. Given | |
| // that the goal of this program is just to spawn another one as a daemon, | |
| // we take the freedom to immediatey exit from anywhere as soon as we | |
| // hit an error. | |
| #include <assert.h> | |
| #include <err.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <getopt.h> | |
| #include <signal.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/types.h> | |
| #include <unistd.h> | |
| #include "src/main/tools/logging.h" | |
| #include "src/main/tools/process-tools.h" | |
| // Configures std{in,out,err} of the current process to serve as a daemon. | |
| // | |
| // stdin is configured to read from /dev/null. | |
| // | |
| // stdout and stderr are configured to write to log_path, which is created and | |
| // truncated unless log_append is set to true, in which case it is open for | |
| // append if it exists. | |
| static void SetupStdio(const char* log_path, bool log_append) { | |
| close(STDIN_FILENO); | |
| int fd = open("/dev/null", O_RDONLY); | |
| if (fd == -1) { | |
| err(EXIT_FAILURE, "Failed to open /dev/null"); | |
| } | |
| assert(fd == STDIN_FILENO); | |
| close(STDOUT_FILENO); | |
| int flags = O_WRONLY | O_CREAT | (log_append ? O_APPEND : O_TRUNC); | |
| fd = open(log_path, flags, 0666); | |
| if (fd == -1) { | |
| err(EXIT_FAILURE, "Failed to create log file %s", log_path); | |
| } | |
| assert(fd == STDOUT_FILENO); | |
| close(STDERR_FILENO); | |
| fd = dup(STDOUT_FILENO); | |
| if (fd == -1) { | |
| err(EXIT_FAILURE, "dup failed"); | |
| } | |
| assert(fd == STDERR_FILENO); | |
| global_debug = stderr; | |
| } | |
| // Writes the given pid to a new file at pid_path. | |
| // | |
| // Once the pid file has been created, this notifies pid_done_fd by writing a | |
| // dummy character to it and closing it. | |
| static void WritePidFile(pid_t pid, const char* pid_path, int pid_done_fd) { | |
| FILE* pid_file = fopen(pid_path, "w"); | |
| if (pid_file == NULL) { | |
| err(EXIT_FAILURE, "Failed to create %s", pid_path); | |
| } | |
| if (fprintf(pid_file, "%d", pid) < 0) { | |
| err(EXIT_FAILURE, "Failed to write pid %d to %s", pid, pid_path); | |
| } | |
| if (fclose(pid_file) < 0) { | |
| err(EXIT_FAILURE, "Failed to write pid %d to %s", pid, pid_path); | |
| } | |
| char dummy = '\0'; | |
| int ret = 0; | |
| while (ret == 0) { | |
| ret = write(pid_done_fd, &dummy, sizeof(dummy)); | |
| if (ret == -1 && errno == EINTR) { | |
| ret = 0; | |
| } | |
| } | |
| if (ret != 1) { | |
| err(EXIT_FAILURE, "Failed to signal pid done"); | |
| } | |
| close(pid_done_fd); | |
| } | |
| #ifdef __linux__ | |
| static bool ShellEscapeNeeded(const char* arg) { | |
| static const char kDontNeedShellEscapeChars[] = | |
| "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
| "abcdefghijklmnopqrstuvwxyz" | |
| "0123456789+-_.=/:,@"; | |
| for (int i = 0; arg[i]; i++) { | |
| if (strchr(kDontNeedShellEscapeChars, arg[i]) == NULL) { | |
| // If any character is not in the list, we need to escape the string. | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Similar to what absl::ShellEscape does. We'd like to escape the arguments | |
| // passed to the shell script, but we cannot use absl::ShellEscape because we | |
| // want to keep this tool pure C. | |
| static char* ShellEscape(const char* arg) { | |
| if (!arg) { | |
| return NULL; | |
| } | |
| if (!ShellEscapeNeeded(arg)) { | |
| return strdup(arg); | |
| } | |
| bool has_single_quotes = false; | |
| for (size_t i = 0; arg[i]; i++) { | |
| if (arg[i] == '\'') { | |
| has_single_quotes = true; | |
| break; | |
| } | |
| } | |
| if (!has_single_quotes) { | |
| // When there are no single quotes, we can just escape the string with | |
| // single quotes. | |
| char* escaped_string; | |
| asprintf(&escaped_string, "'%s'", arg); | |
| return escaped_string; | |
| } | |
| // For all other cases, we wrap everything in double quotes. | |
| size_t escaped_len = 0; | |
| for (size_t i = 0; arg[i]; i++) { | |
| switch (arg[i]) { | |
| case '\\': | |
| case '$': | |
| case '"': | |
| case '`': | |
| escaped_len++; | |
| } | |
| escaped_len++; | |
| } | |
| char* escaped_string = (char*)malloc( | |
| escaped_len + | |
| 3); // +2 for double quotes, and +1 for the null terminator. | |
| size_t j = 0; | |
| escaped_string[j++] = '"'; | |
| for (size_t i = 0; arg[i]; i++) { | |
| switch (arg[i]) { | |
| case '\\': | |
| case '$': | |
| case '"': | |
| case '`': | |
| escaped_string[j++] = '\\'; | |
| } | |
| escaped_string[j++] = arg[i]; | |
| } | |
| escaped_string[j++] = '"'; | |
| escaped_string[j] = '\0'; | |
| return escaped_string; | |
| } | |
| // Prepares the shell script for systemd-run to execute. | |
| // | |
| // We wrap everything inside a shell script file for systemd-run in order to | |
| // create a transient cgroup for the Java server to use. With a user cgroup | |
| // created like this, we can control the resources without root permission. | |
| // | |
| // We cannot do this directly with a command because we'll get a "Filename too | |
| // long" error, which indicates that the command is too long as we have almost | |
| // 100 arguments in the list. | |
| static void WriteSystemdWrapper(const char* systemd_wrapper_path, | |
| const char* exe, char** argv) { | |
| FILE* systemd_wrapper_fp = fopen(systemd_wrapper_path, "w"); | |
| if (systemd_wrapper_fp == NULL) { | |
| err(EXIT_FAILURE, "Failed to create %s", systemd_wrapper_path); | |
| } | |
| char* escaped_argv0 = ShellEscape(argv[0]); | |
| if (fprintf(systemd_wrapper_fp, "#!/bin/bash\nexec -a %s %s", escaped_argv0, | |
| exe) < 0) { | |
| err(EXIT_FAILURE, "Failed to write content to %s", systemd_wrapper_path); | |
| } | |
| free(escaped_argv0); | |
| int argc = 1; | |
| while (argv[argc]) { | |
| char* escaped_arg = ShellEscape(argv[argc]); | |
| if (fprintf(systemd_wrapper_fp, " %s", escaped_arg) < 0) { | |
| err(EXIT_FAILURE, "Failed to write content to %s", systemd_wrapper_path); | |
| } | |
| free(escaped_arg); | |
| argc++; | |
| } | |
| if (fclose(systemd_wrapper_fp) < 0) { | |
| err(EXIT_FAILURE, "Failed to fclose file %s", systemd_wrapper_path); | |
| } | |
| } | |
| static bool IsBinaryExecutable(const char* binary_path) { | |
| return access(binary_path, X_OK) == 0; | |
| } | |
| #endif | |
| static void ExecAsDaemon(const char* log_path, bool log_append, | |
| const char* systemd_wrapper_path, int pid_done_fd, | |
| const char* exe, char** argv) | |
| __attribute__((noreturn)); | |
| // Executes the requested binary configuring it to behave as a daemon. | |
| // | |
| // The stdout and stderr of the current process are redirected to the given | |
| // log_path. See SetupStdio for details on how this is handled. | |
| // | |
| // This blocks execution until pid_done_fd receives a write. We do this | |
| // because the Bazel server process (which is what we start with this helper | |
| // binary) requires the PID file to be present at startup time so we must | |
| // wait until the parent process has created it. | |
| // | |
| // This function never returns. | |
| static void ExecAsDaemon(const char* log_path, bool log_append, | |
| const char* systemd_wrapper_path, int pid_done_fd, | |
| const char* exe, char** argv) { | |
| char dummy; | |
| if (read(pid_done_fd, &dummy, sizeof(dummy)) == -1) { | |
| err(EXIT_FAILURE, "Failed to wait for pid file creation"); | |
| } | |
| close(pid_done_fd); | |
| if (signal(SIGHUP, SIG_IGN) == SIG_ERR) { | |
| err(EXIT_FAILURE, "Failed to install SIGHUP handler"); | |
| } | |
| if (setsid() == -1) { | |
| err(EXIT_FAILURE, "setsid failed"); | |
| } | |
| SetupStdio(log_path, log_append); | |
| #ifdef __linux__ | |
| // When it's running on linux, and the systemd wrapper path is provided, we | |
| // try to replace the current process with systemd-run. In all other cases, | |
| // including the cases when systemd-run is not available, we replace it with | |
| // the original exe. | |
| const char* systemd_run_path = "/usr/bin/systemd-run"; | |
| if (systemd_wrapper_path != NULL && IsBinaryExecutable(systemd_run_path)) { | |
| // Even if systemd-run is present and executable, we still need to run a | |
| // command first to check if we can use it. There are some cases when the | |
| // environment is not set up correctly, e.g. no DBUS available. | |
| char* systemd_test_command; | |
| asprintf(&systemd_test_command, "%s --user --scope -- /bin/true", | |
| systemd_run_path); | |
| int status = system(systemd_test_command); | |
| free(systemd_test_command); | |
| if (status == 0) { | |
| WriteSystemdWrapper(systemd_wrapper_path, exe, argv); | |
| execl(systemd_run_path, systemd_run_path, "--user", "--scope", "--", | |
| "/bin/bash", systemd_wrapper_path, NULL); | |
| err(EXIT_FAILURE, "Failed to execute %s with systemd-run.", exe); | |
| } | |
| } | |
| #endif | |
| execv(exe, argv); | |
| err(EXIT_FAILURE, "Failed to execute %s", exe); | |
| } | |
| #ifdef __linux__ | |
| // Moves the bazel server into the specified cgroup for all the discovered | |
| // cgroups. This is useful when using the cgroup features in bazel and thus the | |
| // server must be started in a user-writable cgroup. Users can specify a | |
| // pre-setup cgroup where the server will be moved to. This is enabled by | |
| // the --experimental_cgroup_parent startup flag. | |
| static void MoveToCgroup(pid_t pid, const char* cgroup_path) { | |
| FILE* mounts_fp = fopen("/proc/self/mounts", "r"); | |
| if (mounts_fp == NULL) { | |
| err(EXIT_FAILURE, "Failed to open /proc/self/mounts"); | |
| } | |
| char* line = NULL; | |
| size_t len = 0; | |
| while (getline(&line, &len, mounts_fp) != -1) { | |
| char* saveptr; | |
| strtok_r(line, " ", &saveptr); | |
| char* fs_file = strtok_r(NULL, " ", &saveptr); | |
| char* fs_vfstype = strtok_r(NULL, " ", &saveptr); | |
| if (strcmp(fs_vfstype, "cgroup") == 0 || | |
| strcmp(fs_vfstype, "cgroup2") == 0) { | |
| char* procs_path; | |
| asprintf(&procs_path, "%s%s/cgroup.procs", fs_file, cgroup_path); | |
| FILE* procs = fopen(procs_path, "w"); | |
| if (procs == NULL) { | |
| PRINT_DEBUG( | |
| "Failed to open %s. Falling back to running without cgroups", | |
| procs_path); | |
| } else if (fprintf(procs, "%d", pid) < 0) { | |
| PRINT_DEBUG( | |
| "Failed to write %s. Falling back to running without cgroups", | |
| procs_path); | |
| } else if (fclose(procs) < 0) { | |
| PRINT_DEBUG( | |
| "Failed to close %s. Falling back to running without cgroups", | |
| procs_path); | |
| } | |
| free(procs_path); | |
| } | |
| } | |
| free(line); | |
| fclose(mounts_fp); | |
| } | |
| #endif | |
| // Starts the given process as a daemon. | |
| // | |
| // This spawns a subprocess that will be configured to run the desired program | |
| // as a daemon. The program to run is supplied in exe and the arguments to it | |
| // are given in the NULL-terminated argv. argv[0] must be present and | |
| // contain the program name (which may or may not match the basename of exe). | |
| static void Daemonize(const char* log_path, bool log_append, | |
| const char* pid_path, const char* cgroup_path, | |
| const char* systemd_wrapper_path, const char* exe, | |
| char** argv) { | |
| assert(argv[0] != NULL); | |
| int pid_done_fds[2]; | |
| if (pipe(pid_done_fds) == -1) { | |
| err(EXIT_FAILURE, "pipe failed"); | |
| } | |
| pid_t pid = fork(); | |
| if (pid == -1) { | |
| err(EXIT_FAILURE, "fork failed"); | |
| } else if (pid == 0) { | |
| close(pid_done_fds[1]); | |
| #ifdef __linux__ | |
| if (cgroup_path != NULL) { | |
| MoveToCgroup(pid, cgroup_path); | |
| } | |
| #endif | |
| ExecAsDaemon(log_path, log_append, systemd_wrapper_path, pid_done_fds[0], | |
| exe, argv); | |
| abort(); // NOLINT Unreachable. | |
| } | |
| close(pid_done_fds[0]); | |
| WritePidFile(pid, pid_path, pid_done_fds[1]); | |
| } | |
| // Program entry point. | |
| // | |
| // The primary responsibility of this function is to parse program options. | |
| // Once that is done, delegates all work to Daemonize. | |
| int main(int argc, char** argv) { | |
| bool log_append = false; | |
| const char* log_path = NULL; | |
| const char* pid_path = NULL; | |
| const char* cgroup_path = NULL; | |
| const char* systemd_wrapper_path = NULL; | |
| int opt; | |
| while ((opt = getopt(argc, argv, ":al:p:c:s:")) != -1) { | |
| switch (opt) { | |
| case 'a': | |
| log_append = true; | |
| break; | |
| case 'l': | |
| log_path = optarg; | |
| break; | |
| case 'p': | |
| pid_path = optarg; | |
| break; | |
| case 'c': | |
| cgroup_path = optarg; | |
| break; | |
| case 's': | |
| systemd_wrapper_path = optarg; | |
| break; | |
| case ':': | |
| errx(EXIT_FAILURE, "Option -%c requires an argument", optopt); | |
| case '?': | |
| default: | |
| errx(EXIT_FAILURE, "Unknown option -%c", optopt); | |
| } | |
| } | |
| argc -= optind; | |
| argv += optind; | |
| if (log_path == NULL) { | |
| errx(EXIT_FAILURE, "Must specify a log file with -l"); | |
| } | |
| if (pid_path == NULL) { | |
| errx(EXIT_FAILURE, "Must specify a pid file with -p"); | |
| } | |
| if (argc < 2) { | |
| errx(EXIT_FAILURE, "Must provide at least an executable name and arg0"); | |
| } | |
| Daemonize(log_path, log_append, pid_path, cgroup_path, systemd_wrapper_path, | |
| argv[0], argv + 1); | |
| return EXIT_SUCCESS; | |
| } |