From e119aa50e9321ac6af3f325d092aa2c404f7162f Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 25 Jun 2026 20:45:14 +0000 Subject: [PATCH] =?UTF-8?q?feat(windows-packaging):=20dev-iteration=20scri?= =?UTF-8?q?pts=20=E2=80=94=20reset=20+=20redeploy=20pf-vdisplay=20driver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today's manual driver recovery (wedged under ADD/REMOVE churn → ERROR_NOT_FOUND) and the manual host-stop/install/host-start dance around drivers/deploy-dev.ps1 are now two scripts: * reset-pf-vdisplay.ps1 — recover a wedged driver: stop host → pnputil /remove-device the ghost "Generic Monitor (punktfunk)" nodes → Disable+Enable the adapter (Restart-PnpDevice doesn't exist on the box PS) → start host. No reboot (the box boots to Proxmox). -Verify probes to confirm ADD recovered. * redeploy-pf-vdisplay.ps1 — one-shot dev redeploy wrapping deploy-dev.ps1 with the host stop/start (the running host holds the driver DLL) + a post-install adapter reload (pnputil updates the store but the live device keeps the old binary). Both standalone (don't touch deploy-dev.ps1). README gains a "Dev iteration on the test box" section. Co-Authored-By: Claude Opus 4.8 (1M context) --- packaging/windows/README.md | 34 +++++- packaging/windows/redeploy-pf-vdisplay.ps1 | 99 ++++++++++++++++ packaging/windows/reset-pf-vdisplay.ps1 | 130 +++++++++++++++++++++ 3 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 packaging/windows/redeploy-pf-vdisplay.ps1 create mode 100644 packaging/windows/reset-pf-vdisplay.ps1 diff --git a/packaging/windows/README.md b/packaging/windows/README.md index 2554699..b26738e 100644 --- a/packaging/windows/README.md +++ b/packaging/windows/README.md @@ -71,21 +71,47 @@ read it from `%ProgramData%\punktfunk\web-password`. | `install-pf-vdisplay.ps1` | Runs at install time (elevated): trust cert → gated device-node create (nefconc) → `pnputil` install. | | `../../scripts/windows/web-run.cmd` | The `PunktfunkWeb` task action: loads the mgmt token + login password env, runs the bundled `bun` on the Nitro server (`:3000`). | | `../../scripts/windows/web-setup.ps1` | Install-time (elevated): write the ACL'd console password, register the `PunktfunkWeb` task + firewall rule, start it. | -| `pf-vdisplay/` | **Vendored** signed pf-vdisplay driver: `pf_vdisplay.inf` / `pf_vdisplay.cat` / `pf_vdisplay.dll` / `punktfunk-driver.cer`. Built from `vdisplay-driver/`. | -| `vdisplay-driver/` | The all-Rust IddCx **driver source** (`pf-vdisplay` crate + vendored `wdf-umdf*` bindings) + `deploy-dev.ps1` (build/sign/install for dev). | +| `pf-vdisplay/` | **Vendored** signed pf-vdisplay driver: `pf_vdisplay.inf` / `pf_vdisplay.cat` / `pf_vdisplay.dll` / `punktfunk-driver.cer`. Built from `drivers/`. | +| `drivers/` | The all-Rust IddCx **driver source** workspace: the `pf-vdisplay` crate on `wdk-sys` / windows-drivers-rs + the owned `pf-vdisplay-proto` ABI + `wdk-iddcx` / `wdk-probe`, plus `deploy-dev.ps1` (build/sign/install for dev). | +| `reset-pf-vdisplay.ps1` | **Dev:** recover a wedged driver — stop host → reap ghost monitor nodes → reload the adapter → start host (no reboot). See *Dev iteration* below. | +| `redeploy-pf-vdisplay.ps1` | **Dev:** one-shot redeploy — (optional) build → stop host → `deploy-dev.ps1 -Install` → reload adapter → start host. | | `nvenc/nvenc.def`, `nvenc/gen-nvenc-importlib.ps1` | Synthesise `nvencodeapi.lib` for the `--features nvenc` link (llvm-dlltool / lib.exe). | > **Vendored driver:** pf-vdisplay is our **all-Rust IddCx** virtual display (UMDF2), built from -> `packaging/windows/vdisplay-driver/`. It replaced the vendored SudoVDA C++ driver — full story in +> `packaging/windows/drivers/`. It replaced the vendored SudoVDA C++ driver — full story in > [`docs/windows-virtual-display-rust-port.md`](../../docs/windows-virtual-display-rust-port.md). The > **signed** output (`pf_vdisplay.dll`/`.inf`/`.cat` + `punktfunk-driver.cer`; signer > `punktfunk-ds-test` — the same cert the gamepad drivers ship, Class=Display, HWID `root\pf_vdisplay`) > is checked in under `pf-vdisplay/`. To refresh it after a driver-source change, rebuild + re-sign with -> `vdisplay-driver/deploy-dev.ps1` and copy the staged `pf_vdisplay.{dll,inf,cat}` over the vendored +> `drivers/deploy-dev.ps1` and copy the staged `pf_vdisplay.{dll,inf,cat}` over the vendored > copies. nefcon (the device-node tool — the install creates the node with it, **never** `devgen`, which > leaves persistent phantom devices) **is** fetched + SHA-256-verified from its pinned release in > `stage-pf-vdisplay.ps1`. +## Dev iteration on the test box (driver) + +Two helpers wrap the painful manual steps of iterating on the pf-vdisplay driver against a live host +service. Run **elevated**; both default to the `PunktfunkHost` service. + +```powershell +# Recover a WEDGED driver. Symptom: every session fails with +# create virtual output: pf-vdisplay ADD ...: DeviceIoControl(0x222400): Element nicht gefunden (0x80070490) +# i.e. ERROR_NOT_FOUND — sustained ADD/REMOVE churn exhausted the IddCx monitor slots (ghost +# "Generic Monitor (punktfunk)" nodes pile up, target_ids climb). A host restart's CLEAR_ALL does NOT +# fix it; the driver instance must be reloaded. This clears the ghosts + cycles the adapter (no reboot — +# this box boots to Proxmox). +powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1 -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe + +# Redeploy a driver build cleanly (stop host → install with a strictly-increasing DriverVer → reload +# adapter → start host). -Build runs `cargo build` first, but ONLY from an MSVC dev shell +# (LIBCLANG_PATH + Version_Number=10.0.26100.0); otherwise build separately and omit -Build. +powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1 -Build -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe +``` + +The driver should reclaim monitor slots on REMOVE so churn can't wedge it; until it does, `reset` is +the recovery. From a Linux box drive either over SSH, e.g. +`ssh user@box 'powershell -ExecutionPolicy Bypass -File C:\...\reset-pf-vdisplay.ps1'`. + ## Build locally (Windows, MSVC + Windows SDK + Inno Setup) ```powershell diff --git a/packaging/windows/redeploy-pf-vdisplay.ps1 b/packaging/windows/redeploy-pf-vdisplay.ps1 new file mode 100644 index 0000000..96c3a00 --- /dev/null +++ b/packaging/windows/redeploy-pf-vdisplay.ps1 @@ -0,0 +1,99 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + One-shot DEV redeploy of the pf-vdisplay (punktfunk) virtual-display driver on the test box: + (optional) build -> stop host -> stage+sign+install -> reload the adapter -> start host. + +.DESCRIPTION + Wraps drivers/deploy-dev.ps1 (which stages the freshly built pf_vdisplay.dll, clears its + FORCE_INTEGRITY PE bit, signs it, stamps a STRICTLY-INCREASING DriverVer, builds+signs the catalog, + and pnputil-installs it) with the two things the dev loop always needs around it: + + * The running host service HOLDS the driver's control device, and pnputil can't replace a busy + DLL - so the host must be stopped across the install. This stops it first and starts it after. + * pnputil /add-driver /install updates the driver STORE, but the OS keeps the LIVE adapter on the + old binary until the device is reloaded - so this cycles the adapter (reset-pf-vdisplay.ps1) + after install, which also clears the ghost monitor nodes for a clean slate. + + Run ELEVATED. Use -Build only from an MSVC dev shell (the driver's cargo build needs LIBCLANG_PATH + + Version_Number=10.0.26100.0, per drivers/deploy-dev.ps1); otherwise build separately and omit it. + +.PARAMETER Build Run `cargo build` in packaging/windows/drivers first (needs the MSVC env). +.PARAMETER Service Host service name. Default PunktfunkHost. +.PARAMETER Thumbprint Passthrough to deploy-dev.ps1 (test-cert SHA-1). Omit to use its default. +.PARAMETER Nefconc Passthrough to deploy-dev.ps1 (nefconc.exe path). Omit to use its default. +.PARAMETER Verify After redeploy, probe to confirm ADD works (passes through to the reset's + -Verify; needs -Probe or punktfunk-probe.exe on PATH). +.PARAMETER Probe Path to punktfunk-probe.exe for -Verify. + +.EXAMPLE + # already built the driver in an MSVC shell -> deploy it cleanly: + powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1 +.EXAMPLE + # build + deploy + verify, from an MSVC dev shell: + powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1 -Build -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe +#> +[CmdletBinding()] +param( + [switch]$Build, + [string]$Service = 'PunktfunkHost', + [string]$Thumbprint, + [string]$Nefconc, + [switch]$Verify, + [string]$Probe +) +$ErrorActionPreference = 'Stop' + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$driversDir = Join-Path $here 'drivers' +$deploy = Join-Path $driversDir 'deploy-dev.ps1' +$reset = Join-Path $here 'reset-pf-vdisplay.ps1' +foreach ($f in @($deploy, $reset)) { if (-not (Test-Path $f)) { throw "missing helper: $f" } } + +# 1) Optional rebuild (MSVC dev shell only). +if ($Build) { + Write-Host "==> cargo build (pf-vdisplay driver, $driversDir)" + Push-Location $driversDir + try { + cargo build + if ($LASTEXITCODE -ne 0) { throw "cargo build failed ($LASTEXITCODE) - is this an MSVC dev shell with LIBCLANG_PATH + Version_Number set?" } + } + finally { Pop-Location } +} + +# 2) Stop the host (it holds the driver DLL; pnputil can't replace a busy binary). +$svc = Get-Service $Service -ErrorAction SilentlyContinue +if ($svc -and $svc.Status -eq 'Running') { + Write-Host "==> stopping $Service" + Stop-Service $Service -Force + Start-Sleep -Seconds 2 +} + +# 3) Stage + sign + install (strictly-increasing DriverVer so pnputil takes the new binary). +$deployArgs = @{ Install = $true } +if ($Thumbprint) { $deployArgs.Thumbprint = $Thumbprint } +if ($Nefconc) { $deployArgs.Nefconc = $Nefconc } +Write-Host "==> deploy-dev.ps1 -Install" +& $deploy @deployArgs + +# 4) Reload the adapter so the OS loads the freshly-installed binary (+ clear ghost nodes). The reset +# leaves the host alone (-NoHost) - we own the service lifecycle here. +Write-Host "==> reloading the pf-vdisplay adapter (clean slate)" +& $reset -NoHost + +# 5) Start the host. +if ($svc) { + Write-Host "==> starting $Service" + Start-Service $Service + Start-Sleep -Seconds 3 + Write-Host " $Service status: $((Get-Service $Service -ErrorAction SilentlyContinue).Status)" +} + +# 6) Optional verification probe. +if ($Verify) { + $vArgs = @{ NoHost = $true; KeepGhosts = $true; Verify = $true } + if ($Probe) { $vArgs.Probe = $Probe } + & $reset @vArgs +} + +Write-Host "pf-vdisplay redeploy done." diff --git a/packaging/windows/reset-pf-vdisplay.ps1 b/packaging/windows/reset-pf-vdisplay.ps1 new file mode 100644 index 0000000..52a066f --- /dev/null +++ b/packaging/windows/reset-pf-vdisplay.ps1 @@ -0,0 +1,130 @@ +#requires -Version 5.1 +<# +.SYNOPSIS + Recover the pf-vdisplay (punktfunk) virtual-display driver after it WEDGES under rapid ADD/REMOVE + churn - no reboot. The dev-iteration counterpart to redeploy-pf-vdisplay.ps1. + +.DESCRIPTION + Sustained connect/disconnect churn (e.g. a client reconnect loop x the host's 8 pipeline-build + retries - ~100 ADD/REMOVE cycles) exhausts the driver's IddCx monitor slots: the per-monitor + target_ids climb, ghost "Generic Monitor (punktfunk)" device nodes pile up, and eventually + IOCTL_ADD returns 0x80070490 ERROR_NOT_FOUND ("Element nicht gefunden"). Every session then fails + to create a virtual output -> the client gets a hard blackscreen. A host-service restart's + IOCTL_CLEAR_ALL does NOT recover it; the driver instance itself must be reloaded. + + Steps (run ELEVATED): + 1. Stop the host service (it holds the driver's control device). + 2. pnputil /remove-device the GHOST (Status != OK = not-present) punktfunk virtual-monitor nodes + that accumulated - the root of the slot exhaustion. + 3. Disable + Enable the pf-vdisplay adapter (ROOT\DISPLAY\*, "punktfunk Virtual Display") to + reload the IddCx driver instance and reset its monitor list. (Restart-PnpDevice does NOT exist + on this box's PowerShell, so we disable+enable explicitly.) + 4. Restart the host service. + Avoids a reboot on purpose (this box boots to Proxmox). + +.PARAMETER Service Host service name. Default PunktfunkHost. +.PARAMETER AdapterName FriendlyName substring of the IddCx adapter to cycle. Default "punktfunk + Virtual Display" (NOT SudoVDA's "SudoMaker Virtual Display Adapter"). +.PARAMETER GhostMatch FriendlyName substring of the virtual monitors to reap. Default "punktfunk". +.PARAMETER KeepGhosts Skip the ghost-node cleanup; only cycle the adapter. +.PARAMETER NoHost Don't stop/start the host service (just reset the driver) - used by + redeploy-pf-vdisplay.ps1, which manages the service itself. +.PARAMETER Verify After recovery, run a punktfunk-probe loopback and report whether ADD works + again (best-effort; needs punktfunk-probe.exe on PATH or via -Probe). +.PARAMETER Probe Path to punktfunk-probe.exe for -Verify. + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1 +.EXAMPLE + powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1 -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe +#> +[CmdletBinding()] +param( + [string]$Service = 'PunktfunkHost', + [string]$AdapterName = 'punktfunk Virtual Display', + [string]$GhostMatch = 'punktfunk', + [switch]$KeepGhosts, + [switch]$NoHost, + [switch]$Verify, + [string]$Probe +) +$ErrorActionPreference = 'Continue' + +function Get-PfAdapter { + Get-PnpDevice -Class Display -ErrorAction SilentlyContinue | + Where-Object { $_.FriendlyName -match $AdapterName } | Select-Object -First 1 +} + +# 1) Stop the host so it isn't mid-IOCTL during the reset (it holds the control device). +$svc = Get-Service $Service -ErrorAction SilentlyContinue +$hostWasRunning = $svc -and $svc.Status -eq 'Running' +if (-not $NoHost -and $hostWasRunning) { + Write-Host "==> stopping $Service" + Stop-Service $Service -Force + Start-Sleep -Seconds 2 +} + +# 2) Reap the ghost (not-present) punktfunk virtual-monitor device nodes. +if (-not $KeepGhosts) { + $ghosts = Get-PnpDevice -Class Monitor -ErrorAction SilentlyContinue | + Where-Object { $_.Status -ne 'OK' -and $_.FriendlyName -match $GhostMatch } + Write-Host "==> removing $($ghosts.Count) ghost virtual-monitor node(s)" + $removed = 0 + foreach ($g in $ghosts) { + pnputil /remove-device $g.InstanceId *> $null + if ($LASTEXITCODE -eq 0) { $removed++ } + } + Write-Host " removed $removed" +} + +# 3) Reload the IddCx adapter instance (disable + enable) to clear its monitor list. +$ad = Get-PfAdapter +if (-not $ad) { + Write-Warning "pf-vdisplay adapter '$AdapterName' not found (Class Display) - is the driver installed?" +} +else { + Write-Host "==> cycling adapter $($ad.InstanceId)" + Disable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false -ErrorAction Continue + Start-Sleep -Seconds 3 + Enable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false -ErrorAction Continue + Start-Sleep -Seconds 3 + $st = (Get-PnpDevice -InstanceId $ad.InstanceId -ErrorAction SilentlyContinue).Status + if ($st -ne 'OK') { + # One retry - a disabled root device occasionally needs a second enable to come back OK. + Enable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false -ErrorAction Continue + Start-Sleep -Seconds 2 + $st = (Get-PnpDevice -InstanceId $ad.InstanceId -ErrorAction SilentlyContinue).Status + } + Write-Host " adapter status: $st" +} + +# 4) Restart the host. +if (-not $NoHost -and $svc) { + Write-Host "==> starting $Service" + Start-Service $Service + Start-Sleep -Seconds 3 + Write-Host " $Service status: $((Get-Service $Service -ErrorAction SilentlyContinue).Status)" +} + +# 5) Optional: probe to confirm ADD recovers. +if ($Verify) { + if (-not $Probe) { + $Probe = (Get-Command punktfunk-probe.exe -ErrorAction SilentlyContinue).Source + } + if (-not $Probe -or -not (Test-Path $Probe)) { + Write-Warning "-Verify: punktfunk-probe.exe not found (pass -Probe ); skipping verification." + } + else { + $log = Join-Path $env:ProgramData 'punktfunk\logs\host.log' + Write-Host "==> verifying with $Probe" + & $Probe *> $null + Start-Sleep -Seconds 2 + $last = Get-Content $log -Tail 80 -ErrorAction SilentlyContinue | + Select-String -Pattern 'pf-vdisplay created|Element nicht|0x80070490' | Select-Object -Last 1 + if ($last -match 'created') { Write-Host " OK: ADD succeeded after reset." } + elseif ($last) { Write-Warning " ADD still failing after reset: $($last.Line.Trim())" } + else { Write-Warning " no ADD outcome found in the log; check $log." } + } +} + +Write-Host "pf-vdisplay reset done."