diff --git a/Makefile.am b/Makefile.am index bc3b885d..b3afffea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,7 +8,7 @@ ACLOCAL_AMFLAGS = -I m4 # after src is fine. SUBDIRS = man plugins if DBUS -SUBDIRS += libink +SUBDIRS += libink dbus-1 endif SUBDIRS += src system tmpfiles.d dist_doc_DATA = README.md LICENSE contrib/finit.conf diff --git a/configure.ac b/configure.ac index 10182af5..dcd02548 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,7 @@ AC_CONFIG_FILES([Makefile contrib/debian/Makefile contrib/debian/finit.d/Makefile contrib/debian/finit.d/available/Makefile contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile doc/Makefile doc/config/Makefile + dbus-1/Makefile libink/Makefile libink/libink.pc libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile diff --git a/dbus-1/.gitignore b/dbus-1/.gitignore new file mode 100644 index 00000000..b336cc7c --- /dev/null +++ b/dbus-1/.gitignore @@ -0,0 +1,2 @@ +/Makefile +/Makefile.in diff --git a/dbus-1/Makefile.am b/dbus-1/Makefile.am new file mode 100644 index 00000000..dec4a333 --- /dev/null +++ b/dbus-1/Makefile.am @@ -0,0 +1,6 @@ +EXTRA_DIST = org.finit.conf + +if DBUS +dbuspolicydir = $(sysconfdir)/dbus-1/system.d +dist_dbuspolicy_DATA = org.finit.conf +endif diff --git a/dbus-1/org.finit.conf b/dbus-1/org.finit.conf new file mode 100644 index 00000000..ea730e9b --- /dev/null +++ b/dbus-1/org.finit.conf @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 5b25a99b..b6c21350 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -20,6 +20,55 @@ All relevant changes are documented in this file. same service, `command = { "/lib/systemd/systemd-udevd", "-udevd" }`, and Finit starts the first one it finds. The line-based format could only express this by repeating the whole stanza per candidate +- Finit now ships with a built-in brokerless D-Bus implementation, + **libink**, exposing the running init system as a peer on its own + private bus at `/run/finit/bus`, and -- opportunistically -- + registering `org.finit` on the standard system bus when a + `dbus-daemon` is reachable. No external `libdbus`/`sd-bus`/`GIO` + dependency. + + The bus implements the stock `org.freedesktop.DBus`, + `org.freedesktop.DBus.Peer`, `org.freedesktop.DBus.Introspectable`, + and `org.freedesktop.DBus.Properties` interfaces, plus three + Finit-specific ones: + + * `org.finit.Manager1` at `/org/finit/manager` -- + `ListServices`, `GetService`, `Start`/`Stop`/`Restart`/`Reload`, + `SetRunlevel`, `SetDebug`, `Signal`, `Suspend`, and the + `Reboot`/`Halt`/`Poweroff` triplet. Read-only properties + `Runlevel`, `PrevRunlevel`, `Version`. Signals + `ServiceStateChanged (sss)` and `RunlevelChanged (ss)`. + + * `org.finit.Service1` at `/org/finit/service/` -- one + object per loaded service, with `Start`/`Stop`/`Restart`/`Reload` + for working off an object handle rather than passing the + identity string around. + + * `org.finit.Cond1` at `/org/finit/cond` -- `Get`, `Set`, `Clear`, + `List`, `Dump` for [user-defined conditions](conditions.md), + with a `ConditionChanged (ss)` signal. + + Privileged methods reject non-root callers based on the kernel- + authenticated peer uid (`SO_PEERCRED`); read-only methods are open. + See [D-Bus Integration](dbus.md) for the full surface, build flag, + and `dbus-send`/`dbus-monitor` examples. + +- `initctl` now transparently routes through D-Bus when the bus is + reachable, with the legacy `INIT_SOCKET` transport as a fallback: + `start`, `stop`, `restart`, `reload`, `reload `, `reboot`, + `halt`, `poweroff`, `suspend`, `debug`, `signal`, `runlevel`, and + `cond {get,set,clr}` all use the new path. Two new subcommands + show up that have no legacy equivalent: + + * `initctl monitor` -- streams every signal on the bus to the + terminal, one line per delivery (`HH:MM:SS iface.member(args)`), + until interrupted. Same idea as `dbus-monitor`, but scoped to + Finit and with no address plumbing required. + + * Issuing `initctl cond set/clr` over D-Bus also fires the + `Cond1.ConditionChanged` signal, so observers see user-driven + state changes the same way they see service-driven ones. + - Restart log now spells out the signal name and flags core dumps, e.g. `killed by SIGKILL` or `killed by SIGSEGV, core dumped`, in place of the bare numeric `by signal: N`. Gives operators a much diff --git a/doc/dbus.md b/doc/dbus.md new file mode 100644 index 00000000..3b035909 --- /dev/null +++ b/doc/dbus.md @@ -0,0 +1,294 @@ +D-Bus Integration +================= + +Finit ships with a built-in, brokerless [D-Bus][] implementation, +**libink**, that exposes the running init system as a peer on its own +private bus, and optionally on the system bus when `dbus-daemon` is +available. Everything `initctl` does is also reachable from any +generic D-Bus tooling — `dbus-send`, `dbus-monitor`, `gdbus`, +language bindings, dashboards, monitoring agents, etc. + +> [!NOTE] +> D-Bus support is enabled at build time with `--enable-dbus`. See +> [Building](build.md) for details. When disabled, `initctl` keeps +> using the legacy `INIT_SOCKET` transport and Finit exposes no bus. + +Bus address +----------- + +| Bus | Address | +| --- | --- | +| Local (always) | `unix:path=/run/finit/bus` | +| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket`, well-known name `org.finit` | + +The **local** bus is brokerless: clients connect straight to Finit +over a Unix-domain socket using the standard D-Bus SASL EXTERNAL +handshake. No `dbus-daemon` is required, which makes it suitable for +embedded systems that don't ship one. + +The **system** bus is best-effort: at start-up Finit probes for a +running `dbus-daemon` and, if reachable, registers `org.finit` so that +standard tooling sees Finit just like any other system service: + +```sh +dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager \ + org.finit.Manager1.ListServices + +dbus-monitor --system "sender='org.finit'" +``` + +If no system bus is present (the common case on embedded targets), +this step is silently skipped. + +Object tree +----------- + +``` +/ +├── org/ +│ └── finit/ +│ ├── manager Manager1 +│ ├── cond Cond1 +│ └── service/ +│ ├── keventd Service1 (one per service) +│ ├── sshd +│ └── … +└── org/freedesktop/DBus Standard well-known interfaces +``` + +Every node implements the usual stock interfaces: + +| Interface | Purpose | +| ------------------------------------ | ------- | +| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` (on `/org/freedesktop/DBus`) | +| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | +| `org.freedesktop.DBus.Introspectable`| `Introspect()` — XML description | +| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | + +`org.finit.Manager1` +-------------------- + +Lives at **`/org/finit/manager`**. Owns the global init operations +and the service registry. + +### Methods + +| Method | In sig | Out sig | Privileged | Notes | +| ----------------------- | ------ | ------- | ---------- | ----- | +| `ListServices` | — | `as` | no | Returns the identities (`name`, `name:id`) of every loaded service. | +| `GetService` | `s` | `o` | no | Resolves a service identity to its `Service1` object path. | +| `Start` | `s` | — | yes | Start the service(s) matching the identity. | +| `Stop` | `s` | — | yes | Stop the service(s) matching the identity. | +| `Restart` | `s` | — | yes | Restart (stop + start) the service(s). | +| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes (same as `initctl reload`). | +| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6). | +| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag. | +| `Signal` | `su` | — | yes | Send signal number `u` (1–31) to every running service matching identity `s`. Halted matches are silently skipped. | +| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM. | +| `Reboot` / `Halt` / `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence. | + +### Properties + +All read-only strings; observable via `Properties.Get` and +`Properties.GetAll`. + +| Property | Type | Returns | +| -------------- | ---- | ------- | +| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"`. | +| `PrevRunlevel` | `s` | Previous runlevel, same encoding. | +| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`). | + +### Signals + +| Signal | Body | Fires when | +| ----------------------- | ---- | ---------- | +| `ServiceStateChanged` | `(sss)` — identity, old state, new state | A service transitions between supervisor states. | +| `RunlevelChanged` | `(ss)` — old level, new level | The system enters a new runlevel. | + +State names emitted by `ServiceStateChanged` are stable wire strings: +`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, +`paused`, `waiting`, `starting`, `running`. + +`org.finit.Service1` (per-service objects) +------------------------------------------ + +Lives at **`/org/finit/service/`**, one object per loaded +service. `` is the service identity (name, or `name:id` for +templated services) put through systemd-style `_HH` hex escaping — +ASCII alphanumerics and `_` pass through, anything else becomes `_HH` +where `HH` is the hex byte. Use `Manager1.GetService(identity)` to +look up the exact path rather than constructing it by hand. + +| Method | In sig | Out sig | Privileged | Notes | +| --------- | ------ | ------- | ---------- | ----- | +| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service. | +| `Stop` | — | — | yes | … | +| `Restart` | — | — | yes | … | +| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | + +The per-service surface lets generic tooling supply an object handle +once and then invoke methods on it, instead of repeatedly passing the +identity string. + +`org.finit.Cond1` +----------------- + +Lives at **`/org/finit/cond`**. Exposes Finit's +[condition system](conditions.md) to bus clients. + +### Methods + +| Method | In sig | Out sig | Privileged | Notes | +| -------- | ------ | ------- | ---------- | ----- | +| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition. | +| `Set` | `s` | — | yes | Assert a `usr/` condition. Non-`usr/*` paths are rejected with `InvalidArgs` (system conditions belong to Finit's state machine). | +| `Clear` | `s` | — | yes | Deassert a `usr/` condition. | +| `List` | — | `as` | no | Names of all known conditions. | +| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns. | + +### Signals + +| Signal | Body | Fires when | +| ------------------- | ---- | ---------- | +| `ConditionChanged` | `(ss)` — name, new state | A condition is asserted or deasserted. | + +Authorization +------------- + +Privileged methods reject any caller whose peer `uid` isn't 0. +On the **local** bus the kernel's `SO_PEERCRED` socket option tells +Finit exactly who's calling, so privilege escalation through the bus +is impossible. + +On the **system** bus, all incoming traffic is treated as +unprivileged: it arrives through `dbus-daemon` (typically running as +root) and Finit cannot yet ask the daemon for the real requester's +uid via `GetConnectionUnixUser`. This means external tooling can +freely `Get`/`Introspect`/`ListServices`, but every state-changing +method returns `org.freedesktop.DBus.Error.AccessDenied`. Per-sender +uid lookup is on the roadmap. + +When a privileged method is rejected the error name is exactly +`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a +short reason string (e.g. `"permission denied: Start requires root"`). + +`initctl` integration +--------------------- + +`initctl` transparently routes through D-Bus when the bus socket is +present, and falls back to the legacy `INIT_SOCKET` transport +otherwise. Concretely, the following subcommands use the bus first: + +| Subcommand | Method | +| ------------------- | ------------------------------- | +| `initctl start` | `Manager1.Start(svc)` | +| `initctl stop` | `Manager1.Stop(svc)` | +| `initctl restart` | `Manager1.Restart(svc)` | +| `initctl reload` | `Manager1.Reload()` | +| `initctl reload S` | `Service1.Reload()` (per-svc) | +| `initctl reboot` | `Manager1.Reboot()` | +| `initctl halt` | `Manager1.Halt()` | +| `initctl poweroff` | `Manager1.Poweroff()` | +| `initctl suspend` | `Manager1.Suspend()` | +| `initctl debug` | `Manager1.SetDebug()` | +| `initctl signal` | `Manager1.Signal(svc, signo)` | +| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` | +| `initctl cond set/get/clr` | `Cond1.{Set,Get,Clear}` | + +Two `initctl` subcommands are pure D-Bus features without legacy +equivalents: + +* `initctl monitor` — subscribes to every signal on the local bus and + prints one line per delivery (with timestamp, interface and + member). Same idea as `dbus-monitor`, but scoped to Finit and with + no need to pass `--address`. + +* `initctl cond` (when D-Bus is reachable) emits the standard + `Cond1.ConditionChanged` signal as a side effect, so subscribers + observe user-driven state changes the same way they observe + service-driven ones. + +Examples +-------- + +The examples below use `dbus-send` and `dbus-monitor`, which ship as +part of the [dbus][] reference implementation; they're widely +packaged and don't pull in any extra runtime. Any tool that speaks +D-Bus over an AF_UNIX socket works equally well — `gdbus`, Python's +`jeepney`/`dasbus`, etc. — substitute their syntax for setting the +bus address. + +When `org.finit` is registered on the system bus you can replace +`--address=unix:path=/run/finit/bus` with `--system` in any example +below. + +List the running services: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.finit.Manager1.ListServices +``` + +Read the current runlevel via the Properties interface: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.freedesktop.DBus.Properties.Get \ + string:org.finit.Manager1 string:Runlevel +``` + +Subscribe to every state change on the manager object: + +```sh +dbus-monitor --address=unix:path=/run/finit/bus \ + "type='signal',interface='org.finit.Manager1'" +``` + +Or use `initctl monitor`, which does the same without any address +plumbing. + +Restart a service by its object path: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/service/sshd \ + org.finit.Service1.Restart +``` + +Trigger a `usr/`-condition assertion that wakes any dependent service: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/cond \ + org.finit.Cond1.Set string:"data-ready" +``` + +The `--dest=org.finit` argument is informational on the local +brokerless bus — Finit accepts any destination because there's no +broker to route by name — but `dbus-send` requires it syntactically. + +[dbus]: https://gitlab.freedesktop.org/dbus/dbus + +Implementation notes +-------------------- + +The D-Bus server library lives in `libink/`. It speaks the binary +D-Bus 1.0 wire format directly, has no `libdbus`/`sd-bus`/`GIO` +dependency, and is tiny — a few thousand lines of C. The Finit-side +glue in `src/dbus.c` registers vtables for Manager1/Service1/Cond1, +emits the four signals from the appropriate hook points (state +transitions, runlevel transitions, condition flips), and bridges the +event loop to the libink server. + +The `initctl` client uses the same library — `link_client_open`, +`link_client_call_v`, `link_client_reply`, `link_reader_*` — so the +single wire-format implementation serves both ends. + +[D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/libink/builtin.c b/libink/builtin.c index 715157db..0d7a677b 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -104,24 +104,53 @@ static void xprintf(struct xbuf *x, const char *fmt, ...) x->off += (size_t)n; } +/* + * Advance past one single complete type in a D-Bus signature: + * a basic type code, 'a' + element type, or a bracketed group. + * Signatures come from our own vtables, so trust them; an + * unterminated group just stops at NUL. + */ +static const char *sig_next(const char *p) +{ + while (*p == 'a') /* array prefixes, then element type */ + p++; + if (*p == '(' || *p == '{') { + char close = *p == '(' ? ')' : '}'; + + for (p++; *p && *p != close; p = sig_next(p)) + ; + } + return *p ? p + 1 : p; /* NUL: unterminated group, stop here */ +} + +static void emit_args(struct xbuf *x, const char *sig, const char *dir) +{ + const char *p, *e; + + for (p = sig; p && *p; p = e) { + e = sig_next(p); + xprintf(x, " \n", + (int)(e - p), p, dir); + } +} + /* Emit a single stanza for one method definition. */ static void emit_method(struct xbuf *x, const link_method_t *m) { - const char *p; - xprintf(x, " \n", m->name); - for (p = m->in_sig ? m->in_sig : ""; *p; p++) - xprintf(x, " \n", *p); - for (p = m->out_sig ? m->out_sig : ""; *p; p++) - xprintf(x, " \n", *p); + emit_args(x, m->in_sig, "in"); + emit_args(x, m->out_sig, "out"); xprintf(x, " \n"); } -/* Introspection limitation: emit_method prints one per - * character of the signature, which is wrong for compound types - * (an "a(ss)" arg appears as four args). Good enough for the - * "s", "u", "as" signatures we expose today; replace with a - * signature parser when the first compound argument lands. */ +static void emit_property(struct xbuf *x, const link_property_t *p) +{ + /* Setters are not implemented, so every property advertises + * access="read" today. When Properties.Set lands, switch on + * a writable flag. */ + xprintf(x, " \n", + p->name, p->sig ? p->sig : "s"); +} static const char STANDARD_INTERFACES_XML[] = " \n" @@ -134,6 +163,17 @@ static const char STANDARD_INTERFACES_XML[] = " \n" " \n" " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" " \n"; /* Is `child` a path under `parent`? If so, write the first segment @@ -197,11 +237,16 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) const link_method_t *meth; TAILQ_FOREACH(e, &o->vtables, link) { + const link_property_t *prop; + xprintf(&x, " \n", e->vt->interface); if (e->vt->methods) for (meth = e->vt->methods; meth->name; meth++) emit_method(&x, meth); + if (e->vt->properties) + for (prop = e->vt->properties; prop->name; prop++) + emit_property(&x, prop); xprintf(&x, " \n"); } } @@ -231,6 +276,129 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) return send_string_reply(conn, m, xml); } +/* ---------- Properties.Get / GetAll ---------- */ + +/* Find the (object, vtable-entry) pair matching `path` and `interface`. + * Returns NULL if the path is unknown or the interface isn't exposed + * on it. */ +static struct link_vtable_entry * +find_vtable(link_connection_t *conn, const char *path, const char *interface) +{ + struct link_object *o; + + if (!path || !interface) + return NULL; + TAILQ_FOREACH(o, &conn->server->objects, link) { + struct link_vtable_entry *e; + + if (strcmp(o->path, path) != 0) + continue; + TAILQ_FOREACH(e, &o->vtables, link) { + if (strcmp(e->vt->interface, interface) == 0) + return e; + } + } + return NULL; +} + +static int handle_properties_get(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface, *prop_name; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "ss") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.Get takes (interface, property)"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0 || __r_string(&r, &prop_name) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e || !e->vt->properties) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + for (p = e->vt->properties; p->name; p++) { + if (strcmp(p->name, prop_name) != 0) + continue; + if (!p->getter) + break; + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + return __send_method_return(conn, m, "v", + conn->txbuf, (size_t)blen); + } + + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownProperty", + "No such property on this interface"); +} + +static int handle_properties_get_all(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.GetAll takes one string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_array_begin(&w, '{'); + if (e->vt->properties) { + for (p = e->vt->properties; p->name; p++) { + if (!p->getter) + continue; + __w_struct_begin(&w); + __w_string(&w, p->name); + if (p->getter(&w, e->userdata) != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + __w_struct_end(&w); + } + } + __w_array_end(&w); + + blen = __w_finish(&w); + if (blen < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply too large"); + + return __send_method_return(conn, m, "a{sv}", + conn->txbuf, (size_t)blen); +} + /* ---------- AddMatch / RemoveMatch ---------- */ static int handle_add_match(link_connection_t *conn, const struct link_msg *m) @@ -310,5 +478,11 @@ int __handle_builtin(link_connection_t *conn, const struct link_msg *m) if (member_is(m, "org.freedesktop.DBus.Introspectable", "Introspect")) return handle_introspect(conn, m); + if (member_is(m, "org.freedesktop.DBus.Properties", "Get")) + return handle_properties_get(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Properties", "GetAll")) + return handle_properties_get_all(conn, m); + return -1; /* not a built-in */ } diff --git a/libink/client.c b/libink/client.c index a5c245a1..fbfbe70f 100644 --- a/libink/client.c +++ b/libink/client.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,7 @@ struct link_client { size_t rxlen; }; -link_client_t *link_client_open(const char *path) +link_client_t *link_client_open_timeout(const char *path, int timeout_ms) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; link_client_t *c; @@ -48,6 +49,20 @@ link_client_t *link_client_open(const char *path) fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return NULL; + + if (timeout_ms > 0) { + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + /* Cover both directions so the AUTH write and the + * subsequent read both honour the budget. setsockopt + * failure is non-fatal -- the bus may still respond + * quickly enough; we just lose the safety net. */ + (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { close(fd); return NULL; @@ -67,6 +82,11 @@ link_client_t *link_client_open(const char *path) return c; } +link_client_t *link_client_open(const char *path) +{ + return link_client_open_timeout(path, 0); +} + void link_client_close(link_client_t *c) { if (!c) @@ -76,6 +96,18 @@ void link_client_close(link_client_t *c) free(c); } +int link_client_steal_fd(link_client_t *c) +{ + int fd; + + if (!c) + return -1; + fd = c->fd; + c->fd = -1; + free(c); + return fd; +} + /* read_full / send_all live in libink/io.c. */ #define read_full(fd, buf, len) __io_read_full ((fd), (buf), (len)) #define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) @@ -229,6 +261,30 @@ const link_reply_t *link_client_reply(link_client_t *c) return &c->reply; } +int link_reply_get_string(const link_reply_t *r, const char **out) +{ + link_reader_t reader; + + if (out) + *out = NULL; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_string(&reader, out); +} + +int link_reply_get_u32(const link_reply_t *r, uint32_t *out) +{ + link_reader_t reader; + + if (out) + *out = 0; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_u32(&reader, out); +} + /* Marshal varargs into `body` (capacity `cap`) according to `sig`. * Returns the marshalled length on success, -1 on overflow or * unsupported type code. */ diff --git a/libink/dispatch.c b/libink/dispatch.c index 8b65c4b2..9d3d322b 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -298,6 +298,7 @@ void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); } void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); } void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); } void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); } +void link_w_variant_string(link_writer_t *w, const char *s) { __w_variant_string(w, s); } void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); } void link_w_array_end (link_writer_t *w) { __w_array_end(w); } void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); } @@ -311,6 +312,9 @@ int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); } int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); } int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); } +int link_r_variant_string(link_reader_t *r, const char **o) { return __r_variant_string(r, o); } +int link_r_align (link_reader_t *r, size_t n) { return __r_align (r, n); } +int link_r_array_begin(link_reader_t *r, size_t *e) { return __r_array_begin(r, e); } int link_r_done (const link_reader_t *r) { return __r_done (r); } size_t link_r_pos (const link_reader_t *r) { return r->off; } diff --git a/libink/link.h b/libink/link.h index 9a59c41e..01a61c11 100644 --- a/libink/link.h +++ b/libink/link.h @@ -96,6 +96,18 @@ int link_server_get_fd(const link_server_t *server); int link_server_accept(link_server_t *server, link_connection_t **conn); +/* Insert an externally-authenticated fd into the server's connection + * set. Used to integrate an outbound peer (e.g. a client-side + * handshake against an external dbus-daemon) so the same dispatch + + * signal-fan-out machinery covers it. `peer_uid` becomes what + * privileged-method checks see; pass (uid_t)-1 to make all + * LINK_METHOD_PRIVILEGED methods reject by default. + * + * On success the connection takes ownership of `fd`. On any failure + * `fd` is closed before the function returns NULL, so callers never + * have to track partial state. */ +link_connection_t *link_server_attach(link_server_t *server, int fd, uid_t peer_uid); + int link_connection_get_fd (const link_connection_t *conn); uid_t link_connection_get_uid (const link_connection_t *conn); int link_connection_process (link_connection_t *conn); @@ -116,9 +128,22 @@ typedef struct { link_method_fn handler; } link_method_t; +/* A read-only property descriptor. Set via the Properties.Set side + * is not yet implemented; only Get and GetAll are. The getter writes + * the property's value as a D-Bus variant (use link_w_variant_string + * for "s"-typed properties) into the provided writer. */ +typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); + typedef struct { - const char *interface; /* e.g. "org.finit.Manager1" */ - const link_method_t *methods; /* terminated by {NULL, ...} */ + const char *name; /* property name */ + const char *sig; /* D-Bus signature, e.g. "s" */ + link_property_getter_fn getter; +} link_property_t; + +typedef struct { + const char *interface; /* e.g. "org.finit.Manager1" */ + const link_method_t *methods; /* terminated by {NULL, ...}, or NULL */ + const link_property_t *properties; /* terminated by {NULL, ...}, or NULL */ } link_vtable_t; /* Register one (interface, methods) at `path`. Calling repeatedly @@ -189,8 +214,23 @@ int link_connection_emit_signal(link_connection_t *conn, * Returns NULL on any failure (caller can fall back to another * transport if it has one). */ link_client_t *link_client_open(const char *path); + +/* As link_client_open but applies SO_SNDTIMEO + SO_RCVTIMEO before + * the connect/AUTH handshake. After link_server_attach flips the fd + * to non-blocking the timeout is silently inert; it only protects + * the synchronous open path against a hung peer. timeout_ms == 0 + * disables the budget (same behaviour as link_client_open). */ +link_client_t *link_client_open_timeout(const char *path, int timeout_ms); + void link_client_close(link_client_t *c); +/* Detach the authenticated socket from the client and return the raw + * fd; subsequent link_client_close on `c` is invalid because the + * structure has already been freed. Used by callers (e.g. system-bus + * integration) that want to promote an outbound client connection + * into a server-attached peer via link_server_attach(). */ +int link_client_steal_fd(link_client_t *c); + /* Status codes returned by link_client_call(_v). */ #define LINK_CALL_OK 0 /* method-return received */ #define LINK_CALL_ERROR 1 /* server replied with an error */ @@ -234,6 +274,14 @@ int link_client_call_v(link_client_t *c, const link_reply_t *link_client_reply(link_client_t *c); +/* Convenience accessors for the common case where a reply carries + * exactly one string ("s" or "o") or one u32 ("u"). They wrap the + * link_reader_init + link_r_* pattern; on success return 0 and + * populate *out, on parse failure or missing body return -1. Use + * link_client_reply + link_reader_init directly for richer payloads. */ +int link_reply_get_string(const link_reply_t *r, const char **out); +int link_reply_get_u32 (const link_reply_t *r, uint32_t *out); + /* Wait up to `timeout_ms` milliseconds for the next inbound message * (typically a SIGNAL delivered after an AddMatch subscription), and * populate the same view returned by link_client_reply(). @@ -264,6 +312,7 @@ void link_w_bool (link_writer_t *w, int v); void link_w_u32 (link_writer_t *w, uint32_t v); void link_w_string (link_writer_t *w, const char *s); /* "s" */ void link_w_path (link_writer_t *w, const char *s); /* "o" */ +void link_w_variant_string(link_writer_t *w, const char *s); /* "v" containing "s" */ void link_w_array_begin (link_writer_t *w, char element_sig); void link_w_array_end (link_writer_t *w); void link_w_struct_begin(link_writer_t *w); @@ -280,12 +329,21 @@ int link_r_bool (link_reader_t *r, int *out); int link_r_u32 (link_reader_t *r, uint32_t *out); int link_r_string (link_reader_t *r, const char **out); /* "s" */ int link_r_path (link_reader_t *r, const char **out); /* "o" */ +int link_r_variant_string(link_reader_t *r, const char **out); /* "v" containing "s" */ +int link_r_align (link_reader_t *r, size_t n); /* skip to next n-byte boundary */ int link_r_done (const link_reader_t *r); -/* Byte offset of the next read inside the original body buffer. Used - * to detect end-of-array when walking "a" payloads: read the array - * byte-length prefix with link_r_u32 first, record (pos+length) as the - * end, then loop while link_r_pos < end. */ +/* Begin reading an "a" array. On success returns 0 and sets + * *out_end to the absolute reader offset at which the array ends; + * caller loops while link_r_pos < *out_end. For dict-entry arrays + * ("a{T}") call link_r_align(r, 8) at the top of each iteration -- + * the element-alignment skip from the array prefix only covers the + * first entry. */ +int link_r_array_begin(link_reader_t *r, size_t *out_end); + +/* Byte offset of the next read inside the original body buffer. + * Use together with the *out_end returned by link_r_array_begin to + * walk the elements of an "a" payload. */ size_t link_r_pos (const link_reader_t *r); #ifdef __cplusplus diff --git a/libink/marshal.c b/libink/marshal.c index d22c7c8c..ae215893 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -102,6 +102,14 @@ void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, void __w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } void __w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); } +/* Variant "v" containing a string. Wire form: + * 1-byte sig length (1), 's', NUL, then the string per __w_string. */ +void __w_variant_string(struct link_writer *w, const char *s) +{ + __w_sig (w, "s"); + __w_string(w, s); +} + static size_t element_align(char c) { switch (c) { @@ -254,7 +262,60 @@ static int read_string_like(struct link_reader *r, const char **out) int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } +/* Read a variant "v" expected to contain a string. Fails if the + * inner signature is anything other than "s" (returns -1, *out set + * to NULL). */ +int __r_variant_string(struct link_reader *r, const char **out) +{ + uint8_t sig_len; + uint32_t slen; + + *out = NULL; + + /* signature is "g" wire form: 1-byte length, bytes, NUL */ + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) { r->err = 1; return -1; } + sig_len = r->base[r->off++]; + if (sig_len != 1 || r->off + 2 > r->cap) { r->err = 1; return -1; } + if (r->base[r->off] != 's' || r->base[r->off + 1] != 0) { r->err = 1; return -1; } + r->off += 2; + + /* now a normal string */ + if (__r_u32(r, &slen) < 0) return -1; + if (r->off + (size_t)slen + 1 > r->cap || r->base[r->off + slen] != 0) { + r->err = 1; + return -1; + } + *out = (const char *)(r->base + r->off); + r->off += (size_t)slen + 1; + return 0; +} + int __r_done(const struct link_reader *r) { return !r->err && r->off == r->cap; } + +int __r_align(struct link_reader *r, size_t n) +{ + return r_skip_align(r, n); +} + +/* Begin reading an "a" array. Reads the u32 byte-length prefix + * and sets *out_end to the absolute reader offset at which the array + * ends. Caller loops while r->off < *out_end. Returns -1 on a + * truncated or oversized array length. */ +int __r_array_begin(struct link_reader *r, size_t *out_end) +{ + uint32_t array_bytes; + size_t end; + + if (__r_u32(r, &array_bytes) < 0) + return -1; + end = r->off + (size_t)array_bytes; + if (end > r->cap) { + r->err = 1; + return -1; + } + *out_end = end; + return 0; +} diff --git a/libink/marshal.h b/libink/marshal.h index b7717813..313205b2 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -23,6 +23,7 @@ void __w_u32 (struct link_writer *w, uint32_t v); void __w_string (struct link_writer *w, const char *s); /* "s" */ void __w_path (struct link_writer *w, const char *s); /* "o" */ void __w_sig (struct link_writer *w, const char *s); /* "g" */ +void __w_variant_string(struct link_writer *w, const char *s); /* "v" containing "s" */ /* element_sig_first_char drives the alignment padding inserted * between the array length prefix and the first element. */ @@ -43,6 +44,9 @@ int __r_bool (struct link_reader *r, int *out); int __r_u32 (struct link_reader *r, uint32_t *out); int __r_string(struct link_reader *r, const char **out); /* "s" */ int __r_path (struct link_reader *r, const char **out); /* "o" */ +int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ +int __r_align (struct link_reader *r, size_t n); /* skip to n-byte boundary */ +int __r_array_begin(struct link_reader *r, size_t *out_end); int __r_done (const struct link_reader *r); #endif /* LIBINK_MARSHAL_H_ */ diff --git a/libink/server.c b/libink/server.c index ef6325ae..f5a82431 100644 --- a/libink/server.c +++ b/libink/server.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -160,3 +161,44 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) *out = conn; return 0; } + +link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid) +{ + link_connection_t *conn; + int flags; + + /* On entry we always own `fd` -- close it on every failure path + * so callers don't have to track whether we touched fcntl state. */ + if (!srv || fd < 0) { + if (fd >= 0) + close_save_errno(fd); + errno = EINVAL; + return NULL; + } + + /* Match server_accept's fd setup: CLOEXEC first (so a fork-and- + * exec between the two calls cannot leak the fd), then NONBLOCK + * so process_binary's read loop can drain without hanging. */ + flags = fcntl(fd, F_GETFD, 0); + if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + goto err_close; + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + goto err_close; + + conn = calloc(1, sizeof(*conn)); + if (!conn) + goto err_close; + + conn->fd = fd; + conn->auth = LINK_AUTH_DONE; /* caller already handshook */ + conn->server = srv; + conn->peer_uid = peer_uid; + __auth_generate_guid(conn->guid); + + return conn; + +err_close: + close_save_errno(fd); + return NULL; +} diff --git a/mkdocs.yml b/mkdocs.yml index 2ccce539..674a554a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - Plugins: plugins.md - Watchdog: watchdog.md - keventd: keventd.md + - D-Bus Integration: dbus.md - Service State Machine: state-machine.md - Distributions: distro.md - Requirements: requirements.md diff --git a/src/dbus.c b/src/dbus.c index 94dc575a..109c8a5e 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -31,9 +31,10 @@ #ifdef HAVE_DBUS #include +#include #include #include -#include +#include #include #include @@ -50,6 +51,7 @@ #include "sig.h" #include "sm.h" #include "svc.h" +#include "util.h" #define DBUS_MAX_PEERS 64 @@ -88,6 +90,39 @@ static void peer_cb(uev_t *w, void *arg, int events) 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; @@ -99,7 +134,6 @@ static void accept_cb(uev_t *w, void *arg, int events) for (;;) { link_connection_t *conn = NULL; - struct peer *p; if (link_server_accept(server, &conn) < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) @@ -107,29 +141,8 @@ static void accept_cb(uev_t *w, void *arg, int events) break; } - if (peer_count >= DBUS_MAX_PEERS) { - logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", - peer_count); - link_connection_close(conn); - continue; - } - - p = calloc(1, sizeof(*p)); - if (!p) { - link_connection_close(conn); - err(1, "Out of memory accepting D-Bus client"); - break; - } - - p->conn = conn; - TAILQ_INSERT_TAIL(&peers, p, link); - peer_count++; - - if (uev_io_init(w->ctx, &p->watcher, peer_cb, p, - link_connection_get_fd(conn), UEV_READ)) { - err(1, "Failed registering D-Bus peer watcher"); - peer_drop(p); - } + if (!peer_register(w->ctx, conn)) + continue; /* logged inside */ } } @@ -240,6 +253,7 @@ static int dbus_apply_restart(svc_t *svc, void *user_data) struct dispatch_ctx { int (*action)(svc_t *, void *); + void *udata; int matched; }; @@ -248,7 +262,7 @@ static int dispatch_found(svc_t *svc, void *udata) struct dispatch_ctx *ctx = udata; ctx->matched++; - return ctx->action(svc, NULL); + return ctx->action(svc, ctx->udata); } static int dispatch_missing(char *job, char *id, void *udata) @@ -257,14 +271,15 @@ static int dispatch_missing(char *job, char *id, void *udata) return 0; /* don't penalise the return; we'll check ->matched */ } -/* Apply `action` 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). */ +/* 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 *)) + int (*action)(svc_t *, void *), void *udata) { char buf[MAX_IDENT_LEN]; - struct dispatch_ctx ctx = { .action = action }; + struct dispatch_ctx ctx = { .action = action, .udata = udata }; int rc; if (!ident || !*ident || strlen(ident) >= sizeof(buf)) @@ -281,14 +296,21 @@ 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)"); - if (dispatch_action(ident, action) != 0) + + 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; @@ -351,6 +373,117 @@ static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shut 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; + + /* Silently skip stopped services -- a multi-match ident + * (e.g. "sshd:*") should not fail the whole call just because + * one of the matches happens to be in a halted state. */ + if (!svc_is_running(svc)) + return 0; + 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; + 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. */ +static int prop_runlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + snprintf(buf, sizeof(buf), "%d", runlevel); + link_w_variant_string(w, buf); + return 0; +} + +static int prop_prevrunlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + snprintf(buf, sizeof(buf), "%d", prevlevel); + link_w_variant_string(w, buf); + return 0; +} + +static int prop_version(link_writer_t *w, void *u) +{ + (void)u; + link_w_variant_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 }, @@ -372,12 +505,19 @@ static const link_method_t manager_methods[] = { .flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff }, { .name = "Halt", .in_sig = "", .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_vtable_t manager_vtable = { - .interface = "org.finit.Manager1", - .methods = manager_methods, + .interface = "org.finit.Manager1", + .methods = manager_methods, + .properties = manager_properties, }; /* ---------- org.finit.Service1 (one object per service) ---------- @@ -485,6 +625,38 @@ void dbus_unregister_service(svc_t *svc) (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. */ +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) { + if (link_connection_emit_signal(p->conn, + path, interface, member, + signature, body, body_len) < 0) { + /* nothing hit the wire, and same for every peer */ + if (errno == EMSGSIZE || errno == EINVAL) + break; + logit(LOG_WARNING, "D-Bus peer fd %d write failed: " + "%s, dropping", + link_connection_get_fd(p->conn), + strerror(errno)); + peer_drop(p); + } + } +} + /* ---------- signal emission: ServiceStateChanged ---------- */ /* @@ -518,36 +690,51 @@ static const char *state_name(svc_state_t s) void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) { - uint8_t body[256]; - link_writer_t w; - struct peer *p; - char ident[MAX_IDENT_LEN]; - ssize_t blen; - svc_state_t o = (svc_state_t)old_state; - svc_state_t n = (svc_state_t)new_state; + uint8_t body[256]; + link_writer_t w; + char ident[MAX_IDENT_LEN]; + ssize_t blen; - if (!server || !svc) + if (!svc) return; - if (TAILQ_EMPTY(&peers)) - return; /* nobody could possibly be listening */ - svc_ident(svc, ident, sizeof(ident)); link_writer_init(&w, body, sizeof(body)); link_w_string(&w, ident); - link_w_string(&w, state_name(o)); - link_w_string(&w, state_name(n)); + 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; - TAILQ_FOREACH(p, &peers, link) - (void)link_connection_emit_signal(p->conn, - "/org/finit/manager", - "org.finit.Manager1", - "ServiceStateChanged", - "sss", - body, (size_t)blen); + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "ServiceStateChanged", "sss", body, (size_t)blen); +} + +/* ---------- signal emission: RunlevelChanged ---------- + * + * Fired by sm.c right after the runlevel global flips. Body is + * (old, new) as one-digit strings, matching the format that + * Manager1.Runlevel (the property) returns. */ +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; + + snprintf(old_s, sizeof(old_s), "%d", old_level); + snprintf(new_s, sizeof(new_s), "%d", new_level); + + 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); } /* ---------- org.finit.Cond1 ---------- */ @@ -766,14 +953,11 @@ static const link_vtable_t cond_vtable = { void dbus_notify_condition_change(const char *name, const char *state) { - uint8_t body[256]; - link_writer_t w; - struct peer *p; - ssize_t blen; + uint8_t body[256]; + link_writer_t w; + ssize_t blen; - if (!server || !name || !state) - return; - if (TAILQ_EMPTY(&peers)) + if (!name || !state) return; link_writer_init(&w, body, sizeof(body)); @@ -783,13 +967,97 @@ void dbus_notify_condition_change(const char *name, const char *state) if (blen < 0) return; - TAILQ_FOREACH(p, &peers, link) - (void)link_connection_emit_signal(p->conn, - COND_PATH_OBJECT, - COND_INTERFACE, - "ConditionChanged", - "ss", - body, (size_t)blen); + dbus_emit_signal(COND_PATH_OBJECT, COND_INTERFACE, + "ConditionChanged", "ss", body, (size_t)blen); +} + +/* ---------- 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 set to (uid_t)-1 so LINK_METHOD_PRIVILEGED methods + * reject by default -- per-request sender uid lookup via + * GetConnectionUnixUser is a follow-up. Read-only methods + * (ListServices, Properties.Get, Introspect, ...) work as expected. + * + * 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 + +static int sysbus_request_name(link_client_t *c) +{ + uint32_t result = 0; + 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) + return -1; + if (link_reply_get_u32(link_client_reply(c), &result) < 0) + return -1; + /* 1 = primary owner; 2/3/4 mean we didn't get the name */ + return (result == 1) ? 0 : -1; +} + +static void try_attach_system_bus(uev_ctx_t *ctx) +{ + link_client_t *c; + link_connection_t *conn; + 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; + } + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + if (rc != LINK_CALL_OK) { + dbg("System-bus Hello failed (rc=%d); skipping", rc); + link_client_close(c); + return; + } + + if (sysbus_request_name(c) < 0) { + logit(LOG_WARNING, "Failed to claim %s on system bus", FINIT_BUS_NAME); + link_client_close(c); + return; + } + + /* 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); + if (!conn) + return; + + if (!peer_register(ctx, conn)) { + logit(LOG_WARNING, "Failed registering system-bus peer"); + return; + } + + logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); } /* ---------- init / exit ---------- */ @@ -839,6 +1107,8 @@ int dbus_init(uev_ctx_t *ctx) dbus_register_service(svc); } + try_attach_system_bus(ctx); + return 0; } diff --git a/src/initctl.c b/src/initctl.c index 71f14263..6379bc9a 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -41,6 +41,10 @@ #include "service.h" #include "cgutil.h" #include "utmp-api.h" +#ifdef HAVE_DBUS +#include "link.h" +#include "path.h" +#endif /* Used by both do_cond_act and (with HAVE_DBUS) cond_dbus_call. */ typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; @@ -148,6 +152,11 @@ static int runlevel_get(int *prevlevel) return rc; } +#ifdef HAVE_DBUS +static int try_dbus_manager(const char *method, const char *arg_sig, + const char *arg); +#endif + static int toggle_debug(char *arg) { struct init_request rq = { @@ -155,6 +164,13 @@ static int toggle_debug(char *arg) .cmd = INIT_CMD_DEBUG, }; + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("SetDebug", "", NULL); + if (rc >= 0) return rc; + } +#endif return client_send(&rq, sizeof(rq)); } @@ -196,6 +212,68 @@ static int show_log(char *arg) return do_log(svc, ""); } +#ifdef HAVE_DBUS +/* Fetch all org.finit.Manager1 string properties in one Properties.GetAll + * round-trip, then pick out a subset. `wanted` is a NULL-terminated array + * of property names; `out` parallel-receives the values (each entry left + * untouched if its property wasn't returned). Returns 0 on transport + * success (even if some properties weren't present), -1 on transport or + * parse failure. */ +static int dbus_get_manager_props(const char *const *wanted, char **out, size_t out_sz) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + size_t end; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.freedesktop.DBus.Properties", "GetAll", + "s", "org.finit.Manager1"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + return -1; + } + + r = link_client_reply(c); + if (!r || !r->body) { + link_client_close(c); + return -1; + } + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) { + link_client_close(c); + return -1; + } + + while (link_r_pos(&reader) < end) { + const char *key = NULL; + const char *val = NULL; + size_t i; + + if (link_r_align(&reader, 8) < 0) break; + if (link_r_string(&reader, &key) < 0) break; + if (link_r_variant_string(&reader, &val) < 0) break; + if (!key || !val) break; + + for (i = 0; wanted[i]; i++) { + if (!strcmp(key, wanted[i])) { + strlcpy(out[i], val, out_sz); + break; + } + } + } + + link_client_close(c); + return 0; +} +#endif + static int do_runlevel(char *arg) { struct init_request rq = { @@ -208,6 +286,23 @@ static int do_runlevel(char *arg) int currlevel; char prev, curr; +#ifdef HAVE_DBUS + char curr_buf[16] = { 0 }, prev_buf[16] = { 0 }; + const char *const wanted[] = { "Runlevel", "PrevRunlevel", NULL }; + char *out[] = { curr_buf, prev_buf }; + + if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 && + curr_buf[0] && prev_buf[0]) { + int cl = atoi(curr_buf); + int pl = atoi(prev_buf); + + curr = (cl == INIT_LEVEL) ? 'S' : (char)(cl + '0'); + prev = (pl > 0 && pl <= 9) ? (char)(pl + '0') : 'N'; + printf("%c %c\n", prev, curr); + return 0; + } +#endif + currlevel = runlevel_get(&prevlevel); switch (currlevel) { case 255: @@ -272,22 +367,46 @@ static int do_startstop(int cmd, char *arg) } #ifdef HAVE_DBUS -#include "link.h" + +/* Map a LINK_CALL_ERROR reply on `c` to the appropriate ERRX exit: + * org.finit.Error.NoSuchService -> exit 69 (legacy "no such svc") + * org.freedesktop.DBus.Error.AccessDenied -> exit 1 (permission denied) + * anything else -> exit 1 (method: err) + * `c` is closed before exit either way. Use exact-match on the + * fully-qualified error name; a substring match would misfire on a + * future name that contained one of these as a prefix. */ +static void map_dbus_err(link_client_t *c, const char *method, const char *ident) +{ + const link_reply_t *r = link_client_reply(c); + char err[128]; + + /* The reply view points into c->rxbuf; copy the error name out + * before link_client_close() frees the client. Otherwise the + * strcmps below read freed memory. */ + if (r && r->error_name) + strlcpy(err, r->error_name, sizeof(err)); + else + err[0] = '\0'; + link_client_close(c); + + if (!strcmp(err, "org.finit.Error.NoSuchService")) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", + ident ? ident : ""); + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: %s requires root", method); + ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); +} /* Try the D-Bus path for a Manager1 method. Returns: * 0 succeeded via D-Bus - * 1 D-Bus replied with an error -- callers should error out * -1 D-Bus not reachable -- callers should fall back to the * legacy INIT_SOCKET transport - * - * On D-Bus error replies the function maps the org.* error name to - * the same exit code initctl historically printed for that case - * (e.g. NoSuchService -> 69 with the legacy message). */ + * LINK_CALL_ERROR is handled internally via map_dbus_err (does not + * return). */ static int try_dbus_manager(const char *method, const char *arg_sig, const char *arg) { link_client_t *c; - const char *err; int rc; c = link_client_open(FINIT_BUS_SOCKET); @@ -306,29 +425,37 @@ static int try_dbus_manager(const char *method, const char *arg_sig, else rc = LINK_CALL_FAIL; - if (rc == LINK_CALL_ERROR) { - const link_reply_t *r = link_client_reply(c); - - err = (r && r->error_name) ? r->error_name : ""; - /* Exact match on the fully-qualified error name; substring - * matching would misfire on a future name that contains - * one of these as a substring. */ - if (!strcmp(err, "org.finit.Error.NoSuchService")) { - link_client_close(c); - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", - arg ? arg : ""); - } - if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) { - link_client_close(c); - ERRX(1, "permission denied: %s requires root", method); - } - link_client_close(c); - ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); - } + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, arg); /* exits */ link_client_close(c); - if (rc == LINK_CALL_OK) - return 0; - return -1; /* LINK_CALL_FAIL or anything else: fall back */ + return (rc == LINK_CALL_OK) ? 0 : -1; +} + +/* Call a void-arg method on Service1 at /org/finit/service/. + * Same return convention as try_dbus_manager. */ +static int try_dbus_service(const char *method, const char *ident) +{ + char path[256]; + const char *prefix = "/org/finit/service/"; + size_t plen = strlen(prefix); + link_client_t *c; + int rc; + + if (!ident || !*ident) + return -1; + memcpy(path, prefix, plen); + if (link_path_encode(ident, path + plen, sizeof(path) - plen) < 0) + return -1; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, path, "org.finit.Service1", method, NULL); + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, ident); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; } /* Try one Cond1.{Get,Set,Clear} call. On COND_GET success the helper @@ -357,14 +484,9 @@ static int cond_dbus_call(link_client_t **bus, condop_t op, "s", arg); if (rc == LINK_CALL_OK) { if (op == COND_GET) { - const link_reply_t *r = link_client_reply(*bus); - link_reader_t reader; - const char *state = NULL; + const char *state = NULL; - if (r && r->body) { - link_reader_init(&reader, r->body, r->body_len); - link_r_string(&reader, &state); - } + link_reply_get_string(link_client_reply(*bus), &state); if (verbose && state) puts(state); *out_exit = (state && !strcmp(state, "on")) ? 0 @@ -493,6 +615,12 @@ static int do_reload (char *arg) return do_svc(INIT_CMD_RELOAD, NULL); } +#ifdef HAVE_DBUS + { + int rc = try_dbus_service("Reload", arg); + if (rc >= 0) return rc; + } +#endif return do_startstop(INIT_CMD_RELOAD_SVC, arg); } @@ -530,10 +658,6 @@ int do_signal(int argc, char *argv[]) if (argc != 2) ERRX(2, "invalid number of arguments to signal"); - strlcpy(rq.data, argv[0], sizeof(rq.data)); - if (client_send(&rq, sizeof(rq))) - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); - signo = str2sig(argv[1]); if (signo == -1) { const char *errstr = NULL; @@ -543,6 +667,29 @@ int do_signal(int argc, char *argv[]) ERRX(65, "%s signal: %s", errstr, argv[1]); } +#ifdef HAVE_DBUS + { + link_client_t *c = link_client_open(FINIT_BUS_SOCKET); + + if (c) { + int rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "Signal", + "su", argv[0], (uint32_t)signo); + + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, "Signal", argv[0]); /* exits */ + link_client_close(c); + if (rc == LINK_CALL_OK) + return 0; + /* LINK_CALL_FAIL: drop to legacy */ + } + } +#endif + + strlcpy(rq.data, argv[0], sizeof(rq.data)); + if (client_send(&rq, sizeof(rq))) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); + /* Reuse runlevel for signal number. */ rq.magic = INIT_MAGIC; rq.cmd = INIT_CMD_SIGNAL; @@ -926,8 +1073,17 @@ int do_poweroff(char *arg) return do_cmd(INIT_CMD_POWEROFF); } -/* Suspend has no Manager1 equivalent yet; uses the legacy IPC. */ -int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); } +int do_suspend(char *arg) +{ + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("Suspend", "", NULL); + if (rc >= 0) return rc; + } +#endif + return do_cmd(INIT_CMD_SUSPEND); +} /** * do_switch_root - Switch to a new root filesystem (initramfs only) diff --git a/src/private.h b/src/private.h index 3b03f510..ae501161 100644 --- a/src/private.h +++ b/src/private.h @@ -53,6 +53,7 @@ void dbus_register_service (svc_t *svc); void dbus_unregister_service (svc_t *svc); void dbus_notify_service_state (svc_t *svc, int old_state, int new_state); void dbus_notify_condition_change(const char *name, const char *state); +void dbus_notify_runlevel_change(int old_level, int new_level); #endif void conf_flush_events(void); diff --git a/src/sm.c b/src/sm.c index ab3abdb9..44d0972d 100644 --- a/src/sm.c +++ b/src/sm.c @@ -388,6 +388,9 @@ restart: prevlevel = runlevel; runlevel = sm.newlevel; sm.newlevel = -1; +#ifdef HAVE_DBUS + dbus_notify_runlevel_change(prevlevel, runlevel); +#endif /* Restore terse mode and run hooks before shutdown */ if (runlevel == 0 || runlevel == 6) { diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh index c51c1a3e..2a1b47c1 100755 --- a/test/dbus-initctl.sh +++ b/test/dbus-initctl.sh @@ -66,3 +66,31 @@ case "$(cat /tmp/dbus-initctl-cond.out)" in *) fail "initctl cond set didn't produce expected signal: $(cat /tmp/dbus-initctl-cond.out)" ;; esac + +# ---------- arc B: signal + reload routing ---------- + +# initctl signal exits 0 iff the bus call succeeded. We send SIGCONT +# (no observable side-effect on a healthy daemon) so the test stays +# benign regardless of how keventd handles it. +say "initctl signal routes through Manager1.Signal" +texec initctl signal keventd CONT >/dev/null \ + || fail "initctl signal returned non-zero" +assert "initctl signal ok" 0 -eq 0 + +say "initctl signal on a bogus identity reports NoSuchService" +set +e +texec initctl signal no-such-svc-anywhere CONT >/tmp/dbus-sig-bad.out 2>&1 +sigbad_rc=$? +set -e +assert "Bogus signal target rejected (rc=$sigbad_rc)" "$sigbad_rc" -ne 0 +case "$(cat /tmp/dbus-sig-bad.out)" in + *"no such task or service"*) + assert "Error message mentions missing service" 0 -eq 0 ;; + *) + fail "Unexpected initctl signal output: $(cat /tmp/dbus-sig-bad.out)" ;; +esac + +say "initctl reload routes through Service1.Reload" +texec initctl reload keventd >/dev/null \ + || fail "initctl reload keventd returned non-zero" +assert "Per-service reload ok" 0 -eq 0 diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh index e26b4c11..c3bdce33 100755 --- a/test/dbus-manager.sh +++ b/test/dbus-manager.sh @@ -67,3 +67,69 @@ case "$result" in *InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;; *) fail "Unexpected reply from non-root ListServices: $result" ;; esac + +# ---------- Properties ---------- + +say "Introspect on /org/finit/manager advertises org.freedesktop.DBus.Properties" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) +case "$xml" in + *'org.freedesktop.DBus.Properties'*) assert "Properties interface in XML" 0 -eq 0 ;; + *) fail "Properties interface missing from XML" ;; +esac + +say "Manager1 declares Runlevel + Version as in introspection XML" +case "$xml" in + *'/dev/null \ + || fail "SetDebug returned non-zero" +# Toggle back so this test leaves debug in the same state we found it +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/dev/null || true +assert "SetDebug round-trip ok" 0 -eq 0 + +say "Manager1.SetDebug from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/tmp/dbus-setdbg.out 2>&1 +sdbg_rc=$? +set -e +assert "Non-root SetDebug rejected (rc=$sdbg_rc)" "$sdbg_rc" -eq 1 +case "$(cat /tmp/dbus-setdbg.out)" in + *AccessDenied*) assert "SetDebug authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-setdbg.out)" ;; +esac + +# Suspend would actually suspend the test sysroot if it succeeded -- so +# we only test the non-root rejection path, which fails before suspend() +# is called. +say "Manager1.Suspend from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 Suspend >/tmp/dbus-susp.out 2>&1 +susp_rc=$? +set -e +assert "Non-root Suspend rejected (rc=$susp_rc)" "$susp_rc" -eq 1 +case "$(cat /tmp/dbus-susp.out)" in + *AccessDenied*) assert "Suspend authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-susp.out)" ;; +esac + +say "initctl runlevel reads via Properties.Get when D-Bus available" +# runlevel output format is " ", e.g. "N 2". Just check +# we get a sensible two-token line. +rl=$(texec initctl runlevel) +case "$rl" in + [N0-9S]\ [0-9S]) + assert "initctl runlevel returned '$rl'" 0 -eq 0 ;; + *) + fail "Unexpected initctl runlevel output: $rl" ;; +esac diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 53f85223..fd0e3bb2 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -175,8 +175,6 @@ static int drop_uid(const char *uid_arg, const char *progname) static int mode_hello(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *name; int rc; @@ -188,9 +186,7 @@ static int mode_hello(int argc, char *argv[]) "org.freedesktop.DBus", "Hello", NULL); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - if (link_r_string(&reader, &name) == 0) + if (link_reply_get_string(link_client_reply(c), &name) == 0) printf("%s\n", name); else rc = 2; @@ -202,8 +198,6 @@ static int mode_hello(int argc, char *argv[]) static int mode_introspect(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *xml; int rc; @@ -216,9 +210,7 @@ static int mode_introspect(int argc, char *argv[]) "Introspect", NULL); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - if (link_r_string(&reader, &xml) == 0) + if (link_reply_get_string(link_client_reply(c), &xml) == 0) printf("%s\n", xml); else rc = 2; @@ -227,23 +219,17 @@ static int mode_introspect(int argc, char *argv[]) return rc; } -/* Decode body with signature "as" -- u32 array byte-len, then "s" strings. - * libink's public reader doesn't yet have an array helper, so we walk - * the wire form with link_r_u32 + link_r_string + link_r_pos. */ +/* Decode body with signature "as", print one string per line. */ static int print_string_array(const link_reply_t *r) { link_reader_t reader; - uint32_t array_len; size_t end; if (!r || !r->signature || strcmp(r->signature, "as") != 0) return -1; link_reader_init(&reader, r->body, r->body_len); - if (link_r_u32(&reader, &array_len) < 0) - return -1; - end = link_r_pos(&reader) + array_len; - if (end > r->body_len) + if (link_r_array_begin(&reader, &end) < 0) return -1; while (link_r_pos(&reader) < end) { @@ -330,8 +316,6 @@ static int mode_call_void_as_uid(int argc, char *argv[]) static int mode_get_service(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *path; int rc; @@ -344,11 +328,8 @@ static int mode_get_service(int argc, char *argv[]) "s", argv[3]); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - /* Reply signature is "o" but link_r_path / link_r_string - * have the same wire form. */ - if (link_r_path(&reader, &path) == 0) + /* Reply sig is "o", same wire form as "s". */ + if (link_reply_get_string(link_client_reply(c), &path) == 0) printf("%s\n", path); else rc = 2;