Skip to content

Latest commit

 

History

History
258 lines (233 loc) · 19.6 KB

installer.md

File metadata and controls

258 lines (233 loc) · 19.6 KB

Installer: one entry point, and uninstall removes what it caused (T2.7/T2.1, 2026-09-12)

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.

#883: deterministic helper environment and the residual-folder sweep (2026-10-24)

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 Pro shortcut (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.ps1 records what it wrote to notices\ffmpeg\installed-files.txt; un.RemoveMediaRuntime deletes 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 than RMDir /r because $INSTDIR is 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:

  1. NEVER run an interactive helper from a silent installer. regsvr32 WITHOUT /s opens a modal result dialog, and Register-VirtualCamera.cmd deliberately omits /s so a tester who double-clicks it gets feedback. Calling that .cmd from the installer hung a silent install forever — two regsvr32 processes waiting on a dialog nobody could see, until the 180 s Test-AlphaInstaller timeout. The installer calls regsvr32 /s directly. 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.
  2. A UTF-8 BOM is three literal characters to NSIS. Windows PowerShell 5.1's Set-Content -Encoding UTF8 writes one, so the manifest's first path became <BOM>ffmpeg.exe, Delete removed 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 the installed: 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 of CoreVideoPro.WinUI.exe, corevideo-native.exe, corevideo-zoom-engine.exe it writes HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\<exe> with DumpFolder = %LOCALAPPDATA%\CoreVideoPro\crashes (REG_EXPAND_SZ), DumpType = 2 (full), DumpCount = 5, plus a CoreVideoProInstaller marker 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 /S setup must already be elevated, exactly like the camera step. It never fails setup: a declined prompt or refused write exits 2, is DetailPrinted, 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 from scripts/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 write HKLM\SOFTWARE\WOW6432Node\..., which WerFault never reads for 64-bit processes (zero dumps, silently). Install-CrashDumps.ps1 therefore opens the hive with OpenBaseKey(LocalMachine, Registry64) for every read, write, marker check and removal, the same precedent as Install-VirtualCamera.ps1 and VirtualCameraRegistration.psm1 (Get-CameraRuntimeRoot). Never use HKLM:, New-Item or Set-ItemProperty against 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). CreateDirectory never 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.

Frame Server refresh after a camera DLL upgrade (#781)

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.ps1 runs Refresh-FrameServer.ps1 -Action Refresh -ExpectedDll <registered DLL, or empty>.
  • The decision is pure (Get-FrameServerRefreshDecision in VirtualCameraRegistration.psm1): NotRunning, Current (only the expected DLL is loaded) and NotLoaded make zero service calls; Stale (another CoreVideo camera DLL is loaded, or the serve log names one for this PID) and Unknown (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. Never Stop-Process, never kills.
  • Exit codes. Refresh-FrameServer.ps1: 0 refreshed/not needed, 3 not removed, 1 error. Install-VirtualCamera.ps1 maps 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 a WARNING: line even with -Quiet; NSIS DetailPrints 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-NativePowerShellPath returns %windir%\Sysnative\WindowsPowerShell\v1.0\powershell.exe from 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, and Refresh-FrameServer.ps1 re-invokes itself through Sysnative if started 32-bit. Note [Environment]::Is64BitOS does not exist in .NET Framework (it reads $null); use Is64BitOperatingSystem.
  • The UAC RunAs child is plain powershell.exe, never a Sysnative path. Start-Process -Verb RunAs hands the target to the 64-bit AppInfo service, where Sysnative does not exist; from the SysWOW64 host (the shipped installer is RequestExecutionLevel user, so that is the real install path) a Sysnative target fails with "The system cannot find the path specified", NSIS takes camera_failed and 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, where Invoke-FrameServerRefresh relays through Sysnative directly. Test-VirtualCameraOwnership.ps1 asserts 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 a refreshed exit 0. The result carries settledFrom. Inspect never waits.
  • The NSIS exit-3 branch ClearErrors after its setup-log write: a failed log write must not reach the later IfErrors install_failed and 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. servingIdentityVerified stays false until a fresh Serving module path= line from the current FrameServer PID names the registered DLL. Install-VirtualCamera.ps1 -Action Inspect now computes it from that evidence (and the manifest hash) instead of a constant; unelevated, module inspection is unverified with a moduleInspectionReason and the serve log decides.
  • Fault injection (tests only): the explicit -FrameServerFault unstoppable parameter 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.ps1 asserts 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.