From 62da37947265b604d98ec194533d00570dbe3a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rgen=20Sigvardsson?= Date: Sat, 12 Feb 2022 20:59:44 +0100 Subject: [PATCH 1/3] initctl signal command parsing: done --- .gitignore | 3 ++ man/initctl.8 | 4 ++ src/finit.h | 1 + src/initctl.c | 135 ++++++++++++++++++++++++++++++++++++-------------- src/util.c | 28 +++++++++++ src/util.h | 1 + 6 files changed, 135 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 9137e14c..55b889b1 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ GTAGS /.autoconf /.automake /.built + +# VS Code user settings +.vscode \ No newline at end of file diff --git a/man/initctl.8 b/man/initctl.8 index c6b283ad..e3d87828 100644 --- a/man/initctl.8 +++ b/man/initctl.8 @@ -140,6 +140,10 @@ Start service by name, with optional ID, e.g., Stop/Pause a running service by name .It Nm Ar reload Cm NAME[:ID] Reload service by name (SIGHUP or restart) +.It Nm Ar signal Cm NAME:[ID] S +Send signal S to service, by name. +.Cm S +may be a complete signal name such as SIGHUP, or short name such as HUP, or the signal number such as 1 (SIGHUP). .It Nm Ar restart Cm NAME[:ID] Restart (stop/start) service by name .It Nm Ar status Cm NAME[:ID] diff --git a/src/finit.h b/src/finit.h index a0dc559b..72d5c8d8 100644 --- a/src/finit.h +++ b/src/finit.h @@ -95,6 +95,7 @@ #define INIT_CMD_SVC_QUERY 130 #define INIT_CMD_SVC_FIND 131 #define INIT_CMD_SVC_FIND_BYC 132 +#define INIT_CMD_SIGNAL 133 #define INIT_CMD_NACK 254 #define INIT_CMD_ACK 255 diff --git a/src/initctl.c b/src/initctl.c index b244fcac..37ba2945 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -52,6 +52,7 @@ struct cmd { struct cmd *ctx; int (*cb)(char *arg); int *cond; + int (*cb_multiarg)(int argc, char **argv); }; int icreate = 0; @@ -275,6 +276,60 @@ static int do_restart(char *arg) return do_startstop(INIT_CMD_RESTART_SVC, arg); } +/** + * do_signal - Ask finit to send a signal to a service. + * @argv: must point to an array of strings, containing a service + * and signal name, in that order. + * @argc: must be 2. + * + * A signal can be a complete signal name such as "SIGHUP", or + * it can be the shortest unique name, such as "HUP" (no SIG prefix). + * It can also be a raw signal number, such as "9" (SIGKILL). + */ +int do_signal(int argc, char **argv) +{ + int signum; + + struct init_request rq = { + .magic = INIT_MAGIC, + .cmd = INIT_CMD_SVC_QUERY + }; + + if (argc != 2) + errx(1, "invalid number of arguments to signal"); + + /* Validate service name */ + strlcpy(rq.data, argv[0], sizeof(rq.data)); + if (client_send(&rq, sizeof(rq))) { + fprintf(stderr, "No such task or service(s): %s\n\n", argv[0]); + goto show_usage; + } + + /* Validate signal name (or number) */ + if ((signum = str2sig(argv[1])) < 0) { + /* It wasn't a signal name, so let's see if argv[1] + is an actual signal number. */ + errno = 0; /* Make sure errno is reset */ + signum = (int) strtol(argv[1], NULL, 10); + + /* Was it a number? Was it a signum that finit recognizes? */ + if (errno || !*(sig2str(signum))) { + fprintf(stderr, "Not a valid signal (or signum): %s\n", argv[1]); + goto show_usage; + } + } + + rq.magic = INIT_MAGIC; + rq.cmd = INIT_CMD_SIGNAL; + strlcpy(rq.data, argv[0], sizeof(rq.data)); + rq.runlevel = signum; /* Reuse runlevel for signal number. */ + return client_send(&rq, sizeof(rq)); + +show_usage: + fprintf(stderr, "Usage: initctl signal [:ID] \n"); + return 1; +} + static int dump_one_cond(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftwbuf) { const char *cond, *asserted; @@ -939,6 +994,7 @@ static int usage(int rc) " stop [:ID] Stop/Pause a running service by name\n" " reload [:ID] Reload service by name (SIGHUP or restart)\n" " restart [:ID] Restart (stop/start) service by name\n" + " signal [:ID] Send signal S to service by name, with optional ID\n" " ident [NAME] Show matching identities for NAME, or all\n" " status [:ID] Show service status, by name\n" " status Show status of services, default command\n"); @@ -1001,6 +1057,9 @@ static int cmd_parse(int argc, char *argv[], struct cmd *command) if (command[i].ctx) return cmd_parse(argc - 1, &argv[1], command[i].ctx); + if (command[i].cb_multiarg) + return command[i].cb_multiarg(argc - 1, argv + 1); + if (command[i].cb) { int rc = 0; @@ -1019,6 +1078,7 @@ static int cmd_parse(int argc, char *argv[], struct cmd *command) return command[0].cb(NULL); /* default cmd */ } + int main(int argc, char *argv[]) { struct option long_options[] = { @@ -1036,52 +1096,53 @@ int main(int argc, char *argv[]) { NULL, 0, NULL, 0 } }; struct cmd cond[] = { - { "status", NULL, do_cond_show, NULL }, /* default cmd */ - { "dump", NULL, do_cond_dump, NULL }, - { "set", NULL, do_cond_set, NULL }, - { "clr", NULL, do_cond_clr, NULL }, - { "clear", NULL, do_cond_clr, NULL }, - { NULL, NULL, NULL, NULL } + { "status", NULL, do_cond_show, NULL, NULL }, /* default cmd */ + { "dump", NULL, do_cond_dump, NULL, NULL }, + { "set", NULL, do_cond_set, NULL, NULL }, + { "clr", NULL, do_cond_clr, NULL, NULL }, + { "clear", NULL, do_cond_clr, NULL, NULL }, + { NULL, NULL, NULL, NULL, NULL } }; struct cmd command[] = { - { "status", NULL, show_status, NULL }, /* default cmd */ - { "ident", NULL, show_ident, NULL }, + { "status", NULL, show_status, NULL, NULL }, /* default cmd */ + { "ident", NULL, show_ident, NULL, NULL }, - { "debug", NULL, toggle_debug, NULL }, - { "devel", NULL, do_devel, NULL }, - { "help", NULL, do_help, NULL }, - { "version", NULL, show_version, NULL }, + { "debug", NULL, toggle_debug, NULL, NULL }, + { "devel", NULL, do_devel, NULL, NULL }, + { "help", NULL, do_help, NULL, NULL }, + { "version", NULL, show_version, NULL, NULL }, - { "list", NULL, serv_list, NULL }, - { "ls", NULL, serv_list, NULL }, - { "enable", NULL, serv_enable, NULL }, - { "disable", NULL, serv_disable, NULL }, - { "touch", NULL, serv_touch, NULL }, - { "show", NULL, serv_show, NULL }, - { "edit", NULL, serv_edit, NULL }, - { "create", NULL, serv_creat, NULL }, - { "delete", NULL, serv_delete, NULL }, - { "reload", NULL, do_reload, NULL }, + { "list", NULL, serv_list, NULL, NULL }, + { "ls", NULL, serv_list, NULL, NULL }, + { "enable", NULL, serv_enable, NULL, NULL }, + { "disable", NULL, serv_disable, NULL, NULL }, + { "touch", NULL, serv_touch, NULL, NULL }, + { "show", NULL, serv_show, NULL, NULL }, + { "edit", NULL, serv_edit, NULL, NULL }, + { "create", NULL, serv_creat, NULL, NULL }, + { "delete", NULL, serv_delete, NULL, NULL }, + { "reload", NULL, do_reload, NULL, NULL }, - { "cond", cond, NULL, NULL }, + { "cond", cond, NULL, NULL, NULL }, - { "log", NULL, do_log, NULL }, - { "start", NULL, do_start, NULL }, - { "stop", NULL, do_stop, NULL }, - { "restart", NULL, do_restart, NULL }, + { "log", NULL, do_log, NULL, NULL }, + { "start", NULL, do_start, NULL, NULL }, + { "stop", NULL, do_stop, NULL, NULL }, + { "restart", NULL, do_restart, NULL, NULL }, + { "signal", NULL, NULL, NULL, do_signal }, - { "cgroup", NULL, show_cgroup, &cgrp }, - { "ps", NULL, show_cgps, &cgrp }, - { "top", NULL, show_cgtop, &cgrp }, + { "cgroup", NULL, show_cgroup, &cgrp, NULL }, + { "ps", NULL, show_cgps, &cgrp, NULL }, + { "top", NULL, show_cgtop, &cgrp, NULL }, - { "runlevel", NULL, do_runlevel, NULL }, - { "reboot", NULL, do_reboot, NULL }, - { "halt", NULL, do_halt, NULL }, - { "poweroff", NULL, do_poweroff, NULL }, - { "suspend", NULL, do_suspend, NULL }, + { "runlevel", NULL, do_runlevel, NULL, NULL }, + { "reboot", NULL, do_reboot, NULL, NULL }, + { "halt", NULL, do_halt, NULL, NULL }, + { "poweroff", NULL, do_poweroff, NULL, NULL }, + { "suspend", NULL, do_suspend, NULL, NULL }, - { "utmp", NULL, do_utmp, &utmp }, - { NULL, NULL, NULL, NULL } + { "utmp", NULL, do_utmp, &utmp, NULL }, + { NULL, NULL, NULL, NULL, NULL } }; int interactive = 1, c; diff --git a/src/util.c b/src/util.c index 1a6b353a..eaff12c0 100644 --- a/src/util.c +++ b/src/util.c @@ -275,6 +275,34 @@ char *sig2str(int sig) return signame; } +/** + * str2sig - Translate signal name to the corresponding signal number. + * @sig: The name of the signal + * + * A signal can be a complete signal name such as "SIGHUP", or + * it can be the shortest unique name, such as "HUP" (no SIG prefix). + */ +int str2sig(char *sig) +{ + int name_offset = 0; + size_t i; + + /* if sig name starts with "SIG", then skip the first + * three characters. signames holds the names without + * the "SIG" prefix. + */ + if (strncasecmp(sig, "SIG", 3) == 0) { + name_offset = 3; + } + + for (i = 1; i < NELEMS(signames); ++i) { + if (strcasecmp(&sig[name_offset], signames[i]) == 0) + return i; + } + + return -1; +} + char *code2str(int code) { static char codename[20]; diff --git a/src/util.h b/src/util.h index f3b5d46f..23cfcf6e 100644 --- a/src/util.h +++ b/src/util.h @@ -61,6 +61,7 @@ int fngetint (char *path, int *val); int strtobytes (char *arg); char *sig2str (int sig); +int str2sig (char *sig); char *code2str (int code); void do_sleep (unsigned int sec); From 4121113688f321fdaa02a7ee178a2ba999dd992d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rgen=20Sigvardsson?= Date: Sun, 13 Feb 2022 13:28:44 +0100 Subject: [PATCH 2/3] Added implementation for the new command INIT_CMD_SIGNAL. The implementation looks up the named service by using `svc_parse_jobstr`. The callbacks for `svc_parse_jobstr` has been augmented to accept a user data parameter. For this use case, a carrier for the actual signal was needed. The address of the signal parameter is taken and passed on as a `void *`. The callback then simply deferences it as an int - the signal number. --- .gitignore | 2 +- src/api.c | 48 +++++++++++++++++++++++++++++++++++++++--------- src/svc.c | 18 +++++++++--------- src/svc.h | 2 +- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 55b889b1..4bc8a37a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,4 @@ GTAGS /.built # VS Code user settings -.vscode \ No newline at end of file +.vscode diff --git a/src/api.c b/src/api.c index 7a0198f1..f78497f0 100644 --- a/src/api.c +++ b/src/api.c @@ -51,12 +51,12 @@ extern svc_t *wdog; static uev_t api_watcher; -static int call(int (*action)(svc_t *), char *buf, size_t len) +static int call(int (*action)(svc_t *, void *), char *buf, size_t len) { - return svc_parse_jobstr(buf, len, action, NULL); + return svc_parse_jobstr(buf, len, NULL, action, NULL); } -static int stop(svc_t *svc) +static int stop(svc_t *svc, void *user_data) { if (!svc) return 1; @@ -67,7 +67,7 @@ static int stop(svc_t *svc) return 0; } -static int start(svc_t *svc) +static int start(svc_t *svc, void *user_data) { if (!svc) return 1; @@ -83,7 +83,7 @@ static int start(svc_t *svc) * responsibility of initctl to do. Otherwise we'd block PID 1, * or introduce some nasty race conditions. */ -static int restart(svc_t *svc) +static int restart(svc_t *svc, void *user_data) { if (!svc) return 1; @@ -95,7 +95,7 @@ static int restart(svc_t *svc) return 0; } -static int reload(svc_t *svc) +static int reload(svc_t *svc, void *user_data) { if (!svc) return 1; @@ -114,8 +114,32 @@ static int do_start (char *buf, size_t len) { return call(start, buf, len); } static int do_restart(char *buf, size_t len) { return call(restart, buf, len); } static int do_reload (char *buf, size_t len) { return call(reload, buf, len); } +static int do_signal_svc(svc_t *svc, void *user_data) +{ + if (!svc) + return 1; + + if (!svc_is_running(svc)) + return 1; + + int sig = *(int *)user_data; + if (kill(svc->pid, sig)) + return 1; + + return 0; +} + +static int do_signal(char *buf, size_t len, int sig) +{ + /* Sanity check: Do we know this signal!? */ + if (!*sig2str(sig)) + return 1; + + return svc_parse_jobstr(buf, len, &sig, do_signal_svc, NULL); +} + static char query_buf[368]; -static int missing(char *job, char *id) +static int missing(char *job, char *id, void *user_data) { char buf[20]; @@ -133,7 +157,7 @@ static int missing(char *job, char *id) static int do_query(char *buf, size_t len) { query_buf[0] = 0; - if (svc_parse_jobstr(buf, len, NULL, missing)) { + if (svc_parse_jobstr(buf, len, NULL, NULL, missing)) { memcpy(buf, query_buf, len); return 1; } @@ -407,7 +431,7 @@ static void api_cb(uev_t *w, void *arg, int events) if (wdog->protect) { logit(LOG_NOTICE, "Stopping and deleting built-in watchdog."); - stop(wdog); + stop(wdog, NULL); svc_del(wdog); } } @@ -443,6 +467,12 @@ static void api_cb(uev_t *w, void *arg, int events) send_svc(sd, do_find_byc(rq.data, sizeof(rq.data))); goto leave; + case INIT_CMD_SIGNAL: + _d("svc signal %d: %s", rq.runlevel, rq.data); + strterm(rq.data, sizeof(rq.data)); + result = do_signal(rq.data, sizeof(rq.data), rq.runlevel /* runlevel is reused for signal! */); + break; + default: _d("Unsupported cmd: %d", rq.cmd); break; diff --git a/src/svc.c b/src/svc.c index ae61e880..a27ee54f 100644 --- a/src/svc.c +++ b/src/svc.c @@ -650,7 +650,7 @@ static char *tokstr(char *str, size_t len) /* * Used by api.c (to start/stop/restart) and initctl.c (for input validation) */ -int svc_parse_jobstr(char *str, size_t len, int (*found)(svc_t *), int (not_found)(char *, char *)) +int svc_parse_jobstr(char *str, size_t len, void *user_data, int (*found)(svc_t *, void *), int (not_found)(char *, char *, void *)) { char *input, *token; int result = 0; @@ -686,11 +686,11 @@ int svc_parse_jobstr(char *str, size_t len, int (*found)(svc_t *), int (not_foun if (!ptr) { svc = svc_job_iterator(&iter, 1, job); if (!svc && not_found) - result += not_found(NULL, token); + result += not_found(NULL, token, user_data); while (svc) { if (found) - result += found(svc); + result += found(svc, user_data); svc = svc_job_iterator(&iter, 0, job); } } else { @@ -699,19 +699,19 @@ int svc_parse_jobstr(char *str, size_t len, int (*found)(svc_t *), int (not_foun svc = svc_find_by_jobid(job, id); if (!svc && not_found) - result += not_found(token, id); + result += not_found(token, id, user_data); else if (found) - result += found(svc); + result += found(svc, user_data); } } else { if (!ptr) { svc = svc_named_iterator(&iter, 1, token); if (!svc && not_found) - result += not_found(token, id); + result += not_found(token, id, user_data); while (svc) { if (found) - result += found(svc); + result += found(svc, user_data); svc = svc_named_iterator(&iter, 0, token); } } else { @@ -720,9 +720,9 @@ int svc_parse_jobstr(char *str, size_t len, int (*found)(svc_t *), int (not_foun svc = svc_find_by_nameid(token, id); if (!svc && not_found) - result += not_found(token, id); + result += not_found(token, id, user_data); else if (found) - result += found(svc); + result += found(svc, user_data); } } diff --git a/src/svc.h b/src/svc.h index 107acdc8..3a670fe4 100644 --- a/src/svc.h +++ b/src/svc.h @@ -222,7 +222,7 @@ void svc_prune_bootstrap (void); void svc_enable (svc_t *svc); int svc_enabled (svc_t *svc); -int svc_parse_jobstr (char *str, size_t len, int (*found)(svc_t *), int (not_found)(char *, char *)); +int svc_parse_jobstr (char *str, size_t len, void *user_data, int (*found)(svc_t *, void *), int (not_found)(char *, char *, void *)); static inline int svc_is_daemon (svc_t *svc) { return svc && SVC_TYPE_SERVICE == svc->type; } static inline int svc_is_sysv (svc_t *svc) { return svc && SVC_TYPE_SYSV == svc->type; } From beec0ee59090800408699bd38bf854ad4be1beab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rgen=20Sigvardsson?= Date: Tue, 15 Feb 2022 12:22:26 +0100 Subject: [PATCH 3/3] Test case for initctl signal This commit implements a test for the signal feature of initctl. The existing service used in tests (`common/service.sh`) has been extended with a signal trap handler. When SIGUSR1 is received, it will write the string `'USR1'` to `/tmp/usr1.log`. The test will run `initctl signal service.sh SIGUSR1` and assert that the contents of the file `/tmp/usr1.log` really is `USR1`. --- test/Makefile.am | 2 ++ test/common/service.sh | 4 ++++ test/signal-service.sh | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100755 test/signal-service.sh diff --git a/test/Makefile.am b/test/Makefile.am index 32cd73b9..7b7e0b1d 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -8,6 +8,7 @@ EXTRA_DIST += add-remove-dynamic-service-sub-config.sh EXTRA_DIST += start-stop-service.sh EXTRA_DIST += start-stop-service-sub-config.sh EXTRA_DIST += start-kill-service.sh +EXTRA_DIST += signal-service.sh AM_TESTS_ENVIRONMENT = TENV_ROOT='$(abs_builddir)/tenv-root/'; AM_TESTS_ENVIRONMENT += export TENV_ROOT; @@ -28,6 +29,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh TESTS += start-stop-service.sh TESTS += start-stop-service-sub-config.sh TESTS += start-kill-service.sh +TESTS += signal-service.sh clean-local: -rm -rf $(builddir)/tenv-root/ diff --git a/test/common/service.sh b/test/common/service.sh index 870a383c..08366777 100755 --- a/test/common/service.sh +++ b/test/common/service.sh @@ -2,6 +2,10 @@ set -eu +# Hook SIGUSR1 and dump trace to file system +# shellcheck disable=SC2172 +trap 'echo USR1 > /tmp/usr1.log' 10 + echo $$ > /run/service.pid while true; do diff --git a/test/signal-service.sh b/test/signal-service.sh new file mode 100755 index 00000000..a8998cc5 --- /dev/null +++ b/test/signal-service.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# Verifies initctl can send arbitrary signals (USR1 in this case) to finit managed services + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/tenv/lib.sh" + +test_teardown() { + say "Test done $(date)" + say "Running test teardown." + + texec rm -f "$FINIT_CONF" + texec rm -f /test_assets/service.sh +} + +say "Test start $(date)" + +cp "$TEST_DIR"/common/service.sh "$TENV_ROOT"/test_assets/ + +say 'Ensure file system is cleared' +texec rm -f /tmp/usr1.log + +say "Add service stanza in $FINIT_CONF" +texec sh -c "echo 'service [2345] kill:20 log /test_assets/service.sh -- Test service' > $FINIT_CONF" + +say 'Reload Finit' +texec sh -c "initctl reload" + +say 'Send SIGUSR1 to service...' +texec sh -c "initctl signal service.sh SIGUSR1" + +# shellcheck disable=SC2016 +retry 'assert "service.sh received SIGUSR" "$(texec cat /tmp/usr1.log)" = "USR1"' 10 1