"""WinRM service: connect to Windows hosts, run PowerShell, stream output. Uses python-winrm (pywinrm). All blocking calls run in a thread pool so the async event loop is never blocked. """ import asyncio from collections.abc import AsyncIterator from dataclasses import dataclass from app.core.config import get_settings from app.core.exceptions import WinRMError from app.core.logging import get_logger settings = get_settings() logger = get_logger(__name__) @dataclass class WinRMCredentials: username: str password: str class WinRMService: """Wraps pywinrm for async usage.""" def __init__( self, hostname: str, port: int = 5985, credentials: WinRMCredentials | None = None, transport: str | None = None, ) -> None: self.hostname = hostname self.port = port self.credentials = credentials self.transport = transport or settings.winrm_transport def _build_session(self): # type: ignore[no-untyped-def] import winrm # pywinrm scheme = "https" if self.port == 5986 else "http" endpoint = f"{scheme}://{self.hostname}:{self.port}/wsman" kwargs: dict = { "transport": self.transport, "server_cert_validation": settings.winrm_cert_validation, "operation_timeout_sec": settings.winrm_operation_timeout, "read_timeout_sec": settings.winrm_read_timeout, } if self.credentials: kwargs["username"] = self.credentials.username kwargs["password"] = self.credentials.password return winrm.Session(endpoint, **kwargs) async def test_connection(self) -> tuple[bool, str]: """Run a trivial command to verify connectivity.""" def _run() -> tuple[bool, str]: try: session = self._build_session() result = session.run_ps("$env:COMPUTERNAME") if result.status_code == 0: name = result.std_out.decode(errors="replace").strip() return True, f"Verbunden mit {name}" return False, result.std_err.decode(errors="replace").strip() except Exception as exc: # noqa: BLE001 - surface any transport error return False, str(exc) return await asyncio.to_thread(_run) async def run_powershell(self, script: str) -> str: """Run a PowerShell script and return stdout. Raises WinRMError on failure.""" def _run() -> str: session = self._build_session() result = session.run_ps(script) if result.status_code != 0: err = result.std_err.decode(errors="replace").strip() raise WinRMError(f"PowerShell exit {result.status_code}: {err}") return result.std_out.decode(errors="replace") return await asyncio.to_thread(_run) async def stream_powershell(self, script: str) -> AsyncIterator[str]: """Yield output lines as they arrive (scaffold: runs to completion, then yields). TODO: switch to winrm Protocol with shell polling for true streaming. """ output = await self.run_powershell(script) for line in output.splitlines(): yield line await asyncio.sleep(0) # keep the loop responsive async def get_pending_updates(self) -> list[str]: """Query Windows Update for pending updates (titles only).""" script = """ $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $result = $searcher.Search("IsInstalled=0") $result.Updates | ForEach-Object { $_.Title } """ output = await self.run_powershell(script) return [line.strip() for line in output.splitlines() if line.strip()] async def install_updates(self) -> AsyncIterator[str]: """Install all pending Windows Updates, yielding progress lines.""" script = """ $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $result = $searcher.Search("IsInstalled=0") Write-Output "Gefundene Updates: $($result.Updates.Count)" $toInstall = New-Object -ComObject Microsoft.Update.UpdateColl $result.Updates | ForEach-Object { $toInstall.Add($_) | Out-Null } $installer = $session.CreateUpdateInstaller() $installer.Updates = $toInstall $installResult = $installer.Install() Write-Output "ResultCode: $($installResult.ResultCode)" Write-Output "RebootRequired: $($installResult.RebootRequired)" """ async for line in self.stream_powershell(script): yield line