fix(windows-installer): build pf-vdisplay from source in CI; ASCII scripts; upgrade-safe web console
windows-drivers / probe-and-proto (push) Successful in 24s
apple / swift (push) Successful in 1m4s
windows-drivers / driver-build (push) Successful in 1m8s
android / android (push) Successful in 4m4s
ci / rust (push) Successful in 4m39s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 53s
apple / screenshots (push) Successful in 5m10s
windows-host / package (push) Failing after 5m35s
deb / build-publish (push) Successful in 2m29s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / bench (push) Successful in 4m42s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m57s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m46s

The pf-vdisplay virtual-display driver shipped as a checked-in PREBUILT binary
that went stale - two field failures on a fresh install (live-repro'd on a
German-locale Dell laptop):

  * Bug A (every box): a repo-wide rename edited the vendored pf_vdisplay.inf
    but never re-signed pf_vdisplay.cat, so the catalog stopped covering the INF
    -> `pnputil /add-driver` fails SPAPI_E_FILE_HASH_NOT_IN_CATALOG -> driver
    never installs -> every session dies "pf-vdisplay driver interface not
    found".
  * the prebuilt binary also predated IOCTL_SET_RENDER_ADAPTER (added to the
    driver source after the vendor freeze) that the host needs to pin the IDD
    render GPU on hybrid/Optimus boxes.

Fix: build the driver FROM SOURCE every release (build-pf-vdisplay.ps1, wired
into pack-host-installer.ps1) so .dll/.inf/.cat are always in lockstep and
current driver features ship. The runner's clang 22 made the driver's pinned
bindgen 0.71 emit opaque structs (157 layout-assert errors), so bump the
vendored wdk-sys/wdk-build bindgen 0.71 -> 0.72 (+ lock). The build self-signs
the driver per build (installer trusts the bundled .cer); a stable
DRIVER_CERT_PFX_B64 secret can override.

  * Bug B (non-English boxes): the installer runs install-pf-vdisplay.ps1 etc.
    via powershell.exe (5.1), which reads a BOM-less script in the ANSI codepage
    - an em-dash's trailing 0x94 byte becomes a curly quote on German
    Windows-1252 and the script aborts "unterminated string", so the driver
    never installed (the gamepad script survived only because it was already
    ASCII). Scrub every installer-run .ps1/.cmd to ASCII + add a CI gate that
    fails on any non-ASCII so it can't regress.

  * Bug C (upgrades): nothing stopped the OLD web console before re-registering
    its task, so a stale server kept :3000 (the new one restart-looped on
    EADDRINUSE) and served a broken old bundle (500 on /login). Stop + reap it
    (runtime-agnostic, by the :3000 listener owner) in web-setup.ps1 and in the
    .iss before the file copy + on uninstall.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:33:34 +00:00
parent 8e87e617df
commit bdfab8e0d5
12 changed files with 257 additions and 61 deletions
+16
View File
@@ -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: |
+137
View File
@@ -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)" }
+5 -5
View File
@@ -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)
+9 -9
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+13 -12
View File
@@ -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"
+19 -13
View File
@@ -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-<ver>.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" }
+33 -14
View File
@@ -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);
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+18 -1
View File
@@ -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