"""Game-agnostic port availability checking.""" from __future__ import annotations import logging import socket logger = logging.getLogger(__name__) def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool: """Return True if the port is already bound.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(0.5) try: s.bind((host, port)) return False except OSError: return True def check_server_ports_available( game_port: int, rcon_port: int | None = None, host: str = "127.0.0.1", port_conventions: dict[str, int] | None = None, ) -> list[int]: """ Check all ports for a server instance. If port_conventions is provided (from adapter), checks all derived ports. Returns list of ports that are already in use (empty = all available). """ ports_to_check: set[int] = set() if port_conventions: ports_to_check.update(port_conventions.values()) else: ports_to_check.add(game_port) if rcon_port is not None: ports_to_check.add(rcon_port) return [p for p in sorted(ports_to_check) if is_port_in_use(p, host)] def check_ports_against_running_servers( new_server_game_port: int, new_server_rcon_port: int | None, exclude_server_id: int | None, db, ) -> list[int]: """ Cross-game port conflict detection. Checks new server's full port set against all running servers' full port sets. Returns list of conflicting ports. """ from adapters.registry import GameAdapterRegistry from sqlalchemy import text rows = db.execute( text("SELECT id, game_type, game_port, rcon_port FROM servers WHERE status IN ('running','starting')") ).fetchall() occupied_ports: set[int] = set() for row in rows: if exclude_server_id and row[0] == exclude_server_id: continue try: adapter = GameAdapterRegistry.get(row[1]) conventions = adapter.get_process_config().get_port_conventions(row[2]) occupied_ports.update(conventions.values()) except KeyError: logger.debug("Unknown game type '%s', falling back to game_port only", row[1]) occupied_ports.add(row[2]) if row[3] is not None: occupied_ports.add(row[3]) # Check new server's ports against occupied set try: adapter = GameAdapterRegistry.get("arma3") # temporary — will be passed in except KeyError: logger.debug("No 'arma3' adapter for port conventions, using defaults") new_ports: set[int] = {new_server_game_port} if new_server_rcon_port: new_ports.add(new_server_rcon_port) return sorted(new_ports & occupied_ports)