- Revamp architecture for modular game server support (Arma 3 first, extensible) - Merge ConfigSchema into ConfigGenerator per council decision (8→7 protocols) - Add has_capability() method to GameAdapter protocol for explicit capability probing - Add FRONTEND.md: production-grade dark neumorphism design with amber/orange palette - Update all docs (ARCHITECTURE, MODULES, DATABASE, API, IMPLEMENTATION_PLAN, THREADING) to reflect protocol merge and multi-game adapter patterns
33 KiB
Languard Servers Manager — System Architecture
Overview
Languard is a multi-game web-based management panel for dedicated game servers. It uses a game adapter architecture where a game-agnostic core handles server lifecycle, monitoring, and real-time communication, while game-specific behavior (config formats, remote admin protocols, log parsing, mission/mod handling) is encapsulated in pluggable adapters.
Arma 3 is the first-class, built-in adapter. Adding a new game server type requires only a new adapter package — no core code changes.
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 server executables, 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 |
| Game adapters | Protocol + Registry | Each game implements capability protocols; core resolves the adapter at runtime from server.game_type |
| Scheduling | APScheduler (BackgroundScheduler) |
Auto-restart, 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 + Vite + Tailwind | See FRONTEND.md for full design system, component architecture, and adapter-aware UI patterns |
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ React Frontend (see FRONTEND.md) │
│ Dashboard │ Server List │ Server Detail │ Logs │ Config UI │
│ Game Type Selector │ Adapter-specific Panels │
└────────────────────────┬────────────────────────────────────┘
│ HTTP REST + WebSocket
▼
┌─────────────────────────────────────────────────────────────┐
│ FastAPI Application (Core) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Auth Router │ │ Server Router│ │ Config Router │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Player Router│ │ Log Router │ │ WS Router │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Metric Router│ │ Event Router │ │ Games Router │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Core Service Layer │ │
│ │ ServerService │ ConfigService │ PlayerService │ │
│ │ LogService │ MetricsService│ EventService │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Game Adapter Registry │ │
│ │ GameAdapterRegistry.get(game_type) → GameAdapter │ │
│ │ │ │
│ │ Delegates to: │ │
│ │ • ConfigGenerator → adapter.get_config_generator()│ │
│ │ • ProcessConfig → adapter.get_process_config() │ │
│ │ • LogParser → adapter.get_log_parser() │ │
│ │ • RemoteAdmin → adapter.get_remote_admin() │ │
│ │ • MissionManager → adapter.get_mission_manager() │ │
│ │ • ModManager → adapter.get_mod_manager() │ │
│ │ • BanManager → adapter.get_ban_manager() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Thread Pool │ │
│ │ ProcessMonitorThread (per server, core) │ │
│ │ LogTailThread (per server, core + adapter parser) │ │
│ │ MetricsCollectorThread (per server, core) │ │
│ │ RemoteAdminPollerThread (per server, core + adapter) │ │
│ │ BroadcastThread (global) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Data Access Layer (DAL) │ │
│ │ ServerRepository │ PlayerRepository │ │
│ │ LogRepository │ MetricsRepository │ │
│ │ ConfigRepository (game_configs table) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────┐ ┌────────────────────────────────┐ │
│ │ SQLite (DB) │ │ Filesystem │ │
│ │ languard.db │ │ servers/{id}/ (layout by │ │
│ │ │ │ adapter.get_process_config() │ │
│ └───────────────────┘ └────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────┴─────────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────┐
│ Game Adapters │ │ Game Server Processes (OS level) │
│ │ │ │
│ ┌────────────────┐ │ │ (Any game executable) │
│ │ Arma 3 │ │ │ Started via adapter's │
│ │ adapter │ │ │ build_launch_args() │
│ │ │ │ │ │
│ │ • ConfigGen │ │ └──────────────────────────────────┘
│ │ • ProcessConfig│ │
│ │ • RPTParser │ │
│ │ • BERConClient │ │
│ │ • MissionMgr │ │
│ │ • ModMgr │ │
│ │ • BanMgr │ │
│ └────────────────┘ │
│ │
│ ┌────────────────┐ │
│ │ (Future Game) │ │
│ │ adapter │ │
│ └────────────────┘ │
└──────────────────────┘
Game Adapter Architecture
Core Principle: Composition Over Inheritance
Each game adapter is a composition of capability objects implementing well-defined Protocol interfaces. Not every game supports every capability — adapters return None for unsupported features, and the core gracefully degrades.
Capability Protocols
| Protocol | Purpose | Required? |
|---|---|---|
ConfigGenerator |
Define config schema + Pydantic models, write config files, build launch args | Yes |
ProcessConfig |
Exe allowlist, port conventions, directory layout | Yes |
LogParser |
Parse game-specific log lines, find log files | Yes |
RemoteAdmin |
Factory for RCon/Telnet/HTTP admin clients | No |
MissionManager |
Mission file format and rotation config | No |
ModManager |
Mod folder convention and CLI args | No |
BanManager |
Ban file sync between DB and game's ban file | No |
Adapter Registry
# adapters/registry.py
class GameAdapterRegistry:
_adapters: dict[str, GameAdapter] = {}
@classmethod
def register(cls, adapter: GameAdapter) -> None: ...
@classmethod
def get(cls, game_type: str) -> GameAdapter: ...
@classmethod
def all(cls) -> list[GameAdapter]: ...
@classmethod
def list_game_types(cls) -> list[dict]: ...
Adapters auto-register at import time. The core never imports adapter internals — it only resolves through the registry.
How Core Delegates to Adapter
Every server has a game_type column. When core code needs game-specific behavior, it:
- Reads
server.game_typefrom DB - Resolves
adapter = GameAdapterRegistry.get(game_type) - Calls the appropriate adapter method
Example — Server start flow:
def start(self, server_id: int) -> dict:
server = ServerRepository(db).get_by_id(server_id)
adapter = GameAdapterRegistry.get(server["game_type"])
process_config = adapter.get_process_config()
# Core validation (game-agnostic)
exe_basename = Path(server["exe_path"]).name
if exe_basename not in process_config.get_allowed_executables():
raise ValueError(f"Executable not allowed: {exe_basename}")
# Adapter generates config files
config_gen = adapter.get_config_generator()
config_gen.write_configs(server_id, server_dir, config_sections)
# Adapter builds launch args
launch_args = config_gen.build_launch_args(config_sections, mod_args)
# Core launches process (game-agnostic)
pid = ProcessManager.get().start(server_id, exe_path, launch_args, cwd=server_dir)
# Core starts threads (delegates to adapter for parsers/clients)
ThreadRegistry.start_server_threads(server_id, db)
Component Responsibilities
FastAPI Routers (Core)
- Validate input (Pydantic models)
- Resolve adapter from
server.game_type - Delegate game-specific work to adapter
- Return JSON responses
- Handle WebSocket connections
- Return 404 with clear message if adapter lacks a capability
Core Service Layer
- Orchestrate operations (start server = resolve adapter + generate config + launch process + start threads)
- No direct DB access — delegates to repositories
- No direct process access — delegates to ProcessManager
- No game-specific logic — delegates to adapter
ProcessManager (Core)
- 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 SIGTERM) — graceful shutdown must go through adapter's RemoteAdmin - Provides:
start(),stop(),restart(),is_running(),send_command() - Exe validation is delegated to adapter's ProcessConfig — core no longer has a hardcoded allowlist
Thread Pool (per running server)
| Thread | Source | Interval | Purpose |
|---|---|---|---|
ProcessMonitorThread |
Core | 1s | Detect crash / unexpected exit; update DB status; trigger auto-restart |
LogTailThread |
Core + adapter's LogParser | 100ms | Read new lines from log file; parse via adapter; store in DB; push to WS |
MetricsCollectorThread |
Core | 5s | Collect CPU%, RAM MB via psutil; write to DB |
RemoteAdminPollerThread |
Core + adapter's RemoteAdmin | 10s | Query players via adapter's admin client; update DB player table |
BroadcastThread |
Core | event-driven | Consume from internal queue; push JSON to all subscribed WS clients |
ConfigRepository (Core)
- Manages the generic
game_configstable - Stores config as JSON blobs keyed by
(server_id, section) - Validation is delegated to adapter's Pydantic models — core never inspects config content
- Sensitive field encryption: calls
adapter.get_config_generator().get_sensitive_fields(section)to identify which JSON keys to encrypt/decrypt via Fernet - Optimistic locking: each row includes
config_version(integer). On PUT, client sends the version they read. If version mismatch, return 409 Conflict. - Provides:
get_section(),get_all_sections(),upsert_section(),delete_sections()
Adapter Exceptions (Standard Error Types)
Adapters raise specific exception types so the core can handle errors precisely:
| Exception | When Raised | Core Action |
|---|---|---|
ConfigWriteError |
File write fails (disk full, permissions) | Set server status='error', return 500 with detail |
ConfigValidationError |
Config values violate adapter constraints | Return 400 with field-level errors |
LaunchArgsError |
build_launch_args() fails (missing mod, bad path) | Set server status='error', return 400 |
RemoteAdminError |
Remote admin connection/command fails | Log warning, return 503 with detail |
ExeNotAllowedError |
Executable not in adapter's allowlist | Return 400 with allowed list |
Data Flow: Start Server
Frontend → POST /api/servers/{id}/start
→ ServerService.start(server_id)
├── Load server from DB (includes game_type)
├── adapter = GameAdapterRegistry.get(server.game_type)
├── Validate exe against adapter.get_process_config().get_allowed_executables()
│ (raises ExeNotAllowedError → 400)
├── Check ALL derived ports across ALL running servers
│ (resolve each server's adapter, get port conventions, check full set)
├── Load config sections from game_configs table
├── adapter.get_config_generator().write_configs(server_id, dir, config)
│ ATOMIC: writes to .tmp files first, then os.replace() to final paths
│ On failure: cleans up .tmp files, raises ConfigWriteError
│ Core: sets status='error', returns 500
├── launch_args = adapter.get_config_generator().build_launch_args(config, mods)
│ On failure: raises LaunchArgsError → 400
├── ProcessManager.start(server_id, exe_path, launch_args, cwd=dir)
├── DB: update server.status = "starting"
├── ThreadRegistry.start_server_threads(server_id, db)
│ ├── ProcessMonitorThread (core, always)
│ ├── LogTailThread(server_id, adapter.get_log_parser())
│ ├── MetricsCollectorThread (core, always)
│ └── RemoteAdminPollerThread(server_id, adapter.get_remote_admin())
│ (only if adapter has remote_admin capability)
│ Core wraps client with threading.Lock for thread safety
└── BroadcastThread pushes status update to WS clients
Data Flow: Real-time Logs
Game server writes log file (path determined by adapter.get_log_parser().get_log_file_resolver())
→ LogTailThread reads new lines (core tailing logic, game-agnostic)
→ adapter.get_log_parser().parse_line(line) → {timestamp, level, message}
→ LogRepository.insert(server_id, entry)
→ BroadcastQueue.put({type: "log", server_id, entry})
→ BroadcastThread sends to all WS subscribers for this server
→ React frontend appends to log viewer
Data Flow: Player List
RemoteAdminPollerThread (every 10s, core thread)
→ adapter.get_remote_admin().create_client() → client instance
→ client.get_players() → list of player dicts
→ PlayerService.update_from_remote_admin(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)
- Sensitive config fields (passwords, RCon passwords) stored encrypted in
game_configsJSON (AES-256 via Fernet, key from env) - Port conflict validation at server creation and start: uses adapter's
get_port_conventions()to determine all derived ports - Ban file sync: adapter's BanManager handles bidirectional sync between DB bans table and game's ban file format
- Generated config files containing passwords 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
- Exe validation: core checks against adapter's
get_allowed_executables()— prevents executing arbitrary binaries
Core vs Adapter Responsibility Boundary
| Concern | Owner | Rationale |
|---|---|---|
| Server CRUD (name, description, status, game_type, ports) | Core | Universal concept |
| Process lifecycle (subprocess start, stop, kill) | Core | subprocess.Popen is game-agnostic |
| Config schema definition + file generation | Adapter | Schema, validation models, file formats, and launch args are all game-specific — unified in ConfigGenerator |
| Config CRUD in DB | Core | game_configs table is generic; adapter validates JSON |
| Process monitoring (crash detection, auto-restart) | Core | OS-level, game-agnostic |
| System metrics (CPU, RAM) | Core | psutil is game-agnostic |
| Log file tailing mechanics | Core | Tail -f is universal |
| Log line parsing | Adapter | RPT vs. server.log vs. custom JSON |
| Log file discovery | Adapter | Different naming conventions per game |
| Log storage and querying | Core | logs table is game-agnostic |
| Remote admin protocol | Adapter | BattlEye UDP vs. Source RCON vs. none |
| Remote admin polling | Core | Thread and interval logic are generic |
| Player identification | Adapter | GUID, Steam ID, UUID — game-specific |
| Player storage and history | Core | Generic table with game_data JSON |
| Ban concept | Core | Universal |
| Ban file sync | Adapter | battleye/ban.txt vs. banned-players.json vs. none |
| Mission/scenario file format | Adapter | PBO vs. PK3 vs. directory |
| Mission rotation config | Adapter | Game-specific format |
| Mission storage in DB | Core | Generic filename/metadata |
| Mod folder convention | Adapter | @mod_folder vs. mods/ vs. plugins/ |
| Mod CLI argument building | Adapter | -mod= vs. +moddir vs. none |
| Mod storage in DB | Core | Generic mod registration |
| Exe name allowlist | Adapter | Game-specific binary names |
| Port convention (derived ports) | Adapter | Arma 3: game+1/+2/+3 for Steam; varies per game |
| Profile directory convention | Adapter | Arma 3: -name=server subdirectory; varies |
| WebSocket real-time | Core | Transport is game-agnostic |
| Auth and user management | Core | No game dependency |
| Event/audit trail | Core | Generic; adapter can define additional event types |
| Scheduled cleanup | Core | Cron jobs are game-agnostic |
Configuration (Environment Variables)
LANGUARD_SECRET_KEY=<jwt-signing-secret>
LANGUARD_ENCRYPTION_KEY=<Fernet-base64-key>
LANGUARD_DB_PATH=./languard.db
LANGUARD_SERVERS_DIR=./servers
LANGUARD_HOST=0.0.0.0
LANGUARD_PORT=8000
LANGUARD_CORS_ORIGINS=http://localhost:5173,http://localhost:3000
LANGUARD_LOG_RETENTION_DAYS=7
LANGUARD_METRICS_RETENTION_DAYS=30
LANGUARD_PLAYER_HISTORY_RETENTION_DAYS=90
# Game-specific defaults (adapter may use these)
LANGUARD_ARMA3_EXE=C:/Arma3Server/arma3server_x64.exe
# LANGUARD_MINECRAFT_JAR=C:/minecraft/server.jar
# LANGUARD_RUST_EXE=C:/RustDedicated/RustDedicated.exe
Directory Layout
languard-servers-manager/
├── backend/
│ ├── main.py # FastAPI app factory
│ ├── config.py # Settings from env
│ ├── database.py # SQLAlchemy engine + session
│ ├── dependencies.py # Auth deps, server lookup
│ │
│ ├── core/ # Game-agnostic core
│ │ ├── auth/
│ │ │ ├── router.py
│ │ │ ├── service.py
│ │ │ ├── schemas.py
│ │ │ └── utils.py
│ │ ├── servers/
│ │ │ ├── router.py # REST endpoints for servers
│ │ │ ├── service.py # ServerService (delegates to adapter)
│ │ │ ├── process_manager.py # ProcessManager singleton
│ │ │ └── schemas.py # Generic Pydantic schemas
│ │ ├── players/
│ │ │ ├── router.py
│ │ │ ├── service.py
│ │ │ └── schemas.py
│ │ ├── logs/
│ │ │ ├── router.py
│ │ │ └── service.py
│ │ ├── metrics/
│ │ │ ├── router.py
│ │ │ └── service.py
│ │ ├── bans/
│ │ │ ├── router.py
│ │ │ └── service.py
│ │ ├── events/
│ │ │ ├── router.py
│ │ │ └── service.py
│ │ ├── websocket/
│ │ │ ├── router.py
│ │ │ ├── manager.py
│ │ │ └── broadcaster.py
│ │ ├── threads/
│ │ │ ├── base_thread.py
│ │ │ ├── process_monitor.py
│ │ │ ├── log_tail.py # Generic, takes adapter LogParser
│ │ │ ├── metrics_collector.py
│ │ │ ├── remote_admin_poller.py # Generic, takes adapter RemoteAdmin
│ │ │ └── thread_registry.py
│ │ ├── games/
│ │ │ └── router.py # /api/games — type discovery, schemas
│ │ ├── system/
│ │ │ └── router.py
│ │ ├── dal/
│ │ │ ├── base_repository.py
│ │ │ ├── server_repository.py
│ │ │ ├── config_repository.py # game_configs table
│ │ │ ├── player_repository.py
│ │ │ ├── log_repository.py
│ │ │ ├── metrics_repository.py
│ │ │ ├── mission_repository.py
│ │ │ ├── mod_repository.py
│ │ │ ├── ban_repository.py
│ │ │ └── event_repository.py
│ │ ├── migrations/
│ │ │ ├── runner.py
│ │ │ └── 001_initial_schema.sql
│ │ └── utils/
│ │ ├── crypto.py
│ │ ├── file_utils.py
│ │ └── port_checker.py
│ │
│ └── adapters/ # Game-specific adapters
│ ├── __init__.py
│ ├── registry.py # GameAdapterRegistry
│ ├── protocols.py # All capability Protocol definitions
│ │
│ └── arma3/ # Arma 3 adapter (built-in)
│ ├── __init__.py # Exports ARMA3_ADAPTER, registers on import
│ ├── adapter.py # Arma3Adapter class
│ ├── config_generator.py # Pydantic models + server.cfg, basic.cfg, Arma3Profile, beserver.cfg
│ ├── rcon_client.py # BERConClient (BattlEye UDP protocol)
│ ├── rcon_service.py # Wraps BERConClient for RemoteAdmin protocol
│ ├── log_parser.py # RPTParser
│ ├── mission_manager.py # PBO upload, mission rotation config
│ ├── mod_manager.py # @mod_folder convention, -mod=/-serverMod=
│ ├── process_config.py # Exe allowlist, port conventions, profile dir
│ ├── ban_manager.py # battleye/ban.txt bidirectional sync
│ ├── schemas.py # Arma 3 specific request/response models
│ └── migrations/
│ └── 001_arma3_metadata.sql # Arma 3 specific tables (if any)
│
├── servers/ # Runtime data per server instance
│ └── {server_id}/ # Layout determined by adapter.get_process_config()
│ └── (Arma 3: server.cfg, basic.cfg, server/, battleye/, mpmissions/)
│
├── frontend/ # React app
├── requirements.txt
├── .env.example
├── ARCHITECTURE.md
├── DATABASE.md
├── API.md
├── MODULES.md
├── THREADING.md
├── FRONTEND.md
└── IMPLEMENTATION_PLAN.md
Adding a New Game Adapter
To add support for a new game, create an adapter package:
adapters/<game_type>/
├── __init__.py # Export adapter, register in registry
├── adapter.py # Implement GameAdapter protocol
├── config_generator.py # Pydantic models + write game config files
├── log_parser.py # Parse game log format
├── process_config.py # Exe allowlist, port conventions
├── ... # Optional: rcon_client, mission_manager, mod_manager, ban_manager
└── migrations/ # Optional: game-specific DB extensions
Steps:
- Implement the required protocols:
ConfigGenerator,ProcessConfig,LogParser - Implement optional protocols as needed:
RemoteAdmin,MissionManager,ModManager,BanManager - Create the
GameAdapterclass composing all capabilities - Register the adapter — either:
- Built-in: register in
adapters/__init__.pyvia import - Third-party: register via setuptools entry_point in
pyproject.toml:Core scans[project.entry-points."languard.adapters"] mygame = "mygame_adapter:MYGAME_ADAPTER"languard.adaptersentry_point group at startup and auto-registers.
- Built-in: register in
- No core code changes, no DB migrations required
Key Design Decisions
| Decision | Choice | Reason |
|---|---|---|
| Game-specific logic | Adapter pattern with Protocol + Registry | Structural subtyping with mypy enforcement; optional capabilities return None; zero core changes per game |
| Capability probe | has_capability() on GameAdapter | Instead of scattered None checks, GameAdapter.has_capability(name) returns bool. Core code uses this to check support before calling get methods. Cleaner than if adapter.get_remote_admin() is not None: everywhere. |
| Protocol granularity | 7 protocols (ConfigGenerator merged from ConfigSchema+ConfigGenerator) | ConfigSchema and ConfigGenerator always co-occur (no game has schema without generation). Merged into single ConfigGenerator with schema + generation methods. ProcessConfig kept separate — may evolve independently. |
| Config storage | Hybrid: core normalized + game_configs JSON | Core tables stay clean; config is always whole-read/write; adapter Pydantic models validate; zero migration per new game |
| Sync vs async DB | Sync SQLAlchemy only | All DB access is synchronous; background threads are non-async; no aiosqlite dependency |
| WebSocket auth | JWT in query param on connect | Browser WS API doesn't support headers |
| Process ownership | ProcessManager singleton | Single source of truth; prevents duplicate launches |
| Config files | Adapter regenerates on each start | Always fresh from DB; no sync drift; adapter's structured builder prevents config injection |
| Config write failure | Atomic write + rollback | Adapter writes to temp files first, then atomic rename. On failure, temp files are cleaned up — original files remain untouched. Server start never proceeds with partial config. |
| Sensitive field encryption | Adapter declares via get_sensitive_fields() | ConfigGenerator protocol returns list of JSON keys per section that need Fernet encryption. Core's ConfigRepository handles encrypt/decrypt transparently. |
| Adapter schema versioning | config_version in game_configs row | Each config section row stores a version string. On adapter update, if version differs, adapter provides a migration function. |
| Adapter error communication | Typed adapter exceptions | Adapters raise specific exception types (ConfigWriteError, ConfigValidationError, LaunchArgsError, RemoteAdminError). Core catches specifically and sets appropriate DB status + returns clear API errors. |
| Remote admin thread safety | Core wraps with lock | Core wraps RemoteAdminClient calls with a threading.Lock. Adapter clients don't need to be thread-safe. One lock per server — API requests and poller thread share safely. |
| Third-party adapter loading | Setuptools entry_points | Third-party adapters register via languard.adapters entry_point group. Core scans entry_points at startup and auto-registers. Built-in adapters registered on import. |
| Port conflict detection | Full cross-game check | When checking ports for a new/starting server, query ALL running servers, resolve each adapter, get port conventions for that game, and check the full derived port set. |
| Config preview | Dict of label → content | preview_config() returns {label: rendered_content}. File-based games use filename as label; env-var games use variable name; CLI games use argument name. Frontend renders all as labeled text blocks. |
| Ban file sync timing | Immediate + startup | On every ban add/delete via API, adapter's BanManager syncs to file immediately. On startup, adapter reads ban file and upserts into DB. Ensures consistency. |
| Config concurrency | Optimistic locking | game_configs rows include config_version (integer). On PUT, client sends the version they read. If version mismatch, return 409 Conflict. Frontend re-reads and merges. |
| game_data JSON schema | Adapter declares via get_game_data_schema() | Each capability protocol (MissionManager, ModManager, etc.) optionally returns a Pydantic model for the game_data JSON. Core validates on write. |
| Log storage | DB + rolling file | DB for fast queries/streaming; raw logs preserved on disk |
| Player identification | slot_id (string) + game_data JSON | Flexible across games; Arma 3 uses int slot, others may use UUID |
| Route URLs | Game-agnostic with adapter delegation | Frontend doesn't need game-type-specific URLs; 404 if adapter lacks capability |
| API route registration | Core defines all routes; adapter dispatch at request time | Simpler than dynamic route mounting; clear 404 for unsupported features |