diff --git a/.gitea/workflows/windows-host.yml b/.gitea/workflows/windows-host.yml index 8ce3294..30a5176 100644 --- a/.gitea/workflows/windows-host.yml +++ b/.gitea/workflows/windows-host.yml @@ -56,6 +56,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Locale-safety gate (installer-run scripts must be ASCII) + shell: pwsh + # The installer runs these via powershell.exe (Windows PowerShell 5.1) and cmd.exe on the END + # USER's box. PS 5.1 reads a BOM-less script in the active ANSI codepage, so on a non-UTF-8 locale + # (e.g. German Windows-1252) a stray em-dash mis-decodes into a curly quote and the script aborts + # with "unterminated string" - exactly how the pf-vdisplay driver install silently failed in the + # field. Keep every installer-run script pure ASCII (matches install-gamepad-drivers.ps1). + run: | + $bad = Get-ChildItem packaging/windows/*.ps1, scripts/windows/*.ps1, scripts/windows/*.cmd -ErrorAction SilentlyContinue | + Where-Object { [IO.File]::ReadAllText($_.FullName) -match '[^\x00-\x7F]' } + if ($bad) { + $bad.FullName | ForEach-Object { Write-Output "::error::non-ASCII in installer-run script: $_" } + throw "installer-run scripts must be pure ASCII (PS 5.1 mis-parses them on non-UTF-8 locales)" + } + Write-Output "installer-run scripts are ASCII-clean" + - name: Configure + version shell: pwsh run: | diff --git a/packaging/windows/build-pf-vdisplay.ps1 b/packaging/windows/build-pf-vdisplay.ps1 new file mode 100644 index 0000000..c4679e7 --- /dev/null +++ b/packaging/windows/build-pf-vdisplay.ps1 @@ -0,0 +1,137 @@ +<# +.SYNOPSIS + Build + sign the pf-vdisplay UMDF IddCx virtual-display driver FROM SOURCE, in CI, and stage it for the + host installer. This REPLACES the old vendored-prebuilt-binary model (packaging/windows/pf-vdisplay/) - + the binary went stale (frozen mid-June while the driver source kept moving), which silently shipped two + field bugs: (1) the catalog no longer covered the edited INF (pnputil SPAPI_E_FILE_HASH_NOT_IN_CATALOG on + every box), and (2) the binary predated IOCTL_SET_RENDER_ADAPTER that the host needs to pin the IDD render + GPU on hybrid/Optimus boxes. Building every release from source keeps the .dll/.inf/.cat in lockstep and + ships current driver features. + +.DESCRIPTION + Mirrors packaging/windows/drivers/deploy-dev.ps1 but for CI (release build, output to -Out, cert from a + secret OR a fresh self-signed). Steps: cargo build (the wdk-sys/windows-drivers-rs driver workspace) -> + CLEAR the FORCE_INTEGRITY PE bit (wdk-build links /INTEGRITYCHECK, which a non-EV cert can't satisfy) -> + sign the .dll -> stampinf a strictly-increasing DriverVer into the INF -> Inf2Cat the catalog -> sign the + catalog -> export the public .cer. Output (-Out): pf_vdisplay.{dll,inf,cat} + punktfunk-driver.cer. + + Requires the WDK build env: cargo + the x64 MSVC toolset, an LLVM compatible with the driver's bindgen + (>= 0.72 supports current clang), LIBCLANG_PATH, and the Windows 10/11 WDK (the runner has these). Sets + Version_Number for wdk-build if the caller didn't. + +.EXAMPLE + pwsh -File build-pf-vdisplay.ps1 -Out C:\t\pfvd -DriverVer 9.9.0626.1612 +#> +[CmdletBinding()] +param( + [string]$DriversDir = (Join-Path $PSScriptRoot 'drivers'), + [Parameter(Mandatory = $true)][string]$Out, + [string]$DriverVer, # default: 9.9.MMdd.HHmm (strictly-increasing) + [string]$CertPfxB64 = $env:DRIVER_CERT_PFX_B64, # optional stable driver-signing cert (CI secret) + [string]$CertPassword = $env:DRIVER_CERT_PASSWORD, + [switch]$SkipBuild # reuse an existing target\...\release\pf_vdisplay.dll +) +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$PSNativeCommandUseErrorActionPreference = $false + +$DriversDir = (Resolve-Path $DriversDir).Path +$inx = Join-Path $DriversDir 'pf-vdisplay\pf_vdisplay.inx' +$clear = Join-Path $PSScriptRoot 'clear-force-integrity.ps1' +if (-not (Test-Path $inx)) { throw "no pf_vdisplay.inx under $DriversDir" } + +# --- WDK build env (wdk-build needs Version_Number; bindgen needs LIBCLANG_PATH) -------------- +if (-not $env:Version_Number) { $env:Version_Number = '10.0.26100.0' } +if (-not $env:LIBCLANG_PATH -and (Test-Path 'C:\Program Files\LLVM\bin\libclang.dll')) { + $env:LIBCLANG_PATH = 'C:\Program Files\LLVM\bin' +} +# Isolate the driver's CARGO_TARGET_DIR from the host's (CI sets a shared C:\t): the driver is a +# SEPARATE workspace (own [workspace] + an explicit --target via .cargo/config), so give it its own +# output tree both to avoid cross-workspace churn and to make the .dll path predictable here. +$drvTarget = Join-Path (Split-Path -Parent $Out) 'pfvd-target' +$dll = Join-Path $drvTarget 'x86_64-pc-windows-msvc\release\pf_vdisplay.dll' + +# --- 1. build (release) ----------------------------------------------------------------------- +if (-not $SkipBuild) { + Write-Host "==> cargo build --release (pf-vdisplay) in $DriversDir (target -> $drvTarget)" + $prevTarget = $env:CARGO_TARGET_DIR + $env:CARGO_TARGET_DIR = $drvTarget + Push-Location $DriversDir + & cargo build --release + $rc = $LASTEXITCODE + Pop-Location + if ($prevTarget) { $env:CARGO_TARGET_DIR = $prevTarget } else { Remove-Item Env:\CARGO_TARGET_DIR -ErrorAction SilentlyContinue } + if ($rc -ne 0) { throw "pf-vdisplay cargo build failed ($rc)" } +} +if (-not (Test-Path $dll)) { throw "driver not built: $dll" } + +# --- 2. WDK sign tools ------------------------------------------------------------------------ +$kits = 'C:\Program Files (x86)\Windows Kits\10\bin' +function Find-Tool([string]$name, [string]$arch) { + (Get-ChildItem "$kits\*\$arch\$name" -ErrorAction SilentlyContinue | Sort-Object FullName | Select-Object -Last 1).FullName +} +$signtool = Find-Tool 'signtool.exe' 'x64' +$stampinf = Find-Tool 'stampinf.exe' 'x64' +$inf2cat = Find-Tool 'Inf2Cat.exe' 'x86' +foreach ($t in @($signtool, $stampinf, $inf2cat)) { + if (-not $t) { throw 'a WDK tool (signtool/stampinf/Inf2Cat) was not found - install the Windows 10/11 WDK.' } +} + +# --- 3. signing cert (supplied stable pfx OR fresh self-signed) ------------------------------- +$cleanupCert = $null +if ($CertPfxB64) { + Write-Host '==> signing with supplied driver cert (DRIVER_CERT_PFX_B64)' + $pfx = Join-Path $Out '..\driver-signing.pfx' + [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($CertPfxB64)) + $sec = if ($CertPassword) { ConvertTo-SecureString $CertPassword -AsPlainText -Force } else { $null } + $signArgs = @('/f', $pfx); if ($CertPassword) { $signArgs += @('/p', $CertPassword) } + $pubForCer = if ($sec) { Get-PfxCertificate -FilePath $pfx -Password $sec } else { Get-PfxCertificate -FilePath $pfx } +} +else { + Write-Host '==> no DRIVER_CERT_PFX_B64 -> generating a fresh self-signed driver cert (the installer trusts the bundled .cer at install time)' + $cleanupCert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=punktfunk-driver' ` + -CertStoreLocation Cert:\CurrentUser\My -KeyExportPolicy Exportable -NotAfter (Get-Date).AddYears(10) + $signArgs = @('/sha1', $cleanupCert.Thumbprint) + $pubForCer = $cleanupCert +} + +# --- 4. stage + clear FORCE_INTEGRITY + sign + cat -------------------------------------------- +if (Test-Path $Out) { Remove-Item $Out -Recurse -Force } +New-Item -ItemType Directory -Force -Path $Out | Out-Null +$sDll = Join-Path $Out 'pf_vdisplay.dll' +$sInf = Join-Path $Out 'pf_vdisplay.inf' +$sCat = Join-Path $Out 'pf_vdisplay.cat' +$sCer = Join-Path $Out 'punktfunk-driver.cer' +Copy-Item $dll $sDll -Force +Copy-Item $inx $sInf -Force # stampinf rewrites this copy in place + +# Clear FORCE_INTEGRITY BEFORE signing (it edits the PE, invalidating any signature). +& powershell -NoProfile -ExecutionPolicy Bypass -File $clear -Path $sDll | Out-Null + +if (-not $DriverVer) { $now = Get-Date; $DriverVer = '9.9.{0}.{1}' -f $now.ToString('MMdd'), $now.ToString('HHmm') } + +& $signtool sign /fd SHA256 @signArgs $sDll | Out-Null +if ($LASTEXITCODE -ne 0) { throw "signtool sign (dll) failed ($LASTEXITCODE)" } +& $stampinf -f $sInf -d '*' -a 'amd64' -u '2.15.0' -v $DriverVer | Out-Null +& $inf2cat /driver:$Out /os:10_X64 /uselocaltime | Out-Null +if (-not (Test-Path $sCat)) { throw "Inf2Cat did not produce $sCat" } +& $signtool sign /fd SHA256 @signArgs $sCat | Out-Null +if ($LASTEXITCODE -ne 0) { throw "signtool sign (cat) failed ($LASTEXITCODE)" } +Export-Certificate -Cert $pubForCer -FilePath $sCer | Out-Null +if ($cleanupCert) { Remove-Item "Cert:\CurrentUser\My\$($cleanupCert.Thumbprint)" -Force -ErrorAction SilentlyContinue } + +# --- 5. guard: assert the freshly-built catalog covers the inf + dll --------------------------- +# (Built-from-source can't drift, but this catches a botched stampinf/Inf2Cat ordering before it ships.) +$cat = Test-FileCatalog -CatalogFilePath $sCat -Path $Out -FilesToSkip 'pf_vdisplay.cat', 'punktfunk-driver.cer' -Detailed -ErrorAction SilentlyContinue +if ($cat) { + $covered = @($cat.CatalogItems.Keys) + foreach ($need in @('pf_vdisplay.inf', 'pf_vdisplay.dll')) { + if (-not ($covered | Where-Object { $_ -like "*$need" })) { + throw "catalog coverage guard: $need is NOT in $sCat (stampinf/Inf2Cat ordering bug?)" + } + } + Write-Host " catalog covers pf_vdisplay.inf + pf_vdisplay.dll (status=$($cat.Status))" +} + +Write-Host "==> built + signed pf-vdisplay DriverVer=$DriverVer -> $Out" +Get-ChildItem $Out -File | ForEach-Object { " $($_.Name) ($($_.Length) bytes)" } diff --git a/packaging/windows/clear-force-integrity.ps1 b/packaging/windows/clear-force-integrity.ps1 index 98a0f7d..6f3afee 100644 --- a/packaging/windows/clear-force-integrity.ps1 +++ b/packaging/windows/clear-force-integrity.ps1 @@ -1,10 +1,10 @@ # Clear the PE FORCE_INTEGRITY bit (IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY = 0x0080) from a driver DLL. # # windows-drivers-rs / wdk-build links UMDF drivers with /INTEGRITYCHECK (sets the bit) UNCONDITIONALLY -# (wdk-build configure_binary_build → cargo::rustc-cdylib-link-arg=/INTEGRITYCHECK; no opt-out). With the +# (wdk-build configure_binary_build -> cargo::rustc-cdylib-link-arg=/INTEGRITYCHECK; no opt-out). With the # bit set, Windows Code Integrity refuses to load a binary whose signature doesn't chain to a Microsoft -# root (errors 3004/3089) — so a SELF-SIGNED driver won't load. Clearing the bit (then re-signing) lets a -# self-signed driver load under Secure Boot — the same recipe the punktfunk gamepad drivers use, here as a +# root (errors 3004/3089) - so a SELF-SIGNED driver won't load. Clearing the bit (then re-signing) lets a +# self-signed driver load under Secure Boot - the same recipe the punktfunk gamepad drivers use, here as a # deterministic, idempotent, reusable step instead of a hand-run patch. # # Order in the packaging flow: cargo build -> THIS -> signtool (sign .dll) -> Inf2Cat (.cat) -> sign .cat. @@ -28,7 +28,7 @@ $FORCE_INTEGRITY = 0x0080 $dllchar = [BitConverter]::ToUInt16($b, $off) if (($dllchar -band $FORCE_INTEGRITY) -eq 0) { - Write-Host ("clear-force-integrity: already clear (DllCharacteristics=0x{0:X4}) — no change: $Path" -f $dllchar) + Write-Host ("clear-force-integrity: already clear (DllCharacteristics=0x{0:X4}) - no change: $Path" -f $dllchar) } else { $new = [uint16]($dllchar -band (-bnot $FORCE_INTEGRITY)) [BitConverter]::GetBytes($new).CopyTo($b, $off) @@ -36,7 +36,7 @@ if (($dllchar -band $FORCE_INTEGRITY) -eq 0) { Write-Host ("clear-force-integrity: cleared FORCE_INTEGRITY 0x{0:X4} -> 0x{1:X4} in $Path" -f $dllchar, $new) } -# Verify on disk (re-read) — the assertion. +# Verify on disk (re-read) - the assertion. $v = [BitConverter]::ToUInt16([IO.File]::ReadAllBytes($Path), $off) if (($v -band $FORCE_INTEGRITY) -ne 0) { throw ("FORCE_INTEGRITY still set after clear (0x{0:X4})" -f $v) } Write-Host ("clear-force-integrity: verified DllCharacteristics=0x{0:X4}, FORCE_INTEGRITY clear." -f $v) diff --git a/packaging/windows/drivers/Cargo.lock b/packaging/windows/drivers/Cargo.lock index 5fac84d..1525c2c 100644 --- a/packaging/windows/drivers/Cargo.lock +++ b/packaging/windows/drivers/Cargo.lock @@ -69,9 +69,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", @@ -394,6 +394,13 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pf-driver-proto" +version = "0.0.1" +dependencies = [ + "bytemuck", +] + [[package]] name = "pf-vdisplay" version = "0.0.1" @@ -407,13 +414,6 @@ dependencies = [ "windows", ] -[[package]] -name = "pf-driver-proto" -version = "0.0.1" -dependencies = [ - "bytemuck", -] - [[package]] name = "pin-project-lite" version = "0.2.17" diff --git a/packaging/windows/drivers/vendor/wdk-build/Cargo.toml b/packaging/windows/drivers/vendor/wdk-build/Cargo.toml index 7ca7134..11cd3cd 100644 --- a/packaging/windows/drivers/vendor/wdk-build/Cargo.toml +++ b/packaging/windows/drivers/vendor/wdk-build/Cargo.toml @@ -48,7 +48,7 @@ path = "src/lib.rs" version = "1.0.97" [dependencies.bindgen] -version = "0.71.0" +version = "0.72" [dependencies.camino] version = "1.1.9" diff --git a/packaging/windows/drivers/vendor/wdk-sys/Cargo.toml b/packaging/windows/drivers/vendor/wdk-sys/Cargo.toml index 505b4ea..3341943 100644 --- a/packaging/windows/drivers/vendor/wdk-sys/Cargo.toml +++ b/packaging/windows/drivers/vendor/wdk-sys/Cargo.toml @@ -67,7 +67,7 @@ version = "=0.5.1" version = "1.0.97" [build-dependencies.bindgen] -version = "0.71.0" +version = "0.72" [build-dependencies.cargo_metadata] version = "0.19.2" diff --git a/packaging/windows/install-pf-vdisplay.ps1 b/packaging/windows/install-pf-vdisplay.ps1 index 5a8faae..b541ff0 100644 --- a/packaging/windows/install-pf-vdisplay.ps1 +++ b/packaging/windows/install-pf-vdisplay.ps1 @@ -1,19 +1,20 @@ <# .SYNOPSIS - Install the bundled pf-vdisplay (punktfunk) virtual-display driver — our all-Rust IddCx replacement - for SudoVDA. Runs ELEVATED at setup time (invoked from the installer's [Run] section). Best-effort: - warns and exits 0 on any failure — the host degrades to a physical display without a virtual display, - so a driver hiccup must never abort the whole install. + Install the bundled pf-vdisplay (punktfunk) virtual-display driver - our own all-Rust UMDF IddCx + indirect-display driver, built from source per release (packaging/windows/build-pf-vdisplay.ps1). + Runs ELEVATED at setup time (invoked from the installer's [Run] section). Best-effort: warns and exits + 0 on any failure - the host degrades to a physical display without a virtual display, so a driver + hiccup must never abort the whole install. .DESCRIPTION -Dir holds the staged payload (pf_vdisplay.inf/.cat/.dll + signing .cer + nefconc.exe). Steps: 1. Trust the self-signed driver cert (machine Root + TrustedPublisher) so PnP installs it silently - (the same punktfunk-ds-test cert the gamepad drivers ship). - 2. Create the ROOT device node IF ABSENT (gated — a blind re-create spawns a phantom duplicate, and - the host's open_device() binds interface index 0; crates/punktfunk-host/src/vdisplay/sudovda.rs). - ALWAYS via nefconc (a clean ROOT\DISPLAY node) — NEVER devgen, which makes persistent SWD\DEVGEN + (the punktfunk-driver cert the build signs the driver + catalog with). + 2. Create the ROOT device node IF ABSENT (gated - a blind re-create spawns a phantom duplicate, and + the host's open_device() binds interface index 0; crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs). + ALWAYS via nefconc (a clean ROOT\DISPLAY node) - NEVER devgen, which makes persistent SWD\DEVGEN software devices that survive reboot + registry deletion and resurrect on every driver install. - 3. Stage + bind the driver (pnputil /add-driver /install — modern, in-box, idempotent). + 3. Stage + bind the driver (pnputil /add-driver /install - modern, in-box, idempotent). Class/ClassGuid are read from the .inf so they always match the shipped driver. @@ -52,11 +53,11 @@ if ($cer) { certutil.exe -addstore -f Root "$($cer.FullName)" | Out-Null certutil.exe -addstore -f TrustedPublisher "$($cer.FullName)" | Out-Null } -else { Write-Warning "no .cer in $Dir — driver may not install silently (untrusted publisher)" } +else { Write-Warning "no .cer in $Dir - driver may not install silently (untrusted publisher)" } # 2) Create the root device node only if it isn't already there. nefconc, NEVER devgen. if (Test-PfVdisplayPresent) { - Write-Host "pf-vdisplay device node already present — leaving it as-is." + Write-Host "pf-vdisplay device node already present - leaving it as-is." } elseif ($nef) { $infText = Get-Content -Raw $inf.FullName @@ -69,7 +70,7 @@ elseif ($nef) { Write-Warning "nefconc --create-device-node returned $LASTEXITCODE" } } -else { Write-Warning "nefconc.exe not found in $Dir — cannot create the pf-vdisplay device node." } +else { Write-Warning "nefconc.exe not found in $Dir - cannot create the pf-vdisplay device node." } # 3) Stage + bind the driver (idempotent; re-staging the same .inf is harmless). Write-Host "==> pnputil /add-driver $($inf.Name) /install" diff --git a/packaging/windows/pack-host-installer.ps1 b/packaging/windows/pack-host-installer.ps1 index 41f5bae..59e936c 100644 --- a/packaging/windows/pack-host-installer.ps1 +++ b/packaging/windows/pack-host-installer.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION From a release `cargo build -p punktfunk-host --features nvenc` output (the exe), this: 1. resolves a code-signing cert (supplied stable .pfx from CI secrets OR an ephemeral self-signed - CN=unom — same scheme as the client's pack-msix.ps1) and exports the public .cer, + CN=unom - same scheme as the client's pack-msix.ps1) and exports the public .cer, 2. signs the inner punktfunk-host.exe, 3. stages the pf-vdisplay virtual-display driver bundle (unless -NoDriver), 4. runs ISCC to build punktfunk-host-setup-.exe, @@ -52,7 +52,7 @@ function Find-Iscc { } $c = Get-Command iscc -ErrorAction SilentlyContinue if ($c) { return $c.Source } - throw "ISCC.exe (Inno Setup 6, any 6.x) not found — install it (choco install innosetup -y)." + throw "ISCC.exe (Inno Setup 6, any 6.x) not found - install it (choco install innosetup -y)." } function Find-SdkTool([string]$name) { $root = 'C:\Program Files (x86)\Windows Kits\10\bin' @@ -60,7 +60,7 @@ function Find-SdkTool([string]$name) { Where-Object { $_.FullName -match '\\(10\.0\.\d+\.\d+)\\x64\\' } | Sort-Object { [version]([regex]::Match($_.FullName, '\\(10\.0\.\d+\.\d+)\\x64\\').Groups[1].Value) } | Select-Object -Last 1 - if (-not $hit) { throw "$name not found under $root — install the Windows 10/11 SDK." } + if (-not $hit) { throw "$name not found under $root - install the Windows 10/11 SDK." } $hit.FullName } $iscc = Find-Iscc @@ -103,7 +103,7 @@ function Sign-File([string]$Path) { if ($PfxPassword) { $signArgs += @('/p', $PfxPassword) } & $signtool ($signArgs + @('/tr', 'http://timestamp.digicert.com', '/td', 'SHA256', $Path)) if ($LASTEXITCODE -ne 0) { - Write-Warning "timestamped sign failed for $Path — retrying without a timestamp" + Write-Warning "timestamped sign failed for $Path - retrying without a timestamp" & $signtool ($signArgs + @($Path)) if ($LASTEXITCODE -ne 0) { throw "signtool sign failed for $Path ($LASTEXITCODE)" } } @@ -140,12 +140,18 @@ $defines = @( "/DReadme=$readme" ) -# --- stage the pf-vdisplay virtual-display driver bundle -------------------------------------- -# pf-vdisplay is our all-Rust IddCx driver (packaging/windows/drivers/), vendored signed under -# packaging/windows/pf-vdisplay/. It replaced the vendored SudoVDA C++ driver. +# --- build (from source) + stage the pf-vdisplay virtual-display driver ----------------------- +# pf-vdisplay is our all-Rust IddCx driver (packaging/windows/drivers/). It is now BUILT FROM SOURCE +# every release (build-pf-vdisplay.ps1) instead of shipping a checked-in prebuilt binary: the vendored +# binary went stale (its .cat stopped covering an edited .inf -> pnputil SPAPI_E_FILE_HASH_NOT_IN_CATALOG +# on every box, and it predated IOCTL_SET_RENDER_ADAPTER the host needs on hybrid/Optimus GPUs). Building +# here keeps the .dll/.inf/.cat in lockstep + ships current driver features. stage-pf-vdisplay.ps1 then +# adds the fetched nefcon device tool. (Needs the WDK build env; -NoDriver skips it for a WDK-less pack.) if (-not $NoDriver) { + $built = Join-Path $OutDir 'pfvd-built' + & (Join-Path $here 'build-pf-vdisplay.ps1') -Out $built $stage = Join-Path $OutDir 'stage' - & (Join-Path $here 'stage-pf-vdisplay.ps1') -OutDir $stage + & (Join-Path $here 'stage-pf-vdisplay.ps1') -OutDir $stage -VendorDir $built Copy-Item (Join-Path $here 'install-pf-vdisplay.ps1') (Join-Path $stage 'install-pf-vdisplay.ps1') -Force $defines += "/DStageDir=$stage" } @@ -165,12 +171,12 @@ if (-not $NoDriver) { $defines += "/DGamepadStageDir=$gpStage" Write-Host "==> staged vendored gamepad UMDF drivers from $gpVendor" } - else { Write-Warning "no vendored gamepad drivers under $gpVendor — installer built WITHOUT them" } + else { Write-Warning "no vendored gamepad drivers under $gpVendor - installer built WITHOUT them" } } # --- stage the FFmpeg shared DLLs (AMD/Intel AMF/QSV build) ------------------------------------ # A host built with --features amf-qsv link-imports avcodec/avutil/swscale/... so the shared DLLs -# MUST sit next to the exe (it won't start otherwise). Bundle them from $FfmpegDir\bin — the same +# MUST sit next to the exe (it won't start otherwise). Bundle them from $FfmpegDir\bin - the same # BtbN gpl-shared tree the build linked against. A nvenc/software-only build doesn't import them, so # this is a harmless extra there; skipped entirely when $FfmpegDir is unset. $ffmpegBinSrc = if ($FfmpegDir) { Join-Path $FfmpegDir 'bin' } else { $null } @@ -190,7 +196,7 @@ else { Write-Host "no FFMPEG_DIR\bin -> installer built WITHOUT FFmpeg DLLs (nve # The console runs as the PunktfunkWeb scheduled task (`bun {app}\web\.output\server\index.mjs`), # auto-wired to the host's loopback mgmt API. Stage everything ISCC reads into $OutDir (the # non-WOW64-redirected C:\t area, same reason as the .iss/host.env staging above). The .output is -# self-contained (Nitro noExternals — deps bundled + tree-shaken, no node_modules), so bun runs it +# self-contained (Nitro noExternals - deps bundled + tree-shaken, no node_modules), so bun runs it # directly; omitted when -WebDir/-BunExe are unset (host-only installer, e.g. a local debug pack). if ($WebDir -and (Test-Path $WebDir) -and $BunExe -and (Test-Path $BunExe)) { $webStage = Join-Path $OutDir 'web' @@ -213,7 +219,7 @@ else { Write-Host "no -WebDir/-BunExe -> installer built WITHOUT the web console # --- build + stage the HDR Vulkan layer (pf-vkhdr-layer) -------------------------------------- # A tiny always-on Vulkan implicit layer (cdylib) that advertises HDR10/scRGB surface formats on the -# virtual display so Vulkan games (Doom: The Dark Ages, etc.) can enable HDR while streaming — the +# virtual display so Vulkan games (Doom: The Dark Ages, etc.) can enable HDR while streaming - the # NVIDIA/AMD ICDs hide HDR formats on an indirect display even though they accept+present a forced HDR # swapchain there. Self-gated on the display's actual advanced-color state, so it's a no-op on SDR. # Standalone crate (own [workspace]); built here and registered by the installer. Skipped if cargo @@ -239,7 +245,7 @@ if (Test-Path (Join-Path $layerSrc 'Cargo.toml')) { $defines += "/DVkLayerDir=$layerStage" Write-Host "==> staged pf-vkhdr-layer -> $layerStage" } - else { Write-Warning "pf-vkhdr-layer build failed ($layerExit) — installer built WITHOUT the HDR Vulkan layer" } + else { Write-Warning "pf-vkhdr-layer build failed ($layerExit) - installer built WITHOUT the HDR Vulkan layer" } } else { Write-Host "no pf-vkhdr-layer crate -> installer built WITHOUT the HDR Vulkan layer" } diff --git a/packaging/windows/punktfunk-host.iss b/packaging/windows/punktfunk-host.iss index 18a1bbd..0454024 100644 --- a/packaging/windows/punktfunk-host.iss +++ b/packaging/windows/punktfunk-host.iss @@ -28,7 +28,7 @@ #ifndef Readme #define Readme "README.md" #endif -; The web console launcher (the PunktfunkWeb task action) + its post-install provisioner — committed +; The web console launcher (the PunktfunkWeb task action) + its post-install provisioner - committed ; scripts staged next to the .iss by pack-host-installer.ps1 (absolute paths passed in). #ifndef WebRunCmd #define WebRunCmd "..\..\scripts\windows\web-run.cmd" @@ -44,19 +44,19 @@ #ifdef GamepadStageDir #define WithGamepad #endif -; FfmpegBin (a dir of FFmpeg shared DLLs) is optional — present when the host is built with +; FfmpegBin (a dir of FFmpeg shared DLLs) is optional - present when the host is built with ; --features amf-qsv (the AMD/Intel AMF/QSV encode backend link-imports the FFmpeg libs). #ifdef FfmpegBin #define WithFfmpeg #endif ; WebDir (the built web .output tree) + BunExe (a portable bun.exe) are passed together by -; pack-host-installer.ps1 to bundle the management console. Both required → WithWeb. +; pack-host-installer.ps1 to bundle the management console. Both required -> WithWeb. #ifdef WebDir #ifdef BunExe #define WithWeb #endif #endif -; VkLayerDir (the staged pf-vkhdr-layer: pf_vkhdr_layer.dll + .json) is optional — present when the +; VkLayerDir (the staged pf-vkhdr-layer: pf_vkhdr_layer.dll + .json) is optional - present when the ; HDR Vulkan layer was built. It lets Vulkan games (Doom: The Dark Ages, etc.) enable HDR over the ; virtual display (the ICD won't advertise HDR there; the layer injects the surface formats, self- ; gated on the display's actual HDR state). @@ -94,7 +94,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "installdriver"; Description: "Install the pf-vdisplay virtual display driver (required for native-resolution streaming)" #endif #ifdef WithGamepad -Name: "installgamepad"; Description: "Install the virtual gamepad drivers (DualSense / DualShock 4 / Xbox 360 — no ViGEmBus needed)" +Name: "installgamepad"; Description: "Install the virtual gamepad drivers (DualSense / DualShock 4 / Xbox 360 - no ViGEmBus needed)" #endif #ifdef WithVkLayer Name: "installhdrlayer"; Description: "Install the HDR Vulkan layer (lets Vulkan games like Doom use HDR on the virtual display)" @@ -106,15 +106,15 @@ Source: "{#BinDir}\punktfunk-host.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#HostEnv}"; DestDir: "{app}"; Flags: ignoreversion Source: "{#Readme}"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignoreversion #ifdef WithFfmpeg -; FFmpeg shared DLLs (avcodec/avutil/swscale/...) laid down next to the exe — the AMD/Intel +; FFmpeg shared DLLs (avcodec/avutil/swscale/...) laid down next to the exe - the AMD/Intel ; (AMF/QSV) encode backend link-imports them, so the exe won't start without them. NVENC/software- ; only builds simply omit this block. Source: "{#FfmpegBin}\*.dll"; DestDir: "{app}"; Flags: ignoreversion #endif #ifdef WithWeb ; The web management console: the self-contained Nitro SSR bundle (.output = server + public; deps -; bundled in, no node_modules) → {app}\web\.output, a portable bun runtime → {app}\bun\bun.exe, and -; the launcher the PunktfunkWeb task runs → {app}\web\web-run.cmd. web-setup.ps1 (the provisioner) +; bundled in, no node_modules) -> {app}\web\.output, a portable bun runtime -> {app}\bun\bun.exe, and +; the launcher the PunktfunkWeb task runs -> {app}\web\web-run.cmd. web-setup.ps1 (the provisioner) ; goes to {tmp} and is removed after install. Source: "{#WebDir}\*"; DestDir: "{app}\web\.output"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#BunExe}"; DestDir: "{app}\bun"; DestName: "bun.exe"; Flags: ignoreversion @@ -180,7 +180,7 @@ Filename: "{app}\punktfunk-host.exe"; Parameters: "service uninstall"; Flags: ru ; Stop + remove the PunktfunkWeb task and its firewall rule (leaves %ProgramData%\punktfunk config, ; like the host uninstall does). Filename: "powershell.exe"; \ - Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Stop-ScheduledTask -TaskName PunktfunkWeb -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName PunktfunkWeb -Confirm:$false -ErrorAction SilentlyContinue; Get-NetFirewallRule -Name 'PunktfunkWeb-TCP-3000' -ErrorAction SilentlyContinue | Remove-NetFirewallRule"""; \ + Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Stop-ScheduledTask -TaskName PunktfunkWeb -ErrorAction SilentlyContinue; Get-NetTCPConnection -LocalPort 3000 -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Unregister-ScheduledTask -TaskName PunktfunkWeb -Confirm:$false -ErrorAction SilentlyContinue; Get-NetFirewallRule -Name 'PunktfunkWeb-TCP-3000' -ErrorAction SilentlyContinue | Remove-NetFirewallRule"""; \ Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkWebCleanup" #endif @@ -188,7 +188,7 @@ Filename: "powershell.exe"; \ #ifdef WithWeb var WebPwPage: TInputQueryWizardPage; - FreshWebInstall: Boolean; { captured at start — web-setup creates the file mid-run } + FreshWebInstall: Boolean; { captured at start - web-setup creates the file mid-run } function WebPasswordPath: String; begin @@ -196,7 +196,7 @@ begin end; { Pre-fill the console password field with a crypto-strong default (Inno has no RNG): a one-shot - PowerShell writes 12 random bytes as dashed hex; strip the dashes → a 24-char hex password. } + PowerShell writes 12 random bytes as dashed hex; strip the dashes -> a 24-char hex password. } procedure GenerateRandomWebPassword(var Pw: String); var ResultCode: Integer; @@ -229,7 +229,7 @@ begin WebPwPage := CreateInputQueryPage(wpSelectTasks, 'Web console', 'Set the punktfunk web console login password', 'The management console is served on http://this-computer:3000 and is login-gated. Keep the ' + - 'secure password generated below (it is shown again on the final page) or enter your own — you ' + + 'secure password generated below (it is shown again on the final page) or enter your own - you ' + 'can change it later in %ProgramData%\punktfunk\web-password.'); WebPwPage.Add('Console password:', False); { visible, so the admin can read the generated default } DefaultPw := ''; @@ -239,7 +239,7 @@ end; function ShouldSkipPage(PageID: Integer): Boolean; begin - { On upgrade the password already exists — keep it, don't re-prompt. } + { On upgrade the password already exists - keep it, don't re-prompt. } Result := (PageID = WebPwPage.ID) and (not FreshWebInstall); end; @@ -264,7 +264,7 @@ end; function WebSetupParams(Param: String): String; begin { Pass the password to web-setup.ps1 via a temp file, not the cmdline (which lands in the install - log). Only on a fresh install — on upgrade web-setup keeps the existing file. } + log). Only on a fresh install - on upgrade web-setup keeps the existing file. } Result := '-AppDir "' + ExpandConstant('{app}') + '"'; if FreshWebInstall then Result := Result + ' -PasswordFile "' + ExpandConstant('{tmp}\webpw.txt') + '"'; @@ -287,12 +287,31 @@ begin '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; +#ifdef WithWeb +{ Stop a running web console + free :3000 BEFORE the file copy, so the old server doesn't lock + .output / web-run.cmd / bun.exe and the new task can bind. Killing the :3000 listener owner is + runtime-agnostic (an early install may have run node, the current one runs bun). web-setup.ps1 + repeats this idempotently after the copy. Best-effort; a fresh install is a no-op. } +procedure StopWebConsole; +var + ResultCode: Integer; +begin + Exec('powershell.exe', + '-NoProfile -ExecutionPolicy Bypass -Command "' + + '$ErrorActionPreference=''SilentlyContinue''; ' + + 'Stop-ScheduledTask -TaskName PunktfunkWeb; ' + + 'Get-NetTCPConnection -LocalPort 3000 -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; +#endif + procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssInstall then begin StopHostServiceAndWait; #ifdef WithWeb + StopWebConsole; { upgrade-safe: free :3000 + unlock the web files before the copy } { Stash the chosen password for web-setup.ps1 (fresh install only); the temp copy is auto-cleaned. } if FreshWebInstall then SaveStringToFile(ExpandConstant('{tmp}\webpw.txt'), Trim(WebPwPage.Values[0]), False); diff --git a/packaging/windows/stage-pf-vdisplay.ps1 b/packaging/windows/stage-pf-vdisplay.ps1 index 5497f29..5b976fb 100644 --- a/packaging/windows/stage-pf-vdisplay.ps1 +++ b/packaging/windows/stage-pf-vdisplay.ps1 @@ -6,11 +6,11 @@ .DESCRIPTION pf-vdisplay (our all-Rust IddCx virtual display) is built from packaging/windows/drivers/, and the SIGNED output (pf_vdisplay.dll/.inf/.cat + punktfunk-driver.cer) is VENDORED under - packaging/windows/pf-vdisplay/ (signer punktfunk-ds-test — shared with the gamepad drivers — Class= + packaging/windows/pf-vdisplay/ (signer punktfunk-ds-test - shared with the gamepad drivers - Class= Display, HWID root\pf_vdisplay). Rebuild + re-vendor with packaging/windows/drivers/deploy-dev.ps1 when the driver source changes, then copy the staged pf_vdisplay.{dll,inf,cat} over the vendored copies. nefcon publishes a pinned release, so we fetch + - SHA-256-verify it (it provides nefconc.exe, used to create the root-enumerated device node — pnputil + SHA-256-verify it (it provides nefconc.exe, used to create the root-enumerated device node - pnputil can't). Output (consumed by punktfunk-host.iss): -OutDir gets pf_vdisplay.inf/.cat/.dll + punktfunk-driver.cer @@ -36,7 +36,7 @@ New-Item -ItemType Directory -Force -Path $OutDir | Out-Null # --- vendored pf-vdisplay driver -------------------------------------------------------------- $inf = Get-ChildItem -Path $VendorDir -Filter pf_vdisplay.inf -ErrorAction SilentlyContinue | Select-Object -First 1 -if (-not $inf) { throw "no vendored pf_vdisplay.inf under $VendorDir — re-vendor via drivers/deploy-dev.ps1" } +if (-not $inf) { throw "no vendored pf_vdisplay.inf under $VendorDir - re-vendor via drivers/deploy-dev.ps1" } Copy-Item (Join-Path $VendorDir '*') $OutDir -Force Write-Host "==> vendored pf-vdisplay staged from $VendorDir" @@ -54,7 +54,7 @@ try { } Write-Host " sha256 ok ($got)" } - else { Write-Warning "no pinned nefcon SHA-256 — computed $got (PIN THIS in stage-pf-vdisplay.ps1)" } + else { Write-Warning "no pinned nefcon SHA-256 - computed $got (PIN THIS in stage-pf-vdisplay.ps1)" } Expand-Archive -Path $zip -DestinationPath $work -Force $nefc = Get-ChildItem -Path $work -Recurse -Filter 'nefconc.exe' | Where-Object { $_.FullName -match '(?i)\\x64\\' } | Select-Object -First 1 diff --git a/scripts/windows/web-run.cmd b/scripts/windows/web-run.cmd index a9e4cbd..9acba84 100644 --- a/scripts/windows/web-run.cmd +++ b/scripts/windows/web-run.cmd @@ -1,5 +1,5 @@ @echo off -rem punktfunk web console launcher — the action the PunktfunkWeb scheduled task runs at boot. +rem punktfunk web console launcher - the action the PunktfunkWeb scheduled task runs at boot. rem rem Lays out next to the installed payload: {app}\web\web-run.cmd, {app}\web\.output\... and rem {app}\bun\bun.exe (so %~dp0 = {app}\web\). Auto-wires the console the same way the Linux diff --git a/scripts/windows/web-setup.ps1 b/scripts/windows/web-setup.ps1 index 9e9772e..b33a5c3 100644 --- a/scripts/windows/web-setup.ps1 +++ b/scripts/windows/web-setup.ps1 @@ -14,7 +14,7 @@ 3. Opens inbound TCP 3000 (the console port) on all profiles. 4. Waits briefly for the host's mgmt token, then starts the task. - The mgmt bearer token is NOT managed here — the host owns %ProgramData%\punktfunk\mgmt-token + The mgmt bearer token is NOT managed here - the host owns %ProgramData%\punktfunk\mgmt-token (crates/punktfunk-host/src/mgmt_token.rs writes it on `serve`); web-run.cmd sources it. #> [CmdletBinding()] @@ -38,6 +38,22 @@ function New-RandomPassword { return $s.Substring(0, [Math]::Min(20, $s.Length)) } +function Stop-WebConsole { + # On an upgrade a console is already running. Stop + reap it before re-registering so (a) the new + # task can bind :3000 (else the old server keeps it and the new one restart-loops on EADDRINUSE) and + # (b) the installer can overwrite .output / web-run.cmd / bun.exe (a held file blocks the copy). A + # prior install may have run a DIFFERENT runtime (node vs bun), so kill by the script it serves AND + # by the :3000 owner - the latter is runtime-agnostic and future-proofs the next runtime swap. + Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + Get-CimInstance Win32_Process -Filter "Name='bun.exe' OR Name='node.exe'" -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -match 'index\.mjs' } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Get-NetTCPConnection -LocalPort 3000 -State Listen -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty OwningProcess -Unique | + ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue } + Start-Sleep -Seconds 1 +} + # --- 1. login password ----------------------------------------------------------------------- $password = $null if ($PasswordFile -and (Test-Path -LiteralPath $PasswordFile)) { @@ -60,6 +76,7 @@ if ($password) { } # --- 2. PunktfunkWeb scheduled task ---------------------------------------------------------- +Stop-WebConsole # reap any running (possibly old-runtime) console before re-registering (upgrade-safe) $cmd = Join-Path $AppDir 'web\web-run.cmd' if (-not (Test-Path -LiteralPath $cmd)) { throw "web launcher missing: $cmd" } $action = New-ScheduledTaskAction -Execute $cmd