mirror of
https://github.com/troglobit/finit.git
synced 2026-10-02 22:13:01 +07:00
This patch adds service readiness notification to support daemons employing systemd and s6 notification. Complementing the native Finit readiness support using PID files that exist already. The two have slightly different ways of implementing readiness: - https://www.freedesktop.org/software/systemd/man/sd_notify.html - https://skarnet.org/software/s6/notifywhenup.html Finit now provides both a NOTIFY_SOCKET environemnt variable, for systemd, and a way to start s6 daemons with a descriptor argument. For details on the syntax, see the `service` documentation. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
606 lines
12 KiB
C
606 lines
12 KiB
C
/* External client API, using UNIX domain socket.
|
|
*
|
|
* Copyright (c) 2015-2022 Joachim Wiberg <troglobit@gmail.com>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
#include "config.h" /* Generated by configure script */
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#ifdef _LIBITE_LITE
|
|
# include <libite/lite.h>
|
|
#else
|
|
# include <lite/lite.h>
|
|
#endif
|
|
#include <uev/uev.h>
|
|
|
|
#include "finit.h"
|
|
#include "cond.h"
|
|
#include "conf.h"
|
|
#include "helpers.h"
|
|
#include "log.h"
|
|
#include "plugin.h"
|
|
#include "private.h"
|
|
#include "schedule.h"
|
|
#include "service.h"
|
|
#include "sig.h"
|
|
#include "util.h"
|
|
|
|
extern svc_t *wdog;
|
|
static uev_t api_watcher;
|
|
|
|
static int call(int (*action)(svc_t *, void *), char *buf, size_t len)
|
|
{
|
|
return svc_parse_jobstr(buf, len, NULL, action, NULL);
|
|
}
|
|
|
|
static int stop(svc_t *svc, void *user_data)
|
|
{
|
|
if (!svc)
|
|
return 1;
|
|
|
|
svc_stop(svc);
|
|
service_step(svc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int start(svc_t *svc, void *user_data)
|
|
{
|
|
if (!svc)
|
|
return 1;
|
|
|
|
svc_start(svc);
|
|
service_step(svc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* NOTE: this does not wait for svc to be stopped first, that is the
|
|
* responsibility of initctl to do. Otherwise we'd block PID 1,
|
|
* or introduce some nasty race conditions.
|
|
*/
|
|
static int restart(svc_t *svc, void *user_data)
|
|
{
|
|
if (!svc)
|
|
return 1;
|
|
|
|
if (!svc_is_running(svc))
|
|
return start(svc, user_data);
|
|
|
|
service_stop(svc);
|
|
service_step(svc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int reload(svc_t *svc, void *user_data)
|
|
{
|
|
if (!svc)
|
|
return 1;
|
|
|
|
if (svc_is_blocked(svc))
|
|
svc_start(svc);
|
|
|
|
svc_mark_dirty(svc);
|
|
service_step(svc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int do_stop (char *buf, size_t len) { return call(stop, buf, len); }
|
|
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)
|
|
{
|
|
int signo;
|
|
|
|
if (!svc || !user_data || !svc_is_running(svc))
|
|
return 1;
|
|
|
|
signo = *(int *)user_data;
|
|
return !!kill(svc->pid, signo);
|
|
}
|
|
|
|
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, void *user_data)
|
|
{
|
|
char buf[20];
|
|
|
|
if (!job)
|
|
job = "";
|
|
if (!id)
|
|
id = "";
|
|
|
|
snprintf(buf, sizeof(buf), "%s:%s ", job, id);
|
|
strlcat(query_buf, buf, sizeof(query_buf));
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int do_query(char *buf, size_t len)
|
|
{
|
|
query_buf[0] = 0;
|
|
if (svc_parse_jobstr(buf, len, NULL, NULL, missing)) {
|
|
memcpy(buf, query_buf, len);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static svc_t *do_find(char *buf, size_t len)
|
|
{
|
|
char *id = NULL;
|
|
char *ptr, *input;
|
|
svc_t *iter = NULL;
|
|
|
|
input = sanitize(buf, len);
|
|
if (!input)
|
|
return NULL;
|
|
|
|
ptr = strchr(input, ':');
|
|
if (ptr) {
|
|
*ptr++ = 0;
|
|
id = ptr;
|
|
}
|
|
|
|
if (isdigit(input[0])) {
|
|
char *ep;
|
|
long job = 0;
|
|
|
|
errno = 0;
|
|
job = strtol(input, &ep, 10);
|
|
if ((errno == ERANGE && (job == LONG_MAX || job == LONG_MIN)) ||
|
|
(errno != 0 && job == 0) ||
|
|
(input == ep))
|
|
return NULL;
|
|
|
|
if (!id)
|
|
return svc_job_iterator(&iter, 1, job);
|
|
|
|
return svc_find_by_jobid(job, id);
|
|
}
|
|
|
|
if (!id)
|
|
return svc_named_iterator(&iter, 1, input);
|
|
|
|
return svc_find(input, id);
|
|
}
|
|
|
|
static svc_t *do_find_byc(char *buf, size_t len)
|
|
{
|
|
char *input;
|
|
|
|
input = sanitize(buf, len);
|
|
if (!input) {
|
|
dbg("Invalid input");
|
|
return NULL;
|
|
}
|
|
|
|
return svc_find_by_cond(input);
|
|
}
|
|
|
|
static void bypass_shutdown(void *);
|
|
struct wq emergency = { .cb = bypass_shutdown };
|
|
|
|
static void bypass_shutdown(void *unused)
|
|
{
|
|
cprintf("TIMEOUT TIMEOUT SHUTTING DOWN NOW!!\n");
|
|
do_shutdown(halt);
|
|
}
|
|
|
|
static int do_reboot(int cmd, int timeout, char *buf, size_t len)
|
|
{
|
|
int rc = 1;
|
|
|
|
switch (cmd) {
|
|
case INIT_CMD_REBOOT:
|
|
halt = SHUT_REBOOT;
|
|
break;
|
|
|
|
case INIT_CMD_HALT:
|
|
halt = SHUT_HALT;
|
|
break;
|
|
|
|
case INIT_CMD_POWEROFF:
|
|
halt = SHUT_OFF;
|
|
break;
|
|
|
|
case INIT_CMD_SUSPEND:
|
|
break;
|
|
|
|
default:
|
|
return 255;
|
|
}
|
|
|
|
if (timeout > 0) {
|
|
emergency.delay = timeout * 1000;
|
|
schedule_work(&emergency);
|
|
}
|
|
|
|
switch (cmd) {
|
|
case INIT_CMD_REBOOT:
|
|
dbg("reboot");
|
|
halt = SHUT_REBOOT;
|
|
service_runlevel(6);
|
|
break;
|
|
|
|
case INIT_CMD_HALT:
|
|
dbg("halt");
|
|
halt = SHUT_HALT;
|
|
service_runlevel(0);
|
|
break;
|
|
|
|
case INIT_CMD_POWEROFF:
|
|
dbg("poweroff");
|
|
halt = SHUT_OFF;
|
|
service_runlevel(0);
|
|
break;
|
|
|
|
case INIT_CMD_SUSPEND:
|
|
dbg("suspend");
|
|
sync();
|
|
rc = suspend();
|
|
if (rc) {
|
|
if (errno == EINVAL)
|
|
snprintf(buf, len, "Kernel does not support suspend to RAM.");
|
|
else
|
|
snprintf(buf, len, "Failed: %s", strerror(errno));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
/* Handled previously */
|
|
break;
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
typedef struct {
|
|
char *event;
|
|
void (*cb)(void);
|
|
} ev_t;
|
|
|
|
ev_t ev_list[] = {
|
|
{ "RELOAD", service_reload_dynamic },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static void send_svc(int sd, svc_t *svc)
|
|
{
|
|
svc_t empty = { 0 };
|
|
size_t len;
|
|
|
|
if (!svc) {
|
|
empty.pid = -1;
|
|
svc = ∅
|
|
}
|
|
|
|
len = write(sd, svc, sizeof(*svc));
|
|
if (len != sizeof(*svc))
|
|
dbg("Failed sending svc_t to client");
|
|
}
|
|
|
|
static void api_cb(uev_t *w, void *arg, int events)
|
|
{
|
|
static svc_t *iter = NULL;
|
|
struct init_request rq;
|
|
int sd, lvl;
|
|
svc_t *svc;
|
|
|
|
sd = accept(w->fd, NULL, NULL);
|
|
if (sd < 0) {
|
|
err(1, "Failed serving API request");
|
|
goto error;
|
|
}
|
|
|
|
while (1) {
|
|
int result = 0;
|
|
ssize_t len;
|
|
|
|
len = read(sd, &rq, sizeof(rq));
|
|
if (len <= 0) {
|
|
if (-1 == len) {
|
|
if (EINTR == errno)
|
|
continue;
|
|
|
|
if (EAGAIN == errno)
|
|
break;
|
|
|
|
/* we get here when client restarts itself */
|
|
if (ECONNRESET == errno)
|
|
break;
|
|
|
|
errx(1, "Failed reading initctl request, error %d: %s", errno, strerror(errno));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (rq.magic != INIT_MAGIC || len != sizeof(rq)) {
|
|
errx(1, "Invalid initctl request");
|
|
break;
|
|
}
|
|
|
|
switch (rq.cmd) {
|
|
case INIT_CMD_RUNLVL:
|
|
switch (rq.runlevel) {
|
|
case 's':
|
|
case 'S':
|
|
rq.runlevel = '1'; /* Single user mode */
|
|
/* fallthrough */
|
|
|
|
case '0'...'9':
|
|
dbg("Setting new runlevel %c", rq.runlevel);
|
|
lvl = rq.runlevel - '0';
|
|
if (lvl == 0)
|
|
halt = SHUT_OFF;
|
|
if (lvl == 6)
|
|
halt = SHUT_REBOOT;
|
|
service_runlevel(lvl);
|
|
break;
|
|
|
|
default:
|
|
dbg("Unsupported runlevel: %d", rq.runlevel);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case INIT_CMD_DEBUG:
|
|
dbg("debug");
|
|
log_debug();
|
|
break;
|
|
|
|
case INIT_CMD_RELOAD: /* 'init q' and 'initctl reload' */
|
|
dbg("reload");
|
|
service_reload_dynamic();
|
|
break;
|
|
|
|
case INIT_CMD_START_SVC:
|
|
dbg("start %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
result = do_start(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_RESTART_SVC:
|
|
dbg("restart %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
result = do_restart(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_STOP_SVC:
|
|
dbg("stop %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
result = do_stop(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_RELOAD_SVC:
|
|
dbg("reload %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
result = do_reload(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_GET_PLUGINS:
|
|
result = plugin_list(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_PLUGIN_DEPS:
|
|
result = plugin_deps(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_GET_RUNLEVEL:
|
|
dbg("get runlevel");
|
|
rq.runlevel = runlevel;
|
|
rq.sleeptime = prevlevel;
|
|
break;
|
|
|
|
case INIT_CMD_REBOOT:
|
|
case INIT_CMD_HALT:
|
|
case INIT_CMD_POWEROFF:
|
|
case INIT_CMD_SUSPEND:
|
|
result = do_reboot(rq.cmd, rq.sleeptime, rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_ACK:
|
|
dbg("Client failed reading ACK");
|
|
goto leave;
|
|
|
|
case INIT_CMD_WDOG_HELLO:
|
|
dbg("wdog hello");
|
|
if (rq.runlevel <= 0) {
|
|
result = 1;
|
|
break;
|
|
}
|
|
|
|
dbg("Request to hand-over wdog ... to PID %d", rq.runlevel);
|
|
svc = svc_find_by_pid(rq.runlevel);
|
|
if (!svc) {
|
|
logit(LOG_ERR, "Cannot find PID %d, not registered.", rq.runlevel);
|
|
break;
|
|
}
|
|
|
|
if (wdog && wdog != svc) {
|
|
char name[32];
|
|
|
|
svc_ident(svc, name, sizeof(name));
|
|
logit(LOG_NOTICE, "Handing over wdog ctrl from %s[%d] to %s[%d]",
|
|
svc_ident(wdog, NULL, 0), wdog->pid, name, svc->pid);
|
|
|
|
if (wdog->protect) {
|
|
logit(LOG_NOTICE, "Stopping and deleting built-in watchdog.");
|
|
stop(wdog, NULL);
|
|
svc_del(wdog);
|
|
}
|
|
}
|
|
wdog = svc;
|
|
break;
|
|
|
|
case INIT_CMD_SVC_ITER:
|
|
// dbg("svc iter, first: %d", rq.runlevel);
|
|
/*
|
|
* XXX: This severely limits the number of
|
|
* simultaneous client connections, but will
|
|
* have to do for now.
|
|
*/
|
|
svc = svc_iterator(&iter, rq.runlevel);
|
|
send_svc(sd, svc);
|
|
goto leave;
|
|
|
|
case INIT_CMD_SVC_QUERY:
|
|
dbg("svc query: %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
result = do_query(rq.data, sizeof(rq.data));
|
|
break;
|
|
|
|
case INIT_CMD_SVC_FIND:
|
|
dbg("svc find: %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
send_svc(sd, do_find(rq.data, sizeof(rq.data)));
|
|
goto leave;
|
|
|
|
case INIT_CMD_SVC_FIND_BYC:
|
|
dbg("svc find by cond: %s", rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
send_svc(sd, do_find_byc(rq.data, sizeof(rq.data)));
|
|
goto leave;
|
|
|
|
case INIT_CMD_SIGNAL:
|
|
/* runlevel is reused for signal */
|
|
dbg("svc signal %d: %s", rq.runlevel, rq.data);
|
|
strterm(rq.data, sizeof(rq.data));
|
|
result = do_signal(rq.data, sizeof(rq.data), rq.runlevel);
|
|
break;
|
|
|
|
case INIT_CMD_NOTIFY_SOCKET:
|
|
svc = svc_find_by_pid(rq.runlevel);
|
|
if (!svc) {
|
|
errx(1, "Unknown PID, cannot register notify socket");
|
|
result = 1;
|
|
break;
|
|
}
|
|
|
|
if (uev_io_init(ctx, &svc->notify_watcher, service_notify_cb, svc, sd, UEV_READ)) {
|
|
err(1, "Falied initializing %s readiness notifier", svc_ident(svc, NULL, 0));
|
|
break;
|
|
}
|
|
return; /* Don't close sd, used for notify */
|
|
|
|
default:
|
|
dbg("Unsupported cmd: %d", rq.cmd);
|
|
break;
|
|
}
|
|
|
|
if (result)
|
|
rq.cmd = INIT_CMD_NACK;
|
|
else
|
|
rq.cmd = INIT_CMD_ACK;
|
|
len = write(sd, &rq, sizeof(rq));
|
|
if (len != sizeof(rq))
|
|
dbg("Failed sending ACK/NACK back to client");
|
|
}
|
|
|
|
leave:
|
|
close(sd);
|
|
if (UEV_ERROR == events)
|
|
goto error;
|
|
return;
|
|
error:
|
|
api_exit();
|
|
if (api_init(w->ctx))
|
|
errx(1, "Unrecoverable error on API socket");
|
|
}
|
|
|
|
int api_init(uev_ctx_t *ctx)
|
|
{
|
|
struct sockaddr_un sun = {
|
|
.sun_family = AF_UNIX,
|
|
.sun_path = INIT_SOCKET,
|
|
};
|
|
mode_t oldmask;
|
|
int uid, gid;
|
|
int sd;
|
|
|
|
dbg("Setting up external API socket ...");
|
|
sd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
|
|
if (-1 == sd) {
|
|
err(1, "Failed starting external API socket");
|
|
return 1;
|
|
}
|
|
|
|
uid = geteuid();
|
|
gid = getgroup(DEFGROUP);
|
|
|
|
erase(INIT_SOCKET);
|
|
oldmask = umask(0117);
|
|
if (-1 == bind(sd, (struct sockaddr*)&sun, sizeof(sun)))
|
|
goto error;
|
|
|
|
if (-1 == listen(sd, 10))
|
|
goto error;
|
|
|
|
if (chown(INIT_SOCKET, uid, gid))
|
|
err(1, "Failed setting group %s on %s", DEFGROUP, INIT_SOCKET);
|
|
|
|
umask(oldmask);
|
|
if (!uev_io_init(ctx, &api_watcher, api_cb, NULL, sd, UEV_READ))
|
|
return 0;
|
|
|
|
error:
|
|
err(1, "Failed initializing API socket");
|
|
umask(oldmask);
|
|
close(sd);
|
|
return 1;
|
|
}
|
|
|
|
int api_exit(void)
|
|
{
|
|
uev_io_stop(&api_watcher);
|
|
|
|
return close(api_watcher.fd);
|
|
}
|
|
|
|
/**
|
|
* Local Variables:
|
|
* indent-tabs-mode: t
|
|
* c-file-style: "linux"
|
|
* End:
|
|
*/
|