You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#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:
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:
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.
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:
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.
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.
Context
#3265 added pre-upgrade backup, automatic NSIS
.onInstFailedrollback, retained-backup recovery after hooklessQuit, 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,
uninstallOldVersiondestroys the previous installation before several built-in NSISFileinstructions run. NSIS documents that when aFilecannot be extracted/created/overwritten and the user cancels,.onInstFailedruns. This is a real post-destruction production entry into Maka's rollback hook. The template's 7z retry/cancel branch is different: it callsQuit, so no callback runs and the retained sibling backup is the only recovery evidence.Non-goal: do not remove the backup or automatic
.onInstFailedrollback. A manifest or registry record cannot recreate the deleted application bytes. The Windows gate simulates the callback with an explicitAbort; it does not currently force a real disk-full/permissionFilefailure.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)
makaDirHasEntriesusesFindFirst/FindNextto 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
$INSTDIRaside still fails after retries. It decides whether to copy the verified old backup into a held-but-empty directory.$INSTDIR; the current rollback gate does not execute this branch.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.
3. The 11-value registry snapshot and rollback verification
The snapshot is on the real
.onInstFailedpath. It mirrors registry values electron-builder's old uninstaller deletes:InstallLocation,KeepShortcuts,ShortcutName,MenuDirectory;DisplayName,UninstallString,QuietUninstallString,DisplayVersion,DisplayIcon,Publisher,Comments;NoModifyandNoRepairare 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:
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:The rollback verifier already imports shared manifest, CDP, process, cleanup, and lifecycle primitives.
verify-windows-harness.test.mjsunit-tests those helpers;product-release.test.mjsonly 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
assertLaunchableCDP wrapper withverifyPackagedWindowsApp(installDirectory, { workingDirectory, expectedVersion, artifactContract: 'legacy-baseline' }), the authority already used by the lifecycle verifier for an installed previous release.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.
Quitand hard-kill paths still cannot delete the backup because no callback runs.Expected reduction
Quitrerun recovery with manual-only recovery: another 150–190 lines.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 wanteduntil those decisions are made and a single accepted behavior is written into the acceptance criteria.References
apps/desktop/build/installer.nshscripts/verify-windows-installer-rollback.mjsscripts/verify-windows-installer-lifecycle.mjs