Converts Spring Boot 4.0.3 ARMA Server Web GUI to FastAPI/Python. Each phase file is fully self-contained: lists Java source files to read, output files to create, implementation patterns, REST endpoint contracts, and a completion checklist. A future agent can execute any single phase without rescanning the Java project. Phases: - 01: Foundation — SQLAlchemy models, Alembic, settings, base schemas - 02: Auth & Users — JWT middleware, RBAC, user CRUD - 03: CFG parser + server process — server.cfg round-trip, start/stop - 04: Server settings — general/network/logging/security/difficulty - 05: Mod management — mod CRUD, presets, settings, WebSocket progress - 06: Steam integration — SteamCMD queue, Workshop API, python-a2s - 07: Missions, CDLC, Discord, APScheduler jobs - 08: Middleware & polish — global exception handler, SPA redirect, structlog - 09: Testing — pytest-asyncio, respx, 80% coverage target
9.2 KiB
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:
- Arma
.cfgfile parser — reads and writes Arma server config files. Used by Phases 4 and 5. - 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
<JAVA_SRC> = E:\TestScript\ARMA-Server-Web-Gui\src\main\java\pl\bartlomiejstepien\armaserverwebgui\
CFG Parser — read every file in this tree
<JAVA_SRC>domain/server/storage/util/cfg/CfgFileHandler.java
<JAVA_SRC>domain/server/storage/util/cfg/DefaultCfgConfigReader.java
<JAVA_SRC>domain/server/storage/util/cfg/DefaultCfgConfigWriter.java
<JAVA_SRC>application/config/CfgHandlerConfig.java (shows how parsers are wired)
<JAVA_SRC>domain/server/storage/util/cfg/parser/ (all 8 parser classes)
<JAVA_SRC>domain/server/storage/util/cfg/annotation/ (CfgProperty, ClassName, ClassList)
<JAVA_SRC>domain/server/storage/util/cfg/util/ (all utility classes)
<JAVA_SRC>domain/server/storage/config/ServerConfigStorageImpl.java
<JAVA_SRC>domain/server/storage/config/model/ (all model classes)
Server Process
<JAVA_SRC>domain/server/process/ProcessServiceImpl.java (388 lines — read fully)
<JAVA_SRC>domain/server/process/ArmaServerParametersGeneratorImpl.java
<JAVA_SRC>domain/server/process/WindowsProcessAliveChecker.java
<JAVA_SRC>domain/server/process/UnixProcessAliveChecker.java
<JAVA_SRC>domain/server/process/ServerStatusService.java
<JAVA_SRC>domain/server/process/dto/ServerStatus.java
<JAVA_SRC>domain/server/process/model/ServerProcessStatus.java
<JAVA_SRC>domain/server/process/model/ArmaServerParameters.java
<JAVA_SRC>domain/server/process/log/ServerProcessLogMessageObserver.java
<JAVA_SRC>domain/server/process/log/SseServerServerProcessLogsObserver.java
<JAVA_SRC>domain/server/process/log/LoggerServerProcessLogsObserver.java
<JAVA_SRC>domain/server/process/log/FileServerProcessLogsObserver.java
<JAVA_SRC>web/StatusController.java
<JAVA_SRC>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"}).
# 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:
@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:
- Strip
//line comments and/* */block comments - Tokenize:
class,{,},;,=, identifiers, quoted strings, numbers - Recursively process
class NAME { ... };blocks - For
key = value;lines: look up field in dataclass bycfg_namemetadata, call appropriate parser
CFG Parsers (port 8 Java parser classes as functions)
# 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:
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:
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:
<server_dir>/arma_server.pid— plain text integer,0means not running - Startup lock:
asyncio.Lock()(replaces JavaReentrantLock) - Status transitions:
NOT_RUNNING → STARTING → RUNNING,RUNNING → NOT_RUNNING - Process launched:
subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE) - stdout/stderr reader: two
threading.Threaddaemons 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
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
NetworkConfigandArmaServerConfig
Server Process
is_pid_alive(pid)works on both Windows and Linuxstart_server()launches Arma executable and saves PID to filestop_server()terminates the process and writes pid=0get_status()reads PID file and checks liveness- Log lines reach all connected SSE clients
GET /api/v1/status→ correct status JSONPOST /api/v1/status/toggle→ starts or stops serverGET /api/v1/logging/logs-ssestreams 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 CfgFileHandlerfrom src.domain.server.storage.config.server_config_storage import ServerConfigStoragefrom src.domain.server.storage.config.models import ArmaServerConfig, NetworkConfigfrom src.domain.server.process.process_service import ProcessService