Files
finit/src/dbus.c
T
Joachim Wiberg 558c0f8c3b dbus: notify externals when reconfiguration completes
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>
2026-08-16 22:03:44 +02:00

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:
*/