summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client.c335
-rw-r--r--src/console/a2s.c200
-rw-r--r--src/console/a2s.h27
-rw-r--r--src/umbrella.h2
-rw-r--r--src/unit.c3
5 files changed, 530 insertions, 37 deletions
diff --git a/src/client.c b/src/client.c
index 034ea74..53b8e53 100644
--- a/src/client.c
+++ b/src/client.c
@@ -4,6 +4,7 @@
#include "log.h"
#include "umbrella.h"
#include "console/rcon.h"
+#include "console/a2s.h"
#include <stdio.h>
#include <stdlib.h>
@@ -15,6 +16,7 @@
#include <sys/un.h>
#include <sys/epoll.h>
#include <sys/stat.h>
+#include <systemd/sd-bus.h>
/* ── Socket setup ────────────────────────────────────────────────────────── */
@@ -134,8 +136,174 @@ void client_broadcast_output(const char *unit_name,
/* ── Command handlers ────────────────────────────────────────────────────── */
+/*
+ * systemd_active_state: query the ActiveState of a systemd unit via D-Bus.
+ *
+ * Returns a pointer to a static string: "active", "activating",
+ * "deactivating", "inactive", "failed", or "unknown" on error.
+ *
+ * Only called when u->service[0] is set. Works for any unit type
+ * (RCON or STDIN) that declares a systemd service.
+ */
+static const char *systemd_active_state(const char *service_name)
+{
+ sd_bus *bus = NULL;
+ sd_bus_message *reply = NULL;
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ const char *obj_path = NULL;
+ char *state_str = NULL;
+ const char *result = "unknown";
+
+ if (sd_bus_open_system(&bus) < 0)
+ goto out;
+
+ /* GetUnit(service_name) → object path */
+ if (sd_bus_call_method(
+ bus,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "GetUnit",
+ &error, &reply,
+ "s", service_name) < 0)
+ goto out;
+
+ if (sd_bus_message_read(reply, "o", &obj_path) < 0 || !obj_path)
+ goto out;
+
+ /* Query ActiveState property on the unit object */
+ if (sd_bus_get_property_string(
+ bus,
+ "org.freedesktop.systemd1",
+ obj_path,
+ "org.freedesktop.systemd1.Unit",
+ "ActiveState",
+ &error, &state_str) < 0)
+ goto out;
+
+ /* Map to one of our known strings */
+ if (strcmp(state_str, "active") == 0) result = "active";
+ else if (strcmp(state_str, "activating") == 0) result = "activating";
+ else if (strcmp(state_str, "deactivating") == 0) result = "deactivating";
+ else if (strcmp(state_str, "inactive") == 0) result = "inactive";
+ else if (strcmp(state_str, "failed") == 0) result = "failed";
+
+out:
+ free(state_str);
+ sd_bus_message_unref(reply);
+ sd_bus_error_free(&error);
+ sd_bus_unref(bus);
+ return result;
+}
+
+/*
+ * ProbeResult: returned by probe_unit_state().
+ * Carries richer information than a plain state string.
+ */
+typedef struct {
+ const char *state; /* pointer to static string */
+ int players; /* -1 = not available (non-A2S unit) */
+ int max_players; /* -1 = not available */
+ char map[64]; /* empty = not available */
+} ProbeResult;
+
+/*
+ * probe_unit_state: determine current state of a unit using all available
+ * probing methods, honouring per-unit configuration.
+ *
+ * Decision tree:
+ *
+ * STDIN unit:
+ * → {state = internal process state} (tracked directly by daemon)
+ *
+ * Any unit with service[0] set → sd-bus D-Bus:
+ * "activating" → {state="starting"}
+ * "deactivating" → {state="stopping"}
+ * "inactive" → {state="stopped"}
+ * "failed" → {state="crashed"}
+ * "active" → continue to application-level probe
+ *
+ * RCON unit, health.type == HEALTH_A2S → A2S UDP query:
+ * success, players == 0 → {state="hibernating", players, max_players, map}
+ * success, players > 0 → {state="running", players, max_players, map}
+ * failure → RCON echo probe:
+ * success → {state="changing_map"}
+ * failure → {state="unreachable"}
+ *
+ * RCON unit, health.type == HEALTH_NONE → RCON echo probe:
+ * success → {state="running"}
+ * failure → {state="unreachable"}
+ */
+static ProbeResult probe_unit_state(Unit *u)
+{
+ ProbeResult r = { .state = "unknown", .players = -1, .max_players = -1 };
+ r.map[0] = '\0';
+
+ /* STDIN units: state is maintained directly by the process layer */
+ if (u->console.type != CONSOLE_RCON) {
+ r.state = unit_state_str(u->state);
+ return r;
+ }
+
+ /* For any unit (RCON or STDIN) with a systemd service, check D-Bus first */
+ if (u->service[0]) {
+ const char *sd = systemd_active_state(u->service);
+ if (strcmp(sd, "activating") == 0) { r.state = "starting"; return r; }
+ else if (strcmp(sd, "deactivating") == 0) { r.state = "stopping"; return r; }
+ else if (strcmp(sd, "inactive") == 0) { r.state = "stopped"; return r; }
+ else if (strcmp(sd, "failed") == 0) { r.state = "crashed"; return r; }
+ /* "active" or "unknown" → fall through to application probe */
+ }
+
+ /* A2S health probe (Valve games: TF2, GMod, …) */
+ if (u->health.type == HEALTH_A2S) {
+ const char *h_host = u->health.host[0] ? u->health.host : u->console.host;
+ uint16_t h_port = u->health.port ? u->health.port : u->console.port;
+ int h_tmo = u->health.timeout_ms > 0 ? u->health.timeout_ms : 5000;
+
+ A2SInfo info;
+ if (a2s_query(h_host, h_port, h_tmo, &info) == 0) {
+ r.players = info.players;
+ r.max_players = info.max_players;
+ strncpy(r.map, info.map, sizeof(r.map) - 1);
+ r.state = (r.players == 0) ? "hibernating" : "running";
+ return r;
+ }
+
+ /* A2S failed — try RCON to distinguish changing_map from unreachable */
+ char response[64] = {0};
+ int ok = rcon_exec(u->console.host, u->console.port,
+ u->console.password, "echo umbrella_probe",
+ response, sizeof(response));
+ r.state = (ok == 0) ? "changing_map" : "unreachable";
+ return r;
+ }
+
+ /* Default: plain RCON echo probe */
+ char response[64] = {0};
+ int ok = rcon_exec(u->console.host, u->console.port,
+ u->console.password, "echo umbrella_probe",
+ response, sizeof(response));
+ r.state = (ok == 0) ? "running" : "unreachable";
+ return r;
+}
+
+/* ── JSON helpers ────────────────────────────────────────────────────────── */
+
+/* Append probe result's optional fields to an in-progress JSON buffer.
+ * Call after the last fixed field; this adds players/max_players/map. */
+static int append_probe_fields(char *buf, int pos, int size,
+ const ProbeResult *r)
+{
+ pos += snprintf(buf + pos, size - pos,
+ ",\"players\":%d,\"max_players\":%d,\"map\":\"%s\"",
+ r->players, r->max_players, r->map);
+ return pos;
+}
+
+/* ── cmd_list ─────────────────────────────────────────────────────────────── */
+
static void cmd_list(Client *c) {
- /* Build a JSON array of unit summaries */
char buf[PROTO_MAX_MSG];
int pos = 0;
@@ -144,52 +312,139 @@ static void cmd_list(Client *c) {
for (int i = 0; i < g.unit_count; i++) {
Unit *u = &g.units[i];
+ ProbeResult r = probe_unit_state(u);
+
if (i > 0)
pos += snprintf(buf + pos, sizeof(buf) - pos, ",");
+
pos += snprintf(buf + pos, sizeof(buf) - pos,
- "{\"name\":\"%s\",\"display\":\"%s\","
- "\"state\":\"%s\"}",
- u->name, u->display, unit_state_str(u->state));
+ "{\"name\":\"%s\",\"display\":\"%s\",\"state\":\"%s\"",
+ u->name, u->display, r.state);
+ pos = append_probe_fields(buf, pos, sizeof(buf), &r);
+ pos += snprintf(buf + pos, sizeof(buf) - pos, "}");
}
snprintf(buf + pos, sizeof(buf) - pos, "]}");
proto_send(c->fd, buf);
}
-static const char *probe_rcon_state(Unit *u) {
- if (u->console.type != CONSOLE_RCON)
- return unit_state_str(u->state);
+/* ── cmd_status ──────────────────────────────────────────────────────────── */
- /* Check systemd service state first -- cheap, no network */
- if (u->service[0]) {
- char cmd[256];
- snprintf(cmd, sizeof(cmd),
- "systemctl is-active --quiet %s 2>/dev/null", u->service);
- if (system(cmd) != 0)
- return "stopped";
- }
+static void cmd_status(Client *c, const char *unit_name) {
+ Unit *u = unit_find(unit_name);
+ if (!u) { proto_send_error(c->fd, "unit not found"); return; }
- /* Service is active -- probe RCON to confirm reachability */
- char response[64] = {0};
- int r = rcon_exec(u->console.host, u->console.port,
- u->console.password, "echo umbrella_probe",
- response, sizeof(response));
- return (r == 0) ? "running" : "unreachable";
+ ProbeResult r = probe_unit_state(u);
+
+ char buf[512];
+ int pos = 0;
+ pos += snprintf(buf + pos, sizeof(buf) - pos,
+ "{\"type\":\"status\",\"unit\":\"%s\",\"display\":\"%s\","
+ "\"state\":\"%s\",\"pid\":%d",
+ u->name, u->display, r.state, (int)u->pid);
+ pos = append_probe_fields(buf, pos, sizeof(buf), &r);
+ snprintf(buf + pos, sizeof(buf) - pos, "}");
+ proto_send(c->fd, buf);
}
-static void cmd_status(Client *c, const char *unit_name) {
+/* ── cmd_tail ────────────────────────────────────────────────────────────── */
+
+static void cmd_tail(Client *c, const char *unit_name) {
Unit *u = unit_find(unit_name);
if (!u) { proto_send_error(c->fd, "unit not found"); return; }
- const char *state = probe_rcon_state(u);
+ /* Concatenate all ring buffer lines into a single payload */
+ char data[PROTO_MAX_MSG - 256];
+ int dpos = 0;
- char buf[512];
+ RingBuffer *rb = u->output;
+ if (rb && rb->count > 0) {
+ int start = (rb->head - rb->count + RING_BUF_LINES) % RING_BUF_LINES;
+ for (int i = 0; i < rb->count && dpos < (int)sizeof(data) - 2; i++) {
+ int idx = (start + i) % RING_BUF_LINES;
+ const char *line = rb->lines[idx];
+ int len = strlen(line);
+ if (dpos + len + 1 >= (int)sizeof(data))
+ break;
+ memcpy(data + dpos, line, len);
+ dpos += len;
+ /* Ensure each line ends with a newline */
+ if (dpos > 0 && data[dpos - 1] != '\n')
+ data[dpos++] = '\n';
+ }
+ }
+ data[dpos] = '\0';
+
+ /* Build response — use proto_send_output with history flag */
+ proto_send_output(c->fd, unit_name, data, 1);
+}
+
+/* ── cmd_broadcast ───────────────────────────────────────────────────────── */
+
+static void cmd_broadcast(Client *c, const char *message) {
+ int sent = 0, failed = 0;
+
+ for (int i = 0; i < g.unit_count; i++) {
+ Unit *u = &g.units[i];
+
+ /* Skip units without a broadcast command configured */
+ if (!u->broadcast_cmd[0])
+ continue;
+
+ /* Only broadcast to units the daemon considers running */
+ if (u->state != STATE_RUNNING)
+ continue;
+
+ /* Substitute {msg} with the message in the broadcast command */
+ char cmd[256];
+ const char *tmpl = u->broadcast_cmd;
+ const char *placeholder = strstr(tmpl, "{msg}");
+ if (placeholder) {
+ int prefix_len = (int)(placeholder - tmpl);
+ snprintf(cmd, sizeof(cmd), "%.*s%s%s",
+ prefix_len, tmpl,
+ message,
+ placeholder + 5 /* strlen("{msg}") */);
+ } else {
+ /* No placeholder — append message after a space */
+ snprintf(cmd, sizeof(cmd), "%s %s", tmpl, message);
+ }
+
+ int ok = 0;
+ if (u->console.type == CONSOLE_RCON) {
+ char response[256] = {0};
+ ok = (rcon_exec(u->console.host, u->console.port,
+ u->console.password, cmd,
+ response, sizeof(response)) == 0);
+ } else if (u->console.type == CONSOLE_STDIN) {
+ if (u->stdin_fd >= 0) {
+ /* Append newline so the server processes the command */
+ size_t clen = strlen(cmd);
+ if (clen < sizeof(cmd) - 1) {
+ cmd[clen] = '\n';
+ cmd[clen + 1] = '\0';
+ ok = (write(u->stdin_fd, cmd, clen + 1) > 0);
+ }
+ }
+ }
+
+ if (ok) {
+ log_info("broadcast: sent to %s: %s", u->name, cmd);
+ sent++;
+ } else {
+ log_warn("broadcast: failed for %s", u->name);
+ failed++;
+ }
+ }
+
+ char buf[128];
snprintf(buf, sizeof(buf),
- "{\"type\":\"status\",\"unit\":\"%s\",\"display\":\"%s\","
- "\"state\":\"%s\",\"pid\":%d}",
- u->name, u->display, state, (int)u->pid);
+ "{\"type\":\"ok\",\"sent\":%d,\"failed\":%d}", sent, failed);
proto_send(c->fd, buf);
}
+
+/* ── cmd_attach ──────────────────────────────────────────────────────────── */
+
static void cmd_attach(Client *c, const char *unit_name) {
Unit *u = unit_find(unit_name);
if (!u) { proto_send_error(c->fd, "unit not found"); return; }
@@ -301,19 +556,25 @@ int client_handle(int fd) {
char action[MAX_NAME] = {0};
char data[RING_BUF_LINE_MAX] = {0};
- json_get_str(buf, "cmd", cmd, sizeof(cmd));
- json_get_str(buf, "unit", unit, sizeof(unit));
- json_get_str(buf, "action", action, sizeof(action));
- json_get_str(buf, "data", data, sizeof(data));
+ json_get_str(buf, "cmd", cmd, sizeof(cmd));
+ json_get_str(buf, "unit", unit, sizeof(unit));
+ json_get_str(buf, "action", action, sizeof(action));
+ json_get_str(buf, "data", data, sizeof(data));
+
+ /* For broadcast, "message" maps to the data field */
+ if (strcmp(cmd, "broadcast") == 0)
+ json_get_str(buf, "message", data, sizeof(data));
log_debug("Client fd=%d cmd=%s unit=%s", fd, cmd, unit);
- if (strcmp(cmd, "list") == 0) cmd_list(c);
- else if (strcmp(cmd, "status") == 0) cmd_status(c, unit);
- else if (strcmp(cmd, "attach") == 0) cmd_attach(c, unit);
- else if (strcmp(cmd, "detach") == 0) cmd_detach(c);
- else if (strcmp(cmd, "input") == 0) cmd_input(c, unit, data);
- else if (strcmp(cmd, "action") == 0) cmd_action(c, unit, action);
+ if (strcmp(cmd, "list") == 0) cmd_list(c);
+ else if (strcmp(cmd, "status") == 0) cmd_status(c, unit);
+ else if (strcmp(cmd, "tail") == 0) cmd_tail(c, unit);
+ else if (strcmp(cmd, "broadcast") == 0) cmd_broadcast(c, data);
+ else if (strcmp(cmd, "attach") == 0) cmd_attach(c, unit);
+ else if (strcmp(cmd, "detach") == 0) cmd_detach(c);
+ else if (strcmp(cmd, "input") == 0) cmd_input(c, unit, data);
+ else if (strcmp(cmd, "action") == 0) cmd_action(c, unit, action);
else proto_send_error(fd, "unknown command");
return 0;
diff --git a/src/console/a2s.c b/src/console/a2s.c
new file mode 100644
index 0000000..96e556b
--- /dev/null
+++ b/src/console/a2s.c
@@ -0,0 +1,200 @@
+#include "a2s.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+
+/*
+ * A2S_INFO — Valve Source Engine server query protocol (UDP).
+ *
+ * Reference: https://developer.valvesoftware.com/wiki/Server_queries
+ *
+ * Wire protocol (modern Source, post-Orange Box):
+ *
+ * Request:
+ * FF FF FF FF 54 "Source Engine Query" 00
+ *
+ * Some servers issue a challenge before responding:
+ * FF FF FF FF 41 <4-byte challenge>
+ * In that case resend the request with the challenge appended.
+ *
+ * Response (type 0x49 = 'I'):
+ * FF FF FF FF 49
+ * <byte> protocol
+ * <string> name
+ * <string> map ← we want this
+ * <string> folder
+ * <string> game
+ * <short> appid
+ * <byte> players ← we want this
+ * <byte> max_players ← we want this
+ * ... (remaining fields not parsed)
+ */
+
+/* ── helpers ─────────────────────────────────────────────────────────────── */
+
+/* Read a null-terminated string from buf starting at *pos.
+ * Advances *pos past the null byte.
+ * Returns 0 on success, -1 if the buffer would be overrun. */
+static int read_string(const uint8_t *buf, int buf_len,
+ int *pos, char *out, int out_size)
+{
+ int start = *pos;
+ while (*pos < buf_len && buf[*pos] != '\0')
+ (*pos)++;
+ if (*pos >= buf_len)
+ return -1; /* no null terminator found */
+
+ int len = *pos - start;
+ if (len >= out_size)
+ len = out_size - 1;
+ memcpy(out, buf + start, len);
+ out[len] = '\0';
+
+ (*pos)++; /* skip null byte */
+ return 0;
+}
+
+/* ── a2s_query ───────────────────────────────────────────────────────────── */
+
+int a2s_query(const char *host, uint16_t port, int timeout_ms, A2SInfo *out)
+{
+ memset(out, 0, sizeof(*out));
+ out->players = -1;
+ out->max_players = -1;
+
+ /* Resolve host */
+ struct addrinfo hints, *res, *rp;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+
+ char port_str[8];
+ snprintf(port_str, sizeof(port_str), "%u", port);
+
+ if (getaddrinfo(host, port_str, &hints, &res) != 0)
+ return -1;
+
+ int fd = -1;
+ for (rp = res; rp; rp = rp->ai_next) {
+ fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (fd < 0) continue;
+ if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
+ close(fd);
+ fd = -1;
+ }
+ freeaddrinfo(res);
+ if (fd < 0) return -1;
+
+ /* Set receive timeout */
+ struct timeval tv;
+ tv.tv_sec = timeout_ms / 1000;
+ tv.tv_usec = (timeout_ms % 1000) * 1000;
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+
+ /* Build initial A2S_INFO request */
+ static const uint8_t a2s_prefix[] = {
+ 0xFF, 0xFF, 0xFF, 0xFF, 0x54
+ };
+ static const char a2s_payload[] = "Source Engine Query";
+ /* Full request: prefix + payload + null byte */
+ uint8_t req[32];
+ int req_len = 0;
+ memcpy(req + req_len, a2s_prefix, sizeof(a2s_prefix));
+ req_len += sizeof(a2s_prefix);
+ memcpy(req + req_len, a2s_payload, sizeof(a2s_payload)); /* includes \0 */
+ req_len += sizeof(a2s_payload);
+
+ uint8_t resp[2048];
+ int resp_len;
+ int attempts = 0;
+
+retry:
+ if (attempts++ >= 2) {
+ close(fd);
+ return -1;
+ }
+
+ if (send(fd, req, req_len, 0) != req_len) {
+ close(fd);
+ return -1;
+ }
+
+ resp_len = recv(fd, resp, sizeof(resp), 0);
+ if (resp_len < 5) {
+ close(fd);
+ return -1;
+ }
+
+ /* Check for challenge response: FF FF FF FF 41 <4 bytes> */
+ if (resp_len >= 9 &&
+ resp[0] == 0xFF && resp[1] == 0xFF &&
+ resp[2] == 0xFF && resp[3] == 0xFF &&
+ resp[4] == 0x41)
+ {
+ /* Append the 4-byte challenge to the request and resend */
+ if (req_len + 4 > (int)sizeof(req)) {
+ close(fd);
+ return -1;
+ }
+ memcpy(req + req_len, resp + 5, 4);
+ req_len += 4;
+ goto retry;
+ }
+
+ /* Expect response type 0x49 ('I') */
+ if (resp[0] != 0xFF || resp[1] != 0xFF ||
+ resp[2] != 0xFF || resp[3] != 0xFF ||
+ resp[4] != 0x49)
+ {
+ close(fd);
+ return -1;
+ }
+
+ close(fd);
+
+ /* Parse response body starting after the 5-byte header */
+ int pos = 5;
+
+ /* Skip protocol version (1 byte) */
+ if (pos + 1 > resp_len) return -1;
+ pos++;
+
+ /* Skip name string */
+ char scratch[256];
+ if (read_string(resp, resp_len, &pos, scratch, sizeof(scratch)) != 0)
+ return -1;
+
+ /* Map string — this is what we want */
+ if (read_string(resp, resp_len, &pos, out->map, sizeof(out->map)) != 0)
+ return -1;
+
+ /* Skip folder string */
+ if (read_string(resp, resp_len, &pos, scratch, sizeof(scratch)) != 0)
+ return -1;
+
+ /* Skip game string */
+ if (read_string(resp, resp_len, &pos, scratch, sizeof(scratch)) != 0)
+ return -1;
+
+ /* Skip app ID (2 bytes) */
+ if (pos + 2 > resp_len) return -1;
+ pos += 2;
+
+ /* Players (1 byte) */
+ if (pos + 1 > resp_len) return -1;
+ out->players = resp[pos++];
+
+ /* Max players (1 byte) */
+ if (pos + 1 > resp_len) return -1;
+ out->max_players = resp[pos++];
+
+ return 0;
+}
diff --git a/src/console/a2s.h b/src/console/a2s.h
new file mode 100644
index 0000000..c96e445
--- /dev/null
+++ b/src/console/a2s.h
@@ -0,0 +1,27 @@
+#ifndef A2S_H
+#define A2S_H
+
+#include <stdint.h>
+
+/*
+ * A2S_INFO query (Valve Source engine query protocol).
+ * Retrieves server information over UDP.
+ *
+ * Only used for units with health.type == HEALTH_A2S.
+ */
+
+typedef struct {
+ int players; /* current player count */
+ int max_players; /* server player limit */
+ char map[64]; /* current map name */
+} A2SInfo;
+
+/*
+ * a2s_query: Send an A2S_INFO request and parse the response.
+ *
+ * Returns 0 on success with *out populated.
+ * Returns -1 on timeout, parse error, or any network failure.
+ */
+int a2s_query(const char *host, uint16_t port, int timeout_ms, A2SInfo *out);
+
+#endif /* A2S_H */
diff --git a/src/umbrella.h b/src/umbrella.h
index e44d626..2e9d440 100644
--- a/src/umbrella.h
+++ b/src/umbrella.h
@@ -45,6 +45,7 @@ typedef enum {
STATE_STARTING = 1,
STATE_RUNNING = 2,
STATE_CRASHED = 3,
+ STATE_STOPPING = 4,
} ProcessState;
/* ── Action: a named script ──────────────────────────────────────────────── */
@@ -82,6 +83,7 @@ typedef struct {
char name[MAX_NAME];
char display[MAX_DISPLAY];
char service[MAX_NAME]; /* systemd unit name, informational */
+ char broadcast_cmd[128]; /* command template for !broadcast, e.g. "say {msg}" */
ConsoleConfig console;
HealthConfig health;
char log_paths[4][MAX_PATH];
diff --git a/src/unit.c b/src/unit.c
index 3dd9d26..5baea20 100644
--- a/src/unit.c
+++ b/src/unit.c
@@ -41,6 +41,7 @@ const char *unit_state_str(ProcessState state) {
case STATE_STARTING: return "starting";
case STATE_RUNNING: return "running";
case STATE_CRASHED: return "crashed";
+ case STATE_STOPPING: return "stopping";
default: return "unknown";
}
}
@@ -160,6 +161,8 @@ int unit_load_file(const char *path, Unit *out) {
strncpy(out->display, val, MAX_DISPLAY - 1);
else if (strcmp(last_key, "service") == 0)
strncpy(out->service, val, MAX_NAME - 1);
+ else if (strcmp(last_key, "broadcast_cmd") == 0)
+ strncpy(out->broadcast_cmd, val, sizeof(out->broadcast_cmd) - 1);
break;
case SECTION_CONSOLE: