# Umbrella — AI Agent Context This file is for AI coding assistants. Read it before touching any code. ## What this is A C11 daemon (`umbrella`) and CLI client (`umbrella-cli`) for managing game server processes. The daemon tails log files, dispatches RCON commands, and streams output to connected CLI clients over a Unix socket. Current targets: TF2, GMod, Minecraft, Terraria. ## Hard design rules — never violate these These are not stylistic. They exist because this runs unattended on a production game server. 1. **No dynamic memory allocation** — no `malloc`/`calloc`/`realloc`/`free` except in `unit.c::ring_init()` (one calloc per unit at load time, never freed while running). All buffers are statically sized by constants in `umbrella.h`. 2. **No recursion** — all loops are bounded by compile-time constants (`MAX_UNITS`, `MAX_CLIENTS`, `RING_BUF_LINES`, etc.). 3. **No function pointers** — dispatch is done with explicit `if/switch`. 4. **Minimum 2 `assert()` calls per function** — at entry, checking pointer arguments and invariants. 5. **All return values checked** — `read()`, `write()`, `epoll_ctl()`, `fcntl()` etc. must have their return values inspected and logged on error. No silent discards. 6. **Functions under 60 lines** — split if needed. 7. **Bounded loops only** — every `for`/`while` has a clear upper bound. ## File map ``` src/ umbrella.h Central types and limits (Unit, Client, Daemon, RingBuffer). Edit constants here, not in source files. main.c Entry point. epoll event loop. Signal handling (SIGTERM, SIGCHLD, SIGHUP). Process stdout handler. daemon.c/h Daemonize, PID file, signal fd setup. unit.c/h YAML parser (libyaml event API), unit loading, ring buffer ops (ring_init, ring_push, ring_free). client.c/h Unix socket listener, client lifecycle, JSON command dispatch (list, status, attach, detach, input, action, tail, broadcast). systemd D-Bus state queries here. proto.c/h Length-prefixed JSON wire protocol. 4-byte BE length + JSON. log.c/h File/stderr logger with timestamps and level filter. log_tail.c/h inotify-based log file tailing. One inotify_fd per watched file. Resolves symlinks at init via realpath(). Handles log rotation (IN_MOVE_SELF / IN_DELETE_SELF). filter.c/h Persistent filter subprocess per unit. fork/exec with stdin/stdout pipes. filter_apply() writes chunk, polls 250ms for output, falls back to pass-through on timeout. console/ rcon.c/h Valve RCON TCP protocol (auth + command). a2s.c/h Valve A2S_INFO UDP health probe with challenge handling. clients/ umbrella-cli/main.c CLI client. JSON over Unix socket. Interactive attach mode (epoll on socket + stdin). filters/ source.py Source engine filter (TF2, GMod): strips server_cvar, stuck/path_goal spam, strips timestamp prefix. minecraft.py Minecraft filter: strips keepAlive, autosave, internal class logs, strips [HH:MM:SS] [thread] prefix. terraria.py Terraria filter: strips blank lines and tModLoader startup noise. ``` ## Key data flows ### Log tail → client ``` srcds writes to log file → inotify IN_MODIFY fires on epoll → log_tail_handle() reads new bytes into char buf[1024] → filter_apply() pipes buf through filter subprocess (if configured) → ring_push() stores in unit's RingBuffer (500 × 1024) → client_broadcast_output() sends to all clients with c->attached == unit ``` ### Client attach → history replay ``` client sends {"cmd":"attach","unit":"tf2"} → cmd_attach() finds unit, iterates ring buffer → sends each stored line as {"type":"output","history":true,...} → sends {"type":"ok"} → future output arrives via client_broadcast_output() ``` ### RCON command ``` client sends {"cmd":"input","unit":"tf2","data":"status\n"} → cmd_input() calls rcon_exec() → TCP connect → auth → send → read → response echoed back as {"type":"output","history":false,...} ``` ## Wire protocol ``` [4 bytes big-endian length][JSON payload] ``` Client → daemon commands: ```json {"cmd":"attach", "unit":"tf2"} {"cmd":"detach"} {"cmd":"input", "unit":"tf2", "data":"say hello\n"} {"cmd":"tail", "unit":"tf2"} {"cmd":"list"} {"cmd":"status", "unit":"tf2"} {"cmd":"action", "unit":"tf2", "name":"update"} {"cmd":"broadcast", "message":"Server restarting"} ``` Daemon → client responses: ```json {"type":"ok"} {"type":"error", "message":"unit not found"} {"type":"output", "unit":"tf2", "data":"...", "history":false} {"type":"list", "units":[{"name":"tf2","display":"...","state":"running"}]} {"type":"status", "name":"tf2", "state":"running", "players":4, "max":24, "map":"cp_badlands"} ``` ## YAML parser gotcha `unit.c` uses libyaml's event API with a hand-rolled state machine. Sequence items (the `logs:` list) arrive as `YAML_SCALAR_EVENT` with `in_value == 0` — they have no preceding key. The parser handles this by checking `section == SECTION_LOGS` before the normal key/value path. If you add another sequence field, you must do the same. ## Log tail setup (important for operators) Most game servers create new timestamped log files per session. The inotify watch is set up once (at start or SIGHUP) against the resolved real path. The recommended pattern: 1. Point unit YAML `logs:` at a stable symlink, e.g. `current.log` 2. Startup script does: `ln -sf /path/to/actual.log /path/to/current.log` 3. SIGHUP umbrella — `log_tail_init` calls `realpath()` to resolve the symlink to the real file before opening and watching it 4. inotify then watches the actual inode, so `IN_MODIFY` fires correctly Source engine: use `sv_log_onefile 1` so the file is stable within a session; only update the symlink on server restart. ## Filter contract A log filter is any executable that: - Reads lines from **stdin** - Writes transformed output to **stdout** - **Flushes stdout after each write** (critical — Python: `sys.stdout.flush()`) - Can suppress lines by writing nothing for a given input The daemon gives it 250ms to respond per chunk. On timeout, the raw data passes through unchanged. On write error, the filter is stopped and that unit runs unfiltered. ## Limits (all in umbrella.h) | Constant | Value | Meaning | |----------|-------|---------| | `MAX_UNITS` | 64 | Max loaded unit descriptors | | `MAX_CLIENTS` | 32 | Max concurrent CLI connections | | `RING_BUF_LINES` | 500 | Buffered output lines per unit | | `RING_BUF_LINE_MAX` | 1024 | Max bytes per ring buffer entry | | `PROTO_MAX_MSG` | 65536 | Max wire message size | | `MAX_PATH` | 256 | Max filesystem path length | | `MAX_ACTIONS` | 16 | Named actions per unit | ## Build ```bash # Deps: libyaml-dev libsystemd-dev make && sudo make install ``` Reload after config changes (no restart needed): ```bash kill -HUP $(cat /run/umbrella/umbrella.pid) ```