Skip to content

Simplify Windows installer rollback without weakening recovery guarantees #3575

Description

@Astro-Han

Context

#3265 added pre-upgrade backup, automatic NSIS .onInstFailed rollback, retained-backup recovery after hookless Quit, and a Windows behavioral gate. It is a real protection, not 1,000+ lines built only around an empty-directory check.

In electron-builder 26.15.3's one-click template, uninstallOldVersion destroys the previous installation before several built-in NSIS File instructions run. NSIS documents that when a File cannot be extracted/created/overwritten and the user cancels, .onInstFailed runs. This is a real post-destruction production entry into Maka's rollback hook. The template's 7z retry/cancel branch is different: it calls Quit, so no callback runs and the retained sibling backup is the only recovery evidence.

Non-goal: do not remove the backup or automatic .onInstFailed rollback. A manifest or registry record cannot recreate the deleted application bytes. The Windows gate simulates the callback with an explicit Abort; it does not currently force a real disk-full/permission File failure.

This issue tracks the simplifications that remain after preserving that safety boundary. The four areas below are independent unless a dependency is called out.

1. Empty-directory enumeration (21 lines)

makaDirHasEntries uses FindFirst/FindNext to distinguish a truly empty directory from the . and .. entries matched by ${FileExists} "$INSTDIR\*.*". The earlier wildcard form was incorrect.

This helper has one consumer: the restore branch where moving the partial/new $INSTDIR aside still fails after retries. It decides whether to copy the verified old backup into a held-but-empty directory.

  • Safe conclusion while the current behavior remains: keep the helper. NSIS has no equivalent reliable wildcard emptiness predicate.
  • Dependency: it can be deleted only with item 2's decision to remove empty-shell auto-salvage.
  • Validation if kept: add a deterministic Windows fault-injection test for a held empty $INSTDIR; the current rollback gate does not execute this branch.
  • Line effect: 21 NSIS lines, but not independently deletable without changing item 2's behavior.

2. Aside/copy salvage after restore conflicts

Today rollback first renames the partial/new tree to <install>.failed-upgrade, then renames the verified backup into place. If the first rename fails, it distinguishes populated from empty targets; an empty shell gets a copy fallback, while a populated target returns 103 and retains both trees/recovery evidence.

Proposed simplification: after rename retries are exhausted, always take the existing fail-safe outcome—return 103, retain the verified sibling backup and recovery note, and require manual recovery. This removes the empty-shell classification and copy fallback without risking the backup.

  • Why the weaker branch is still safe: the verified backup has not been moved or modified when step 1 fails. The deleted logic improves automatic recovery only for a held-but-empty target; it is not what preserves the previous bytes.
  • Behavior change requiring maintainer approval: a rare held empty shell changes from automatic recovery to exit 103 + manual recovery.
  • Estimated net deletion: 45–55 NSIS lines including item 1.
  • Acceptance: exact-head Windows rollback/control/Quit gates stay green; docs and exit-103 wording say that a verified sibling backup may require manual recovery; either add a Windows handle-conflict test or explicitly remove the unexecuted auto-salvage promise.

3. The 11-value registry snapshot and rollback verification

The snapshot is on the real .onInstFailed path. It mirrors registry values electron-builder's old uninstaller deletes:

  • install key: InstallLocation, KeepShortcuts, ShortcutName, MenuDirectory;
  • uninstall key: DisplayName, UninstallString, QuietUninstallString, DisplayVersion, DisplayIcon, Publisher, Comments;
  • NoModify and NoRepair are deterministic template constants and are recreated rather than snapshotted.

Safe conclusion under the current contract: do not remove or replace these values with a manifest. Automatic rollback restores old program bytes; without corresponding old registration, Settings uninstall, quiet uninstall, version identity, links, and updater/installer behavior can describe a different installation. The gate directly verifies restored DisplayVersion, registration, launch, and later uninstall, while the implementation restores the remaining template-owned values exactly.

There are two possible contract changes, each requiring a maintainer decision before implementation:

  1. Define a smaller documented registration contract and name which old values may be lost or recomputed. Do not infer that cosmetic-looking fields are unused.
  2. On registry write-back/read-back failure, stop rebuilding another full sibling backup for a later self-healing rerun. Keep the already-restored old tree, persisted snapshot, aside tree, recovery note, and exit 103 for manual repair. This second option removes approximately 60–75 lines across NSIS, verifier, and docs, but narrows the current “may recover on rerun” behavior for a rollback-of-the-rollback failure.

Acceptance for either change must exercise real Windows registry behavior and still prove that the restored installation is registered, launchable at the old version, and uninstallable. Source inspection alone is insufficient.

4. The 637-line rollback verifier

Do not merge the whole verifier into verify-windows-installer-lifecycle.mjs. The files share mechanics but own different contracts:

  • lifecycle: install → optional upgrade → smoke → uninstall;
  • rollback: nonzero exit codes and abnormal filesystem/registry states (Abort restore, registry mismatch, stale/incomplete backup, hookless Quit, rerun adoption, registrationless refusal, cleanup).

The rollback verifier already imports shared manifest, CDP, process, cleanup, and lifecycle primitives. verify-windows-harness.test.mjs unit-tests those helpers; product-release.test.mjs only pins workflow ordering. Neither duplicates the behavioral gate. Folding the files would retain almost all scenarios while creating an approximately 1,000-line modeful lifecycle authority, saving only setup/cleanup boilerplate and making failures harder to locate.

One high-confidence simplification is available: replace rollback's bespoke assertLaunchable CDP wrapper with verifyPackagedWindowsApp(installDirectory, { workingDirectory, expectedVersion, artifactContract: 'legacy-baseline' }), the authority already used by the lifecycle verifier for an installed previous release.

  • Why safe: it validates the same installed-directory artifact, launches it, waits for a usable renderer, and verifies the expected product version; no rollback-specific rule lives in the local wrapper.
  • Estimated net deletion: 35–45 lines.
  • Acceptance: harness tests and the full Windows rollback gate stay green, including old-version launch after rollback.

Optional product-level simplification

The largest remaining state machine exists to make a later installer safely adopt a backup left by Quit. If maintainers change the support contract to “retained backup + README, manual recovery only,” persistent snapshot/adoption/identity conflicts and their scenarios can be removed while keeping same-process automatic rollback. Estimated deletion: another 150–190 lines across installer, verifier, and bilingual docs.

This is not cleanup: it removes the documented supported rerun recovery path. Quit and hard-kill paths still cannot delete the backup because no callback runs.

Expected reduction

  • No behavior change: approximately 35–45 lines.
  • Accept manual recovery for the two rollback-of-the-rollback contingencies in items 2 and 3: another 105–130 lines.
  • Also replace supported Quit rerun recovery with manual-only recovery: another 150–190 lines.
  • Maximum identified reduction: approximately 290–365 lines. There is no evidence-backed way to remove roughly 1,000 lines while retaining feat(win): Abort-path installer rollback with backup retention #3265's current guarantees.

Contribution prerequisites

The safe verifier reuse is self-contained. The installer changes are not suitable for an unassisted first contribution: they require NSIS callback/error semantics, access to a real Windows environment, the ability to build two installer versions, and fault-injection validation. More importantly, items 2 and 3 require maintainers—not contributors—to choose the recovery contract first. For that reason this issue should not receive help wanted until those decisions are made and a single accepted behavior is written into the acceptance criteria.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions