Skip to content

Stop automatic update retries when the update helper rejects the installation and report its code - #139

Merged
Views2k merged 1 commit into
mainfrom
fix/updater-helper-rejection
Oct 20, 2026
Merged

Views2k merged 1 commit into
mainfrom
fix/updater-helper-rejection

Conversation

@Views2k

@Views2k Views2k commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Purpose

Wisp Tools (2026-10-19) shows one PC on Wisp 2.6.12 that cannot update to 2.6.13. Its update helper exits with UPDATE_INSTALL_REGISTRATION ("The installed Wisp location could not be verified") while Wisp is still open. Wisp reports update / install-handoff / failure with the generic reason update-verification-failed and retries every two minutes for as long as it runs (100 failures since 2026-10-18 02:38 UTC). Each launch then consumes the helper's result file and reports the same failure once more under the update-installed stage with recovery application-still-running.

#133 (shipped in 2.6.13) stops the automatic retries only when Wisp.Updater.exe is missing. A helper that starts and rejects the install was still a Retry outcome.

Root cause and solution

Root cause. App.TryBeginApplicationUpdateAsync treated every HelperExited handoff state the same way: a fixed update-verification-failed incident, a fixed message, and a plain (false, error) result that maps to AutomaticUpdateApplyOutcome.Retry. The helper's own result file (%LOCALAPPDATA%\Wisp\update-result.json, written before it exits) was never read at that point, so neither the reported reason nor the retry decision knew which check had failed. The registration check compares the HKCU uninstall key's InstallLocation plus Wisp.exe with the running executable's path; that cannot change while this Wisp process runs, so retrying within the session cannot succeed.

