From 6310d9e760495d51f9504058f31486c4dd719347 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:26 +0200 Subject: [PATCH] initctl: the status views over D-Bus The summary table, the per-service detail, JSON and the quiet and ident forms all read state Finit already publishes, so they read it from the bus like everything else rather than through a second path that has to be kept in step. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 10 +- doc/dbus.md | 35 ++ libink/builtin.c | 2 + libink/dispatch.c | 2 + libink/link.h | 10 +- libink/marshal.c | 89 ++- libink/marshal.h | 3 + src/dbus.c | 253 ++++++++- src/initctl.c | 1028 +++++++++++++++++++++++++---------- src/service.c | 2 +- src/service.h | 1 + test/Makefile.am | 2 + test/dbus-initctl.sh | 48 ++ test/dbus-introspect.sh | 31 ++ test/dbus-service.sh | 28 +- test/src/dbus-auth-client.c | 42 ++ 16 files changed, 1271 insertions(+), 315 deletions(-) create mode 100755 test/dbus-introspect.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec7aeab..84e7cae3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,16 +62,16 @@ jobs: tree /tmp || true - name: Check dependencies run: | - ldd /tmp/sbin/finit + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/finit size /tmp/sbin/finit - ldd /tmp/sbin/initctl + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/initctl size /tmp/sbin/initctl - ldd /tmp/sbin/reboot + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/reboot size /tmp/sbin/reboot - name: Verify starting and showing usage text run: | - sudo /tmp/sbin/finit -h - sudo /tmp/sbin/initctl -h + sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/finit -h + sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/initctl -h - name: Enable unprivileged userns (unshare) run: | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 diff --git a/doc/dbus.md b/doc/dbus.md index 3b035909..60efff2d 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -127,6 +127,41 @@ look up the exact path rather than constructing it by hand. | `Restart` | — | — | yes | … | | `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | +### Properties + +All read-only; observable via `Properties.Get` and `Properties.GetAll`. + +| Property | Type | Returns | +| -------------- | ---- | ------- | +| `Identity` | `s` | Service identity, `name` or `name:id`. | +| `Name` | `s` | Program name (basename of the command). | +| `State` | `s` | Current status, same vocabulary as `initctl status` (richer than the coarse `ServiceStateChanged` strings). | +| `Pid` | `u` | Current PID, 0 when not running. | +| `RestartCount` | `u` | Restarts since the last stable run. | +| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S. | +| `Description` | `s` | The service's `description` string. | +| `Command` | `s` | Full command line, arguments included. | +| `Conditions` | `s` | Declared conditions, raw `.conf` form. | +| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free`. | +| `Origin` | `s` | Source `.conf` file, empty for built-ins. | +| `Environment` | `s` | The service's `env` setting, raw. | +| `PidFile` | `s` | Declared PID file, raw (`!` prefix included). | +| `User` | `s` | User the service runs as. | +| `Group` | `s` | Group the service runs as. | +| `Uptime` | `u` | Seconds since start, 0 when not running. | +| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit. | +| `RestartsTotal`| `u` | Restarts over the service's lifetime. | +| `RestartMax` | `u` | Restart limit before the service is blocked. | +| `Starts` | `u` | Times started, for `manual-start` units. | +| `ManualStart` | `b` | `manual-start` set in the `.conf`. | +| `Forking` | `b` | Daemon forks to background. | +| `Started` | `b` | Run/task completed successfully. | + +On every state transition the object also emits the standard +`org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State` +in the changed dictionary, `Pid` and `RestartCount` invalidated (call +`Get` for fresh values). + 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. diff --git a/libink/builtin.c b/libink/builtin.c index 0d7a677b..f1d5dd23 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -333,6 +333,7 @@ static int handle_properties_get(link_connection_t *conn, const struct link_msg if (!p->getter) break; __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_sig(&w, p->sig ? p->sig : "s"); if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0) return __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", @@ -380,6 +381,7 @@ static int handle_properties_get_all(link_connection_t *conn, const struct link_ continue; __w_struct_begin(&w); __w_string(&w, p->name); + __w_sig(&w, p->sig ? p->sig : "s"); if (p->getter(&w, e->userdata) != 0) return __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", diff --git a/libink/dispatch.c b/libink/dispatch.c index 9d3d322b..2899d8ff 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -312,6 +312,8 @@ 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_begin (link_reader_t *r, char *type) { return __r_variant_begin(r, type); } +int link_r_skip_basic (link_reader_t *r, char type) { return __r_skip_basic(r, type); } 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); } diff --git a/libink/link.h b/libink/link.h index 01a61c11..265905cb 100644 --- a/libink/link.h +++ b/libink/link.h @@ -129,9 +129,11 @@ typedef struct { } 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. */ + * is not yet implemented; only Get and GetAll are. The framework + * emits the variant signature from `sig`; the getter writes only the + * bare value into the provided writer (link_w_string for "s", + * link_w_u32 for "u", ...), so the declared type is the single + * source of truth. */ typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); typedef struct { @@ -329,6 +331,8 @@ 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_begin (link_reader_t *r, char *type); /* sig header, cursor at value */ +int link_r_skip_basic (link_reader_t *r, char type); /* skip one basic value */ 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); diff --git a/libink/marshal.c b/libink/marshal.c index ae215893..48f00363 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -110,6 +110,7 @@ void __w_variant_string(struct link_writer *w, const char *s) __w_string(w, s); } + static size_t element_align(char c) { switch (c) { @@ -262,32 +263,88 @@ 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); } +/* + * Parse a variant's signature header, i.e. "g" wire form: 1-byte + * length, bytes, NUL. Only single-character inner signatures are + * supported. On success the cursor sits at the value and the type + * code is returned in *type. + */ +int __r_variant_begin(struct link_reader *r, char *type) +{ + uint8_t sig_len; + + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) + goto fail; + sig_len = r->base[r->off++]; + if (sig_len != 1 || r->off + 2 > r->cap) + goto fail; + if (r->base[r->off + 1] != 0) + goto fail; + *type = (char)r->base[r->off]; + r->off += 2; + return 0; +fail: + r->err = 1; + return -1; +} + /* 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; + char type; *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) { + if (__r_variant_begin(r, &type) < 0) + return -1; + if (type != 's') { r->err = 1; return -1; } - *out = (const char *)(r->base + r->off); - r->off += (size_t)slen + 1; - return 0; + return read_string_like(r, out); +} + +/* + * Skip one basic value of the given type code, as returned by + * __r_variant_begin(). Lets a{sv} consumers tolerate value types + * they don't know. Returns -1 on non-basic types. + */ +int __r_skip_basic(struct link_reader *r, char type) +{ + const char *s; + uint32_t u; + uint8_t y; + + switch (type) { + case 's': + case 'o': + return read_string_like(r, &s); + case 'b': + case 'u': + case 'i': + return __r_u32(r, &u); + case 'y': + return __r_byte(r, &y); + default: + r->err = 1; + return -1; + } +} + +/* Read a variant "v" expected to contain a uint32. */ +int __r_variant_u32(struct link_reader *r, uint32_t *out) +{ + char type; + + if (__r_variant_begin(r, &type) < 0) + return -1; + if (type != 'u') { + r->err = 1; + return -1; + } + + return __r_u32(r, out); } int __r_done(const struct link_reader *r) diff --git a/libink/marshal.h b/libink/marshal.h index 313205b2..3246d3e7 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -44,7 +44,10 @@ 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_begin (struct link_reader *r, char *type); /* sig header, cursor at value */ +int __r_skip_basic (struct link_reader *r, char type); /* skip one basic value */ int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ +int __r_variant_u32 (struct link_reader *r, uint32_t *out); /* "v" containing "u" */ 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); diff --git a/src/dbus.c b/src/dbus.c index 109c8a5e..8f98b3d1 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -65,14 +65,23 @@ static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); static link_server_t *server; static uev_t accept_watcher; static size_t peer_count; +static struct peer *sysbus_peer; + +static void sysbus_probe(void); static void peer_drop(struct peer *p) { + int was_sysbus = p == sysbus_peer; + uev_io_stop(&p->watcher); link_connection_close(p->conn); TAILQ_REMOVE(&peers, p, link); peer_count--; free(p); + + /* broker gone; the notify paths probe for its return */ + if (was_sysbus) + sysbus_peer = NULL; } static void peer_cb(uev_t *w, void *arg, int events) @@ -456,7 +465,7 @@ static int prop_runlevel(link_writer_t *w, void *u) (void)u; snprintf(buf, sizeof(buf), "%d", runlevel); - link_w_variant_string(w, buf); + link_w_string(w, buf); return 0; } @@ -466,14 +475,14 @@ static int prop_prevrunlevel(link_writer_t *w, void *u) (void)u; snprintf(buf, sizeof(buf), "%d", prevlevel); - link_w_variant_string(w, buf); + link_w_string(w, buf); return 0; } static int prop_version(link_writer_t *w, void *u) { (void)u; - link_w_variant_string(w, PACKAGE_VERSION); + link_w_string(w, PACKAGE_VERSION); return 0; } @@ -576,9 +585,146 @@ static const link_method_t service_methods[] = { { NULL, NULL, NULL, 0, NULL } }; +/* + * Getters write the bare value; the framework emits the variant + * signature from the table below. `State` deliberately uses the + * initctl status vocabulary from svc_status(), not the coarser + * ServiceStateChanged strings -- a client that only tracks edges + * has the signal, a client that asks gets the full story. + */ +#define SVC_PROP_STR(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_string(w, ((svc_t *)arg)->field); \ + return 0; \ +} + +#define SVC_PROP_U32(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_u32(w, (uint32_t)((svc_t *)arg)->field); \ + return 0; \ +} + +#define SVC_PROP_BOOL(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_bool(w, ((svc_t *)arg)->field); \ + return 0; \ +} + +static int svc_prop_identity(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_ident(arg, NULL, 0)); + return 0; +} + +static int svc_prop_state(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_status(arg)); + return 0; +} + +static int svc_prop_type(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_typestr((svc_t *)arg)); + return 0; +} + +static int svc_prop_command(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + char buf[512]; + + compose_cmdline(svc, buf, sizeof(buf)); + if (svc_is_sysv(svc)) { + strlcat(buf, " ", sizeof(buf)); + strlcat(buf, svc->state == SVC_HALTED_STATE + ? "stop" : "start", sizeof(buf)); + } + + link_w_string(w, buf); + return 0; +} + +static int svc_prop_pid(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + + link_w_u32(w, svc->pid > 0 ? (uint32_t)svc->pid : 0); + return 0; +} + +static int svc_prop_restarts(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + + link_w_u32(w, svc->restart_cnt > 0 ? (uint32_t)svc->restart_cnt : 0); + return 0; +} + +static int svc_prop_uptime(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + long up = 0; + + if (svc->pid > 0) { + up = jiffies() - svc->start_time; + if (up < 0) + up = 0; + } + link_w_u32(w, (uint32_t)up); + return 0; +} + +SVC_PROP_STR (svc_prop_name, name) +SVC_PROP_STR (svc_prop_desc, desc) +SVC_PROP_STR (svc_prop_conditions, cond) +SVC_PROP_STR (svc_prop_origin, file) +SVC_PROP_STR (svc_prop_environ, env) +SVC_PROP_STR (svc_prop_pidfile, pidfile) +SVC_PROP_STR (svc_prop_user, username) +SVC_PROP_STR (svc_prop_group, group) +SVC_PROP_U32 (svc_prop_runlevels, runlevels) +SVC_PROP_U32 (svc_prop_exitstatus, status) +SVC_PROP_U32 (svc_prop_restarts_tot, restart_tot) +SVC_PROP_U32 (svc_prop_restart_max, restart_max) +SVC_PROP_U32 (svc_prop_starts, once) +SVC_PROP_BOOL(svc_prop_manual, manual) +SVC_PROP_BOOL(svc_prop_forking, forking) +SVC_PROP_BOOL(svc_prop_started, started) + +static const link_property_t service_properties[] = { + { .name = "Identity", .sig = "s", .getter = svc_prop_identity }, + { .name = "Name", .sig = "s", .getter = svc_prop_name }, + { .name = "State", .sig = "s", .getter = svc_prop_state }, + { .name = "Pid", .sig = "u", .getter = svc_prop_pid }, + { .name = "RestartCount", .sig = "u", .getter = svc_prop_restarts }, + { .name = "Runlevels", .sig = "u", .getter = svc_prop_runlevels }, + { .name = "Description", .sig = "s", .getter = svc_prop_desc }, + { .name = "Command", .sig = "s", .getter = svc_prop_command }, + { .name = "Conditions", .sig = "s", .getter = svc_prop_conditions }, + { .name = "Type", .sig = "s", .getter = svc_prop_type }, + { .name = "Origin", .sig = "s", .getter = svc_prop_origin }, + { .name = "Environment", .sig = "s", .getter = svc_prop_environ }, + { .name = "PidFile", .sig = "s", .getter = svc_prop_pidfile }, + { .name = "User", .sig = "s", .getter = svc_prop_user }, + { .name = "Group", .sig = "s", .getter = svc_prop_group }, + { .name = "Uptime", .sig = "u", .getter = svc_prop_uptime }, + { .name = "ExitStatus", .sig = "u", .getter = svc_prop_exitstatus }, + { .name = "RestartsTotal",.sig = "u", .getter = svc_prop_restarts_tot }, + { .name = "RestartMax", .sig = "u", .getter = svc_prop_restart_max }, + { .name = "Starts", .sig = "u", .getter = svc_prop_starts }, + { .name = "ManualStart", .sig = "b", .getter = svc_prop_manual }, + { .name = "Forking", .sig = "b", .getter = svc_prop_forking }, + { .name = "Started", .sig = "b", .getter = svc_prop_started }, + { NULL, NULL, NULL } +}; + static const link_vtable_t service_vtable = { - .interface = "org.finit.Service1", - .methods = service_methods, + .interface = "org.finit.Service1", + .methods = service_methods, + .properties = service_properties, }; /* Build the canonical object path for a service. Identity is @@ -693,10 +839,14 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) uint8_t body[256]; link_writer_t w; char ident[MAX_IDENT_LEN]; + char path[FINIT_SVC_PATH_MAX]; ssize_t blen; if (!svc) return; + + sysbus_probe(); + svc_ident(svc, ident, sizeof(ident)); link_writer_init(&w, body, sizeof(body)); @@ -709,6 +859,35 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", "ServiceStateChanged", "sss", body, (size_t)blen); + + /* + * Dual emission: Properties-aware clients track one object via + * the standard PropertiesChanged instead of filtering the + * manager-wide signal. Volatile numerics are invalidated, not + * marshalled -- interested clients re-Get. + */ + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, "org.finit.Service1"); + link_w_array_begin(&w, '{'); + link_w_struct_begin(&w); + link_w_string(&w, "State"); + link_w_variant_string(&w, svc_status(svc)); + link_w_struct_end(&w); + link_w_array_end(&w); + link_w_array_begin(&w, 's'); + link_w_string(&w, "Pid"); + link_w_string(&w, "RestartCount"); + link_w_array_end(&w); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal(path, "org.freedesktop.DBus.Properties", + "PropertiesChanged", "sa{sv}as", + body, (size_t)blen); } /* ---------- signal emission: RunlevelChanged ---------- @@ -960,6 +1139,8 @@ void dbus_notify_condition_change(const char *name, const char *state) if (!name || !state) return; + sysbus_probe(); + link_writer_init(&w, body, sizeof(body)); link_w_string(&w, name); link_w_string(&w, state); @@ -1018,17 +1199,18 @@ static int sysbus_request_name(link_client_t *c) return (result == 1) ? 0 : -1; } -static void try_attach_system_bus(uev_ctx_t *ctx) +static int try_attach_system_bus(uev_ctx_t *ctx) { link_client_t *c; link_connection_t *conn; + struct peer *p; int rc; c = link_client_open_timeout(SYSTEM_BUS_PATH, SYSTEM_BUS_TIMEOUT_MS); if (!c) { dbg("System bus unavailable at %s; skipping registration", SYSTEM_BUS_PATH); - return; + return -1; } rc = link_client_call_v(c, "/org/freedesktop/DBus", @@ -1036,13 +1218,13 @@ static void try_attach_system_bus(uev_ctx_t *ctx) if (rc != LINK_CALL_OK) { dbg("System-bus Hello failed (rc=%d); skipping", rc); link_client_close(c); - return; + return -1; } if (sysbus_request_name(c) < 0) { logit(LOG_WARNING, "Failed to claim %s on system bus", FINIT_BUS_NAME); link_client_close(c); - return; + return -1; } /* link_server_attach owns the fd from this point on whether it @@ -1050,14 +1232,55 @@ static void try_attach_system_bus(uev_ctx_t *ctx) * window. */ conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1); if (!conn) - return; + return -1; - if (!peer_register(ctx, conn)) { + p = peer_register(ctx, conn); + if (!p) { logit(LOG_WARNING, "Failed registering system-bus peer"); - return; + return -1; } + sysbus_peer = p; logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); + return 0; +} + +/* + * The broker is usually not up yet when dbus_init() runs -- it is + * typically a finit service itself -- and it may restart at any + * time. The service and condition notify paths call sysbus_probe() + * on every event: when org.finit is unclaimed and the broker's + * socket exists, one coalesced attach attempt is scheduled. This + * stays daemon-agnostic -- only the socket is probed, never a + * service name -- and the broker's own service transitions are what + * trigger it. + */ +static uev_t sysbus_tmr; +static int sysbus_tmr_up; + +static void sysbus_probe_cb(uev_t *w, void *arg, int events) +{ + (void)arg; + (void)events; + + if (!sysbus_peer) + (void)try_attach_system_bus(w->ctx); +} + +static void sysbus_probe(void) +{ + if (sysbus_peer || !server) + return; + if (access(SYSTEM_BUS_PATH, F_OK)) + return; + + /* coalesce bursts to a single probe */ + if (!sysbus_tmr_up) + sysbus_tmr_up = !uev_timer_init(ctx, &sysbus_tmr, + sysbus_probe_cb, NULL, + 200, 0); + else + uev_timer_set(&sysbus_tmr, 200, 0); } /* ---------- init / exit ---------- */ @@ -1107,7 +1330,7 @@ int dbus_init(uev_ctx_t *ctx) dbus_register_service(svc); } - try_attach_system_bus(ctx); + (void)try_attach_system_bus(ctx); return 0; } @@ -1116,6 +1339,10 @@ int dbus_exit(void) { struct peer *p; + if (sysbus_tmr_up) { + uev_timer_stop(&sysbus_tmr); + sysbus_tmr_up = 0; + } uev_io_stop(&accept_watcher); while ((p = TAILQ_FIRST(&peers))) diff --git a/src/initctl.c b/src/initctl.c index 6379bc9a..f246a861 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -79,7 +79,6 @@ int iw, pw; extern int reboot_main(int argc, char *argv[]); - /* figure ut width of IDENT and PID columns */ static void col_widths(void) { @@ -174,21 +173,12 @@ static int toggle_debug(char *arg) return client_send(&rq, sizeof(rq)); } -static int do_log(svc_t *svc, char *tail) +static int do_log_named(const char *nm, pid_t pid, char *tail) { const char *logfile = "/var/log/syslog"; - pid_t pid; - char *nm; - if (svc) { - nm = svc_ident(svc, NULL, 0); - pid = svc->pid; - if (!pid) - return 0; /* not running */ - } else { - nm = "finit"; - pid = 1; - } + if (!pid) + return 0; /* not running */ if (!fexist(logfile)) { logfile = "/var/log/messages"; @@ -199,6 +189,13 @@ static int do_log(svc_t *svc, char *tail) return systemf("cat %s | grep '\\[%d\\]\\|%s' %s", logfile, pid, nm, tail); } +static int do_log(svc_t *svc, char *tail) +{ + if (svc) + return do_log_named(svc_ident(svc, NULL, 0), svc->pid, tail); + return do_log_named("finit", 1, tail); +} + static int show_log(char *arg) { svc_t *svc = NULL; @@ -213,6 +210,19 @@ static int show_log(char *arg) } #ifdef HAVE_DBUS +/* + * Advance one a{sv} dict entry: key + variant type; the caller reads + * the value with the link_r_* matching `type`. + */ +static int dbus_dict_next(link_reader_t *r, const char **key, char *type) +{ + if (link_r_align(r, 8) < 0) + return -1; + if (link_r_string(r, key) < 0) + return -1; + return link_r_variant_begin(r, type); +} + /* 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 @@ -254,12 +264,13 @@ static int dbus_get_manager_props(const char *const *wanted, char **out, size_t while (link_r_pos(&reader) < end) { const char *key = NULL; const char *val = NULL; + char type; 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; + if (dbus_dict_next(&reader, &key, &type) < 0) + goto fail; + if (type != 's' || link_r_string(&reader, &val) < 0) + goto fail; for (i = 0; wanted[i]; i++) { if (!strcmp(key, wanted[i])) { @@ -271,7 +282,11 @@ static int dbus_get_manager_props(const char *const *wanted, char **out, size_t link_client_close(c); return 0; +fail: + link_client_close(c); + return -1; } + #endif static int do_runlevel(char *arg) @@ -431,20 +446,28 @@ static int try_dbus_manager(const char *method, const char *arg_sig, return (rc == LINK_CALL_OK) ? 0 : -1; } +/* Build the object path for a service identity, same encoding as the + * server side; use instead of a Manager1.GetService round-trip. */ +static int dbus_svc_path(const char *ident, char *path, size_t len) +{ + const char *prefix = "/org/finit/service/"; + size_t plen = strlen(prefix); + + if (!ident || !*ident || plen >= len) + return -1; + memcpy(path, prefix, plen); + return link_path_encode(ident, path + plen, len - plen); +} + /* 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) + if (dbus_svc_path(ident, path, sizeof(path)) < 0) return -1; c = link_client_open(FINIT_BUS_SOCKET); @@ -893,16 +916,16 @@ static int do_cond_get(char *arg) { return do_cond_act(arg, COND_GET); } static int do_cond_set(char *arg) { return do_cond_act(arg, COND_SET); } static int do_cond_clr(char *arg) { return do_cond_act(arg, COND_CLR); } -static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) +static char *cond_string(const char *condstr, char *buf, size_t len, int ansi) { char *cond, *conds; buf[0] = 0; - if (!svc->cond[0]) + if (!condstr || !condstr[0]) return buf; - conds = strdupa(svc->cond); + conds = strdupa(condstr); if (!conds) return buf; @@ -954,6 +977,11 @@ static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) return buf; } +static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) +{ + return cond_string(svc->cond, buf, len, ansi); +} + static int do_cond_show(char *arg) { enum cond_state cond; @@ -1267,7 +1295,6 @@ char *runlevel_string(int currlevel, int levels) strlcat(lvl, i == INIT_LEVEL ? "S" : l, sizeof(lvl)); - if (!plain && currlevel == i) strlcat(lvl, "\e[0m", sizeof(lvl)); } else { @@ -1281,7 +1308,6 @@ char *runlevel_string(int currlevel, int levels) i++; } while (i < INIT_LEVEL); - strlcat(lvl, "]", sizeof(lvl)); return lvl; @@ -1313,23 +1339,9 @@ char *runlevel_arr(int levels) return lvl; } -static int missing(svc_t *svc) +static char *svc_command(svc_t *svc, char *buf, size_t len) { - if (svc->state == SVC_HALTED_STATE && svc_is_missing(svc)) - return 1; - - return 0; -} - -static char *svc_command(svc_t *svc, char *buf, size_t len, int ansi) -{ - int bold = missing(svc) && ansi; - - if (whichp(svc->cmd)) - bold = 0; - - strlcpy(buf, bold ? "\e[1m" : "", len); - strlcat(buf, svc->cmd, len); + strlcpy(buf, svc->cmd, len); for (int i = 1; i < MAX_NUM_SVC_ARGS; i++) { if (!svc->args[i][0]) @@ -1346,40 +1358,24 @@ static char *svc_command(svc_t *svc, char *buf, size_t len, int ansi) strlcat(buf, cmd, len); } - strlcat(buf, bold ? "\e[0m" : "", len); - return buf; } -static char *svc_environ(svc_t *svc, char *buf, size_t len, int ansi) -{ - int bold = missing(svc); - - if (!ansi || svc_checkenv(svc)) - bold = 0; - - strlcpy(buf, bold ? "\e[1m" : "", len); - strlcat(buf, svc->env, len); - strlcat(buf, bold ? "\e[0m" : "", len); - - return buf; -} - -static char *exit_status(svc_t *svc, char *buf, size_t len) +static char *exit_status_raw(int status, int manual, char *buf, size_t len) { int rc, sig; char *str; - rc = WEXITSTATUS(svc->status); - sig = WTERMSIG(svc->status); + rc = WEXITSTATUS(status); + sig = WTERMSIG(status); - if (WIFEXITED(svc->status)) { + if (WIFEXITED(status)) { str = code2str(rc); snprintf(buf, len, " (code=exited, status=%d%s%s%s)", rc, str[0] ? "/" : "", str, - svc->manual ? ", manual=yes" : ""); + manual ? ", manual=yes" : ""); } - else if (WIFSIGNALED(svc->status)) { + else if (WIFSIGNALED(status)) { str = sig2str(sig); snprintf(buf, len, " (code=signal, status=%d%s%s)", sig, str[0] ? "/" : "", str); } @@ -1387,57 +1383,6 @@ static char *exit_status(svc_t *svc, char *buf, size_t len) return buf; } -static char *status(svc_t *svc, int full) -{ - static char buf[96]; - const char *color; - char ok[48] = {0}; - char *s; - - s = svc_status(svc); - switch (svc->state) { - case SVC_HALTED_STATE: - exit_status(svc, ok, sizeof(ok)); - color = "\e[1m"; - break; - - case SVC_RUNNING_STATE: - color = "\e[1;32m"; - break; - - case SVC_DONE_STATE: - exit_status(svc, ok, sizeof(ok)); - if (WIFEXITED(svc->status)) { - if (WEXITSTATUS(svc->status)) - color = "\e[1;31m"; - else - color = "\e[1;32m"; - } else { - if (full && WIFSIGNALED(svc->status)) - color = "\e[1;31m"; - else - color = "\e[1;33m"; - } - break; - - default: - exit_status(svc, ok, sizeof(ok)); - color = "\e[1;33m"; - break; - } - - if (!full || plain) - color = NULL; - - if (!full) - snprintf(buf, sizeof(buf), "%-8.8s", s); - else - snprintf(buf, sizeof(buf), "%s%s%s%s", - color ? color : "", s, ok, color ? "\e[0m" : ""); - - return buf; -} - static void show_cgroup_tree(char *group, char *pfx) { char path[256]; @@ -1475,6 +1420,176 @@ static int svc_compare(svc_t *svc, char *arg) return 0; } +/* + * All fields the status views need, transport-independent: filled + * from a raw svc_t on the legacy socket path or from Service1 + * properties on the D-Bus path, rendered by the same printers. + */ +struct svc_row { + char ident[MAX_IDENT_LEN]; + char state[16]; + char type[16]; + char desc[MAX_STR_LEN]; + char cmdline[512]; + char env[MAX_CMD_LEN]; + char cond[MAX_COND_LEN]; + char pidfile[MAX_CMD_LEN]; + char user[MAX_USER_LEN]; + char grp[MAX_USER_LEN]; + char origin[MAX_ARG_LEN]; + uint32_t pid; + uint32_t runlevels; + uint32_t uptime; /* seconds, 0 when not running */ + uint32_t exitstatus; /* raw waitpid(2) status */ + uint32_t starts; + int32_t restart_tot; + int32_t restart_cnt; + int32_t restart_max; /* -1 = restart:always */ + int manual; + int forking; + int started; +}; + +static void fill_row_from_svc(struct svc_row *r, svc_t *svc) +{ + long now = jiffies(); + + memset(r, 0, sizeof(*r)); + svc_ident(svc, r->ident, sizeof(r->ident)); + strlcpy(r->state, svc_status(svc), sizeof(r->state)); + strlcpy(r->type, svc_typestr(svc), sizeof(r->type)); + strlcpy(r->desc, svc->desc, sizeof(r->desc)); + svc_command(svc, r->cmdline, sizeof(r->cmdline)); + strlcpy(r->env, svc->env, sizeof(r->env)); + strlcpy(r->cond, svc->cond, sizeof(r->cond)); + strlcpy(r->pidfile, svc->pidfile, sizeof(r->pidfile)); + strlcpy(r->user, svc->username, sizeof(r->user)); + strlcpy(r->grp, svc->group, sizeof(r->grp)); + strlcpy(r->origin, svc->file, sizeof(r->origin)); + r->pid = svc->pid > 0 ? (uint32_t)svc->pid : 0; + r->runlevels = (uint32_t)svc->runlevels; + if (svc->pid && now > svc->start_time) + r->uptime = (uint32_t)(now - svc->start_time); + r->exitstatus = (uint32_t)svc->status; + r->starts = (uint32_t)svc->once; + r->restart_tot = (int32_t)svc->restart_tot; + r->restart_cnt = (int32_t)svc->restart_cnt; + r->restart_max = (int32_t)svc->restart_max; + r->manual = svc->manual; + r->forking = svc->forking; + r->started = svc->started; +} + +static const char *pidfile_str(struct svc_row *r) +{ + const char *pidfn = r->pidfile; + + if (pidfn[0] == '!') + pidfn++; + else if (pidfn[0] == 0) + pidfn = "none"; + + return pidfn; +} + +/* string-only twin of svc_checkenv() */ +static int checkenv_str(const char *env) +{ + if (!env || !env[0] || env[0] == '-') + return 1; + + return fexist(env); +} + +/* same matching rules as svc_compare() */ +static int row_compare(struct svc_row *r, char *arg) +{ + char ident[MAX_IDENT_LEN]; + char *ptr; + + strlcpy(ident, r->ident, sizeof(ident)); + ptr = strchr(ident, ':'); + if (ptr && !strchr(arg, ':')) + *ptr = 0; + + return !strcmp(ident, arg); +} + +static int row_missing(struct svc_row *r) +{ + char argv0[512]; + char *sep; + + if (strcmp(r->state, "missing")) + return 0; + + strlcpy(argv0, r->cmdline, sizeof(argv0)); + sep = strchr(argv0, ' '); + if (sep) + *sep = 0; + + return !whichp(argv0); +} + +/* the words svc_status() emits for a blocked/halted service */ +static const char *halted_words[] = { + "halted", "missing", "crashed", "stopped", "busy", + "restart", "conflict", "unknown", NULL +}; + +/* transport-independent status(svc, full) */ +static char *row_status(struct svc_row *r, int full) +{ + static char buf[96]; + const char *color; + char ok[48] = {0}; + int st = (int)r->exitstatus; + size_t i; + + if (!full) { + snprintf(buf, sizeof(buf), "%-8.8s", r->state); + return buf; + } + + if (!strcmp(r->state, "running")) { + color = "\e[1;32m"; + } else if (!strcmp(r->state, "done") || !strcmp(r->state, "failed")) { + exit_status_raw(st, r->manual, ok, sizeof(ok)); + if (WIFEXITED(st)) + color = WEXITSTATUS(st) ? "\e[1;31m" : "\e[1;32m"; + else if (WIFSIGNALED(st)) + color = "\e[1;31m"; + else + color = "\e[1;33m"; + } else { + exit_status_raw(st, r->manual, ok, sizeof(ok)); + color = "\e[1;33m"; + for (i = 0; halted_words[i]; i++) { + if (!strcmp(r->state, halted_words[i])) { + color = "\e[1m"; + break; + } + } + } + + if (plain) + color = NULL; + + snprintf(buf, sizeof(buf), "%s%s%s%s", + color ? color : "", r->state, ok, color ? "\e[0m" : ""); + + return buf; +} + +/* scripting mode: exit code only, same rules as the legacy path */ +static int quiet_row(struct svc_row *r) +{ + if (!strcmp(r->type, "run") || !strcmp(r->type, "task")) + return r->started ? 0 : 1; + + return strcmp(r->state, "running") != 0; +} + /* * Escape a string for safe JSON output. Handles quotes, backslashes, * and control characters. Returns pointer to static buffer. @@ -1531,46 +1646,40 @@ static char *json_escape(const char *str) return buf; } -static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) +static int json_status_one(FILE *fp, struct svc_row *r, char *indent, int prev) { - long now = jiffies(); - char *pidfn = NULL; + const char *pidfn = pidfile_str(r); char buf[512]; - pidfn = svc->pidfile; - if (pidfn[0] == '!') - pidfn++; - else if (pidfn[0] == 0) - pidfn = "none"; - fprintf(fp, "%s" "%s{\n" "%s \"identity\": \"%s\",\n", prev ? ",\n" : indent, prev ? indent : "", - indent, svc_ident(svc, NULL, 0)); + indent, r->ident); fprintf(fp, "%s \"description\": \"%s\",\n", - indent, json_escape(svc->desc)); + indent, json_escape(r->desc)); fprintf(fp, "%s \"type\": \"%s\",\n" "%s \"forking\": %s,\n" "%s \"status\": \"%s\",\n", - indent, svc_typestr(svc), - indent, svc->forking ? "true" : "false", - indent, svc_status(svc)); + indent, r->type, + indent, r->forking ? "true" : "false", + indent, r->state); - if (svc->state != SVC_RUNNING_STATE) { + if (strcmp(r->state, "running")) { + int st = (int)r->exitstatus; int rc, sig; - rc = WEXITSTATUS(svc->status); - sig = WTERMSIG(svc->status); + rc = WEXITSTATUS(st); + sig = WTERMSIG(st); - if (WIFEXITED(svc->status)) + if (WIFEXITED(st)) fprintf(fp, "%s \"exit\": { \"%s\": %d },\n", indent, "code", rc); - else if (WIFSIGNALED(svc->status)) + else if (WIFSIGNALED(st)) fprintf(fp, "%s \"exit\": { \"%s\": %d },\n", indent, "signal", sig); @@ -1578,32 +1687,30 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) fprintf(fp, "%s \"origin\": \"%s\",\n", - indent, svc->file[0] ? svc->file : "built-in"); - svc_command(svc, buf, sizeof(buf), 0); + indent, r->origin[0] ? r->origin : "built-in"); fprintf(fp, "%s \"command\": \"%s\",\n", - indent, json_escape(buf)); + indent, json_escape(r->cmdline)); - svc_environ(svc, buf, sizeof(buf), 0); - if (buf[0]) + if (r->env[0]) fprintf(fp, - "%s \"environment\": \"%s\",\n", indent, json_escape(buf)); + "%s \"environment\": \"%s\",\n", indent, json_escape(r->env)); - svc_cond(svc, buf, sizeof(buf), 0); + cond_string(r->cond, buf, sizeof(buf), 0); if (buf[0]) fprintf(fp, "%s \"condition\": %s,\n", indent, buf); - if (svc->manual) + if (r->manual) fprintf(fp, - "%s \"starts\": %d,\n", indent, svc->once); + "%s \"starts\": %u,\n", indent, r->starts); fprintf(fp, - "%s \"restarts\": %d,\n", indent, svc->restart_tot); /* XXX: add restart_cnt and restart_max */ + "%s \"restarts\": %d,\n", indent, r->restart_tot); /* XXX: add restart_cnt and restart_max */ /* Add memory and CPU information if cgroup support is available */ - if (cgrp && svc->pid > 1) { - char *group = pid_cgroup(svc->pid); + if (cgrp && r->pid > 1) { + char *group = pid_cgroup(r->pid); if (group) { uint64_t throttled_usec = 0; @@ -1707,37 +1814,466 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) fprintf(fp, "%s \"pidfile\": \"%s\",\n" - "%s \"pid\": %d,\n" + "%s \"pid\": %u,\n" "%s \"user\": \"%s\",\n" "%s \"group\": \"%s\",\n" "%s \"uptime\": %ld,\n" "%s \"runlevels\": %s\n" "%s}", indent, pidfn, - indent, svc->pid, - indent, svc->username, - indent, svc->group, - indent, svc->pid ? now - svc->start_time : 0, - indent, runlevel_arr(svc->runlevels), + indent, r->pid, + indent, r->user, + indent, r->grp, + indent, (long)r->uptime, + indent, runlevel_arr((int)r->runlevels), indent); return 0; } +static void status_heading(int pidw, int identw) +{ + char title[80]; + + snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", + pidw, "PID", identw, "IDENT", "STATUS", "RUNLEVELS"); + strlcat(title, !verbose ? "DESCRIPTION" : "COMMAND", sizeof(title)); + print_header("%s", title); +} + +static void print_runlevels(const char *lvls) +{ + /* ANSI escapes for the active level eat into the field width */ + if (strchr(lvls, '\e')) + printf("%-21.21s ", lvls); + else + printf("%-13.13s ", lvls); +} + +/* + * The detail view, shared by the legacy and D-Bus paths; the caller + * fills a svc_row from its transport first. Cgroup statistics and + * the log tail are always local operations, they only need the PID. + */ +static int show_one_row(struct svc_row *r) +{ + char uptm[42] = "N/A"; + char buf[512]; + int bold; + + printf(" Status : %s\n", row_status(r, 1)); + printf(" Identity : %s\n", r->ident); + printf("Description : %s\n", r->desc); + printf(" Origin : %s\n", r->origin[0] ? r->origin : "built-in"); + + if (r->env[0]) { + bold = !plain && !strcmp(r->state, "missing") && + !checkenv_str(r->env); + printf("Environment : %s%s%s\n", bold ? "\e[1m" : "", + r->env, bold ? "\e[0m" : ""); + } + + cond_string(r->cond, buf, sizeof(buf), !plain); + if (buf[0]) + printf("Condition(s): <%s>\n", buf); + + bold = !plain && row_missing(r); + printf(" Command : %s%s%s\n", bold ? "\e[1m" : "", + r->cmdline, bold ? "\e[0m" : ""); + printf(" PID file : %s\n", pidfile_str(r)); + printf(" PID : %u\n", r->pid); + printf(" User : %s\n", r->user); + printf(" Group : %s\n", r->grp); + printf(" Uptime : %s\n", r->pid + ? uptime((long)r->uptime, uptm, sizeof(uptm)) : uptm); + if (r->manual) + printf(" Starts : %u\n", r->starts); + printf(" Restarts : %d (%d/%d)\n", + r->restart_tot, r->restart_cnt, r->restart_max); + printf(" Runlevels : %s\n", + runlevel_string(runlevel, (int)r->runlevels)); + + if (cgrp && r->pid > 1) { + const struct cg *cg; + uint64_t throttled_usec = 0; + uint64_t nr_throttled = 0; + char path[256]; + char *group; + + group = pid_cgroup(r->pid); + if (!group) + goto no_cgroup; /* ... or PID doesn't exist (anymore) */ + + snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group); + cg = cg_conf(path); + + printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm))); + + if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) { + printf("CPU Throttle : %lu usec (%lu times)\n", + throttled_usec, nr_throttled); + } + + printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n", + group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max, + cg->cg_mem.min, cg->cg_mem.max); + show_cgroup_tree(group, " "); + + free(group); + } +no_cgroup: + printf("\n"); + + return do_log_named(r->ident, r->pid, "| tail -10"); +} + +static void render_table(struct svc_row *rows, int n, char *filter) +{ + int i, identw = 0, pidw = 0; + + for (i = 0; i < n; i++) { + int w; + + w = (int)strlen(rows[i].ident); + if (w > identw) + identw = w; + w = snprintf(NULL, 0, "%u", rows[i].pid); + if (w > pidw) + pidw = w; + } + if (identw < 6) + identw = 6; + if (pidw < 3) + pidw = 3; + + if (heading) + status_heading(pidw, identw); + + for (i = 0; i < n; i++) { + struct svc_row *r = &rows[i]; + + if (filter && !row_compare(r, filter)) + continue; + + printf("%-*u ", pidw, r->pid); + printf("%-*s %s ", identw, r->ident, row_status(r, 0)); + print_runlevels(runlevel_string(runlevel, (int)r->runlevels)); + + if (!verbose) + puts(r->desc); + else { + int bold = !plain && row_missing(r); + + printf("%s%s%s\n", bold ? "\e[1m" : "", + r->cmdline, bold ? "\e[0m" : ""); + } + } +} + +#ifdef HAVE_DBUS +/* + * Service1 wire properties -> svc_row fields. Unknown keys and + * value types are skipped so newer finit keeps working with older + * initctl. + */ +#define ROW_STR(k, f) { k, 's', offsetof(struct svc_row, f), \ + sizeof(((struct svc_row *)0)->f) } +#define ROW_U32(k, f) { k, 'u', offsetof(struct svc_row, f), 0 } +#define ROW_BOOL(k, f) { k, 'b', offsetof(struct svc_row, f), 0 } + +static const struct row_field { + const char *key; + char type; + size_t off; + size_t len; +} row_fields[] = { + ROW_STR ("State", state), + ROW_STR ("Type", type), + ROW_STR ("Description", desc), + ROW_STR ("Command", cmdline), + ROW_STR ("Environment", env), + ROW_STR ("Conditions", cond), + ROW_STR ("PidFile", pidfile), + ROW_STR ("User", user), + ROW_STR ("Group", grp), + ROW_STR ("Origin", origin), + ROW_U32 ("Pid", pid), + ROW_U32 ("Runlevels", runlevels), + ROW_U32 ("Uptime", uptime), + ROW_U32 ("ExitStatus", exitstatus), + ROW_U32 ("Starts", starts), + ROW_U32 ("RestartsTotal", restart_tot), + ROW_U32 ("RestartCount", restart_cnt), + ROW_U32 ("RestartMax", restart_max), + ROW_BOOL("ManualStart", manual), + ROW_BOOL("Forking", forking), + ROW_BOOL("Started", started), + { NULL, 0, 0, 0 } +}; + +/* + * One service's Properties.GetAll into a row. Returns 0 on success, + * 1 when the object vanished since ListServices (bus error reply), + * -1 on transport failure. + */ +static int dbus_fill_row(link_client_t *c, struct svc_row *row) +{ + const link_reply_t *r; + link_reader_t reader; + char path[256]; + size_t end; + int rc; + + if (dbus_svc_path(row->ident, path, sizeof(path)) < 0) + return -1; + + rc = link_client_call_v(c, path, + "org.freedesktop.DBus.Properties", + "GetAll", "s", "org.finit.Service1"); + if (rc == LINK_CALL_ERROR) + return 1; /* gone since ListServices */ + if (rc != LINK_CALL_OK) + return -1; + r = link_client_reply(c); + if (!r || !r->body) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) + return -1; + + while (link_r_pos(&reader) < end) { + const struct row_field *f; + const char *key, *val; + uint32_t u; + int b; + char type; + + if (dbus_dict_next(&reader, &key, &type) < 0) + return -1; + + for (f = row_fields; f->key; f++) + if (f->type == type && !strcmp(f->key, key)) + break; + + if (!f->key) { + if (link_r_skip_basic(&reader, type) < 0) + return -1; + continue; + } + + switch (type) { + case 's': + if (link_r_string(&reader, &val) < 0) + return -1; + strlcpy((char *)row + f->off, val, f->len); + break; + case 'u': + if (link_r_u32(&reader, &u) < 0) + return -1; + memcpy((char *)row + f->off, &u, sizeof(u)); + break; + case 'b': + if (link_r_bool(&reader, &b) < 0) + return -1; + memcpy((char *)row + f->off, &b, sizeof(b)); + break; + } + } + + return 0; +} + +/* + * ListServices into a caller-freed ident array. One round trip; + * show_ident() needs nothing more. Returns count or -1. + */ +static int dbus_list_idents(link_client_t *c, char (**out)[MAX_IDENT_LEN]) +{ + char (*idents)[MAX_IDENT_LEN] = NULL, (*tmp)[MAX_IDENT_LEN]; + const link_reply_t *r; + link_reader_t reader; + size_t n = 0, end; + int rc; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "ListServices", NULL); + if (rc != LINK_CALL_OK) + return -1; + + r = link_client_reply(c); + if (!r || !r->body) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) + return -1; + + while (link_r_pos(&reader) < end) { + const char *ident; + + if (link_r_string(&reader, &ident) < 0) + goto fail; + tmp = realloc(idents, (n + 1) * sizeof(*idents)); + if (!tmp) + goto fail; + idents = tmp; + /* copied: the next call clobbers the client rx buffer */ + strlcpy(idents[n], ident, sizeof(idents[n])); + n++; + } + + *out = idents; + return (int)n; +fail: + free(idents); + return -1; +} + +/* + * Rows for every service matching `arg` (all when NULL), fetched over + * D-Bus. Returns the row count (caller frees *out), or -1 to fall + * back to the legacy socket. + */ +static int dbus_fetch_svc_rows(char *arg, struct svc_row **out) +{ + char (*idents)[MAX_IDENT_LEN] = NULL; + struct svc_row *rows = NULL; + link_client_t *c; + int n, i, m = 0; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + n = dbus_list_idents(c, &idents); + if (n < 0) + goto fail; + + rows = calloc(n ? n : 1, sizeof(*rows)); + if (!rows) + goto fail; + + for (i = 0; i < n; i++) { + struct svc_row *row = &rows[m]; + int rc; + + strlcpy(row->ident, idents[i], sizeof(row->ident)); + if (arg && arg[0] && !row_compare(row, arg)) + continue; + + rc = dbus_fill_row(c, row); + if (rc < 0) + goto fail; + if (rc > 0) + continue; /* vanished, skip */ + m++; + } + + free(idents); + link_client_close(c); + *out = rows; + return m; +fail: + free(idents); + free(rows); + link_client_close(c); + return -1; +} + +/* + * All show_status() views over D-Bus, self-contained (the current + * runlevel comes from Manager1, not the legacy socket). Returns 0 + * with the command exit code in *retval, or -1 to fall back. + */ +static int dbus_show_status(char *arg, int *retval) +{ + static const char *const wanted[] = { "Runlevel", NULL }; + char curr[16] = "0"; + char *outv[] = { curr }; + struct svc_row *rows = NULL; + char *filter; + int n, i; + + n = dbus_fetch_svc_rows(arg, &rows); + if (n < 0) + return -1; + + *retval = 0; + + if (arg && arg[0]) { + if (!n) { + free(rows); + /* exits, exactly like the legacy path */ + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", arg); + } + if (n == 1) { + if (quiet) { + *retval = quiet_row(&rows[0]); + free(rows); + return 0; + } + /* runlevel feeds the detail view's Runlevels line */ + if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) + goto fail; + runlevel = atoi(curr); + if (json) { + *retval = json_status_one(stdout, &rows[0], "", 0); + puts(""); + } else + *retval = show_one_row(&rows[0]); + free(rows); + return 0; + } + /* several matches: filtered table below */ + } + + if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) + goto fail; + runlevel = atoi(curr); + + filter = (arg && arg[0]) ? arg : NULL; + if (json) { + int prev = 0; + + for (i = 0; i < n; i++) { + if (filter && !row_compare(&rows[i], filter)) + continue; + if (!prev) + fputs("[\n", stdout); + json_status_one(stdout, &rows[i], " ", prev++); + } + if (prev) + fputs("\n]\n", stdout); + } else + render_table(rows, n, filter); + + free(rows); + return 0; +fail: + free(rows); + return -1; +} +#endif /* HAVE_DBUS */ + static int show_status(char *arg) { - char ident[MAX_IDENT_LEN]; - char buf[512]; + struct svc_row row; int num = 0; svc_t *svc; +#ifdef HAVE_DBUS + { + int rc; + + if (dbus_show_status(arg, &rc) == 0) + return rc; + } +#endif + runlevel = runlevel_get(NULL); while (arg && arg[0]) { - long now = jiffies(); - char uptm[42] = "N/A"; - char *pidfn = NULL; - for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) num += svc_compare(svc, arg); @@ -1748,95 +2284,32 @@ static int show_status(char *arg) if (!svc) ERRX(noerr ? 0 : 69, "no such task or service(s): %s", arg); - if (quiet) { - if (svc_is_runtask(svc)) { - if (svc->started) - return 0; - return 1; - } - return svc->state != SVC_RUNNING_STATE; - } + fill_row_from_svc(&row, svc); + if (quiet) + return quiet_row(&row); if (json) { int rc; - rc = json_status_one(stdout, svc, "", 0); + rc = json_status_one(stdout, &row, "", 0); puts(""); return rc; } - pidfn = svc->pidfile; - if (pidfn[0] == '!') - pidfn++; - else if (pidfn[0] == 0) - pidfn = "none"; - - printf(" Status : %s\n", status(svc, 1)); - printf(" Identity : %s\n", svc_ident(svc, ident, sizeof(ident))); - printf("Description : %s\n", svc->desc); - printf(" Origin : %s\n", svc->file[0] ? svc->file : "built-in"); - svc_environ(svc, buf, sizeof(buf), !plain); - if (buf[0]) - printf("Environment : %s\n", buf); - svc_cond(svc, buf, sizeof(buf), !plain); - if (buf[0]) - printf("Condition(s): <%s>\n", buf); - printf(" Command : %s\n", svc_command(svc, buf, sizeof(buf), !plain)); - printf(" PID file : %s\n", pidfn); - printf(" PID : %d\n", svc->pid); - printf(" User : %s\n", svc->username); - printf(" Group : %s\n", svc->group); - printf(" Uptime : %s\n", svc->pid ? uptime(now - svc->start_time, uptm, sizeof(uptm)) : uptm); - if (svc->manual) - printf(" Starts : %d\n", svc->once); - printf(" Restarts : %d (%d/%d)\n", svc->restart_tot, svc->restart_cnt, svc->restart_max); - printf(" Runlevels : %s\n", runlevel_string(runlevel, svc->runlevels)); - - if (cgrp && svc->pid > 1) { - const struct cg *cg; - uint64_t throttled_usec = 0; - uint64_t nr_throttled = 0; - char path[256]; - char *group; - - group = pid_cgroup(svc->pid); - if (!group) - goto no_cgroup; /* ... or PID doesn't exist (anymore) */ - - snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group); - cg = cg_conf(path); - - printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm))); - - if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) { - printf("CPU Throttle : %lu usec (%lu times)\n", - throttled_usec, nr_throttled); - } - - printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n", - group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max, - cg->cg_mem.min, cg->cg_mem.max); - show_cgroup_tree(group, " "); - - free(group); - } - no_cgroup: - printf("\n"); - - return do_log(svc, "| tail -10"); + return show_one_row(&row); } if (json) { int prev = 0; for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { - svc_ident(svc, ident, sizeof(ident)); if (num && !svc_compare(svc, arg)) continue; if (!prev) fputs("[\n", stdout); - json_status_one(stdout, svc, " ", prev++); + fill_row_from_svc(&row, svc); + json_status_one(stdout, &row, " ", prev++); } if (prev) fputs("\n]\n", stdout); @@ -1844,64 +2317,71 @@ static int show_status(char *arg) return 0; } - col_widths(); - if (heading) { - char title[80]; + { + struct svc_row *rows = NULL, *tmp; + int n = 0; - snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", - pw, "PID", iw, "IDENT", "STATUS", "RUNLEVELS"); - if (!verbose) - strlcat(title, "DESCRIPTION", sizeof(title)); - else - strlcat(title, "COMMAND", sizeof(title)); - - print_header("%s", title); - } - - for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { - char *lvls; - - svc_ident(svc, ident, sizeof(ident)); - if (num && !svc_compare(svc, arg)) - continue; - - printf("%-*d ", pw, svc->pid); - printf("%-*s %s ", iw, ident, status(svc, 0)); - - lvls = runlevel_string(runlevel, svc->runlevels); - if (strchr(lvls, '\e')) - printf("%-21.21s ", lvls); - else - printf("%-13.13s ", lvls); - - if (!verbose) - puts(svc->desc); - else - puts(svc_command(svc, buf, sizeof(buf), !plain)); + for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { + tmp = realloc(rows, (n + 1) * sizeof(*rows)); + if (!tmp) { + free(rows); + return 1; + } + rows = tmp; + fill_row_from_svc(&rows[n++], svc); + } + render_table(rows, n, num > 1 ? arg : NULL); + free(rows); } return 0; } +static void ident_line(const char *identity, char *arg) +{ + size_t len; + char *pos; + + pos = strchr(identity, ':'); + if (pos) + len = (size_t)(pos - identity); + else + len = strlen(identity); + if (arg && arg[0] && strncasecmp(identity, arg, len)) + return; + + puts(identity); +} + static int show_ident(char *arg) { svc_t *svc; +#ifdef HAVE_DBUS + { + char (*idents)[MAX_IDENT_LEN]; + link_client_t *c; + int n = -1, i; + + c = link_client_open(FINIT_BUS_SOCKET); + if (c) { + n = dbus_list_idents(c, &idents); + link_client_close(c); + } + if (n >= 0) { + for (i = 0; i < n; i++) + ident_line(idents[i], arg); + free(idents); + return 0; + } + } +#endif + for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { char ident[MAX_IDENT_LEN]; - size_t len; - char *pos; svc_ident(svc, ident, sizeof(ident)); - pos = strchr(ident, ':'); - if (pos) - len = pos - ident; - else - len = strlen(ident); - if (arg && arg[0] && strncasecmp(ident, arg, len)) - continue; - - puts(ident); + ident_line(ident, arg); } return 0; diff --git a/src/service.c b/src/service.c index abb1a00e..3c2e05f4 100644 --- a/src/service.c +++ b/src/service.c @@ -520,7 +520,7 @@ static int is_norespawn(void) fexist("/tmp/norespawn"); } -static void compose_cmdline(svc_t *svc, char *buf, size_t len) +void compose_cmdline(svc_t *svc, char *buf, size_t len) { size_t i; diff --git a/src/service.h b/src/service.h index 4d6038a8..59699f3f 100644 --- a/src/service.h +++ b/src/service.h @@ -54,6 +54,7 @@ void service_ready_script (svc_t *svc); /* XXX: only for pidfile plugin b int service_timeout_after (svc_t *svc, int timeout, void (*cb)(svc_t *svc)); int service_timeout_cancel (svc_t *svc); +void compose_cmdline (svc_t *svc, char *buf, size_t len); void service_forked (svc_t *svc); void service_ready (svc_t *svc, int ready); diff --git a/test/Makefile.am b/test/Makefile.am index e5a3d5ac..98723738 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -77,6 +77,7 @@ EXTRA_DIST += dbus-manager.sh EXTRA_DIST += dbus-service.sh EXTRA_DIST += dbus-cond.sh EXTRA_DIST += dbus-initctl.sh +EXTRA_DIST += dbus-introspect.sh AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -139,6 +140,7 @@ TESTS += dbus-manager.sh TESTS += dbus-service.sh TESTS += dbus-cond.sh TESTS += dbus-initctl.sh +TESTS += dbus-introspect.sh endif check-recursive: setup-chroot diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh index 2a1b47c1..f903f0cf 100755 --- a/test/dbus-initctl.sh +++ b/test/dbus-initctl.sh @@ -94,3 +94,51 @@ 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 + +say "initctl status views render over D-Bus alone" +# hide the legacy socket: everything below must come over the bus +texec mv /run/finit/socket /run/finit/socket.hidden + +set +e +out=$(texec initctl -p status) +status_rc=$? +one=$(texec initctl -p status keventd) +one_rc=$? +texec initctl -q status keventd +quiet_rc=$? +jout=$(texec initctl -j status) +json_rc=$? +idout=$(texec initctl ident) +ident_rc=$? +set -e + +texec mv /run/finit/socket.hidden /run/finit/socket + +assert "status table ok without legacy socket (rc=$status_rc)" \ + "$status_rc" -eq 0 +case "$out" in + *keventd*running*) assert "keventd running in table" 0 -eq 0 ;; + *) fail "keventd/running missing from status table" ;; +esac + +assert "status detail view ok (rc=$one_rc)" "$one_rc" -eq 0 +case "$one" in + *"Status : running"*"Identity : keventd"*"Restarts :"*) + assert "detail view fields present" 0 -eq 0 ;; + *) fail "detail view incomplete: $one" ;; +esac + +assert "quiet mode exit code (rc=$quiet_rc)" "$quiet_rc" -eq 0 + +assert "json mode ok (rc=$json_rc)" "$json_rc" -eq 0 +case "$jout" in + *'"identity": "keventd"'*'"status": "running"'*) + assert "json fields present" 0 -eq 0 ;; + *) fail "json output incomplete: $jout" ;; +esac + +assert "ident ok (rc=$ident_rc)" "$ident_rc" -eq 0 +case "$idout" in + *keventd*) assert "ident lists keventd" 0 -eq 0 ;; + *) fail "ident missing keventd" ;; +esac diff --git a/test/dbus-introspect.sh b/test/dbus-introspect.sh new file mode 100755 index 00000000..08e4927c --- /dev/null +++ b/test/dbus-introspect.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# libink: introspection XML well-formedness. +# +# Covers compound type signatures in generated introspection XML: +# Cond1.Dump declares out_sig "a(ss)", which must appear as a single +# , not one per signature character. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Introspect on /org/finit/cond" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/cond) + +say "Cond1.Dump advertises a single a(ss) out arg" +case "$xml" in + *''*) assert "a(ss) intact" 0 -eq 0 ;; + *) fail "a(ss) not found as a single arg" ;; +esac + +say "No per-character type fragments in the XML" +case "$xml" in + *'type="("'* | *'type=")"'* | *'type="{"'* | *'type="}"'* | *'type="a"'*) + fail "per-character fragment leaked" ;; + *) assert "no bracket/array fragments" 0 -eq 0 ;; +esac diff --git a/test/dbus-service.sh b/test/dbus-service.sh index 9099a637..e812651b 100755 --- a/test/dbus-service.sh +++ b/test/dbus-service.sh @@ -2,9 +2,10 @@ # libink: org.finit.Service1 vtable + ServiceStateChanged signal. # # Covers per-service objects exposed at /org/finit/service/: -# GetService lookup, Introspect on a service object, Service1.Restart, -# authorization (non-root rejected), and the Manager1.ServiceStateChanged -# signal that Service1.Restart triggers. +# GetService lookup, Introspect on a service object, Service1 +# properties via Properties.Get, Service1.Restart, authorization +# (non-root rejected), and the Manager1.ServiceStateChanged signal +# that Service1.Restart triggers. set -eu @@ -29,6 +30,27 @@ case "$xml" in fail "Service1 not visible on /org/finit/service/keventd: $xml" ;; esac +say "Service1 properties: Identity, State, Pid" +ident=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Identity) +assert "Identity is keventd (got: $ident)" "$ident" = "keventd" + +state=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 State) +assert "State is running (got: $state)" "$state" = "running" + +pid=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Pid) +assert "Pid is non-zero (got: $pid)" "$pid" -gt 0 + +say "Service1 properties are advertised in introspection XML" +case "$xml" in + *'/dev/null \ diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index fd0e3bb2..14796647 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -338,6 +338,47 @@ static int mode_get_service(int argc, char *argv[]) return rc; } +/* + * getprop BUS PATH IFACE PROP -- Properties.Get, prints the variant + * value; dispatches on the wire signature, "s" and "u" supported. + */ +static int mode_getprop(int argc, char *argv[]) +{ + const link_reply_t *r; + link_client_t *c; + link_reader_t reader; + const char *s; + uint32_t u; + int rc; + + if (argc != 6) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], + "org.freedesktop.DBus.Properties", "Get", + "ss", argv[4], argv[5]); + rc = report_rc(c, rc); + if (rc == 0) { + char type = 0; + + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_variant_begin(&reader, &type) != 0) + rc = 2; + else if (type == 's' && link_r_string(&reader, &s) == 0) + printf("%s\n", s); + else if (type == 'u' && link_r_u32(&reader, &u) == 0) + printf("%u\n", u); + else { + fprintf(stderr, "unsupported variant type '%c'\n", type); + rc = 2; + } + } + link_client_close(c); + return rc; +} + static int mode_monitor_signal(int argc, char *argv[]) { link_client_t *c; @@ -441,6 +482,7 @@ int main(int argc, char *argv[]) if (!strcmp(argv[1], "call-s-as-uid")) return mode_call_s_as_uid (argc, argv); if (!strcmp(argv[1], "call-void-as-uid")) return mode_call_void_as_uid(argc, argv); if (!strcmp(argv[1], "get-service")) return mode_get_service (argc, argv); + if (!strcmp(argv[1], "getprop")) return mode_getprop (argc, argv); if (!strcmp(argv[1], "monitor-signal")) return mode_monitor_signal (argc, argv); if (!strcmp(argv[1], "unknown")) return mode_unknown (argc, argv); fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]);