#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"; case STATE_STOPPING: return "stopping"; 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->filter_pid = 0; out->filter_in_fd = -1; out->filter_out_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) { /* Sequence items (logs list) arrive as scalars without a * preceding key — handle them directly as values */ if (section == SECTION_LOGS) { if (out->log_count < 4) strncpy(out->log_paths[out->log_count++], val, MAX_PATH - 1); } else { /* 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); else if (strcmp(last_key, "broadcast_cmd") == 0) strncpy(out->broadcast_cmd, val, sizeof(out->broadcast_cmd) - 1); else if (strcmp(last_key, "log_filter") == 0) strncpy(out->log_filter, val, MAX_PATH - 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; }