mirror of
https://github.com/troglobit/finit.git
synced 2026-10-02 05:52:48 +07:00
initctl reload freezes conditions to the old generation and each owner re-asserts. Finit's own providers do this in-process; an external provider whose conditions are generation files, rather than the oneshot symlinks keventd uses, has no way to know the moment. Emit Manager1.ConfigReloaded when reconfiguration completes. keventd needs no subscriber: its conditions are symlinks to the reconf marker itself, so they read the current generation by construction and never flux, which the device bus test now pins down. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1730 lines
49 KiB
C
1730 lines
49 KiB
C
/* Finit-side glue between the event loop and libink.
|
|
*
|
|
* Owns the libink server, accepts new peers, drives each peer's
|
|
* state machine, and registers the Finit-specific D-Bus object
|
|
* tree (org.finit.Manager1 et al). Nothing in libink/ depends on
|
|
* finit-internal types: the boundary lives in this file, by design.
|
|
*
|
|
* Copyright (c) 2026 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"
|
|
|
|
#ifdef HAVE_DBUS
|
|
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <uev/uev.h>
|
|
|
|
#include <ftw.h>
|
|
|
|
#include "link.h"
|
|
#include "path.h"
|
|
|
|
#include "finit.h"
|
|
#include "cond.h"
|
|
#include "conf.h"
|
|
#include "log.h"
|
|
#include "private.h"
|
|
#include "schedule.h"
|
|
#include "service.h"
|
|
#include "sig.h"
|
|
#include "sm.h"
|
|
#include "svc.h"
|
|
#include "util.h"
|
|
|
|
#define DBUS_MAX_PEERS 64
|
|
|
|
struct peer {
|
|
uev_t watcher;
|
|
link_connection_t *conn;
|
|
int dead;
|
|
TAILQ_ENTRY(peer) link;
|
|
};
|
|
|
|
static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers);
|
|
static TAILQ_HEAD(, peer) reapq = TAILQ_HEAD_INITIALIZER(reapq);
|
|
static link_server_t *server;
|
|
static uev_t accept_watcher;
|
|
static size_t peer_count;
|
|
static struct peer *sysbus_peer;
|
|
|
|
static void sysbus_probe(void);
|
|
static void sender_cache_flush(void);
|
|
|
|
/* libink has no logger of its own, so give it ours. The func name it
|
|
* passes takes the place of dbg()'s __func__, which would otherwise
|
|
* always read "link_log_cb". */
|
|
static void link_log_cb(void *userdata, const char *func, const char *msg)
|
|
{
|
|
(void)userdata;
|
|
logit(LOG_DEBUG, "%s():%s", func, msg);
|
|
}
|
|
|
|
/* Close and free everything dropped since the last time round the
|
|
* event loop. Safe here because no connection's read loop is on the
|
|
* stack; see peer_drop(). */
|
|
static void peer_reap(void *arg)
|
|
{
|
|
struct peer *p;
|
|
|
|
(void)arg;
|
|
while ((p = TAILQ_FIRST(&reapq))) {
|
|
TAILQ_REMOVE(&reapq, p, link);
|
|
link_connection_close(p->conn);
|
|
free(p);
|
|
}
|
|
}
|
|
|
|
/* Not zero: a uev timer armed with a zero timeout is a disarmed
|
|
* timer, so the work would never run and the connections would leak.
|
|
* Any short delay does, the peer is already unlinked and its watcher
|
|
* stopped, so nothing touches it in the meantime. */
|
|
static struct wq reap_work = { .cb = peer_reap, .delay = 10 };
|
|
|
|
/*
|
|
* Nothing in libink can time itself out, it has no event loop, so the
|
|
* deadline for a parked call and for a call we made on the broker is
|
|
* ours to keep. The sweep only runs while something is outstanding:
|
|
* expire_arm() starts it, and it stops rearming as soon as nothing is
|
|
* left, so a system that never talks to a broker never wakes up for
|
|
* this.
|
|
*/
|
|
#define DBUS_CALL_TIMEOUT_MS 5000
|
|
#define DBUS_SWEEP_MS 1000
|
|
|
|
static void expire_sweep(void *arg);
|
|
static struct wq expire_work = { .cb = expire_sweep, .delay = DBUS_SWEEP_MS };
|
|
static int expire_armed;
|
|
|
|
/* Idempotent: several parks in one turn of the loop share one sweep. */
|
|
static void expire_arm(void)
|
|
{
|
|
if (expire_armed)
|
|
return;
|
|
if (!schedule_work(&expire_work))
|
|
expire_armed = 1;
|
|
}
|
|
|
|
static void expire_sweep(void *arg)
|
|
{
|
|
struct peer *p, *tmp;
|
|
int live = 0;
|
|
|
|
(void)arg;
|
|
expire_armed = 0;
|
|
|
|
/* Expiring a call runs its callback, which may drop the peer it
|
|
* belongs to; _SAFE keeps this loop walking when that peer is
|
|
* `p`. It does NOT cover a callback that drops a *different*
|
|
* peer, which would unlink our saved `tmp` -- no path does that
|
|
* today (the only callback here denies on the same connection),
|
|
* and if one ever does it must not free from under the sweep. */
|
|
TAILQ_FOREACH_SAFE(p, &peers, link, tmp) {
|
|
if (!p->dead)
|
|
live += link_connection_expire(p->conn, DBUS_CALL_TIMEOUT_MS);
|
|
}
|
|
|
|
if (live)
|
|
expire_arm();
|
|
}
|
|
|
|
/*
|
|
* A peer can be dropped from inside its own read loop: a handler emits
|
|
* a signal, the write to this very peer fails, and dbus_emit_signal()
|
|
* lands here while link_connection_process() still holds the
|
|
* connection and will touch its rx buffer on the way out. Freeing now
|
|
* would pull that out from under it, so unlink the peer and let the
|
|
* event loop free it once the stack has unwound.
|
|
*/
|
|
static void peer_drop(struct peer *p)
|
|
{
|
|
if (p->dead)
|
|
return; /* already on its way out */
|
|
p->dead = 1;
|
|
|
|
uev_io_stop(&p->watcher);
|
|
TAILQ_REMOVE(&peers, p, link);
|
|
peer_count--;
|
|
|
|
/* broker gone; the notify paths probe for its return. Its unique
|
|
* names die with it, so nothing we learned about them is safe to
|
|
* carry over to whatever takes its place. */
|
|
if (p == sysbus_peer) {
|
|
sysbus_peer = NULL;
|
|
sender_cache_flush();
|
|
}
|
|
|
|
TAILQ_INSERT_TAIL(&reapq, p, link);
|
|
schedule_work(&reap_work);
|
|
}
|
|
|
|
static void peer_cb(uev_t *w, void *arg, int events)
|
|
{
|
|
struct peer *p = arg;
|
|
|
|
(void)w;
|
|
|
|
if (UEV_ERROR == events) {
|
|
peer_drop(p);
|
|
return;
|
|
}
|
|
|
|
if (link_connection_process(p->conn) < 0)
|
|
peer_drop(p);
|
|
}
|
|
|
|
/* Wrap an authenticated connection in a struct peer, insert into the
|
|
* peer list, and register an event-loop watcher. Enforces
|
|
* DBUS_MAX_PEERS. Closes the connection and returns NULL on failure.
|
|
* Used by both the accept path and the system-bus attach path. */
|
|
static struct peer *peer_register(uev_ctx_t *ctx, link_connection_t *conn)
|
|
{
|
|
struct peer *p;
|
|
|
|
if (peer_count >= DBUS_MAX_PEERS) {
|
|
logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping",
|
|
peer_count);
|
|
link_connection_close(conn);
|
|
return NULL;
|
|
}
|
|
|
|
p = calloc(1, sizeof(*p));
|
|
if (!p) {
|
|
link_connection_close(conn);
|
|
return NULL;
|
|
}
|
|
|
|
p->conn = conn;
|
|
TAILQ_INSERT_TAIL(&peers, p, link);
|
|
peer_count++;
|
|
|
|
if (uev_io_init(ctx, &p->watcher, peer_cb, p,
|
|
link_connection_get_fd(conn), UEV_READ)) {
|
|
peer_drop(p);
|
|
return NULL;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static void accept_cb(uev_t *w, void *arg, int events)
|
|
{
|
|
(void)arg;
|
|
|
|
if (UEV_ERROR == events) {
|
|
err(1, "D-Bus accept watcher error");
|
|
return;
|
|
}
|
|
|
|
for (;;) {
|
|
link_connection_t *conn = NULL;
|
|
|
|
if (link_server_accept(server, &conn) < 0) {
|
|
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
|
err(1, "Failed accepting D-Bus client");
|
|
break;
|
|
}
|
|
|
|
if (!peer_register(w->ctx, conn))
|
|
continue; /* logged inside */
|
|
}
|
|
}
|
|
|
|
/* ---------- org.finit.Manager1 ---------- */
|
|
|
|
/* Forward decl + buffer-size constant — both consumed by Manager1
|
|
* handlers below, defined in the Service1 block further down. */
|
|
#define SERVICE_PATH_PREFIX "/org/finit/service/"
|
|
#define SERVICE_PATH_PREFIX_LEN (sizeof(SERVICE_PATH_PREFIX) - 1)
|
|
#define FINIT_SVC_PATH_MAX 512
|
|
static int service_path_for(svc_t *svc, char *buf, size_t bufsz);
|
|
|
|
static int manager_list_services(link_call_t *call, void *userdata)
|
|
{
|
|
link_writer_t *w;
|
|
svc_t *iter = NULL;
|
|
svc_t *svc;
|
|
|
|
(void)userdata;
|
|
|
|
w = link_call_reply(call);
|
|
if (!w)
|
|
return -1;
|
|
|
|
link_w_array_begin(w, 's');
|
|
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
|
|
char ident[MAX_IDENT_LEN];
|
|
|
|
svc_ident(svc, ident, sizeof(ident));
|
|
link_w_string(w, ident);
|
|
}
|
|
link_w_array_end(w);
|
|
return 0;
|
|
}
|
|
|
|
static int manager_get_service(link_call_t *call, void *userdata)
|
|
{
|
|
const char *ident;
|
|
svc_t *svc;
|
|
char path[FINIT_SVC_PATH_MAX];
|
|
link_writer_t *w;
|
|
|
|
(void)userdata;
|
|
|
|
if (link_call_read_string(call, &ident) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (s)");
|
|
|
|
svc = svc_find_by_str(ident);
|
|
if (!svc)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.NoSuchService", ident);
|
|
|
|
if (service_path_for(svc, path, sizeof(path)) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed",
|
|
"Path encoding overflow");
|
|
|
|
w = link_call_reply(call);
|
|
if (!w)
|
|
return -1;
|
|
link_w_path(w, path);
|
|
return 0;
|
|
}
|
|
|
|
/* svc_parse_jobstr-style adapters over the shared service helpers */
|
|
|
|
static int dbus_apply_stop(svc_t *svc, void *user_data)
|
|
{
|
|
(void)user_data;
|
|
return service_stop_now(svc);
|
|
}
|
|
|
|
static int dbus_apply_start(svc_t *svc, void *user_data)
|
|
{
|
|
(void)user_data;
|
|
return service_start_now(svc);
|
|
}
|
|
|
|
static int dbus_apply_restart(svc_t *svc, void *user_data)
|
|
{
|
|
(void)user_data;
|
|
return service_restart_now(svc);
|
|
}
|
|
|
|
struct dispatch_ctx {
|
|
int (*action)(svc_t *, void *);
|
|
void *udata;
|
|
int matched;
|
|
};
|
|
|
|
static int dispatch_found(svc_t *svc, void *udata)
|
|
{
|
|
struct dispatch_ctx *ctx = udata;
|
|
|
|
ctx->matched++;
|
|
return ctx->action(svc, ctx->udata);
|
|
}
|
|
|
|
static int dispatch_missing(char *job, char *id, void *udata)
|
|
{
|
|
(void)job; (void)id; (void)udata;
|
|
return 0; /* don't penalise the return; we'll check ->matched */
|
|
}
|
|
|
|
/* Apply `action(svc, udata)` to every service matched by `ident`.
|
|
* Returns 0 if at least one service matched and the action succeeded
|
|
* on all; -1 if no service matched the identity (caller sends
|
|
* NoSuchService). */
|
|
static int dispatch_action(const char *ident,
|
|
int (*action)(svc_t *, void *), void *udata)
|
|
{
|
|
char buf[MAX_IDENT_LEN];
|
|
struct dispatch_ctx ctx = { .action = action, .udata = udata };
|
|
int rc;
|
|
|
|
if (!ident || !*ident || strlen(ident) >= sizeof(buf))
|
|
return -1;
|
|
memcpy(buf, ident, strlen(ident) + 1);
|
|
rc = svc_parse_jobstr(buf, sizeof(buf), &ctx,
|
|
dispatch_found, dispatch_missing);
|
|
if (ctx.matched == 0)
|
|
return -1;
|
|
return rc;
|
|
}
|
|
|
|
static int manager_take_string_method(link_call_t *call,
|
|
int (*action)(svc_t *, void *))
|
|
{
|
|
const char *ident;
|
|
int rc;
|
|
|
|
if (link_call_read_string(call, &ident) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (s)");
|
|
|
|
rc = dispatch_action(ident, action, NULL);
|
|
if (rc < 0)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.NoSuchService", ident);
|
|
if (rc)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed",
|
|
"failed on matched service(s)");
|
|
|
|
(void)link_call_reply(call); /* empty reply */
|
|
return 0;
|
|
}
|
|
|
|
static int manager_start (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); }
|
|
static int manager_stop (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); }
|
|
static int manager_restart(link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); }
|
|
|
|
static int manager_reload(link_call_t *call, void *userdata)
|
|
{
|
|
(void)userdata;
|
|
/*
|
|
* Same semantics as api.c: harmless no-op during bootstrap
|
|
* and shutdown, the client still sees success.
|
|
*/
|
|
if (IS_RESERVED_RUNLEVEL(runlevel))
|
|
warnx("Ignoring reload in runlevel S and 6/0.");
|
|
else
|
|
sm_reload();
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int manager_set_runlevel(link_call_t *call, void *userdata)
|
|
{
|
|
uint32_t lvl;
|
|
|
|
(void)userdata;
|
|
if (link_call_read_u32(call, &lvl) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (u)");
|
|
if (lvl > 9 || lvl == INIT_LEVEL)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"runlevel must be 0-9 (excluding internal levels)");
|
|
|
|
sm_request_runlevel((int)lvl);
|
|
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int dbus_shutdown(link_call_t *call, shutop_t target, int level)
|
|
{
|
|
uint32_t timeout;
|
|
|
|
if (link_call_read_u32(call, &timeout) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (u)");
|
|
if (IS_RESERVED_RUNLEVEL(runlevel))
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.WrongRunlevel",
|
|
"Already in shutdown");
|
|
halt = target;
|
|
shutdown_bypass((int)timeout);
|
|
sm_runlevel(level);
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); }
|
|
static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); }
|
|
static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); }
|
|
|
|
static int manager_set_debug(link_call_t *call, void *u)
|
|
{
|
|
(void)u;
|
|
log_debug();
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int signal_one(svc_t *svc, void *udata)
|
|
{
|
|
int signo = *(int *)udata;
|
|
|
|
/* Signalling a stopped service is an error, like the legacy API */
|
|
if (!svc_is_running(svc))
|
|
return 1;
|
|
return !!kill(svc->pid, signo);
|
|
}
|
|
|
|
static int manager_signal(link_call_t *call, void *u)
|
|
{
|
|
const char *ident;
|
|
uint32_t signo;
|
|
int sig, rc;
|
|
|
|
(void)u;
|
|
if (link_call_read_string(call, &ident) < 0 ||
|
|
link_call_read_u32 (call, &signo) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (s, u)");
|
|
/* Match the upper bound `initctl signal` allows (1..31). RT
|
|
* signals are a future story; keep both sides in lockstep so
|
|
* users see the same range regardless of transport. */
|
|
if (signo == 0 || signo > 31)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"signal out of range (1..31)");
|
|
|
|
sig = (int)signo;
|
|
rc = dispatch_action(ident, signal_one, &sig);
|
|
if (rc < 0)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.NoSuchService", ident);
|
|
if (rc)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed",
|
|
"failed signalling matched service(s)");
|
|
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int manager_suspend(link_call_t *call, void *u)
|
|
{
|
|
(void)u;
|
|
if (IS_RESERVED_RUNLEVEL(runlevel))
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.WrongRunlevel",
|
|
"Unsupported command in runlevel S and 6/0");
|
|
sync();
|
|
if (suspend() < 0) {
|
|
const char *msg = (errno == EINVAL)
|
|
? "Kernel does not support suspend to RAM"
|
|
: strerror(errno);
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed", msg);
|
|
}
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- Manager1 properties ----------
|
|
*
|
|
* Read-only string properties exposed via the standard
|
|
* org.freedesktop.DBus.Properties interface. Getters write a
|
|
* variant containing a single string. */
|
|
|
|
/*
|
|
* Two distinct getters because the property table is static const --
|
|
* we can't bind &runlevel/&prevlevel through userdata. The values
|
|
* use the same encoding as the runlevel(8) command and `initctl
|
|
* runlevel`: "S" for single-user, "N" for no previous runlevel --
|
|
* the internal digit is not a wire format.
|
|
*/
|
|
static const char *runlevel_encode(int level, char *buf, size_t len)
|
|
{
|
|
if (level == INIT_LEVEL)
|
|
strlcpy(buf, "S", len);
|
|
else if (level >= 0 && level <= 9)
|
|
snprintf(buf, len, "%d", level);
|
|
else
|
|
strlcpy(buf, "N", len);
|
|
|
|
return buf;
|
|
}
|
|
|
|
static int prop_runlevel(link_writer_t *w, void *u)
|
|
{
|
|
char buf[8];
|
|
|
|
(void)u;
|
|
link_w_string(w, runlevel_encode(runlevel, buf, sizeof(buf)));
|
|
return 0;
|
|
}
|
|
|
|
static int prop_prevrunlevel(link_writer_t *w, void *u)
|
|
{
|
|
char buf[8];
|
|
|
|
(void)u;
|
|
if (prevlevel <= 0 || prevlevel > 9)
|
|
strlcpy(buf, "N", sizeof(buf));
|
|
else
|
|
snprintf(buf, sizeof(buf), "%d", prevlevel);
|
|
link_w_string(w, buf);
|
|
return 0;
|
|
}
|
|
|
|
static int prop_version(link_writer_t *w, void *u)
|
|
{
|
|
(void)u;
|
|
link_w_string(w, PACKAGE_VERSION);
|
|
return 0;
|
|
}
|
|
|
|
static const link_property_t manager_properties[] = {
|
|
{ .name = "Runlevel", .sig = "s", .getter = prop_runlevel },
|
|
{ .name = "PrevRunlevel", .sig = "s", .getter = prop_prevrunlevel },
|
|
{ .name = "Version", .sig = "s", .getter = prop_version },
|
|
{ NULL, NULL, NULL }
|
|
};
|
|
|
|
static const link_method_t manager_methods[] = {
|
|
{ .name = "ListServices", .in_sig = "", .out_sig = "as",
|
|
.handler = manager_list_services },
|
|
{ .name = "GetService", .in_sig = "s", .out_sig = "o",
|
|
.handler = manager_get_service },
|
|
{ .name = "Start", .in_sig = "s", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_start },
|
|
{ .name = "Stop", .in_sig = "s", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_stop },
|
|
{ .name = "Restart", .in_sig = "s", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_restart },
|
|
{ .name = "Reload", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload },
|
|
{ .name = "SetRunlevel", .in_sig = "u", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel },
|
|
{ .name = "Reboot", .in_sig = "u", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot },
|
|
{ .name = "Poweroff", .in_sig = "u", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff },
|
|
{ .name = "Halt", .in_sig = "u", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt },
|
|
{ .name = "Suspend", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_suspend },
|
|
{ .name = "SetDebug", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_debug },
|
|
{ .name = "Signal", .in_sig = "su", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_signal },
|
|
{ NULL, NULL, NULL, 0, NULL }
|
|
};
|
|
|
|
static const link_signal_t manager_signals[] = {
|
|
{ .name = "ServiceStateChanged", .sig = "sss" },
|
|
{ .name = "RunlevelChanged", .sig = "ss" },
|
|
{ .name = "ConfigReloaded", .sig = "" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const link_vtable_t manager_vtable = {
|
|
.interface = "org.finit.Manager1",
|
|
.methods = manager_methods,
|
|
.properties = manager_properties,
|
|
.signals = manager_signals,
|
|
};
|
|
|
|
/* ---------- org.finit.Service1 (one object per service) ----------
|
|
*
|
|
* Per-service object at /org/finit/service/<encoded-identity>.
|
|
* The vtable's `userdata` is the svc_t * for the specific service.
|
|
* Registration is driven dynamically from svc_new()/svc_del() via
|
|
* dbus_register_service() / dbus_unregister_service() below. */
|
|
|
|
/* SERVICE_PATH_PREFIX / SERVICE_PATH_PREFIX_LEN / FINIT_SVC_PATH_MAX
|
|
* defined near the top of the file so Manager1.GetService can refer
|
|
* to them. */
|
|
|
|
static int service_action_method(link_call_t *call, void *userdata,
|
|
int (*action)(svc_t *, void *))
|
|
{
|
|
svc_t *svc = userdata;
|
|
|
|
if (!svc)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.NoSuchService",
|
|
"Service object no longer valid");
|
|
|
|
if (action(svc, NULL))
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed",
|
|
"failed on service");
|
|
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int service1_start (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); }
|
|
static int service1_stop (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); }
|
|
static int service1_restart(link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); }
|
|
|
|
static int dbus_apply_reload(svc_t *svc, void *user_data)
|
|
{
|
|
(void)user_data;
|
|
return service_reload(svc);
|
|
}
|
|
|
|
static int service1_reload(link_call_t *c, void *u)
|
|
{
|
|
return service_action_method(c, u, dbus_apply_reload);
|
|
}
|
|
|
|
static const link_method_t service_methods[] = {
|
|
{ .name = "Start", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_start },
|
|
{ .name = "Stop", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_stop },
|
|
{ .name = "Restart", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_restart },
|
|
{ .name = "Reload", .in_sig = "", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_reload },
|
|
{ NULL, NULL, NULL, 0, NULL }
|
|
};
|
|
|
|
/*
|
|
* Getters write the bare value; the framework emits the variant
|
|
* signature from the table below. `State` deliberately uses the
|
|
* initctl status vocabulary from svc_status(), not the coarser
|
|
* ServiceStateChanged strings -- a client that only tracks edges
|
|
* has the signal, a client that asks gets the full story.
|
|
*/
|
|
#define SVC_PROP_STR(fn, field) \
|
|
static int fn(link_writer_t *w, void *arg) \
|
|
{ \
|
|
link_w_string(w, ((svc_t *)arg)->field); \
|
|
return 0; \
|
|
}
|
|
|
|
#define SVC_PROP_U32(fn, field) \
|
|
static int fn(link_writer_t *w, void *arg) \
|
|
{ \
|
|
link_w_u32(w, (uint32_t)((svc_t *)arg)->field); \
|
|
return 0; \
|
|
}
|
|
|
|
#define SVC_PROP_BOOL(fn, field) \
|
|
static int fn(link_writer_t *w, void *arg) \
|
|
{ \
|
|
link_w_bool(w, ((svc_t *)arg)->field); \
|
|
return 0; \
|
|
}
|
|
|
|
static int svc_prop_identity(link_writer_t *w, void *arg)
|
|
{
|
|
link_w_string(w, svc_ident(arg, NULL, 0));
|
|
return 0;
|
|
}
|
|
|
|
static int svc_prop_state(link_writer_t *w, void *arg)
|
|
{
|
|
link_w_string(w, svc_status(arg));
|
|
return 0;
|
|
}
|
|
|
|
static int svc_prop_type(link_writer_t *w, void *arg)
|
|
{
|
|
link_w_string(w, svc_typestr((svc_t *)arg));
|
|
return 0;
|
|
}
|
|
|
|
static int svc_prop_command(link_writer_t *w, void *arg)
|
|
{
|
|
svc_t *svc = arg;
|
|
char buf[512];
|
|
|
|
compose_cmdline(svc, buf, sizeof(buf));
|
|
if (svc_is_sysv(svc)) {
|
|
strlcat(buf, " ", sizeof(buf));
|
|
strlcat(buf, svc->state == SVC_HALTED_STATE
|
|
? "stop" : "start", sizeof(buf));
|
|
}
|
|
|
|
link_w_string(w, buf);
|
|
return 0;
|
|
}
|
|
|
|
static int svc_prop_pid(link_writer_t *w, void *arg)
|
|
{
|
|
svc_t *svc = arg;
|
|
|
|
link_w_u32(w, svc->pid > 0 ? (uint32_t)svc->pid : 0);
|
|
return 0;
|
|
}
|
|
|
|
static int svc_prop_restarts(link_writer_t *w, void *arg)
|
|
{
|
|
svc_t *svc = arg;
|
|
|
|
link_w_u32(w, svc->restart_cnt > 0 ? (uint32_t)svc->restart_cnt : 0);
|
|
return 0;
|
|
}
|
|
|
|
static int svc_prop_uptime(link_writer_t *w, void *arg)
|
|
{
|
|
svc_t *svc = arg;
|
|
long up = 0;
|
|
|
|
if (svc->pid > 0) {
|
|
up = jiffies() - svc->start_time;
|
|
if (up < 0)
|
|
up = 0;
|
|
}
|
|
link_w_u32(w, (uint32_t)up);
|
|
return 0;
|
|
}
|
|
|
|
SVC_PROP_STR (svc_prop_name, name)
|
|
SVC_PROP_STR (svc_prop_desc, desc)
|
|
SVC_PROP_STR (svc_prop_conditions, cond)
|
|
SVC_PROP_STR (svc_prop_origin, file)
|
|
SVC_PROP_STR (svc_prop_environ, env)
|
|
SVC_PROP_STR (svc_prop_pidfile, pidfile)
|
|
SVC_PROP_STR (svc_prop_user, username)
|
|
SVC_PROP_STR (svc_prop_group, group)
|
|
SVC_PROP_U32 (svc_prop_runlevels, runlevels)
|
|
SVC_PROP_U32 (svc_prop_exitstatus, status)
|
|
SVC_PROP_U32 (svc_prop_restarts_tot, restart_tot)
|
|
SVC_PROP_U32 (svc_prop_restart_max, restart_max)
|
|
SVC_PROP_U32 (svc_prop_starts, once)
|
|
SVC_PROP_BOOL(svc_prop_manual, manual)
|
|
SVC_PROP_BOOL(svc_prop_forking, forking)
|
|
SVC_PROP_BOOL(svc_prop_started, started)
|
|
|
|
static const link_property_t service_properties[] = {
|
|
{ .name = "Identity", .sig = "s", .getter = svc_prop_identity },
|
|
{ .name = "Name", .sig = "s", .getter = svc_prop_name },
|
|
{ .name = "State", .sig = "s", .getter = svc_prop_state },
|
|
{ .name = "Pid", .sig = "u", .getter = svc_prop_pid },
|
|
{ .name = "RestartCount", .sig = "u", .getter = svc_prop_restarts },
|
|
{ .name = "Runlevels", .sig = "u", .getter = svc_prop_runlevels },
|
|
{ .name = "Description", .sig = "s", .getter = svc_prop_desc },
|
|
{ .name = "Command", .sig = "s", .getter = svc_prop_command },
|
|
{ .name = "Conditions", .sig = "s", .getter = svc_prop_conditions },
|
|
{ .name = "Type", .sig = "s", .getter = svc_prop_type },
|
|
{ .name = "Origin", .sig = "s", .getter = svc_prop_origin },
|
|
{ .name = "Environment", .sig = "s", .getter = svc_prop_environ },
|
|
{ .name = "PidFile", .sig = "s", .getter = svc_prop_pidfile },
|
|
{ .name = "User", .sig = "s", .getter = svc_prop_user },
|
|
{ .name = "Group", .sig = "s", .getter = svc_prop_group },
|
|
{ .name = "Uptime", .sig = "u", .getter = svc_prop_uptime },
|
|
{ .name = "ExitStatus", .sig = "u", .getter = svc_prop_exitstatus },
|
|
{ .name = "RestartsTotal",.sig = "u", .getter = svc_prop_restarts_tot },
|
|
{ .name = "RestartMax", .sig = "u", .getter = svc_prop_restart_max },
|
|
{ .name = "Starts", .sig = "u", .getter = svc_prop_starts },
|
|
{ .name = "ManualStart", .sig = "b", .getter = svc_prop_manual },
|
|
{ .name = "Forking", .sig = "b", .getter = svc_prop_forking },
|
|
{ .name = "Started", .sig = "b", .getter = svc_prop_started },
|
|
{ NULL, NULL, NULL }
|
|
};
|
|
|
|
static const link_vtable_t service_vtable = {
|
|
.interface = "org.finit.Service1",
|
|
.methods = service_methods,
|
|
.properties = service_properties,
|
|
};
|
|
|
|
/* Build the canonical object path for a service. Identity is
|
|
* "name" for single-instance services, "name:id" otherwise. */
|
|
static int service_path_for(svc_t *svc, char *buf, size_t bufsz)
|
|
{
|
|
char ident[MAX_IDENT_LEN];
|
|
size_t plen = SERVICE_PATH_PREFIX_LEN;
|
|
int enc;
|
|
|
|
if (bufsz <= plen)
|
|
return -1;
|
|
memcpy(buf, SERVICE_PATH_PREFIX, plen);
|
|
|
|
svc_ident(svc, ident, sizeof(ident));
|
|
enc = link_path_encode(ident, buf + plen, bufsz - plen);
|
|
if (enc < 0)
|
|
return -1;
|
|
return (int)plen + enc;
|
|
}
|
|
|
|
void dbus_register_service(svc_t *svc)
|
|
{
|
|
char path[FINIT_SVC_PATH_MAX];
|
|
|
|
if (!server || !svc)
|
|
return;
|
|
if (service_path_for(svc, path, sizeof(path)) < 0)
|
|
return;
|
|
|
|
if (link_server_add_object(server, path, &service_vtable, svc) < 0)
|
|
logit(LOG_WARNING, "dbus: failed registering %s", path);
|
|
}
|
|
|
|
void dbus_unregister_service(svc_t *svc)
|
|
{
|
|
char path[FINIT_SVC_PATH_MAX];
|
|
|
|
if (!server || !svc)
|
|
return;
|
|
if (service_path_for(svc, path, sizeof(path)) < 0)
|
|
return;
|
|
|
|
(void)link_server_remove_object(server, path);
|
|
}
|
|
|
|
/* ---------- signal fan-out helper ----------
|
|
*
|
|
* Fan out a pre-marshalled signal body to every connected peer,
|
|
* letting each connection apply its AddMatch filter. Short-circuits
|
|
* when no peers are connected so dbus_notify_* callers don't have
|
|
* to inspect that state themselves. */
|
|
/* A peer that has gone away is routine rather than a fault: initctl
|
|
* calls and exits, and on the way down every peer goes at once, which
|
|
* is not something an operator watching the shutdown needs to read
|
|
* about. Keep the warning for a write that failed for some other
|
|
* reason, where something really is wrong. */
|
|
static int quiet_drop(int err)
|
|
{
|
|
if (err == EPIPE || err == ECONNRESET || err == ENOTCONN)
|
|
return 1;
|
|
|
|
return runlevel == 0 || runlevel == 6;
|
|
}
|
|
|
|
static void dbus_emit_signal(const char *path,
|
|
const char *interface,
|
|
const char *member,
|
|
const char *signature,
|
|
const uint8_t *body, size_t body_len)
|
|
{
|
|
struct peer *p, *tmp;
|
|
|
|
if (!server || TAILQ_EMPTY(&peers))
|
|
return;
|
|
TAILQ_FOREACH_SAFE(p, &peers, link, tmp) {
|
|
int err;
|
|
|
|
if (link_connection_emit_signal(p->conn,
|
|
path, interface, member,
|
|
signature, body, body_len) >= 0)
|
|
continue;
|
|
|
|
err = errno;
|
|
|
|
/* nothing hit the wire, and same for every peer */
|
|
if (err == EMSGSIZE || err == EINVAL)
|
|
break;
|
|
|
|
logit(quiet_drop(err) ? LOG_DEBUG : LOG_WARNING,
|
|
"D-Bus peer fd %d write failed: %s, dropping",
|
|
link_connection_get_fd(p->conn), strerror(err));
|
|
peer_drop(p);
|
|
}
|
|
}
|
|
|
|
/* ---------- signal emission: ServiceStateChanged ---------- */
|
|
|
|
/*
|
|
* Coarse svc_state_t -> string. svc_status() in svc.h returns a
|
|
* richer string that also considers svc->block, but emitting just
|
|
* the state-machine state is enough for clients to track lifecycle
|
|
* transitions. Keep the strings stable -- they're a wire-API
|
|
* commitment once shipped.
|
|
*
|
|
* No `default:` on purpose: a new SVC_*_STATE added to svc.h must
|
|
* also pick a wire name here, and -Wall (-Wswitch) flags the
|
|
* missing case.
|
|
*/
|
|
static const char *state_name(svc_state_t s)
|
|
{
|
|
switch (s) {
|
|
case SVC_HALTED_STATE: return "halted";
|
|
case SVC_DONE_STATE: return "done";
|
|
case SVC_DEAD_STATE: return "dead";
|
|
case SVC_CLEANUP_STATE: return "cleanup";
|
|
case SVC_TEARDOWN_STATE: return "teardown";
|
|
case SVC_STOPPING_STATE: return "stopping";
|
|
case SVC_SETUP_STATE: return "setup";
|
|
case SVC_PAUSED_STATE: return "paused";
|
|
case SVC_WAITING_STATE: return "waiting";
|
|
case SVC_STARTING_STATE: return "starting";
|
|
case SVC_RUNNING_STATE: return "running";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
void dbus_notify_service_state(svc_t *svc, int old_state, int new_state)
|
|
{
|
|
uint8_t body[256];
|
|
link_writer_t w;
|
|
char ident[MAX_IDENT_LEN];
|
|
char path[FINIT_SVC_PATH_MAX];
|
|
ssize_t blen;
|
|
|
|
if (!svc)
|
|
return;
|
|
|
|
sysbus_probe();
|
|
|
|
svc_ident(svc, ident, sizeof(ident));
|
|
|
|
link_writer_init(&w, body, sizeof(body));
|
|
link_w_string(&w, ident);
|
|
link_w_string(&w, state_name((svc_state_t)old_state));
|
|
link_w_string(&w, state_name((svc_state_t)new_state));
|
|
blen = link_writer_finish(&w);
|
|
if (blen < 0)
|
|
return;
|
|
|
|
dbus_emit_signal("/org/finit/manager", "org.finit.Manager1",
|
|
"ServiceStateChanged", "sss", body, (size_t)blen);
|
|
|
|
/*
|
|
* Dual emission: Properties-aware clients track one object via
|
|
* the standard PropertiesChanged instead of filtering the
|
|
* manager-wide signal. Volatile numerics are invalidated, not
|
|
* marshalled -- interested clients re-Get.
|
|
*/
|
|
if (service_path_for(svc, path, sizeof(path)) < 0)
|
|
return;
|
|
|
|
link_writer_init(&w, body, sizeof(body));
|
|
link_w_string(&w, "org.finit.Service1");
|
|
link_w_array_begin(&w, '{');
|
|
link_w_struct_begin(&w);
|
|
link_w_string(&w, "State");
|
|
link_w_variant_string(&w, svc_status(svc));
|
|
link_w_struct_end(&w);
|
|
link_w_array_end(&w);
|
|
link_w_array_begin(&w, 's');
|
|
link_w_string(&w, "Pid");
|
|
link_w_string(&w, "RestartCount");
|
|
link_w_array_end(&w);
|
|
blen = link_writer_finish(&w);
|
|
if (blen < 0)
|
|
return;
|
|
|
|
dbus_emit_signal(path, "org.freedesktop.DBus.Properties",
|
|
"PropertiesChanged", "sa{sv}as",
|
|
body, (size_t)blen);
|
|
}
|
|
|
|
/* ---------- signal emission: RunlevelChanged ----------
|
|
*
|
|
* Fired by sm.c right after the runlevel global flips. Body is
|
|
* (old, new) in the same runlevel(8) encoding as the Manager1
|
|
* Runlevel property: digits, "S", or "N". */
|
|
void dbus_notify_runlevel_change(int old_level, int new_level)
|
|
{
|
|
uint8_t body[64];
|
|
link_writer_t w;
|
|
char old_s[8], new_s[8];
|
|
ssize_t blen;
|
|
|
|
runlevel_encode(old_level, old_s, sizeof(old_s));
|
|
runlevel_encode(new_level, new_s, sizeof(new_s));
|
|
|
|
link_writer_init(&w, body, sizeof(body));
|
|
link_w_string(&w, old_s);
|
|
link_w_string(&w, new_s);
|
|
blen = link_writer_finish(&w);
|
|
if (blen < 0)
|
|
return;
|
|
|
|
dbus_emit_signal("/org/finit/manager", "org.finit.Manager1",
|
|
"RunlevelChanged", "ss", body, (size_t)blen);
|
|
}
|
|
|
|
/*
|
|
* Reconfiguration complete: all conditions have been re-asserted by
|
|
* their in-Finit owners. External providers whose conditions are
|
|
* generation files, rather than the oneshot symlinks keventd uses,
|
|
* subscribe to this to re-assert theirs.
|
|
*/
|
|
void dbus_notify_reload(void)
|
|
{
|
|
dbus_emit_signal("/org/finit/manager", "org.finit.Manager1",
|
|
"ConfigReloaded", "", NULL, 0);
|
|
}
|
|
|
|
/* ---------- org.finit.Cond1 ---------- */
|
|
|
|
#define COND_PATH_OBJECT "/org/finit/cond"
|
|
#define COND_INTERFACE "org.finit.Cond1"
|
|
|
|
/* Cond1.Set/Clear refuse anything that isn't a usr/ condition --
|
|
* pid/, sys/, hook/ are owned by Finit's state machine and giving
|
|
* clients write access there would let them corrupt service state.
|
|
* Bare names ("foo") are normalised to "usr/foo" the same way
|
|
* initctl does. The returned pointer is valid for the duration
|
|
* of the caller's stack frame (`buf` must be at least 128 bytes). */
|
|
static const char *normalise_usr_cond(const char *name, char *buf, size_t bufsz)
|
|
{
|
|
const char *tail;
|
|
|
|
if (!name || !*name)
|
|
return NULL;
|
|
if (strchr(name, '.'))
|
|
return NULL;
|
|
|
|
if (strchr(name, '/')) {
|
|
if (strncmp(name, "usr/", 4) != 0)
|
|
return NULL;
|
|
tail = name + 4;
|
|
/* Match initctl's policy: no further slashes in the tail,
|
|
* and no empty tail ("usr/" alone). */
|
|
if (!*tail || strchr(tail, '/'))
|
|
return NULL;
|
|
if (strlen(name) >= bufsz)
|
|
return NULL;
|
|
memcpy(buf, name, strlen(name) + 1);
|
|
return buf;
|
|
}
|
|
|
|
if ((size_t)snprintf(buf, bufsz, "usr/%s", name) >= bufsz)
|
|
return NULL;
|
|
return buf;
|
|
}
|
|
|
|
/* Reject names that would escape /run/finit/cond/. cond_get(name)
|
|
* boils down to fopen(_PATH_COND + name), so without this check any
|
|
* caller can make PID 1 open arbitrary files -- a path traversal
|
|
* primitive that also stalls PID 1 if pointed at a FIFO or a slow
|
|
* device. Legal cond names look like "usr/foo", "pid/sshd",
|
|
* "service/keventd/ready"; no leading slash, no ".." segment. */
|
|
static int cond_name_valid(const char *name)
|
|
{
|
|
const char *p;
|
|
|
|
if (!name || !*name || *name == '/')
|
|
return 0;
|
|
for (p = name; *p; p++) {
|
|
if (*p == '.' && p[1] == '.' &&
|
|
(p[2] == '\0' || p[2] == '/'))
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int cond1_get(link_call_t *call, void *userdata)
|
|
{
|
|
const char *name;
|
|
link_writer_t *w;
|
|
|
|
(void)userdata;
|
|
|
|
if (link_call_read_string(call, &name) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (s)");
|
|
if (!cond_name_valid(name))
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"invalid condition name");
|
|
|
|
w = link_call_reply(call);
|
|
if (!w)
|
|
return -1;
|
|
link_w_string(w, condstr(cond_get(name)));
|
|
return 0;
|
|
}
|
|
|
|
static int cond1_set_or_clear(link_call_t *call, int do_set)
|
|
{
|
|
const char *name;
|
|
char buf[128];
|
|
const char *full;
|
|
|
|
if (link_call_read_string(call, &name) < 0)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"expected (s)");
|
|
|
|
full = normalise_usr_cond(name, buf, sizeof(buf));
|
|
if (!full)
|
|
return link_call_reply_error(call,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Set/Clear is restricted to usr/* conditions");
|
|
|
|
if (do_set) {
|
|
/* cond_set_oneshot, not cond_set: a user-asserted condition
|
|
* is a symlink to _PATH_RECONF, so it tracks the reconf
|
|
* generation automatically and stays "on" across reloads
|
|
* and runlevel switches. cond_set() writes a fixed
|
|
* generation that goes "flux" on the next reload -- wrong
|
|
* semantics for user conditions, and what initctl cond set
|
|
* has done forever via the filesystem path. */
|
|
cond_set_oneshot(full);
|
|
if (cond_get(full) != COND_ON)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed",
|
|
"failed asserting condition");
|
|
} else {
|
|
cond_clear(full);
|
|
if (cond_get(full) != COND_OFF)
|
|
return link_call_reply_error(call,
|
|
"org.finit.Error.Failed",
|
|
"failed clearing condition");
|
|
}
|
|
|
|
(void)link_call_reply(call);
|
|
return 0;
|
|
}
|
|
|
|
static int cond1_set (link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); }
|
|
static int cond1_clear(link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 0); }
|
|
|
|
/* nftw() can't pass user data so a single static handle ferries the
|
|
* writer into the callback. Safe because dispatch is single-threaded. */
|
|
static link_writer_t *cond_walk_writer;
|
|
static int cond_walk_dump;
|
|
|
|
static int cond_walk_cb(const char *fpath, const struct stat *sb,
|
|
int tflag, struct FTW *ftwbuf)
|
|
{
|
|
const char *name;
|
|
const char *state;
|
|
size_t prefix_len;
|
|
|
|
(void)sb;
|
|
(void)ftwbuf;
|
|
|
|
if (tflag != FTW_F)
|
|
return 0;
|
|
if (!strcmp(fpath, _PATH_RECONF))
|
|
return 0;
|
|
|
|
prefix_len = strlen(_PATH_COND);
|
|
if (strlen(fpath) <= prefix_len)
|
|
return 0;
|
|
name = fpath + prefix_len;
|
|
|
|
if (cond_walk_dump) {
|
|
state = condstr(cond_get_path(fpath));
|
|
link_w_struct_begin(cond_walk_writer);
|
|
link_w_string(cond_walk_writer, name);
|
|
link_w_string(cond_walk_writer, state);
|
|
link_w_struct_end(cond_walk_writer);
|
|
} else {
|
|
link_w_string(cond_walk_writer, name);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int cond1_list(link_call_t *call, void *userdata)
|
|
{
|
|
link_writer_t *w;
|
|
|
|
(void)userdata;
|
|
|
|
w = link_call_reply(call);
|
|
if (!w)
|
|
return -1;
|
|
|
|
link_w_array_begin(w, 's');
|
|
cond_walk_writer = w;
|
|
cond_walk_dump = 0;
|
|
(void)nftw(_PATH_COND, cond_walk_cb, 20, 0);
|
|
cond_walk_writer = NULL;
|
|
link_w_array_end(w);
|
|
return 0;
|
|
}
|
|
|
|
static int cond1_dump(link_call_t *call, void *userdata)
|
|
{
|
|
link_writer_t *w;
|
|
|
|
(void)userdata;
|
|
|
|
w = link_call_reply(call);
|
|
if (!w)
|
|
return -1;
|
|
|
|
link_w_array_begin(w, '(');
|
|
cond_walk_writer = w;
|
|
cond_walk_dump = 1;
|
|
(void)nftw(_PATH_COND, cond_walk_cb, 20, 0);
|
|
cond_walk_writer = NULL;
|
|
link_w_array_end(w);
|
|
return 0;
|
|
}
|
|
|
|
static const link_method_t cond_methods[] = {
|
|
{ .name = "Get", .in_sig = "s", .out_sig = "s",
|
|
.handler = cond1_get },
|
|
{ .name = "Set", .in_sig = "s", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = cond1_set },
|
|
{ .name = "Clear", .in_sig = "s", .out_sig = "",
|
|
.flags = LINK_METHOD_PRIVILEGED, .handler = cond1_clear },
|
|
{ .name = "List", .in_sig = "", .out_sig = "as",
|
|
.handler = cond1_list },
|
|
{ .name = "Dump", .in_sig = "", .out_sig = "a(ss)",
|
|
.handler = cond1_dump },
|
|
{ NULL, NULL, NULL, 0, NULL }
|
|
};
|
|
|
|
static const link_signal_t cond_signals[] = {
|
|
{ .name = "ConditionChanged", .sig = "ss" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const link_vtable_t cond_vtable = {
|
|
.interface = COND_INTERFACE,
|
|
.methods = cond_methods,
|
|
.signals = cond_signals,
|
|
};
|
|
|
|
/* ---------- signal emission: ConditionChanged ---------- */
|
|
|
|
void dbus_notify_condition_change(const char *name, const char *state)
|
|
{
|
|
uint8_t body[256];
|
|
link_writer_t w;
|
|
ssize_t blen;
|
|
|
|
if (!name || !state)
|
|
return;
|
|
|
|
sysbus_probe();
|
|
|
|
link_writer_init(&w, body, sizeof(body));
|
|
link_w_string(&w, name);
|
|
link_w_string(&w, state);
|
|
blen = link_writer_finish(&w);
|
|
if (blen < 0)
|
|
return;
|
|
|
|
dbus_emit_signal(COND_PATH_OBJECT, COND_INTERFACE,
|
|
"ConditionChanged", "ss", body, (size_t)blen);
|
|
}
|
|
|
|
/* ---------- who may change things ----------
|
|
*
|
|
* Root, or a member of the group the bus socket is owned by, which is
|
|
* the same set --with-group already lets connect. Both gates then say
|
|
* the same thing, rather than the socket admitting the wheel group and
|
|
* every method turning it away.
|
|
*
|
|
* The group set is the caller's own, captured by libink from the kernel
|
|
* (SO_PEERCRED + SO_PEERGROUPS) at connect, so this never asks NSS --
|
|
* getpwuid/getgrouplist can block on a slow LDAP/SSSD backend, and PID 1
|
|
* must never block. The owning group's gid is resolved once, at init.
|
|
*
|
|
* A caller reaching us through a broker carries no group set (libink
|
|
* passes ngroups 0), so system-bus privileged methods are root-only.
|
|
* The wheel group acts over the local bus, which is where initctl and
|
|
* operators connect. See libink/README.md for lifting that limit.
|
|
*/
|
|
static gid_t privileged_gid = (gid_t)-1; /* DEFGROUP, resolved at init */
|
|
|
|
static int caller_is_privileged(uid_t uid, const gid_t *groups, int ngroups,
|
|
void *userdata)
|
|
{
|
|
(void)userdata;
|
|
|
|
if (uid == 0)
|
|
return 1;
|
|
|
|
if (privileged_gid == (gid_t)-1)
|
|
return 0; /* no owning group to match against */
|
|
|
|
for (int i = 0; i < ngroups; i++) {
|
|
if (groups[i] == privileged_gid)
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- caller identity on the system bus ----------
|
|
*
|
|
* libink parks a privileged call and asks us who sent it; we ask the
|
|
* bus driver with GetConnectionUnixUser and answer when the reply
|
|
* lands, through the same event loop as everything else.
|
|
*
|
|
* A bus never reuses a unique name, so an answer holds for as long as
|
|
* that bus runs. It does not survive the bus restarting, though:
|
|
* a new dbus-daemon numbers from scratch and :1.7 becomes somebody
|
|
* else, so peer_drop() empties the cache when the broker goes.
|
|
*
|
|
* It is a ring: the oldest entry loses on overflow, and losing one
|
|
* only costs another round trip.
|
|
*/
|
|
#define SENDER_CACHE_LEN 16
|
|
|
|
struct sender_uid {
|
|
char name[LINK_SENDER_MAX];
|
|
uid_t uid;
|
|
};
|
|
|
|
static struct sender_uid sender_cache[SENDER_CACHE_LEN];
|
|
static unsigned sender_next;
|
|
|
|
static void sender_cache_flush(void)
|
|
{
|
|
memset(sender_cache, 0, sizeof(sender_cache));
|
|
sender_next = 0;
|
|
}
|
|
|
|
static int sender_cached(const char *sender, uid_t *uid)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < SENDER_CACHE_LEN; i++) {
|
|
if (sender_cache[i].name[0] && !strcmp(sender_cache[i].name, sender)) {
|
|
*uid = sender_cache[i].uid;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void sender_remember(const char *sender, uid_t uid)
|
|
{
|
|
unsigned i = sender_next++ % SENDER_CACHE_LEN;
|
|
|
|
strlcpy(sender_cache[i].name, sender, sizeof(sender_cache[i].name));
|
|
sender_cache[i].uid = uid;
|
|
}
|
|
|
|
/* One outstanding GetConnectionUnixUser. Freed by the reply callback,
|
|
* which libink guarantees to run exactly once, with a NULL reply if
|
|
* the connection drops first. */
|
|
struct uid_query {
|
|
link_authz_t tok;
|
|
char sender[LINK_SENDER_MAX];
|
|
};
|
|
|
|
static void uid_reply_cb(link_connection_t *conn, const link_reply_t *reply, void *userdata)
|
|
{
|
|
struct uid_query *q = userdata;
|
|
uid_t uid = (uid_t)-1;
|
|
uint32_t val;
|
|
|
|
if (!reply) {
|
|
dbg("connection dropped before %s was identified", q->sender);
|
|
} else if (reply->type == LINK_MSG_METHOD_RETURN &&
|
|
link_reply_get_u32(reply, &val) == 0) {
|
|
uid = (uid_t)val;
|
|
sender_remember(q->sender, uid);
|
|
dbg("sender %s is uid %d", q->sender, (int)uid);
|
|
} else {
|
|
dbg("GetConnectionUnixUser(%s) failed: %s", q->sender,
|
|
reply->error_name ? reply->error_name : "unexpected reply");
|
|
}
|
|
|
|
link_uid_resolved(conn, q->tok, uid);
|
|
free(q);
|
|
}
|
|
|
|
static int sysbus_uid_resolver(link_connection_t *conn, const char *sender,
|
|
link_authz_t tok, uid_t *uid, void *userdata)
|
|
{
|
|
struct uid_query *q;
|
|
|
|
(void)userdata;
|
|
|
|
/* Never truncate: a shortened key could match a different
|
|
* sender and hand it someone else's privileges. */
|
|
if (strlen(sender) >= LINK_SENDER_MAX)
|
|
return -1;
|
|
|
|
if (sender_cached(sender, uid)) {
|
|
dbg("sender %s is uid %d, from cache", sender, (int)*uid);
|
|
return 0;
|
|
}
|
|
|
|
dbg("asking the bus driver who %s is ...", sender);
|
|
|
|
q = calloc(1, sizeof(*q));
|
|
if (!q)
|
|
return -1;
|
|
q->tok = tok;
|
|
strlcpy(q->sender, sender, sizeof(q->sender));
|
|
|
|
if (link_connection_call(conn, "org.freedesktop.DBus", "/org/freedesktop/DBus",
|
|
"org.freedesktop.DBus", "GetConnectionUnixUser",
|
|
uid_reply_cb, q, "s", sender) < 0) {
|
|
dbg("Failed asking the bus driver about %s: %s", sender, strerror(errno));
|
|
free(q);
|
|
return -1;
|
|
}
|
|
|
|
/* Both the call and the park it belongs to now have a deadline
|
|
* to answer by, so make sure something is watching the clock. */
|
|
expire_arm();
|
|
|
|
return 1; /* parked; uid_reply_cb() answers */
|
|
}
|
|
|
|
/* ---------- system-bus attach (opportunistic) ----------
|
|
*
|
|
* If /var/run/dbus/system_bus_socket is reachable, libink connects to
|
|
* the system bus as a regular client, claims org.finit as a well-known
|
|
* name, then promotes the authenticated fd into a server-attached
|
|
* peer so the same vtables serve incoming method calls and outgoing
|
|
* signal fan-out reaches the system bus.
|
|
*
|
|
* peer_uid is (uid_t)-1 because the connection has no single owner;
|
|
* who is calling is established per message by sysbus_uid_resolver()
|
|
* below.
|
|
*
|
|
* A bounded SO_SNDTIMEO/SO_RCVTIMEO budget is applied via
|
|
* link_client_open_timeout so a hung dbus-daemon can't stall boot;
|
|
* once the connection is attached and flipped to non-blocking, those
|
|
* timeouts are silently inert. */
|
|
|
|
#define SYSTEM_BUS_PATH "/var/run/dbus/system_bus_socket"
|
|
#define FINIT_BUS_NAME "org.finit"
|
|
/* Budget for the synchronous AUTH + Hello + RequestName round-trips.
|
|
* If the system bus is alive but the daemon is wedged we'd rather
|
|
* give up after a couple of seconds than stall the rest of dbus_init
|
|
* (and through it, boot). */
|
|
#define SYSTEM_BUS_TIMEOUT_MS 2000
|
|
/* DBUS_NAME_FLAG_DO_NOT_QUEUE: fail fast if the name is taken
|
|
* (something else owns org.finit -- shouldn't happen and we'd
|
|
* rather log than silently sit in the queue). */
|
|
#define DBUS_NAME_FLAG_DO_NOT_QUEUE 0x04
|
|
|
|
/* sysbus_probe() re-runs on every service and condition change, so a
|
|
* broker that keeps refusing would repeat itself for every event, and
|
|
* before syslog is up each line is an open/write/close on /dev/kmsg.
|
|
* Say it once, then trace, until an attach succeeds. */
|
|
static int sysbus_warned;
|
|
|
|
#define sysbus_level() (sysbus_warned ? LOG_DEBUG : LOG_WARNING)
|
|
|
|
/* What the broker said, for the log. A refused call carries an error
|
|
* name; anything that failed below that has nothing to quote. */
|
|
static const char *sysbus_errstr(link_client_t *c)
|
|
{
|
|
const link_reply_t *r = link_client_reply(c);
|
|
|
|
return (r && r->error_name) ? r->error_name : "transport or parse failure";
|
|
}
|
|
|
|
static int sysbus_request_name(link_client_t *c)
|
|
{
|
|
const char *reason;
|
|
uint32_t result;
|
|
int rc;
|
|
|
|
rc = link_client_call_v(c, "/org/freedesktop/DBus",
|
|
"org.freedesktop.DBus", "RequestName",
|
|
"su", FINIT_BUS_NAME,
|
|
(uint32_t)DBUS_NAME_FLAG_DO_NOT_QUEUE);
|
|
if (rc != LINK_CALL_OK)
|
|
reason = sysbus_errstr(c);
|
|
else if (link_reply_get_u32(link_client_reply(c), &result) < 0)
|
|
reason = "malformed RequestName reply";
|
|
else if (result != 1) /* 2/3/4 mean we did not get the name */
|
|
reason = "name already owned";
|
|
else
|
|
return 0;
|
|
|
|
logit(sysbus_level(), "Failed to claim %s on system bus: %s",
|
|
FINIT_BUS_NAME, reason);
|
|
sysbus_warned = 1;
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int try_attach_system_bus(uev_ctx_t *ctx)
|
|
{
|
|
link_client_t *c;
|
|
link_connection_t *conn;
|
|
struct peer *p;
|
|
int rc;
|
|
|
|
c = link_client_open_timeout(SYSTEM_BUS_PATH, SYSTEM_BUS_TIMEOUT_MS);
|
|
if (!c) {
|
|
dbg("System bus unavailable at %s; skipping registration",
|
|
SYSTEM_BUS_PATH);
|
|
return -1;
|
|
}
|
|
|
|
/* Unlike the local bus, a broker routes by destination, and it
|
|
* drops anything not addressed to the driver before Hello. */
|
|
link_client_set_destination(c, "org.freedesktop.DBus");
|
|
|
|
rc = link_client_call_v(c, "/org/freedesktop/DBus",
|
|
"org.freedesktop.DBus", "Hello", NULL);
|
|
if (rc != LINK_CALL_OK) {
|
|
logit(sysbus_level(), "System-bus Hello failed: %s",
|
|
sysbus_errstr(c));
|
|
sysbus_warned = 1;
|
|
link_client_close(c);
|
|
return -1;
|
|
}
|
|
|
|
if (sysbus_request_name(c) < 0) {
|
|
link_client_close(c);
|
|
return -1;
|
|
}
|
|
|
|
/* link_server_attach owns the fd from this point on whether it
|
|
* succeeds or fails, so the steal-then-attach pair has no leak
|
|
* window. */
|
|
conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1,
|
|
LINK_ATTACH_BROKER);
|
|
if (!conn)
|
|
return -1;
|
|
|
|
p = peer_register(ctx, conn);
|
|
if (!p) {
|
|
logit(LOG_WARNING, "Failed registering system-bus peer");
|
|
return -1;
|
|
}
|
|
|
|
sysbus_peer = p;
|
|
link_server_set_uid_resolver(server, sysbus_uid_resolver, NULL);
|
|
sysbus_warned = 0; /* arm the warning for a later broker restart */
|
|
logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The broker is usually not up yet when dbus_init() runs -- it is
|
|
* typically a finit service itself -- and it may restart at any
|
|
* time. The service and condition notify paths call sysbus_probe()
|
|
* on every event: when org.finit is unclaimed and the broker's
|
|
* socket exists, one coalesced attach attempt is scheduled. This
|
|
* stays daemon-agnostic -- only the socket is probed, never a
|
|
* service name -- and the broker's own service transitions are what
|
|
* trigger it.
|
|
*/
|
|
static uev_t sysbus_tmr;
|
|
static int sysbus_tmr_up;
|
|
|
|
static void sysbus_probe_cb(uev_t *w, void *arg, int events)
|
|
{
|
|
(void)arg;
|
|
(void)events;
|
|
|
|
if (!sysbus_peer)
|
|
(void)try_attach_system_bus(w->ctx);
|
|
}
|
|
|
|
static void sysbus_probe(void)
|
|
{
|
|
if (sysbus_peer || !server)
|
|
return;
|
|
if (access(SYSTEM_BUS_PATH, F_OK))
|
|
return;
|
|
|
|
/* coalesce bursts to a single probe */
|
|
if (!sysbus_tmr_up)
|
|
sysbus_tmr_up = !uev_timer_init(ctx, &sysbus_tmr,
|
|
sysbus_probe_cb, NULL,
|
|
200, 0);
|
|
else
|
|
uev_timer_set(&sysbus_tmr, 200, 0);
|
|
}
|
|
|
|
/* ---------- init / exit ---------- */
|
|
|
|
int dbus_init(uev_ctx_t *ctx)
|
|
{
|
|
dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET);
|
|
|
|
/* Same access policy as INIT_SOCKET: the bus reaches every
|
|
* service operation initctl does, so --with-group has to gate
|
|
* both or it gates neither. */
|
|
link_set_logger(link_log_cb, NULL);
|
|
|
|
if (link_server_new(&server, FINIT_BUS_SOCKET, 0660) < 0) {
|
|
err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET);
|
|
return 1;
|
|
}
|
|
|
|
/* Resolve the owning group once: the authorizer matches callers
|
|
* against it on every privileged call and must not hit NSS then. */
|
|
privileged_gid = getgroup(DEFGROUP);
|
|
if (chown(FINIT_BUS_SOCKET, geteuid(), privileged_gid))
|
|
err(1, "Failed setting group %s on %s", DEFGROUP, FINIT_BUS_SOCKET);
|
|
|
|
link_server_set_authorizer(server, caller_is_privileged, NULL);
|
|
|
|
if (link_server_add_object(server, "/org/finit/manager",
|
|
&manager_vtable, NULL) < 0) {
|
|
err(1, "Failed registering Manager1 object");
|
|
link_server_free(server);
|
|
server = NULL;
|
|
return 1;
|
|
}
|
|
|
|
if (link_server_add_object(server, COND_PATH_OBJECT,
|
|
&cond_vtable, NULL) < 0) {
|
|
err(1, "Failed registering Cond1 object");
|
|
link_server_free(server);
|
|
server = NULL;
|
|
return 1;
|
|
}
|
|
|
|
if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL,
|
|
link_server_get_fd(server), UEV_READ)) {
|
|
err(1, "Failed registering D-Bus accept watcher");
|
|
link_server_free(server);
|
|
server = NULL;
|
|
return 1;
|
|
}
|
|
|
|
/* Register Service1 objects for every service already loaded.
|
|
* Subsequent svc_new()/svc_del() calls into
|
|
* dbus_register_service()/dbus_unregister_service(). */
|
|
{
|
|
svc_t *iter = NULL;
|
|
svc_t *svc;
|
|
|
|
for (svc = svc_iterator(&iter, 1); svc;
|
|
svc = svc_iterator(&iter, 0))
|
|
dbus_register_service(svc);
|
|
}
|
|
|
|
(void)try_attach_system_bus(ctx);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int dbus_exit(void)
|
|
{
|
|
struct peer *p;
|
|
|
|
if (sysbus_tmr_up) {
|
|
uev_timer_stop(&sysbus_tmr);
|
|
sysbus_tmr_up = 0;
|
|
}
|
|
uev_io_stop(&accept_watcher);
|
|
|
|
while ((p = TAILQ_FIRST(&peers)))
|
|
peer_drop(p);
|
|
|
|
/* No read loop is running now, and the timer never will again. */
|
|
peer_reap(NULL);
|
|
|
|
if (server) {
|
|
link_server_free(server);
|
|
server = NULL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif /* HAVE_DBUS */
|
|
|
|
/**
|
|
* Local Variables:
|
|
* indent-tabs-mode: t
|
|
* c-file-style: "linux"
|
|
* End:
|
|
*/
|