summaryrefslogtreecommitdiff
path: root/src/unit.c
diff options
context:
space:
mode:
authorauric <auric@japegames.com>2026-02-21 11:08:36 -0600
committerauric <auric@japegames.com>2026-02-21 11:08:36 -0600
commit0d706ae72ceefd74053ad6cb0900ecce6cf1f085 (patch)
tree6faf7d3919182b8838a6ae69ad1a2a0fac468740 /src/unit.c
Add Umbrella 0.1.5
Diffstat (limited to 'src/unit.c')
-rw-r--r--src/unit.c295
1 files changed, 295 insertions, 0 deletions
diff --git a/src/unit.c b/src/unit.c
new file mode 100644
index 0000000..3dd9d26
--- /dev/null
+++ b/src/unit.c
@@ -0,0 +1,295 @@
+#include "unit.h"
+#include "log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <errno.h>
+#include <yaml.h>
+
+/* ── Ring buffer ─────────────────────────────────────────────────────────── */
+
+RingBuffer *ring_init(void) {
+ RingBuffer *rb = calloc(1, sizeof(RingBuffer));
+ return rb;
+}
+
+void ring_push(RingBuffer *rb, const char *line, int len) {
+ if (!rb) return;
+ if (len <= 0) return;
+ if (len >= RING_BUF_LINE_MAX)
+ len = RING_BUF_LINE_MAX - 1;
+
+ memcpy(rb->lines[rb->head], line, len);
+ rb->lines[rb->head][len] = '\0';
+
+ rb->head = (rb->head + 1) % RING_BUF_LINES;
+ if (rb->count < RING_BUF_LINES)
+ rb->count++;
+}
+
+void ring_free(RingBuffer *rb) {
+ free(rb);
+}
+
+/* ── Helpers ─────────────────────────────────────────────────────────────── */
+
+const char *unit_state_str(ProcessState state) {
+ switch (state) {
+ case STATE_STOPPED: return "stopped";
+ case STATE_STARTING: return "starting";
+ case STATE_RUNNING: return "running";
+ case STATE_CRASHED: return "crashed";
+ default: return "unknown";
+ }
+}
+
+Unit *unit_find(const char *name) {
+ for (int i = 0; i < g.unit_count; i++) {
+ if (strcmp(g.units[i].name, name) == 0)
+ return &g.units[i];
+ }
+ return NULL;
+}
+
+/* ── YAML parsing ────────────────────────────────────────────────────────── */
+/*
+ * We parse the YAML manually using libyaml's event-based parser.
+ * This avoids a dependency on a higher-level YAML library and keeps
+ * us in control of memory. The unit format is shallow enough that
+ * a simple key-value state machine handles it cleanly.
+ *
+ * Expected format:
+ *
+ * name: tf2-novemen
+ * display: "TF2 — novemen"
+ * service: tf2-server.service
+ * console:
+ * type: rcon
+ * host: 127.0.0.1
+ * port: 27015
+ * password_env: RCON_PASSWORD
+ * health:
+ * type: a2s
+ * host: 127.0.0.1
+ * port: 27015
+ * logs:
+ * - /ded/tf/novemen/tf2/logs/service.log
+ * actions:
+ * update: /ded/tf/scripts/tf2_autoupdate.sh
+ * backup: /ded/tf/scripts/backup.sh
+ */
+
+typedef enum {
+ SECTION_ROOT,
+ SECTION_CONSOLE,
+ SECTION_HEALTH,
+ SECTION_LOGS,
+ SECTION_ACTIONS,
+} ParseSection;
+
+int unit_load_file(const char *path, Unit *out) {
+ memset(out, 0, sizeof(Unit));
+ out->state = STATE_STOPPED;
+ out->stdin_fd = -1;
+ out->stdout_fd = -1;
+ out->health.timeout_ms = 5000;
+
+ FILE *f = fopen(path, "r");
+ if (!f) {
+ log_error("Cannot open unit file %s: %s", path, strerror(errno));
+ return -1;
+ }
+
+ yaml_parser_t parser;
+ yaml_event_t event;
+
+ if (!yaml_parser_initialize(&parser)) {
+ log_error("yaml_parser_initialize failed");
+ fclose(f);
+ return -1;
+ }
+ yaml_parser_set_input_file(&parser, f);
+
+ ParseSection section = SECTION_ROOT;
+ char last_key[MAX_NAME] = {0};
+ int in_value = 0; /* expecting a value next */
+ int ok = 1;
+
+ while (ok) {
+ if (!yaml_parser_parse(&parser, &event)) {
+ log_error("YAML parse error in %s: %s", path, parser.problem);
+ ok = 0;
+ break;
+ }
+
+ if (event.type == YAML_STREAM_END_EVENT ||
+ event.type == YAML_DOCUMENT_END_EVENT) {
+ yaml_event_delete(&event);
+ break;
+ }
+
+ if (event.type == YAML_SCALAR_EVENT) {
+ const char *val = (const char *)event.data.scalar.value;
+
+ if (!in_value) {
+ /* This is a key */
+ strncpy(last_key, val, sizeof(last_key) - 1);
+
+ /* Section transitions */
+ if (strcmp(val, "console") == 0)
+ section = SECTION_CONSOLE;
+ else if (strcmp(val, "health") == 0)
+ section = SECTION_HEALTH;
+ else if (strcmp(val, "logs") == 0)
+ section = SECTION_LOGS;
+ else if (strcmp(val, "actions") == 0)
+ section = SECTION_ACTIONS;
+ else
+ in_value = 1;
+ } else {
+ /* This is a value */
+ in_value = 0;
+
+ switch (section) {
+ case SECTION_ROOT:
+ if (strcmp(last_key, "name") == 0)
+ strncpy(out->name, val, MAX_NAME - 1);
+ else if (strcmp(last_key, "display") == 0)
+ strncpy(out->display, val, MAX_DISPLAY - 1);
+ else if (strcmp(last_key, "service") == 0)
+ strncpy(out->service, val, MAX_NAME - 1);
+ break;
+
+ case SECTION_CONSOLE:
+ if (strcmp(last_key, "type") == 0) {
+ if (strcmp(val, "rcon") == 0)
+ out->console.type = CONSOLE_RCON;
+ else if (strcmp(val, "stdin") == 0)
+ out->console.type = CONSOLE_STDIN;
+ else
+ log_warn("Unknown console type '%s' in %s", val, path);
+ } else if (strcmp(last_key, "host") == 0)
+ strncpy(out->console.host, val, 63);
+ else if (strcmp(last_key, "port") == 0)
+ out->console.port = (uint16_t)atoi(val);
+ else if (strcmp(last_key, "password") == 0)
+ strncpy(out->console.password, val, 127);
+ else if (strcmp(last_key, "password_env") == 0)
+ strncpy(out->console.password_env, val, 63);
+ break;
+
+ case SECTION_HEALTH:
+ if (strcmp(last_key, "type") == 0) {
+ if (strcmp(val, "a2s") == 0)
+ out->health.type = HEALTH_A2S;
+ else if (strcmp(val, "tcp") == 0)
+ out->health.type = HEALTH_TCP;
+ else if (strcmp(val, "none") == 0)
+ out->health.type = HEALTH_NONE;
+ } else if (strcmp(last_key, "host") == 0)
+ strncpy(out->health.host, val, 63);
+ else if (strcmp(last_key, "port") == 0)
+ out->health.port = (uint16_t)atoi(val);
+ else if (strcmp(last_key, "timeout_ms") == 0)
+ out->health.timeout_ms = atoi(val);
+ break;
+
+ case SECTION_LOGS:
+ /* List item — last_key is "-" or the index */
+ if (out->log_count < 4)
+ strncpy(out->log_paths[out->log_count++], val, MAX_PATH - 1);
+ break;
+
+ case SECTION_ACTIONS:
+ /* last_key is the action name, val is the script path */
+ if (out->action_count < MAX_ACTIONS) {
+ strncpy(out->actions[out->action_count].name,
+ last_key, MAX_NAME - 1);
+ strncpy(out->actions[out->action_count].script,
+ val, MAX_PATH - 1);
+ out->action_count++;
+ }
+ break;
+ }
+ }
+ } else if (event.type == YAML_MAPPING_END_EVENT ||
+ event.type == YAML_SEQUENCE_END_EVENT) {
+ /* Pop back to root when leaving a nested section */
+ if (section != SECTION_ROOT)
+ section = SECTION_ROOT;
+ in_value = 0;
+ } else if (event.type == YAML_SEQUENCE_START_EVENT) {
+ in_value = 0;
+ }
+
+ yaml_event_delete(&event);
+ }
+
+ yaml_parser_delete(&parser);
+ fclose(f);
+
+ if (!out->name[0]) {
+ log_error("Unit file %s has no 'name' field", path);
+ return -1;
+ }
+
+ /* Resolve password from env if password_env is set */
+ if (out->console.password_env[0] && !out->console.password[0]) {
+ const char *env_val = getenv(out->console.password_env);
+ if (env_val)
+ strncpy(out->console.password, env_val, 127);
+ else
+ log_warn("Unit %s: password_env '%s' not set in environment",
+ out->name, out->console.password_env);
+ }
+
+ /* Allocate ring buffer */
+ out->output = ring_init();
+ if (!out->output) {
+ log_error("Failed to allocate ring buffer for unit %s", out->name);
+ return -1;
+ }
+
+ log_info("Loaded unit: %s (%s)", out->name, out->display);
+ return ok ? 0 : -1;
+}
+
+int unit_load_all(void) {
+ DIR *d = opendir(UMBRELLA_UNITS_DIR);
+ if (!d) {
+ log_error("Cannot open units directory %s: %s",
+ UMBRELLA_UNITS_DIR, strerror(errno));
+ return -1;
+ }
+
+ struct dirent *ent;
+ int count = 0;
+
+ while ((ent = readdir(d)) != NULL) {
+ /* Only load .yaml files */
+ const char *name = ent->d_name;
+ size_t len = strlen(name);
+ if (len < 6 || strcmp(name + len - 5, ".yaml") != 0)
+ continue;
+
+ if (g.unit_count >= MAX_UNITS) {
+ log_warn("Maximum unit count (%d) reached, skipping %s",
+ MAX_UNITS, name);
+ break;
+ }
+
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "%s/%s", UMBRELLA_UNITS_DIR, name);
+
+ if (unit_load_file(path, &g.units[g.unit_count]) == 0) {
+ g.unit_count++;
+ count++;
+ }
+ }
+
+ closedir(d);
+ log_info("Loaded %d unit(s)", count);
+ return count;
+}