Update remaining old-name references in body text: - ARCHITECTURE.md:219 directory layout: languard-server-manager/ → languard-servers-manager/ - IMPLEMENTATION_PLAN.md:405 setup instructions: cd languard-server-manager → cd languard-servers-manager
18 KiB
18 KiB
Languard Servers Manager — System Architecture
Overview
Languard is a web-based management panel for Arma 3 dedicated servers. It provides a Python backend that manages one or more arma3server_x64.exe processes, exposes a REST + WebSocket API to a React frontend, and persists all state in SQLite.
Technology Stack
| Layer | Technology | Rationale |
|---|---|---|
| Backend framework | FastAPI (Python 3.11+) | Async-native, built-in WebSocket, OpenAPI docs auto-generated |
| Database | SQLite via SQLAlchemy (sync) |
Zero-config, file-based, sufficient for single-host server manager; all access is synchronous (WAL mode for concurrent reads) |
| Process management | subprocess + threading |
Wrap arma3server.exe, watch stdout/stderr, check exit codes; cwd set to server instance dir for relative paths; on Windows terminate() is a hard kill (no SIGTERM) |
| Real-time comms | WebSocket (FastAPI) | Push log lines, player lists, server status to React |
| RCon client | Custom UDP client | BattlEye RCon protocol for in-game admin commands |
| Config generation | Python structured builder | Generate server.cfg, basic.cfg, server.Arma3Profile with proper escaping (no f-string injection) |
| Scheduling | APScheduler (BackgroundScheduler) |
Auto-restart, mission rotation timers, log/metrics cleanup (sync DB ops → BackgroundScheduler, not AsyncIOScheduler) |
| Auth | JWT (python-jose) + bcrypt | Secure the API; React stores token in localStorage |
| Frontend | React + TypeScript (external repo) | Connects to this backend's API |
High-Level Architecture
┌─────────────────────────────────────────────────────────────┐
│ React Frontend │
│ Server List │ Server Detail │ Logs │ Players │ Config UI │
└────────────────────────┬────────────────────────────────────┘
│ HTTP REST + WebSocket
▼
┌─────────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Auth Router │ │ Server Router│ │ Config Router │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │Mission Router│ │ Mod Router │ │ WS Router │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ServerService │ ConfigService │ RConService │ │
│ │ LogService │ MetricsService│ MissionService │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Thread Pool │ │
│ │ ProcessMonitorThread (per server) │ │
│ │ LogTailThread (per server) │ │
│ │ MetricsCollectorThread (per server) │ │
│ │ RConPollerThread (per server) │ │
│ │ BroadcastThread (global) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Data Access Layer (DAL) │ │
│ │ ServerRepository │ PlayerRepository │ │
│ │ LogRepository │ MetricsRepository │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────┐ ┌────────────────────────────────┐ │
│ │ SQLite (DB) │ │ Filesystem │ │
│ │ languard.db │ │ servers/{id}/server.cfg │ │
│ │ │ │ servers/{id}/basic.cfg │ │
│ │ │ │ servers/{id}/server/ │ │ ← profile dir (Arma3 -name=server)
│ │ │ │ server.Arma3Profile │ │ ← profile settings
│ │ │ │ arma3server_*.rpt │ │ ← RPT logs (tailable)
│ │ │ │ servers/{id}/battleye/ │ │
│ │ │ │ beserver.cfg │ │ ← RCon config
│ │ │ │ servers/{id}/mpmissions/ │ │
│ └───────────────────┘ └────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ subprocess
▼
┌─────────────────────────────────────────────────────────────┐
│ Arma 3 Server Processes (OS level) │
│ arma3server_x64.exe (port 2302) │
│ arma3server_x64.exe (port 2402) │
│ ... │
└─────────────────────────────────────────────────────────────┘
Component Responsibilities
FastAPI Routers
- Validate input (Pydantic models)
- Call service layer
- Return JSON responses
- Handle WebSocket connections
Service Layer
- Orchestrate operations (start server = generate config + launch process + start threads)
- No direct DB access — delegates to repositories
- No direct process access — delegates to ProcessManager
ProcessManager
- Singleton that owns all subprocess handles
- Thread-safe dict:
{server_id: subprocess.Popen} start()setscwd=servers/{server_id}/so relative config paths resolve correctly- On Windows:
terminate()=TerminateProcess(hard kill), no graceful SIGTERM — graceful shutdown must go through RCon#shutdownfirst - Provides:
start(),stop(),restart(),is_running(),send_command()
Thread Pool (per running server)
| Thread | Interval | Purpose |
|---|---|---|
ProcessMonitorThread |
1s | Detect crash / unexpected exit; update DB status; trigger auto-restart |
LogTailThread |
100ms | Read new lines from .rpt file; store in DB; push to WS clients |
MetricsCollectorThread |
5s | Collect CPU%, RAM MB for the process via psutil; write to DB |
RConPollerThread |
10s | Query connected players via BattlEye RCon; update DB player table |
BroadcastThread |
event-driven | Consume from internal queue; push JSON to all subscribed WS clients |
RCon Client
- UDP socket to BattlEye RCon port (configured in
beserver.cfginside the server'sbattleye/directory) - Implements BE RCon protocol: login, keepalive, send command, parse response
- Request multiplexer: tracks pending requests by sequence byte, routes responses to the correct caller via
threading.Eventper request. Prevents response misrouting when RConPollerThread and API-request RConService calls share the same UDP socket. - Used by:
RConPollerThread,RConService(for admin commands from UI)
Config Generator
- Takes
ServerConfigPydantic model from DB - Renders
server.cfg,basic.cfg,*.Arma3Profileusing a structured builder (NOT f-strings — prevents config injection) - Escapes double quotes and newlines in all user-supplied string values
- Writes files to
servers/{server_id}/directory server.Arma3Profilewritten toservers/{server_id}/server/(Arma 3 reads from the-namesubdirectory)
SQLite DAL
- Sync reads/writes using SQLAlchemy Core (not ORM — simpler for this use case)
- Thread-safe via SQLAlchemy's connection pooling
- One
languard.dbfile at project root - PRAGMA busy_timeout=5000 — prevents "database is locked" errors under concurrent thread writes
- Thread-local connections via
get_thread_db()— one connection per background thread
Data Flow: Start Server
Frontend → POST /api/servers/{id}/start
→ ServerService.start(server_id)
├── Load ServerConfig from DB
├── ConfigGenerator.write_configs(server_id, config)
│ ├── server.cfg → servers/{id}/server.cfg
│ ├── basic.cfg → servers/{id}/basic.cfg
│ ├── server.Arma3Profile → servers/{id}/server/server.Arma3Profile
│ └── beserver.cfg → servers/{id}/battleye/beserver.cfg
├── ProcessManager.start(server_id, exe_path, args, cwd=servers/{id}/)
├── DB: update server.status = "starting"
├── Spawn ProcessMonitorThread(server_id)
├── Spawn LogTailThread(server_id) — tails servers/{id}/server/arma3server_*.rpt
├── Spawn MetricsCollectorThread(server_id)
├── Spawn RConPollerThread(server_id) [after 30s delay for server startup]
└── BroadcastThread pushes status update to WS clients
Data Flow: Real-time Logs
arma3server.exe writes servers/{id}/server/arma3server_*.rpt
→ LogTailThread reads new lines (recursive glob for *.rpt in profile dir)
→ LogRepository.insert(server_id, line, timestamp)
→ BroadcastQueue.put({type: "log", server_id, line, timestamp})
→ BroadcastThread sends to all WS subscribers for this server
→ React frontend appends to log viewer
Data Flow: Player List
RConPollerThread (every 10s)
→ RConClient.send("players")
→ Parse response: [{id, name, guid, ping, verified}]
→ PlayerRepository.upsert_all(server_id, players)
→ BroadcastQueue.put({type: "players", server_id, players})
→ React frontend updates player list
Security Model
- All API routes (except
POST /api/auth/login) require a valid JWT Bearer token - JWT contains:
user_id,username,role(admin|viewer) viewerrole: read-only (GET endpoints, WebSocket)adminrole: all operations- CORS configured to accept only the frontend origin
- Passwords hashed with bcrypt (cost factor 12)
serverCommandPasswordandpasswordAdminstored encrypted in SQLite (AES-256 viacryptographylibrary, key from env)- Port conflict validation at server creation and start: checks game_port through game_port+4 (game, Steam query, Steam master, Steam auth, RCon) against all existing servers
- ban.txt sync: bans table is source of truth for UI; on ban add/delete via API, also write to
battleye/ban.txt; on startup, readban.txtand upsert into DB. Without this sync, DB-only bans are not enforced by BattlEye. - Generated config files containing passwords (server.cfg, beserver.cfg) have restrictive file permissions (0600 on Unix, restricted ACL on Windows)
- Input sanitization on all string fields before config generation — no shell injection or config directive injection
Configuration (Environment Variables)
LANGUARD_SECRET_KEY=<jwt-signing-secret>
LANGUARD_ENCRYPTION_KEY=<Fernet-base64-key — generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())">
LANGUARD_DB_PATH=./languard.db
LANGUARD_SERVERS_DIR=./servers
LANGUARD_ARMA_EXE=C:/Arma3Server/arma3server_x64.exe
LANGUARD_HOST=0.0.0.0
LANGUARD_PORT=8000
LANGUARD_CORS_ORIGINS=http://localhost:5173,http://localhost:3000
LANGUARD_LOG_RETENTION_DAYS=7
Directory Layout
languard-servers-manager/
├── backend/
│ ├── main.py # FastAPI app factory
│ ├── config.py # Settings from env
│ ├── database.py # SQLAlchemy engine + session
│ ├── auth/
│ │ ├── router.py
│ │ ├── service.py
│ │ └── schemas.py
│ ├── servers/
│ │ ├── router.py # REST endpoints for servers
│ │ ├── service.py # ServerService
│ │ ├── process_manager.py # ProcessManager singleton
│ │ ├── config_generator.py # server.cfg / basic.cfg / beserver.cfg writer
│ │ └── schemas.py # Pydantic schemas
│ ├── rcon/
│ │ ├── client.py # BattlEye RCon UDP client
│ │ └── service.py # RConService
│ ├── players/
│ │ ├── router.py
│ │ ├── service.py
│ │ └── schemas.py
│ ├── missions/
│ │ ├── router.py
│ │ └── service.py
│ ├── mods/
│ │ ├── router.py
│ │ └── service.py
│ ├── logs/
│ │ ├── router.py
│ │ └── service.py
│ ├── metrics/
│ │ ├── router.py
│ │ └── service.py
│ ├── websocket/
│ │ ├── router.py # WS connection handler
│ │ ├── manager.py # ConnectionManager (per-server subscriptions)
│ │ └── broadcaster.py # BroadcastThread + queue
│ ├── threads/
│ │ ├── process_monitor.py # ProcessMonitorThread
│ │ ├── log_tail.py # LogTailThread
│ │ ├── metrics_collector.py # MetricsCollectorThread
│ │ └── rcon_poller.py # RConPollerThread
│ ├── system/
│ │ └── router.py # GET /system/status, GET /system/health
│ ├── dal/
│ │ ├── server_repository.py
│ │ ├── config_repository.py
│ │ ├── player_repository.py
│ │ ├── log_repository.py
│ │ ├── metrics_repository.py
│ │ ├── mission_repository.py
│ │ ├── mod_repository.py
│ │ ├── ban_repository.py
│ │ └── event_repository.py
│ └── migrations/
│ └── 001_initial_schema.sql
├── servers/ # Runtime data per server instance
│ └── {server_id}/
│ ├── server.cfg
│ ├── basic.cfg
│ ├── server/ # Arma 3 profile dir (matches -name=server)
│ │ ├── server.Arma3Profile
│ │ └── arma3server_*.rpt # Timestamped RPT logs
│ ├── battleye/
│ │ └── beserver.cfg # BattlEye RCon config (generated on start)
│ └── mpmissions/
├── frontend/ # React app (separate repo or subfolder)
├── requirements.txt
├── .env.example
├── ARCHITECTURE.md
├── DATABASE.md
├── API.md
├── MODULES.md
├── THREADING.md
└── IMPLEMENTATION_PLAN.md
Key Design Decisions
| Decision | Choice | Reason |
|---|---|---|
| Sync vs async DB | Sync SQLAlchemy only | All DB access is synchronous; background threads are non-async; get_thread_db() provides thread-local connections; no aiosqlite dependency |
| ORM vs Core | SQLAlchemy Core | Simpler SQL control, less magic for embedded use case |
| WebSocket auth | JWT in query param on connect | Browser WS API doesn't support headers; query param ?token=... |
| Process ownership | ProcessManager singleton | Single source of truth; prevents duplicate launches |
| Log storage | DB + rolling file | DB for fast queries/streaming; raw .rpt preserved on disk |
| Config files | Regenerate on each start | Always fresh from DB; no sync drift between DB and filesystem; structured builder (not f-strings) prevents config injection |
| RCon port convention | User-configurable | BattlEye RCon port is set in beserver.cfg (inside battleye/ dir). Default suggestion: game port + 4 (e.g., 2302 → 2306). Must not conflict with game (2302), Steam query (2303), VON (2304), or Steam auth (2305) ports. Note: RCon config changes require server restart — BattlEye reads beserver.cfg only at startup. |