Solution.

  • ApplicationUpdateLauncher.DescribeHelperExit reads the helper's result file when the helper exits before signalling Ready. A failed record written during this handoff (its recordedAtUtc within a minute of the launch) is consumed, and its errorCode becomes the Tools incident reason in the catalog's kebab-case form (for example update-install-registration) with failure stage update-install-handoff, the record's nativeErrorCode as the code and its recovery state as update evidence. An older or unreadable record is left for the next launch to consume, and only the helper's exit code is reported (reason update-unexpected-failure, code = exit code). No text from the record is rendered or sent.
  • Session-permanent failures: the helper's InvalidRequest exit code (now the shared constant UpdateApplyContract.InvalidRequestExitCode, which UpdaterExitCode.InvalidRequest references), or one of the installation-identity reasons update-install-registration, update-application-path, update-installed-app-invalid. ApplicationUpdateStartResult.HelperRejected maps them to AutomaticUpdateApplyOutcome.Unavailable, the latch Stop the automatic update retry loop when the update helper is missing #133 added for the missing helper: no further automatic handoff attempts until the next Wisp launch. Other codes (installer validation, parent-process checks, a crash without a record) keep the bounded two-minute retry.
  • The dashboard Update / Install update button still calls TryBeginApplicationUpdateAsync directly, one attempt per click. Once the automatic path has stopped, the 15-minute check shows "Wisp X could not be installed automatically. Download and run the full installer from wispoverlay.com." instead of "verified and ready to install" (the gap noted in Stop the automatic update retry loop when the update helper is missing #133).
  • Messages by reason, all ending in "Wisp stayed open and no files were installed.": registration mismatch → "This copy of Wisp does not match the Wisp installation registered in Windows. Download and run the full installer from wispoverlay.com to reinstall it."; application path or installed executable invalid → "The update helper could not verify this Wisp installation. Download and run the full installer from wispoverlay.com to reinstall it."; other rejected requests → "The update helper rejected the prepared update. Download and run the full installer from wispoverlay.com to update."; retryable → "The update helper stopped before the update could start. Try again, or download and run the full installer from wispoverlay.com."
  • Consuming the record at handoff time removes the duplicate update-installed failure report at the next launch for this case. Records from helpers that ran after Wisp exited are still consumed at launch as before.
  • TryReadRecord is shared by TryConsumeResult and DescribeHelperExit. It now checks that schemaVersion is a JSON number before reading it; a string value would previously have thrown out of TryConsumeResult at startup.
  • Tools needs no change: its console has no catalog of incident reasons, and every symbol used here already exists in SupplementaryIncidentSchema.

Registry lookup stays per-user

The HKCU-only lookup in WindowsInstallationRegistrationReader is kept, with the exact path-equality check. Evidence:

  • installer/Wisp.iss sets PrivilegesRequired=lowest without PrivilegesRequiredOverridesAllowed, so Setup always runs in non-administrative install mode and writes the {A8FC0D58-11E3-4B25-B78D-3B98E9855473}_is1 uninstall key under HKCU; DefaultDirName={localappdata}\Programs\Wisp; the script's own ExistingInstallationPresent reads HKCU.
  • The helper runs the installer with /CURRENTUSER (UpdaterConstants.InstallerArguments), so an update never creates an HKLM registration either. The private test-installer script changes only the display/output version and source paths, not the AppId or privileges. Reading HKLM would only ever match a registration the official installer never writes.
  • A copy run from another folder is correctly rejected: the installer would update the registered folder (Inno Setup reuses the previous install directory) rather than the running copy, and without any registration /WISPUPDATE is treated as a fresh install that re-runs the setup wizard.
  • InstalledApplicationValidatorTests now pin the semantics: a trailing separator and case differences are accepted; a missing, empty or relative InstallLocation, or a different folder, is rejected as UPDATE_INSTALL_REGISTRATION with the InvalidRequest exit code.

Remaining hypothesis for the affected PC, not verifiable from Tools data: UpdatePathSafety.PathsEqual normalises case and trailing separators but does not expand 8.3 short names or resolve junctions, so a Wisp started through a short-name path or a redirected profile folder compares unequal to the registered long path. The specific code now reaching Tools confirms which check fails; the path shape needs diagnostics from that PC.

Validation

Run on a Windows PC from a snapshot of this branch head (.NET SDK 8.0.425, Release build):

  • Wisp.App.Tests filtered to ApplicationUpdate*, AutomaticUpdateCoordinatorTests, SupplementaryIncident*, SupplementaryOperationFailureTests, AppControllerLifecycle* and RunsAutosaveTests.UserExitAndUpdaterPrepareMetadataBeforeClosingTheDispatcher: 155 passed.

    • New ApplicationUpdateLauncherTests: HelperExitRecordNamesTheFailedCheckAndStopsTheSessionWhenItCannotChange (seven codes: reason, stage, code, recovery, schema validity, no record text in the incident or message, permanence per code, record consumed), HelperExitWithoutARecordReportsTheExitCodeAndStopsOnlyForARejectedRequest, StaleHelperRecordIsLeftForTheNextLaunch, UnreadableHelperRecordDoesNotBreakTheReport; MissingHelperFailureStopsAutomaticHandoffWhileOtherFailuresRetry covers the HelperRejected mapping.
    • New AutomaticUpdateCoordinatorTests.RejectedHandoffAfterTransientRetriesStopsForTheSession: two retries, then the rejection; eight hours of five-second ticks and a later 15-minute check add no attempts.
  • Wisp.Updater.Tests: 75 passed, including the new InvalidRequestExitCodeIsTheSharedContractValue, RegistrationComparesTheFullPathIgnoringCaseAndTrailingSeparator and MissingOrRelativeRegistrationIsRejectedAsAnInvalidRequest; the result-file test now also checks recordedAtUtc.

  • Wisp.Update.Tests: 144 passed.

  • dotnet format whitespace --verify-no-changes on the changed files, Test-RepositoryPolicy.ps1 and git diff --check: clean.

  • The change is within the approved scope.

  • The change improves usability, functionality, feel, reliability, or maintenance, or fixes a verified bug.

  • I have read and agree to the contributor-rights terms in CONTRIBUTING.md.

  • Regression coverage has been added or updated where appropriate.

  • Steam and Xbox app / Microsoft Store impacts, checks, and outstanding gaps are recorded separately as required by docs/VALIDATION.md.

  • The complete Release test suite passes.

  • No credentials, personal data, machine paths, game binaries, save data, private captures, or generated output are included, except matching shader bytecode for an approved shader change described in docs/SHADERS.md.

  • User-facing changes are recorded in CHANGELOG.md.

  • Native HUD assets are unchanged, or their distribution rights, provenance, and hashes are documented.

Outstanding validation

  • No live reproduction on a PC whose registration does not match the running copy. App.TryBeginApplicationUpdateAsync is not unit-testable; its change is the branch that calls DescribeHelperExit and returns HelperRejected.
  • The complete Release test suite was not run; the PR gate and the nightly suite cover the rest.
  • The update path is shared by Steam and Microsoft Store installs; no storefront-specific behavior changed, so no separate store checks were recorded.
  • Whether the affected PC's mismatch is a moved copy, a removed registration or a path-shape difference stays open until its next install-handoff failure arrives with the specific code.

…allation and report its code

When the helper exits before it is ready, read the result file it wrote, report its error code to Tools under the install-handoff stage, and treat InvalidRequest exit codes and installation-identity failures as permanent for the session, the way a missing helper already is. Show reinstall instructions when the registered install location does not match. The dashboard button still tries once per click.
@Views2k
Views2k marked this pull request as ready for review October 19, 2026 18:20
@Views2k
Views2k merged commit 621358e into main Oct 20, 2026
10 checks passed
@Views2k
Views2k deleted the fix/updater-helper-rejection branch October 20, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant