Files
KVM_Switch/app/ddm.py
T

204 lines
6.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from threading import Lock
import shutil
import subprocess
import tempfile
import time
from typing import Protocol
from app.config import SUPPORTED_TARGET_PORTS
DEFAULT_DDM_PATH = Path(r"C:\Program Files\Dell\Dell Display Manager 2.0\DDM.exe")
ALIENWARE_MODEL_TOKEN = "AW3423DWF"
@dataclass(slots=True)
class DDMCommandResult:
success: bool
message: str
raw_output: str = ""
class DDMBackend(Protocol):
def is_available(self) -> bool: ...
def supports_monitor_targeting(self) -> bool: ...
def resolve_alienware_slot(self, force: bool = False) -> int | None: ...
def invalidate_slot(self) -> None: ...
def switch_to_port(self, slot: int, port_name: str) -> DDMCommandResult: ...
class RealDDMBackend:
def __init__(
self,
executable_path: Path | None = None,
max_slots: int = 8,
command_timeout_seconds: float = 4.0,
log_timeout_seconds: float = 3.0,
):
self.executable_path = executable_path or self._find_executable()
self.max_slots = max_slots
self.command_timeout_seconds = command_timeout_seconds
self.log_timeout_seconds = log_timeout_seconds
self._cached_slot: int | None = None
self._lock = Lock()
def is_available(self) -> bool:
return self.executable_path is not None and self.executable_path.exists()
def supports_monitor_targeting(self) -> bool:
return True
def invalidate_slot(self) -> None:
with self._lock:
self._cached_slot = None
def resolve_alienware_slot(self, force: bool = False) -> int | None:
if not self.is_available():
return None
with self._lock:
if self._cached_slot is not None and not force:
return self._cached_slot
for slot in range(1, self.max_slots + 1):
result = self._run_logged_command(f"/{slot}:ReadAssetAttributes")
line = _extract_result_line(result, f"{slot}:ReadAssetAttributes")
if not line:
continue
if "INVALID COMMAND" in line.upper():
continue
if ALIENWARE_MODEL_TOKEN in line.upper():
self._cached_slot = slot
return slot
self._cached_slot = None
return None
def switch_to_port(self, slot: int, port_name: str) -> DDMCommandResult:
if not self.is_available():
return DDMCommandResult(False, "DDM.exe was not found.")
port_spec = SUPPORTED_TARGET_PORTS.get(port_name.upper())
if not port_spec:
return DDMCommandResult(False, f"Port {port_name} is not supported.")
ddm_input = str(port_spec["ddm_input"])
monitor_selector = f"/MNT:{ALIENWARE_MODEL_TOKEN}"
targeted_output = self._run_logged_command(
monitor_selector,
"/WriteActiveInput",
ddm_input,
)
targeted_line = _extract_result_line_fragment(targeted_output, "WRITEACTIVEINPUT")
if not _output_has_failure(targeted_line, targeted_output):
return DDMCommandResult(
True,
f"Alienware {ALIENWARE_MODEL_TOKEN} was instructed to switch to {ddm_input}.",
targeted_output,
)
if slot > 0:
legacy_output = self._run_logged_command(f"/{slot}:WriteActiveInput", ddm_input)
legacy_line = _extract_result_line(legacy_output, f"{slot}:WriteActiveInput")
if not _output_has_failure(legacy_line, legacy_output):
return DDMCommandResult(
True,
f"Alienware DDM slot {slot} was instructed to switch to {ddm_input}.",
legacy_output,
)
merged_output = "\n".join(
block
for block in [
"[targeted]",
targeted_output.strip(),
"[legacy]",
legacy_output.strip(),
]
if block
)
return DDMCommandResult(False, "DDM rejected the WriteActiveInput command.", merged_output)
return DDMCommandResult(False, "DDM rejected the WriteActiveInput command.", targeted_output)
def _run_logged_command(self, *command_args: str) -> str:
assert self.executable_path is not None
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as handle:
log_path = Path(handle.name)
try:
subprocess.run(
[str(self.executable_path), "/Log", str(log_path), *command_args],
check=False,
capture_output=True,
text=True,
timeout=self.command_timeout_seconds,
)
deadline = time.monotonic() + self.log_timeout_seconds
while time.monotonic() < deadline:
try:
contents = log_path.read_text(encoding="utf-8", errors="replace")
except OSError:
contents = ""
if contents.strip():
return contents
time.sleep(0.1)
try:
return log_path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
finally:
try:
log_path.unlink()
except OSError:
pass
def _find_executable(self) -> Path | None:
resolved = shutil.which("DDM.exe")
if resolved:
return Path(resolved)
if DEFAULT_DDM_PATH.exists():
return DEFAULT_DDM_PATH
return None
def _extract_result_line(text: str, prefix: str) -> str:
for line in text.splitlines():
normalized = line.strip()
if normalized.startswith(prefix):
return normalized
return ""
def _extract_result_line_fragment(text: str, fragment: str) -> str:
needle = fragment.upper()
for line in text.splitlines():
normalized = line.strip()
if needle in normalized.upper():
return normalized
return ""
def _output_has_failure(line: str, output: str) -> bool:
normalized_line = line.upper()
normalized_output = output.upper()
checks = (
"INVALID COMMAND",
"UNSUPPORTED",
"NOT FOUND",
"FAILED",
"FAIL",
)
return any(token in normalized_line or token in normalized_output for token in checks)