# Phase 3 — CFG Parser + Server Process Management **Status**: PENDING **Depends on**: Phase 1 complete **Next phase**: `phase-04-server-settings.md` **Risk**: HIGH — CFG parser must be bit-perfect; server process management is complex. --- ## Goal Two deliverables built in this phase: 1. **Arma `.cfg` file parser** — reads and writes Arma server config files. Used by Phases 4 and 5. 2. **Server process management** — start/stop the Arma executable, PID tracking, cross-platform liveness check, SSE log streaming, status endpoint. After this phase: `GET /api/v1/status` returns server status; `GET /api/v1/logging/logs-sse` streams server log lines. --- ## Java Source Files to Read `` = `E:\TestScript\ARMA-Server-Web-Gui\src\main\java\pl\bartlomiejstepien\armaserverwebgui\` ### CFG Parser — read every file in this tree ``` domain/server/storage/util/cfg/CfgFileHandler.java domain/server/storage/util/cfg/DefaultCfgConfigReader.java domain/server/storage/util/cfg/DefaultCfgConfigWriter.java application/config/CfgHandlerConfig.java (shows how parsers are wired) domain/server/storage/util/cfg/parser/ (all 8 parser classes) domain/server/storage/util/cfg/annotation/ (CfgProperty, ClassName, ClassList) domain/server/storage/util/cfg/util/ (all utility classes) domain/server/storage/config/ServerConfigStorageImpl.java domain/server/storage/config/model/ (all model classes) ``` ### Server Process ``` domain/server/process/ProcessServiceImpl.java (388 lines — read fully) domain/server/process/ArmaServerParametersGeneratorImpl.java domain/server/process/WindowsProcessAliveChecker.java domain/server/process/UnixProcessAliveChecker.java domain/server/process/ServerStatusService.java domain/server/process/dto/ServerStatus.java domain/server/process/model/ServerProcessStatus.java domain/server/process/model/ArmaServerParameters.java domain/server/process/log/ServerProcessLogMessageObserver.java domain/server/process/log/SseServerServerProcessLogsObserver.java domain/server/process/log/LoggerServerProcessLogsObserver.java domain/server/process/log/FileServerProcessLogsObserver.java web/StatusController.java web/LoggingRestController.java (SSE endpoint only; properties endpoint in Phase 4) ``` --- ## Output Files to Create ``` src/domain/server/__init__.py src/domain/server/storage/__init__.py src/domain/server/storage/cfg/__init__.py src/domain/server/storage/cfg/cfg_file_handler.py src/domain/server/storage/cfg/cfg_reader.py src/domain/server/storage/cfg/cfg_writer.py src/domain/server/storage/cfg/cfg_parsers.py src/domain/server/storage/cfg/cfg_annotations.py src/domain/server/storage/config/__init__.py src/domain/server/storage/config/server_config_storage.py src/domain/server/storage/config/models.py src/domain/server/process/__init__.py src/domain/server/process/process_service.py src/domain/server/process/process_alive_checker.py src/domain/server/process/server_status_service.py src/domain/server/process/parameters_generator.py src/domain/server/process/log_observers.py src/domain/server/process/models.py src/web/schemas/status.py src/web/status_router.py ``` Update `src/main.py`: register `status_router`, add SSE route from `log_observers`. --- ## Implementation Notes ### CFG Parser — replacing Java annotation-reflection with Python dataclass metadata Java uses `@CfgProperty(name="maxPlayers")` + reflection to map fields to cfg keys. Python equivalent: `dataclasses.field(metadata={"cfg_name": "maxPlayers", "parser": "integer"})`. ```python # cfg_annotations.py from dataclasses import field def cfg_property(cfg_name: str, parser: str = "string", default=None): return field(default=default, metadata={"cfg_name": cfg_name, "parser": parser}) def class_name(name: str): return field(default=name, metadata={"class_name": name}) ``` Example config model: ```python @dataclass class NetworkConfig: max_players: int = cfg_property("maxPlayers", parser="integer", default=32) loopback: bool = cfg_property("loopback", parser="bool", default=False) hostname: str = cfg_property("hostname", parser="quoted_string", default="") ``` ### CFG Reader — core algorithm Arma `.cfg` format: ``` maxPlayers = 32; hostname = "My Server"; class Missions { class Mission_1 { template = "Stratis.Stratis"; difficulty = "Regular"; }; }; ``` Parse steps: 1. Strip `//` line comments and `/* */` block comments 2. Tokenize: `class`, `{`, `}`, `;`, `=`, identifiers, quoted strings, numbers 3. Recursively process `class NAME { ... };` blocks 4. For `key = value;` lines: look up field in dataclass by `cfg_name` metadata, call appropriate parser ### CFG Parsers (port 8 Java parser classes as functions) ```python # cfg_parsers.py def parse_quoted_string(s: str) -> str: # strips surrounding quotes def parse_raw_string(s: str) -> str: # as-is def parse_integer(s: str) -> int: def parse_long(s: str) -> int: def parse_bool(s: str) -> bool: # 1/0 or true/false def parse_string_array(s: str) -> list[str]: # parses {"a","b","c"} def parse_class(tokens, cls: type): # recursive class parser def parse_class_list(tokens, cls: type) -> list: # list of class instances ``` **CRITICAL**: Write round-trip tests before implementing Phase 4: ```python def test_roundtrip_network_config(tmp_path): original = 'maxPlayers = 32;\nhostname = "Test";\n' cfg = cfg_reader.read(original, NetworkConfig) written = cfg_writer.write(cfg) re_read = cfg_reader.read(written, NetworkConfig) assert cfg == re_read ``` ### Process alive checker Replace both Java Windows/Unix checkers with one `psutil` function: ```python import psutil, sys def is_pid_alive(pid: int) -> bool: if pid == 0: return False try: proc = psutil.Process(pid) return proc.status() not in (psutil.STATUS_DEAD, psutil.STATUS_ZOMBIE) except psutil.NoSuchProcess: return False ``` ### Process service — key patterns from ProcessServiceImpl.java - PID file: `/arma_server.pid` — plain text integer, `0` means not running - Startup lock: `asyncio.Lock()` (replaces Java `ReentrantLock`) - Status transitions: `NOT_RUNNING → STARTING → RUNNING`, `RUNNING → NOT_RUNNING` - Process launched: `subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)` - stdout/stderr reader: two `threading.Thread` daemons reading lines, calling `_notify_observers(line)` - Stop: `psutil.Process(pid).terminate()`, save pid=0 Observers list (`list[LogObserver]`) is wired in Phase 7 for Discord and at startup for SSE/file observers. ### SSE log streaming — log_observers.py ```python import asyncio, uuid from fastapi import APIRouter from fastapi.responses import StreamingResponse _sse_clients: dict[str, asyncio.Queue] = {} def broadcast(line: str): """Called by process service threads — thread-safe queue put.""" for q in _sse_clients.values(): q.put_nowait(line) async def _stream(client_id: str): q = asyncio.Queue() _sse_clients[client_id] = q try: while True: try: line = await asyncio.wait_for(q.get(), timeout=30.0) yield f"data: {line}\n\n" except asyncio.TimeoutError: yield ": heartbeat\n\n" finally: _sse_clients.pop(client_id, None) router = APIRouter() @router.get("/api/v1/logging/logs-sse") # permit-all — no auth dependency async def logs_sse(): cid = str(uuid.uuid4()) return StreamingResponse(_stream(cid), media_type="text/event-stream") ``` ### Status endpoints (from StatusController.java) ``` GET /api/v1/status → {"status": "RUNNING"|"NOT_RUNNING"|"STARTING"|"UPDATING"} POST /api/v1/status/toggle body: {"performUpdate": true|false} → 200 ``` Permission for toggle: `AswgAuthority.SERVER_START_STOP`. --- ## Completion Checklist ### CFG Parser - [ ] Parses `key = value;` assignments of all types (string, int, bool, array) - [ ] Parses nested `class X { ... };` blocks recursively - [ ] Handles quoted strings, `{a,b,c}` arrays, and `//` / `/* */` comments - [ ] Round-trip test passes for `NetworkConfig` and `ArmaServerConfig` ### Server Process - [ ] `is_pid_alive(pid)` works on both Windows and Linux - [ ] `start_server()` launches Arma executable and saves PID to file - [ ] `stop_server()` terminates the process and writes pid=0 - [ ] `get_status()` reads PID file and checks liveness - [ ] Log lines reach all connected SSE clients - [ ] `GET /api/v1/status` → correct status JSON - [ ] `POST /api/v1/status/toggle` → starts or stops server - [ ] `GET /api/v1/logging/logs-sse` streams lines (no auth required) - [ ] Heartbeat ping sent every 30s to keep SSE alive ## Contract for Phase 4 Phase 4 imports: - `from src.domain.server.storage.cfg.cfg_file_handler import CfgFileHandler` - `from src.domain.server.storage.config.server_config_storage import ServerConfigStorage` - `from src.domain.server.storage.config.models import ArmaServerConfig, NetworkConfig` - `from src.domain.server.process.process_service import ProcessService`