Files
arma-server-web-manager/phases/phase-03-cfg-parser-process.md
Khoa (Revenovich) Tran Gia e02db3ddde feat: add Java→Python migration plan with 9 self-contained phase files
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
2026-04-14 15:06:56 +07:00

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:

  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

<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:

  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)

# 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, 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

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