blob: 16429f5abf6d0088d9552138c5791e24c474ceef [file] [log] [blame]
jmmv18a0e232019-02-21 07:34:02 -08001// Copyright 2019 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// daemonize [-a] -l log_path -p pid_path -- binary_path binary_name [args]
16//
17// daemonize spawns a program as a daemon, redirecting all of its output to the
18// given log_path and writing the daemon's PID to pid_path. binary_path
19// specifies the full location of the program to execute and binary_name
20// indicates its display name (aka argv[0], so the optional args do not have to
21// specify it again). log_path is created/truncated unless the -a (append) flag
22// is specified. Also note that pid_path is guaranteed to exists when this
23// program terminates successfully.
24//
25// Some important details about the implementation of this program:
26//
27// * No threads to ensure the use of fork below does not cause trouble.
28//
29// * Pure C, no C++. This is intentional to keep the program low overhead
30// and to avoid the accidental introduction of heavy dependencies that
31// could spawn threads.
32//
33// * Error handling is extensive but there is no error propagation. Given
34// that the goal of this program is just to spawn another one as a daemon,
35// we take the freedom to immediatey exit from anywhere as soon as we
36// hit an error.
37
38#include <sys/types.h>
39
40#include <assert.h>
41#include <err.h>
42#include <fcntl.h>
43#include <getopt.h>
44#include <inttypes.h>
45#include <signal.h>
46#include <stdbool.h>
47#include <stdio.h>
48#include <stdlib.h>
49#include <unistd.h>
50
51// Configures std{in,out,err} of the current process to serve as a daemon.
52//
53// stdin is configured to read from /dev/null.
54//
55// stdout and stderr are configured to write to log_path, which is created and
56// truncated unless log_append is set to true, in which case it is open for
57// append if it exists.
58static void SetupStdio(const char* log_path, bool log_append) {
59 close(STDIN_FILENO);
60 int fd = open("/dev/null", O_RDONLY);
61 if (fd == -1) {
62 err(EXIT_FAILURE, "Failed to open /dev/null");
63 }
64 assert(fd == STDIN_FILENO);
65
66 close(STDOUT_FILENO);
67 int flags = O_WRONLY | O_CREAT | (log_append ? O_APPEND : O_TRUNC);
68 fd = open(log_path, flags, 0666);
69 if (fd == -1) {
70 err(EXIT_FAILURE, "Failed to create log file %s", log_path);
71 }
72 assert(fd == STDOUT_FILENO);
73
74 close(STDERR_FILENO);
75 fd = dup(STDOUT_FILENO);
76 if (fd == -1) {
77 err(EXIT_FAILURE, "dup failed");
78 }
79 assert(fd == STDERR_FILENO);
80}
81
82// Writes the given pid to a new file at pid_path.
83//
84// Once the pid file has been created, this notifies pid_done_fd by writing a
85// dummy character to it and closing it.
86static void WritePidFile(pid_t pid, const char* pid_path, int pid_done_fd) {
87 FILE* pid_file = fopen(pid_path, "w");
88 if (pid_file == NULL) {
89 err(EXIT_FAILURE, "Failed to create %s", pid_path);
90 }
91 fprintf(pid_file, "%" PRIdMAX, (intmax_t) pid);
92 fclose(pid_file);
93
94 char dummy = '\0';
95 write(pid_done_fd, &dummy, sizeof(dummy));
96 close(pid_done_fd);
97}
98
99static void ExecAsDaemon(const char* log_path, bool log_append, int pid_done_fd,
100 const char* exe, char** argv)
101 __attribute__((noreturn));
102
103// Executes the requested binary configuring it to behave as a daemon.
104//
105// The stdout and stderr of the current process are redirected to the given
106// log_path. See SetupStdio for details on how this is handled.
107//
108// This blocks execution until pid_done_fd receives a write. We do this
109// because the Bazel server process (which is what we start with this helper
110// binary) requires the PID file to be present at startup time so we must
111// wait until the parent process has created it.
112//
113// This function never returns.
114static void ExecAsDaemon(const char* log_path, bool log_append, int pid_done_fd,
115 const char* exe, char** argv) {
116 char dummy;
117 if (read(pid_done_fd, &dummy, sizeof(dummy)) == -1) {
118 err(EXIT_FAILURE, "Failed to wait for pid file creation");
119 }
120 close(pid_done_fd);
121
122 if (signal(SIGHUP, SIG_IGN) == SIG_ERR) {
123 err(EXIT_FAILURE, "Failed to install SIGHUP handler");
124 }
125
126 if (setsid() == -1) {
127 err(EXIT_FAILURE, "setsid failed");
128 }
129
130 SetupStdio(log_path, log_append);
131
132 execv(exe, argv);
133 err(EXIT_FAILURE, "Failed to execute %s", exe);
134}
135
136// Starts the given process as a daemon.
137//
138// This spawns a subprocess that will be configured to run the desired program
139// as a daemon. The program to run is supplied in exe and the arguments to it
140// are given in the NULL-terminated argv. argv[0] must be present and
141// contain the program name (which may or may not match the basename of exe).
142static void Daemonize(const char* log_path, bool log_append,
143 const char* pid_path, const char* exe, char** argv) {
144 assert(argv[0] != NULL);
145
146 int pid_done_fds[2];
147 if (pipe(pid_done_fds) == -1) {
148 err(EXIT_FAILURE, "pipe failed");
149 }
150
151 pid_t pid = fork();
152 if (pid == -1) {
153 err(EXIT_FAILURE, "fork failed");
154 } else if (pid == 0) {
155 close(pid_done_fds[1]);
156 ExecAsDaemon(log_path, log_append, pid_done_fds[0], exe, argv);
157 abort(); // NOLINT Unreachable.
158 }
159 close(pid_done_fds[0]);
160
161 WritePidFile(pid, pid_path, pid_done_fds[1]);
162}
163
164// Program entry point.
165//
166// The primary responsibility of this function is to parse program options.
167// Once that is done, delegates all work to Daemonize.
168int main(int argc, char** argv) {
169 bool log_append = false;
170 const char* log_path = NULL;
171 const char* pid_path = NULL;
172 int opt;
173 while ((opt = getopt(argc, argv, ":al:p:")) != -1) {
174 switch (opt) {
175 case 'a':
176 log_append = true;
177 break;
178
179 case 'l':
180 log_path = optarg;
181 break;
182
183 case 'p':
184 pid_path = optarg;
185 break;
186
187 case ':':
188 errx(EXIT_FAILURE, "Option -%c requires an argument", optopt);
189
190 case '?':
191 default:
192 errx(EXIT_FAILURE, "Unknown option -%c", optopt);
193 }
194 }
195 argc -= optind;
196 argv += optind;
197
198 if (log_path == NULL) {
199 errx(EXIT_FAILURE, "Must specify a log file with -l");
200 }
201 if (pid_path == NULL) {
202 errx(EXIT_FAILURE, "Must specify a pid file with -p");
203 }
204
205 if (argc < 2) {
206 errx(EXIT_FAILURE, "Must provide at least an executable name and arg0");
207 }
208 Daemonize(log_path, log_append, pid_path, argv[0], argv + 1);
209 return EXIT_SUCCESS;
210}