Files
languard-servers-manager/ARCHITECTURE.md
Tran G. (Revernomad) Khoa b17d199301 fix: address design review ACT NOW items (6 risk gaps)
- Add migrate_config() to ConfigGenerator protocol for schema version upgrades
- Add per-server operation lock to ProcessManager to prevent start/stop races
- Add busy_timeout retry/backoff strategy (exponential: 1s, 2s, 4s) for DB lock exhaustion
- Add ConfigForm testing strategy and error boundary for malformed schemas
- Add schema cache invalidation on adapter version change
- Add ConfigMigrationError to typed adapter exceptions
2026-04-16 17:29:19 +07:00

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

  1. Reads server.game_type from DB
  2. Resolves adapter = GameAdapterRegistry.get(game_type)
  3. 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}
  • Per-server operation lock (_operation_locks: dict[int, threading.Lock]) — serializes start/stop/restart for the same server. Prevents race conditions when two admins hit "start" simultaneously or when start+stop overlap. Each server gets its own lock; different servers operate independently.
  • start() sets cwd=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_configs table
  • 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.
  • Schema migration: on read, if schema_version differs from adapter.get_config_version(), core calls adapter.migrate_config(old_version, config_json). On success, updates the row with migrated JSON and new schema_version. On ConfigMigrationError, keeps original config and logs a warning.
  • 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
ConfigMigrationError migrate_config() fails to transform old schema Keep original config, log warning, server runs with old schema

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)
  • viewer role: read-only (GET endpoints, WebSocket)
  • admin role: 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_configs JSON (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:

  1. Implement the required protocols: ConfigGenerator, ProcessConfig, LogParser
  2. Implement optional protocols as needed: RemoteAdmin, MissionManager, ModManager, BanManager
  3. Create the GameAdapter class composing all capabilities
  4. Register the adapter — either:
    • Built-in: register in adapters/__init__.py via import
    • Third-party: register via setuptools entry_point in pyproject.toml:
      [project.entry-points."languard.adapters"]
      mygame = "mygame_adapter:MYGAME_ADAPTER"
      
      Core scans languard.adapters entry_point group at startup and auto-registers.
  5. 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
Server operation safety Per-server operation lock ProcessManager holds a lock per server_id that serializes start/stop/restart. Two concurrent start requests for the same server: the second waits for the first to complete, then sees the server is already running. Different servers are independent (no cross-server locking).
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, core calls adapter.migrate_config(old_version, config_json) which returns the migrated dict. On migration failure (ConfigMigrationError), core keeps the original config and logs a warning — the server can still run with the old schema.
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