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;