blob: 16429f5abf6d0088d9552138c5791e24c474ceef [file] [log] [blame]
// 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 -- 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 <sys/types.h>
#include <assert.h>
#include <err.h>
#include <fcntl.h>
#include <getopt.h>
#include <inttypes.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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);
}
// 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);
}
fprintf(pid_file, "%" PRIdMAX, (intmax_t) pid);
fclose(pid_file);
char dummy = '\0';
write(pid_done_fd, &dummy, sizeof(dummy));
close(pid_done_fd);
}
static void ExecAsDaemon(const char* log_path, bool log_append, 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, 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);
execv(exe, argv);
err(EXIT_FAILURE, "Failed to execute %s", exe);
}
// 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* 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]);
ExecAsDaemon(log_path, log_append, 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;
int opt;
while ((opt = getopt(argc, argv, ":al:p:")) != -1) {
switch (opt) {
case 'a':
log_append = true;
break;
case 'l':
log_path = optarg;
break;
case 'p':
pid_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, argv[0], argv + 1);
return EXIT_SUCCESS;
}