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

257 lines
9.2 KiB
Markdown

# 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"})`.
```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: `<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
```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`