Skip to content

Simplify Windows installer rollback without weakening recovery guarantees #3575

Closed

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

Activity

  1. testikun commented on Aug 24, 2026

    @testikun
    Contributor

    This was generated by AI during triage.

    I would like to work on this issue, limited to the self-contained verifier-reuse slice described in item 4.

    Proposed approach

    • Replace the rollback verifier's private assertLaunchable implementation with the existing verifyPackagedWindowsApp authority.
    • Invoke it against the restored installation directory with artifactContract: 'legacy-baseline', the existing isolated working directory, and the expected pre-upgrade version.
    • Remove only the duplicated process, renderer-smoke, and product-version plumbing that the shared verifier now owns.
    • Preserve every rollback-specific assertion: byte-identical tree restoration, uninstall-registration recovery, backup identity, fail-closed exits, recovery reruns, and cleanup.
    • Add focused platform-neutral harness coverage for the delegation contract, then run the Windows verifier helper suite plus formatting, lint, and type checks available on macOS.

    This deliberately does not remove the backup, registry snapshot, automatic .onInstFailed rollback, or any manual-recovery behavior. It is a code-ownership cleanup with no intended installer behavior change.

    中文摘要

    本次只实现 issue 中可独立完成的安全小切片:让回滚验证复用已有的 packaged-app verifier,删除重复的启动/渲染器检查代码;保留所有回滚、注册表、备份和失败恢复断言,不改变安装器恢复契约。

  2. testikun commented on Aug 24, 2026

    @testikun
    Contributor

    This was generated by AI during triage.

    Implemented in PR #3707.

    Result

    • Replaced the rollback verifier's private assertLaunchable CDP wrapper with verifyPackagedWindowsApp.
    • Kept the restored-install contract explicit with artifactContract: 'legacy-baseline'.
    • Preserved all rollback-specific backup, registry, fail-closed, recovery, and .onInstFailed assertions.
    • Added a regression test for the delegation contract.
    • No installer behavior or recovery policy was removed.

    Verification

    • Windows harness: 40 passed.
    • Product release policy + Windows harness: 66 passed.
    • Full lint and changed-file format checks passed.
    • The real installer rollback gate still needs the Windows CI environment; the local workspace is macOS.

    中文摘要

    实现已提交到 PR #3707:复用已有 packaged-app verifier,删除重复的启动/渲染器检查代码,同时保留完整的回滚、备份、注册表和失败恢复契约,并补充 delegation 回归测试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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