from __future__ import annotations from dataclasses import dataclass, field from datetime import UTC, datetime from threading import Event, Lock, Thread from typing import Any from app.config import ( AppConfig, ConfigStore, LAPTOP_TRIGGER_CODE, SUPPORTED_TARGET_PORTS, TOWER_TRIGGER_CODE, ) from app.ddm import DDMBackend, RealDDMBackend from app.hardware import HardwareScan, MonitorBackend, RealMonitorBackend @dataclass(slots=True) class ServiceStatus: config: AppConfig = field(default_factory=AppConfig) samsung_present: bool = False samsung_connected_session_active: bool = False samsung_session_attempted: bool = False samsung_session_successful: bool = False samsung_session_attempt_count: int = 0 waiting_for_samsung_disconnect: bool = False trigger_input_code: int | None = None alienware_detected: bool = False alienware_input_code: int | None = None resolved_target: str | None = None ddm_slot: int | None = None ddm_ready: bool = False last_switch_result: str = "idle" last_switch_at: str | None = None errors: list[str] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: return { "config": self.config.to_dict(), "samsung_present": self.samsung_present, "samsung_connected_session_active": self.samsung_connected_session_active, "samsung_session_attempted": self.samsung_session_attempted, "samsung_session_successful": self.samsung_session_successful, "samsung_session_attempt_count": self.samsung_session_attempt_count, "waiting_for_samsung_disconnect": self.waiting_for_samsung_disconnect, "trigger_input_code": self.trigger_input_code, "alienware_detected": self.alienware_detected, "alienware_input_code": self.alienware_input_code, "resolved_target": self.resolved_target, "ddm_slot": self.ddm_slot, "ddm_ready": self.ddm_ready, "last_switch_result": self.last_switch_result, "last_switch_at": self.last_switch_at, "errors": list(self.errors), } class KvmSwitcherService: def __init__( self, config_store: ConfigStore | None = None, monitor_backend: MonitorBackend | None = None, ddm_backend: DDMBackend | None = None, poll_interval_seconds: float = 0.25, retry_wait_seconds: float = 5.0, ): self.config_store = config_store or ConfigStore() self.monitor_backend = monitor_backend or RealMonitorBackend() self.ddm_backend = ddm_backend or RealDDMBackend() self.poll_interval_seconds = poll_interval_seconds self.retry_wait_seconds = retry_wait_seconds self._status = ServiceStatus(config=self.config_store.get()) self._samsung_session_active = False self._samsung_session_attempted = False self._samsung_session_successful = False self._samsung_session_attempt_count = 0 self._state_lock = Lock() self._stop_event = Event() self._thread: Thread | None = None def start(self) -> None: with self._state_lock: if self._thread and self._thread.is_alive(): return self._stop_event.clear() self._thread = Thread(target=self._run_loop, name="kvm-switch-poller", daemon=True) self._thread.start() def stop(self) -> None: self._stop_event.set() with self._state_lock: thread = self._thread self._thread = None if thread and thread.is_alive(): thread.join(timeout=2.0) def get_status(self) -> dict[str, Any]: with self._state_lock: return self._status.to_dict() def save_settings(self, device_role: str, device_port: str) -> dict[str, Any]: new_config = AppConfig( device_role=device_role, device_port=device_port, ) self.config_store.save(new_config) return self.poll_once() def poll_once(self) -> dict[str, Any]: config = self.config_store.get() scan = self.monitor_backend.scan() self._update_samsung_session(scan.samsung_present) errors = list(scan.errors) errors.extend(config.validate()) ddm_slot = self.ddm_backend.resolve_alienware_slot(force=False) ddm_ready = self.ddm_backend.is_available() and ddm_slot is not None if not self.ddm_backend.is_available(): errors.append("DDM.exe was not found.") elif ddm_slot is None: errors.append("Alienware DDM slot could not be resolved.") resolved_target, desired_port = self._resolve_target(config, scan) last_switch_result = "idle" with self._state_lock: last_switch_at = self._status.last_switch_at should_attempt_switch = ( not errors and desired_port is not None and scan.alienware_input_code is not None and ddm_slot is not None and scan.samsung_present and not self._samsung_session_successful and self._samsung_session_attempt_count < 3 ) if should_attempt_switch: desired_codes = self._port_input_codes(desired_port) if scan.alienware_input_code in desired_codes: last_switch_result = "noop" self._samsung_session_attempted = True self._samsung_session_successful = True else: last_switch_result, last_switch_at, scan, errors = self._attempt_switch_sequence( ddm_slot=ddm_slot, desired_port=desired_port, scan=scan, last_switch_at=last_switch_at, errors=errors, ) elif desired_port is not None and not config.validate(): last_switch_result = "blocked" elif resolved_target == config.device_role and self._samsung_session_successful: last_switch_result = "waiting_for_disconnect" elif resolved_target == config.device_role and self._samsung_session_attempt_count >= 3: last_switch_result = "max_attempts_waiting_for_disconnect" status = ServiceStatus( config=config, samsung_present=scan.samsung_present, samsung_connected_session_active=self._samsung_session_active, samsung_session_attempted=self._samsung_session_attempted, samsung_session_successful=self._samsung_session_successful, samsung_session_attempt_count=self._samsung_session_attempt_count, waiting_for_samsung_disconnect=self._samsung_session_successful or self._samsung_session_attempt_count >= 3, trigger_input_code=scan.trigger_input_code, alienware_detected=scan.alienware_detected, alienware_input_code=scan.alienware_input_code, resolved_target=resolved_target, ddm_slot=ddm_slot, ddm_ready=ddm_ready, last_switch_result=last_switch_result, last_switch_at=last_switch_at, errors=_dedupe_errors(errors), ) with self._state_lock: self._status = status return self._status.to_dict() def _run_loop(self) -> None: while not self._stop_event.is_set(): try: self.poll_once() except Exception as exc: with self._state_lock: errors = list(self._status.errors) errors.append(f"Unhandled polling error: {exc}") self._status.errors = _dedupe_errors(errors) self._status.last_switch_result = "error" self._stop_event.wait(self.poll_interval_seconds) @staticmethod def _resolve_target( config: AppConfig, scan: HardwareScan, ) -> tuple[str | None, str | None]: trigger_input = scan.trigger_input_code if trigger_input is None: return None, None if trigger_input == TOWER_TRIGGER_CODE: return "tower", config.device_port if config.device_role == "tower" else None if trigger_input == LAPTOP_TRIGGER_CODE: return "laptop", config.device_port if config.device_role == "laptop" else None return None, None @staticmethod def _port_input_codes(port_name: str) -> set[int]: port_spec = SUPPORTED_TARGET_PORTS.get(port_name.upper(), {}) raw_codes = port_spec.get("input_codes", set()) return {int(code) for code in raw_codes} def _update_samsung_session(self, samsung_present: bool) -> None: if samsung_present: if not self._samsung_session_active: self._samsung_session_active = True self._samsung_session_attempted = False self._samsung_session_successful = False self._samsung_session_attempt_count = 0 else: self._samsung_session_active = False self._samsung_session_attempted = False self._samsung_session_successful = False self._samsung_session_attempt_count = 0 def _attempt_switch_sequence( self, ddm_slot: int, desired_port: str, scan: HardwareScan, last_switch_at: str | None, errors: list[str], ) -> tuple[str, str | None, HardwareScan, list[str]]: last_result = "blocked" while self._samsung_session_attempt_count < 3: self._samsung_session_attempted = True self._samsung_session_attempt_count += 1 result = self.ddm_backend.switch_to_port(ddm_slot, desired_port) if not result.success: errors.append(result.message) self.ddm_backend.invalidate_slot() return "error", last_switch_at, scan, errors last_switch_at = datetime.now(UTC).isoformat() last_result = "switched" self._stop_event.wait(self.retry_wait_seconds) verify_scan = self.monitor_backend.scan() scan = verify_scan errors.extend(verify_scan.errors) self._update_samsung_session(verify_scan.samsung_present) if not verify_scan.samsung_present: return "waiting_for_reconnect", last_switch_at, scan, errors desired_codes = self._port_input_codes(desired_port) if verify_scan.alienware_input_code in desired_codes: self._samsung_session_successful = True return last_result, last_switch_at, scan, errors return "max_attempts_waiting_for_disconnect", last_switch_at, scan, errors def _dedupe_errors(errors: list[str]) -> list[str]: unique: list[str] = [] seen: set[str] = set() for error in errors: normalized = error.strip() if not normalized or normalized in seen: continue seen.add(normalized) unique.append(normalized) return unique