Moved verbatim from CLAUDE.md (#737). Paths are relative to the repo root.
Three rules, all learned from the owner's own machine:
Upgrade retirement (#865): after payload, camera ownership, registry and shortcuts
have committed, Setup runs Remove-PreviousInstallations.ps1. The standard per-user
install root retains one registered version. Custom QA destinations never clean
the real install root. Candidates require the exact uninstall registration,
publisher, version marker and package manifest; paths and ancestor reparse points
are checked before deletion. Running versions and unverifiable candidates remain
with a cleanup report and warning. Missing version folders permit removal of their
matching stale registration only. Cleanup removes hash-matching delivered files,
allowlisted first-run runtime files and version shortcuts, using nonrecursive
directory removal. Modified payload files and operator files are left for the residual
sweep below. It never invokes older uninstallers or changes webcam registration: legacy
uninstallers can otherwise unregister the newly installed camera. The upgrade helper
tests use an isolated registry namespace and temporary fixtures.
Root cause of "Get-FileHash is not recognized" (reproduced). In Windows PowerShell
5.1, Get-FileHash (also New-Guid, Format-Hex) is a script function found by
module auto-loading; compiled cmdlets (ConvertTo-Json, Get-ChildItem) are always
loaded. An installer started from a PowerShell 7 shell inherits PS7's PSModulePath
(PS7's $PSHOME\Modules first; pwsh only strips it when it launches powershell.exe
itself), nsExec passes it to the helper untouched, and 5.1 binds Get-FileHash to
PS7's Core-only Utility module: "not recognized". Reproduced through a real nsExec on
both hosts. Payload hashes were already streamed through .NET SHA-256 (91db55bf);
never reintroduce Get-FileHash on the setup path. This is a proven MECHANISM, not the
proven cause of the 2026-10-13 incident: no PowerShell 7 exists on the dev box, and the
environment that launched that install was never captured.
Pinned environment (owner ruling D5). Remove-PreviousInstallations.ps1 first calls
Initialize-SetupHelperEnvironment: it resets PSModulePath for its own process to
$PSHOME\Modules;%WINDIR%\System32\WindowsPowerShell\v1.0\Modules, imports
Utility/Management/CimCmdlets by absolute .psd1 path, and resolves every command the
helper uses (one list, $SetupHelperCommands in PreviousInstallations.psm1; add to it
when you add a cmdlet). A miss throws Setup helper cannot resolve <cmdlet> (module <m>)
with the diagnostics on one line. Every run appends the diagnostics block (PS version,
is64Bit, language mode, $PSHOME, inherited and effective PSModulePath) and the
outcome to %LOCALAPPDATA%\CoreVideoPro\installer-logs\<release>-retire.log, written with
.NET only so the log works when command resolution is what broke. The helper still runs
as the 32-bit SysWOW64 host (32-bit NSIS); the registered and camera checks use the 64-bit
registry view.
One metadata list. Get-OwnedInstallMetadataNames is the only place installer-owned
root-level names live (Uninstall.exe, .camera-registration-before.json,
.camera-desktop-registration-before.json, .corevideo-prerelease-install,
build-manifest.json, previous-installations-cleanup.json). The inline list it replaced
missed the desktop camera backup and stranded 29 folders on the dev box.
Residual sweep. After the registered pass, Get-ResidualInstallationPlan takes every
direct child of the install root that matches the release-id pattern, is not the current
install and has NO uninstall registration (the registered pass cannot see these). Skipped:
a running version, a folder a camera registration (machine or desktop alias) points into.
Retained, never entered: anything with a reparse point. Every file is classified:
root-level owned metadata; payload proven by SHA-256 against the folder's own valid
build-manifest.json (or the allowlisted media runtime); unknown (everything else:
Recordings\, modified payload, a metadata-named file below the root).
Remove-ResidualInstallation then, owner ruling D4, moves unknown files (never
copies-then-deletes, never deletes) to
%LOCALAPPDATA%\CoreVideoPro\backups\retired-install-remnants-<yyyyMMdd-HHmmss>-<id>\<release>\<relative path>,
same volume only. backup-manifest.json there lists source, destination, length,
sha256, status per file and is written BEFORE the first move, so after a crash every
file is at its source or at a listed destination. Collisions get (n), never an
overwrite. Then proven payload and metadata are deleted one file at a time and empty
directories removed deepest first, non-recursively. Any failure keeps that folder,
retained with the reason. -LeaveUnknown (manual runs only) keeps operator content in
place.
Report and exit code. previous-installations-cleanup.json (copy:
installer-logs\<release>-retire.json) is {summary, registered[], residual[]};
summary = registeredRetired, residualRemoved, residualBackedUp, retained,
backupFolder, filesBackedUp. The last stdout line, which NSIS shows, counts
folders, not uninstall entries: Previous installation cleanup: <n> retired, <n> backed up, <n> retained. Whenever ANY file was moved, even from a folder that then
failed and was retained, summary.backupFolder is set and the ...were MOVED, not deleted, to: <folder> line is printed FIRST: nsExec keeps only the head of stdout (1024 characters
on makensis 3.12), so the backup path must not be pushed out by Retained lines. Exit 0 = clean; 2 = clean and operator files were moved (Setup
shows an information box with the backup folder); 1 = something retained (warning box with
the summary and log path). /SD IDOK keeps silent setup unblocked.
Tests (both CI native-shell-windows and local, from the 64-bit and the SysWOW64 host):
Test-PreviousInstallations.ps1 (registered path, also -WithoutHashCommand),
Test-ResidualInstallations.ps1 (classifier + backup: Recordings, modified payload,
collisions, injected crash mid-move, locked file, cross-volume refusal, junctions, running
and camera folders), Test-CleanupEnvironment.ps1 (runs the real helper in 64-bit and
32-bit child hosts with a PS7-inherited, empty and nonexistent PSModulePath, embeds
PS7 7.4's Utility manifest because a hand-trimmed look-alike does not reproduce, uses a real
PS7 Modules dir when installed; plus the end-to-end sweep). With the Get-FileHash call restored (planner from
91db55bf^) and the pin removed (the Task 1 helper), the suite fails with the issue text. Installed acceptance on a real upgrade is still pending (plan Task 7).
Failure rollback must move Setup's working directory out of $INSTDIR before
removing the owned payload and empty installation directory. SetOutPath sets
both the extraction destination and process working directory. A camera preflight
failure otherwise leaves an empty but undeletable install root until Setup exits;
the next attempt treats that root as occupied and refuses to retry. Full upgrade
validation forces camera preflight failure, checks previous installs remain, then
retries the same destination successfully.
- ONE stable
CoreVideo Proshortcut (desktop + Start menu), repointed at each install. The owner accumulated five version-named desktop shortcuts, launched a three-day-old build by mistake and filed a defect against code that already had the fix. The version-named shortcut survives in the Start menu ONLY. Uninstall REMOVES the stable pair rather than repointing it: choosing "the newest remaining release" means ranking sibling folders, and a wrong guess silently opens the wrong build — the exact failure this exists to stop. - Uninstall removes the first-run media runtime, from a MANIFEST.
Install-MediaRuntime.ps1records what it wrote tonotices\ffmpeg\installed-files.txt;un.RemoveMediaRuntimedeletes that set and nothing else. A manifest rather than a hard-coded list because the file set follows the pinned upstream build; a manifest rather thanRMDir /rbecause$INSTDIRis an app folder an operator's files can land in. Measured before the fix: TWO uninstalled versions still holding 10 files / 195 MB each. The loop refuses any manifest line that is absolute, rooted, or contains..— a file that decides what an uninstaller deletes is worth validating. - A refused uninstall SAYS WHY. Refusing when the registry does not match is correct, but refusing silently left an operator with no Apps & Features entry and an uninstaller that appeared to do nothing.
Two traps this cost, both worth remembering:
- NEVER run an interactive helper from a silent installer.
regsvr32WITHOUT/sopens a modal result dialog, andRegister-VirtualCamera.cmddeliberately omits/sso a tester who double-clicks it gets feedback. Calling that .cmd from the installer hung a silent install forever — tworegsvr32processes waiting on a dialog nobody could see, until the 180 sTest-AlphaInstallertimeout. The installer callsregsvr32 /sdirectly. Install registers the virtual camera; uninstall must unregister it, before the payload delete, or a COM registration survives pointing at a deleted DLL and every app that enumerates cameras inherits a broken device. - A UTF-8 BOM is three literal characters to NSIS. Windows PowerShell 5.1's
Set-Content -Encoding UTF8writes one, so the manifest's first path became<BOM>ffmpeg.exe,Deleteremoved nothing, and only that one file was left behind — a failure that looked like flaky file locking. Write manifests with[IO.File]::WriteAllLines(..., New-Object Text.UTF8Encoding $false). Any file NSIS reads must be written BOM-free.
Test-AlphaInstaller.ps1 is the gate and now asserts all of it:
stableShortcutOnly, mediaRuntimeRemoved, manifestEscapeRefused, alongside
the existing real silent install / runtime probe / duplicate rejection /
uninstall / user-file preservation.
T2.1 / #433 is implemented (PR #1033, 2026-10-23); installed acceptance is pending. Setup now does all three:
- Virtual camera - registered by
Install-VirtualCamera.ps1(already so; unchanged). - WER LocalDumps -
Install-CrashDumps.ps1, run in theinstalled:phase after the payload and camera are committed. Windows reads LocalDumps from HKLM only (measured 2026-09-12: an HKCU key for a crashing test exe produced zero dumps), so this is the elevated step. Owner ruling 2026-10-23 permits the HKLM write. For each ofCoreVideoPro.WinUI.exe,corevideo-native.exe,corevideo-zoom-engine.exeit writesHKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\<exe>withDumpFolder=%LOCALAPPDATA%\CoreVideoPro\crashes(REG_EXPAND_SZ),DumpType= 2 (full),DumpCount= 5, plus aCoreVideoProInstallermarker value. Interactive setup passes-AllowElevation(a RunAs child; this is a second UAC prompt after the camera's, and the welcome page says so); silent/Ssetup must already be elevated, exactly like the camera step. It never fails setup: a declined prompt or refused write exits 2, isDetailPrinted, appended to%LOCALAPPDATA%\CoreVideoPro\installer-logs\<release>-crashdumps.log, shown in a warning box (interactive) and setup continues. An existing key WITHOUT the marker (for example one fromscripts/setup-crash-dumps.ps1) is preserved, not overwritten. Uninstall removes only marked keys, and keeps them while another CoreVideo Pro version is still registered (the exe names are shared across versions). - 64-bit registry view is REQUIRED. The installer is a 32-bit NSIS binary, so the
PowerShell it launches is the 32-bit SysWOW64 host, and the
HKLM:PSDrive follows the process view: it would writeHKLM\SOFTWARE\WOW6432Node\..., which WerFault never reads for 64-bit processes (zero dumps, silently).Install-CrashDumps.ps1therefore opens the hive withOpenBaseKey(LocalMachine, Registry64)for every read, write, marker check and removal, the same precedent asInstall-VirtualCamera.ps1andVirtualCameraRegistration.psm1(Get-CameraRuntimeRoot). Never useHKLM:,New-ItemorSet-ItemPropertyagainst LocalDumps. An elevated child that reports no exit code is treated as unknown and verified by reading the 64-bit keys back. - Recording folder - created if missing at the app's own default
(
RecordingFolderPolicy: the user's Videos known folder +CoreVideo Pro, else%LOCALAPPDATA%\CoreVideo Pro; NSIS$VIDEOS, so a redirected Videos folder is honoured).CreateDirectorynever clobbers; uninstall preserves it.
Tests: Test-CrashDumps.ps1 (runs unelevated against a throwaway HKCU key, including from the 32-bit SysWOW64 PowerShell to exercise WOW64 redirection; in CI
native-shell-windows) covers values/kinds, idempotence, foreign-key preservation,
shared-install retention, and the loud non-fatal refusal. Test-AlphaInstaller.ps1 asserts
the recording folder, and either the HKLM keys (elevated run) or the logged refusal
(unelevated run). NOT verified without a real elevated install: the UAC RunAs path of
-AllowElevation, and an actual WER dump landing in the crashes folder.
Implemented 2026-10-24 (PR for #781); installed acceptance is pending (plan Task 7, owner-run).
Spec: docs/superpowers/specs/2026-10-23-frame-server-dll-refresh-design.md; plan:
docs/superpowers/plans/2026-10-23-frame-server-dll-refresh.md.
Why: an in-proc camera DLL stays mapped in the FrameServer service host until that process exits, so after an
upgrade a correct registration (#783) could still be served by the OLD DLL (measured: PID 29648 served 8670d079
while registry and core used cd63ab88). Camera validation could pass against the wrong DLL.
What Setup does now, inside the camera step's existing elevated block (no new UAC prompt, no new elevation path):
- After the Install registration is verified, after a Rollback restore, and on Uninstall after the keys are removed
but before
Remove-UnreferencedCameraRuntime,Install-VirtualCamera.ps1runsRefresh-FrameServer.ps1 -Action Refresh -ExpectedDll <registered DLL, or empty>. - The decision is pure (
Get-FrameServerRefreshDecisioninVirtualCameraRegistration.psm1):NotRunning,Current(only the expected DLL is loaded) andNotLoadedmake zero service calls;Stale(another CoreVideo camera DLL is loaded, or the serve log names one for this PID) andUnknown(no evidence) refresh. Observed modules outrank the serve log; a serve log last written before the instance started is not evidence (PID reuse). - Refresh =
Stop-Service FrameServer -Force, wait at most 20 s for the old PID+creation time to be gone (or replaced by a Windows demand-start), then start it again only if it was running. NeverStop-Process, never kills. - Exit codes.
Refresh-FrameServer.ps1: 0 refreshed/not needed, 3 not removed, 1 error.Install-VirtualCamera.ps1maps any refresh failure to exit 3 = "registered and verified, Frame Server not refreshed". It never rolls back a verified registration for this; exit 1 stays the only rollback trigger. Exit 3 prints aWARNING:line even with-Quiet; NSISDetailPrints it, appends to<release>-setup.log, and shows a warning box on interactive setup (close camera apps or restart Windows). Uninstall and the NSIS rollback path accept 3 the same way. - Every outcome (including
not_needed) is one line in%LOCALAPPDATA%\CoreVideoPro\installer-logs\<release>-frameserver.log(BOM-free; the compact result JSON is on the line). The elevated RunAs child cannot return stdout, so it leaves the result in<app>\.camera-frameserver-refresh.json, which the parent reads and deletes (NSIS deletes it too). - 64-bit / Sysnative required. The 32-bit NSIS host launches SysWOW64 PowerShell, which cannot enumerate a
64-bit process's modules (measured: not even the user's own
explorer.exe).Get-NativePowerShellPathreturns%windir%\Sysnative\WindowsPowerShell\v1.0\powershell.exefrom a 32-bit process on 64-bit Windows, else System32 (never$PSHOME, so a pwsh 7 parent cannot leak in). The refresh and Inspect go through it, andRefresh-FrameServer.ps1re-invokes itself through Sysnative if started 32-bit. Note[Environment]::Is64BitOSdoes not exist in .NET Framework (it reads$null); useIs64BitOperatingSystem. - The UAC RunAs child is plain
powershell.exe, never a Sysnative path.Start-Process -Verb RunAshands the target to the 64-bit AppInfo service, whereSysnativedoes not exist; from the SysWOW64 host (the shipped installer isRequestExecutionLevel user, so that is the real install path) a Sysnative target fails with "The system cannot find the path specified", NSIS takescamera_failedand rolls the install back. Admin CI runners skip the RunAs branch and never see it. The 64-bit requirement is met inside the elevated child, whereInvoke-FrameServerRefreshrelays through Sysnative directly.Test-VirtualCameraOwnership.ps1asserts the RunAs target from the script's AST. - Pending states. A FrameServer caught in Start Pending / Stop Pending WAS running for restart purposes. Refresh
waits (bounded by
-StopTimeoutSeconds) for it to settle, decides from the settled state, and if it never settles treats it as Unknown: stop, then restart. It is never left stopped under arefreshedexit 0. The result carriessettledFrom. Inspect never waits. - The NSIS exit-3 branch
ClearErrorsafter its setup-log write: a failed log write must not reach the laterIfErrors install_failedand roll back a verified install. - Lazy load. A refreshed Frame Server loads the DLL on the next camera activation, so install time can only prove
the stale image is gone.
servingIdentityVerifiedstaysfalseuntil a freshServing module path=line from the current FrameServer PID names the registered DLL.Install-VirtualCamera.ps1 -Action Inspectnow computes it from that evidence (and the manifest hash) instead of a constant; unelevated, module inspection isunverifiedwith amoduleInspectionReasonand the serve log decides. - Fault injection (tests only): the explicit
-FrameServerFault unstoppableparameter swaps in a built-in fake instance that refuses to stop; the real service is never touched in that mode. Production never reads an environment variable for this (Test-VirtualCameraOwnership.ps1asserts it).
Tests: Test-FrameServerRefresh.ps1 (fake provider, 42 checks, plus a read-only real Inspect; CI runs it from the
64-bit and SysWOW64 hosts; -IncludeInstalledInspect adds the desktop-user Inspect locally) and the decision/parser
table in Test-VirtualCameraOwnership.ps1 (55 cases, both hosts). Test-AlphaInstaller.ps1 asserts the
frameserver log after a real silent install and, elevated, a fault-injected exit 3 with the registration intact.
NOT verified until the owner-run acceptance (plan Task 7): a real Stop-Service FrameServer with the camera open
(timing, consumer behaviour, Windows self-restart); the stale image actually disappearing on an upgrade; a fresh
Serving module path= line from the new PID; the NSIS warning box. The acceptance probe is
scripts/qa/probe-frameserver-stop.ps1 (-Mode Observe read-only; -Mode Stop -RestartAfter -AllowConsumers -OutDir D:\... elevated, 64-bit, owner present), then scripts/qa/native-camera-continuity.py after activating the
camera in Zoom. Reboot, fresh-user, alternate-admin and fleet rollout remain out of scope.