fiss

Friedel's Initialization and Service Supervision
Log | Files | Refs | LICENSE

sigremap.c (8264B)


      1 /*  Copyright (c) 2015 Yelp, Inc.
      2     With modification 2023 Friedel Schon
      3 
      4     Permission is hereby granted, free of charge, to any person obtaining a copy
      5     of this software and associated documentation files (the "Software"), to deal
      6     in the Software without restriction, including without limitation the rights
      7     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      8     copies of the Software, and to permit persons to whom the Software is
      9     furnished to do so, subject to the following conditions:
     10 
     11     The above copyright notice and this permission notice shall be included in
     12     all copies or substantial portions of the Software.
     13 
     14     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     15     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     16     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     17     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     18     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     19     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     20     THE SOFTWARE.
     21     */
     22 
     23 
     24 #include "message.h"
     25 #include "signame.h"
     26 #include "util.h"
     27 
     28 #include <assert.h>
     29 #include <errno.h>
     30 #include <getopt.h>
     31 #include <stdbool.h>
     32 #include <stdio.h>
     33 #include <stdlib.h>
     34 #include <string.h>
     35 #include <sys/ioctl.h>
     36 #include <sys/wait.h>
     37 #include <unistd.h>
     38 
     39 
     40 const char* current_prog(void) {
     41 	return "sigremap";
     42 }
     43 
     44 #define DEBUG(...)                  \
     45 	do {                            \
     46 		if (debug)                  \
     47 			fprint(1, __VA_ARGS__); \
     48 	} while (0)
     49 
     50 #define set_signal_undefined(old, new) \
     51 	if (signal_remap[old] == -1)       \
     52 		signal_remap[old] = new;
     53 
     54 
     55 // Signals we care about are numbered from 1 to 31, inclusive.
     56 // (32 and above are real-time signals.)
     57 // TODO: this is likely not portable outside of Linux, or on strange architectures
     58 #define MAXSIG 31
     59 
     60 // Indices are one-indexed (signal 1 is at index 1). Index zero is unused.
     61 // User-specified signal rewriting.
     62 static int signal_remap[MAXSIG + 1];
     63 // One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal.
     64 static bool signal_temporary_ignores[MAXSIG + 1];
     65 
     66 static int  child_pid  = -1;
     67 static bool debug      = false;
     68 static bool use_setsid = true;
     69 
     70 
     71 /*
     72  * The sigremap signal handler.
     73  *
     74  * The main job of this signal handler is to forward signals along to our child
     75  * process(es). In setsid mode, this means signaling the entire process group
     76  * rooted at our child. In non-setsid mode, this is just signaling the primary
     77  * child.
     78  *
     79  * In most cases, simply proxying the received signal is sufficient. If we
     80  * receive a job control signal, however, we should not only forward it, but
     81  * also sleep sigremap itself.
     82  *
     83  * This allows users to run foreground processes using sigremap and to
     84  * control them using normal shell job control features (e.g. Ctrl-Z to
     85  * generate a SIGTSTP and suspend the process).
     86  *
     87  * The libc manual is useful:
     88  * https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html
     89  *
     90  */
     91 static void handle_signal(int signum) {
     92 	DEBUG("Received signal %d.\n", signum);
     93 
     94 	if (signal_temporary_ignores[signum] == 1) {
     95 		DEBUG("Ignoring tty hand-off signal %d.\n", signum);
     96 		signal_temporary_ignores[signum] = 0;
     97 	} else if (signum == SIGCHLD) {
     98 		int status, exit_status;
     99 		int killed_pid;
    100 		while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) {
    101 			if (WIFEXITED(status)) {
    102 				exit_status = WEXITSTATUS(status);
    103 				DEBUG("A child with PID %d exited with exit status %d.\n", killed_pid, exit_status);
    104 			} else {
    105 				assert(WIFSIGNALED(status));
    106 				exit_status = 128 + WTERMSIG(status);
    107 				DEBUG("A child with PID %d was terminated by signal %d.\n", killed_pid, exit_status - 128);
    108 			}
    109 
    110 			if (killed_pid == child_pid) {
    111 				kill(use_setsid ? -child_pid : child_pid, SIGTERM);    // send SIGTERM to any remaining children
    112 				DEBUG("Child exited with status %d. Goodbye.\n", exit_status);
    113 				exit(exit_status);
    114 			}
    115 		}
    116 	} else {
    117 		if (signum <= MAXSIG && signal_remap[signum] != -1) {
    118 			DEBUG("Translating signal %d to %d.\n", signum, signal_remap[signum]);
    119 			signum = signal_remap[signum];
    120 		}
    121 
    122 		kill(use_setsid ? -child_pid : child_pid, signum);
    123 		DEBUG("Forwarded signal %d to children.\n", signum);
    124 
    125 		if (signum == SIGTSTP || signum == SIGTTOU || signum == SIGTTIN) {
    126 			DEBUG("Suspending self due to TTY signal.\n");
    127 			kill(getpid(), SIGSTOP);
    128 		}
    129 	}
    130 }
    131 
    132 static char** parse_command(int argc, char* argv[]) {
    133 	int           opt;
    134 	struct option long_options[] = {
    135 		{ "single", no_argument, NULL, 's' },
    136 		{ "verbose", no_argument, NULL, 'v' },
    137 		{ "version", no_argument, NULL, 'V' },
    138 		{ NULL, 0, NULL, 0 },
    139 	};
    140 	char *old, *new;
    141 	int   oldsig, newsig;
    142 
    143 	while ((opt = getopt_long(argc, argv, "+:hvVs", long_options, NULL)) != -1) {
    144 		switch (opt) {
    145 			case 'v':
    146 				debug = true;
    147 				break;
    148 			case 'V':
    149 				print_version_exit();
    150 			case 'c':
    151 				use_setsid = false;
    152 				break;
    153 			default:
    154 				print_usage_exit(PROG_SIGREMAP, 1);
    155 		}
    156 	}
    157 
    158 	argc -= optind, argv += optind;
    159 
    160 	while (argc > 0) {
    161 		if ((new = strchr(argv[0], '=')) == NULL)
    162 			break;
    163 
    164 		old  = argv[0];
    165 		*new = '\0';
    166 		new ++;
    167 
    168 		if ((oldsig = signame(old)) == -1) {
    169 			fprint(1, "error: invalid old signal '%s'\n", old);
    170 			exit(1);
    171 		}
    172 		if ((newsig = signame(new)) == -1) {
    173 			fprint(1, "error: invalid new signal '%s'\n", new);
    174 			exit(1);
    175 		}
    176 		signal_remap[oldsig] = newsig;
    177 
    178 		argc--, argv++;
    179 	}
    180 
    181 	if (argc < 1) {
    182 		print_usage_exit(PROG_SIGREMAP, 1);
    183 	}
    184 
    185 	if (use_setsid) {
    186 		set_signal_undefined(SIGTSTP, SIGSTOP);
    187 		set_signal_undefined(SIGTSTP, SIGTTOU);
    188 		set_signal_undefined(SIGTSTP, SIGTTIN);
    189 	}
    190 
    191 	return &argv[optind];
    192 }
    193 
    194 // A dummy signal handler used for signals we care about.
    195 // On the FreeBSD kernel, ignored signals cannot be waited on by `sigwait` (but
    196 // they can be on Linux). We must provide a dummy handler.
    197 // https://lists.freebsd.org/pipermail/freebsd-ports/2009-October/057340.html
    198 static void dummy(int signum) {
    199 	(void) signum;
    200 }
    201 
    202 int main(int argc, char* argv[]) {
    203 	char**   cmd = parse_command(argc, argv);
    204 	sigset_t all_signals;
    205 	int      signum;
    206 
    207 	sigfillset(&all_signals);
    208 	sigprocmask(SIG_BLOCK, &all_signals, NULL);
    209 
    210 	for (int i = 1; i <= MAXSIG; i++) {
    211 		signal_remap[i]             = -1;
    212 		signal_temporary_ignores[i] = false;
    213 
    214 		signal(i, dummy);
    215 	}
    216 
    217 	/*
    218 	 * Detach sigremap from controlling tty, so that the child's session can
    219 	 * attach to it instead.
    220 	 *
    221 	 * We want the child to be able to be the session leader of the TTY so that
    222 	 * it can do normal job control.
    223 	 */
    224 	if (use_setsid) {
    225 		if (ioctl(STDIN_FILENO, TIOCNOTTY) == -1) {
    226 			DEBUG(
    227 			    "Unable to detach from controlling tty (errno=%d %s).\n",
    228 			    errno,
    229 			    strerror(errno));
    230 		} else {
    231 			/*
    232 			 * When the session leader detaches from its controlling tty via
    233 			 * TIOCNOTTY, the kernel sends SIGHUP and SIGCONT to the process
    234 			 * group. We need to be careful not to forward these on to the
    235 			 * sigremap child so that it doesn't receive a SIGHUP and
    236 			 * terminate itself (#136).
    237 			 */
    238 			if (getsid(0) == getpid()) {
    239 				DEBUG("Detached from controlling tty, ignoring the first SIGHUP and SIGCONT we receive.\n");
    240 				signal_temporary_ignores[SIGHUP]  = 1;
    241 				signal_temporary_ignores[SIGCONT] = 1;
    242 			} else {
    243 				DEBUG("Detached from controlling tty, but was not session leader.\n");
    244 			}
    245 		}
    246 	}
    247 
    248 	child_pid = fork();
    249 	if (child_pid < 0) {
    250 		fprint(1, "error: unable to fork: %r\n");
    251 		return 1;
    252 	} else if (child_pid == 0) {
    253 		/* child */
    254 		sigprocmask(SIG_UNBLOCK, &all_signals, NULL);
    255 		if (use_setsid) {
    256 			if (setsid() == -1) {
    257 				fprint(1, "error: unable to setsid: %r\n");
    258 				exit(1);
    259 			}
    260 
    261 			if (ioctl(STDIN_FILENO, TIOCSCTTY, 0) == -1) {
    262 				DEBUG(
    263 				    "Unable to attach to controlling tty (errno=%d %s).\n",
    264 				    errno,
    265 				    strerror(errno));
    266 			}
    267 			DEBUG("setsid complete.\n");
    268 		}
    269 		execvp(cmd[0], cmd);
    270 
    271 		// if this point is reached, exec failed, so we should exit nonzero
    272 		print_errno("error: unable to execute %s: %s\n", cmd[0]);
    273 		_exit(2);
    274 	}
    275 
    276 	/* parent */
    277 	DEBUG("Child spawned with PID %d.\n", child_pid);
    278 	for (;;) {
    279 		sigwait(&all_signals, &signum);
    280 		handle_signal(signum);
    281 	}
    282 }