From 0d706ae72ceefd74053ad6cb0900ecce6cf1f085 Mon Sep 17 00:00:00 2001 From: auric Date: Sat, 21 Feb 2026 11:08:36 -0600 Subject: Add Umbrella 0.1.5 --- src/unit.c | 295 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/unit.c (limited to 'src/unit.c') 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 +#include +#include +#include +#include +#include + +/* ── 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; +} -- cgit v1.2.3