Skip to content

fix(v3/updater): keep the staged payload owned by the swap helper during restart - #6350

Merged
leaanthony merged 2 commits into
wailsapp:masterfrom
juggler-ai:fix/updater-staging-handoff
Oct 26, 2026
Merged

leaanthony merged 2 commits into
wailsapp:masterfrom
juggler-ai:fix/updater-staging-handoff

Conversation

@julianstorer

@julianstorer julianstorer commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

Updater.Restart hands the staged payload to a detached swap helper, which waits for the app to exit before renaming the payload over the installed application. Until now the updater kept no claim on the staging directory once the helper had started, which caused two problems:

  • A DownloadAndInstall during shutdown began with discardStaging and deleted the directory the helper was about to install from. A staged .app is a directory and os.Rename succeeds on an empty one, so the helper reported success, discarded the backup and left an empty application.
  • A second Restart (for example a double click in the built-in window, which runs go u.Restart(ctx) on every click) started a second helper against the same target.

This PR makes Restart read the staged path, take the staging directory and mark a restart as pending in one critical section. If a restart is already pending it returns the new ErrRestartPending. discardStaging checks the same flag under the same lock, so DownloadAndInstall returns ErrRestartPending and deletes nothing. If the helper fails to start, or never signals that it is ready, ownership is handed back so later attempts still work.

DownloadAndInstall returns ErrRestartPending without an EventError, the same way it already handles ErrNoPendingRelease and ErrDownloadInProgress: no flow has started at that point. If an app's ShouldQuit vetoes the quit, the restart stays marked as pending until the process exits, because the helper is still waiting on the PID. This is documented on the field.

Fixes #6349

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • WEP (proposal only; no implementation)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

New unit tests in v3/pkg/updater/updater_test.go. The last two make the interleavings deterministic through the package's existing test seams, with no sleeps. Both were run and seen to fail before the single-critical-section locking in Restart/discardStaging ("helper handed a staged path that does not exist" and "second Restart: … helper did not signal readiness"), and both pass with it. All four pass now:

  • TestRestart_StagedPayloadSurvivesConcurrentDownload: a DownloadAndInstall after Restart returns ErrRestartPending and the staged payload is still there.
  • TestRestart_FailedHandoff_ReturnsStagingOwnership: if the spawn fails or the helper never becomes ready, ownership is handed back.
  • TestRestart_HelperNeverHandedDiscardedPayload: runs a DownloadAndInstall inside Restart through the selfExecutable test seam, then checks that the path handed to the helper still exists.
  • TestRestart_SecondCallRefusedWhileRestartPending: calls Restart again from inside the first call's readiness wait. The second call gets ErrRestartPending, only one helper is started, and the first call's claim is still in place.

Commands:

cd v3 && go test -count=1 -race ./pkg/updater/
GOOS=windows GOARCH=amd64 go vet -unsafeptr=false ./pkg/updater/
  • Windows (cross-compiled go vet only; not run on Windows)
  • macOS
  • Linux

Test Configuration

Built from source at master b6167d9 + this commit; macOS arm64, Go 1.26.3.

Checklist:

  • (v2 only) I have updated website/src/pages/changelog.mdx with details of this PR (v3 changelog entries are added automatically)
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where starting a download or another restart while a restart was pending could interfere with the update awaiting installation.
    • If a restart cannot start or become ready, you can retry the download and installation.

Restart spawns a detached helper that waits for this process to exit
before renaming the staged payload over the installed application, then
returns without retaining any claim on the staging directory. A
DownloadAndInstall arriving during that wait begins with discardStaging,
which removes the very directory the helper is about to install from.

os.RemoveAll is depth-first, and a staged .app bundle is a directory, so
the helper is left holding a path whose contents have been unlinked —
and os.Rename succeeds on a directory whether or not it still holds
anything. The swap reports success and discards the backup, leaving an
empty application with nothing to roll back to.

Restart also accepts a second call while the first helper is pending.
The built-in window starts a Restart per click, so a double click starts
a second helper against the same target.

Restart now reads the staged path, takes the staging directory and marks
a restart as pending in one critical section, and returns
ErrRestartPending if one already is. discardStaging makes the same check
under the same lock, so DownloadAndInstall returns ErrRestartPending
rather than deleting a payload a helper owns; either Restart finds
nothing staged or the download finds the helper's claim. A helper that
fails to start, or never signals readiness, hands ownership back, so a
failed spawn does not strand the payload and block every later attempt.
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between b6167d9 and 6e50e21.

Files selected for processing: 3
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/updater/updater.go
  • v3/pkg/updater/updater_test.go

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Walkthrough

The updater now claims the staged payload during restart handoff and rejects downloads or repeated restarts while the helper owns it. It restores staging ownership if helper startup or readiness fails. Tests and a changelog entry cover these changes.

Changes

Updater restart handoff

Layer / File(s) Summary
Claim and release restart ownership
v3/pkg/updater/updater.go, v3/pkg/updater/updater_test.go, v3/UNRELEASED_CHANGELOG.md
Restart claims the staged payload and rejects another restart during the handoff. If helper startup or readiness fails, the updater releases the claim. Tests cover failed handoffs and repeated restarts.
Protect staging during handoff
v3/pkg/updater/updater.go, v3/pkg/updater/updater_test.go
DownloadAndInstall returns staging-discard errors. While a restart is pending, the updater preserves the staged payload and returns ErrRestartPending. Tests cover payload preservation and interleaved downloads.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related issues

Suggested labels: Bug, Documentation, v3

Suggested reviewers: leaanthony

Merge Risk: Minimal · up to 6e50e

The change stops a download or a second restart from deleting or contending for the staged update while the swap helper is waiting to install it. If the helper fails to start or never signals readiness, a later download or restart is still allowed. No merge-blocking issues were identified. The only gap is that Windows and Linux runtime behavior was not exercised.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: preserving staged payload ownership during a pending restart.
Description check Passed The description includes the bug context, linked issue, implementation details, test coverage, test commands, environment, and completed checklist items. It clearly documents that Windows and Linux we…
Linked Issues check Passed Issue [#6349] requires ownership protection for staged payloads and refusal of duplicate restarts. Restart now claims stagingDir and sets restarting in one lock section. discardStaging returns…
Out of Scope Changes check Passed The changes stay within issue [#6349]. The updater changes implement staging ownership and duplicate-restart protection. The tests verify those behaviors. The unreleased changelog entry documents the …
Full details: Description check

Explanation

The description includes the bug context, linked issue, implementation details, test coverage, test commands, environment, and completed checklist items. It clearly documents that Windows and Linux were not fully tested.

Full details: Linked Issues check

Explanation

Issue [#6349] requires ownership protection for staged payloads and refusal of duplicate restarts. Restart now claims stagingDir and sets restarting in one lock section. discardStaging returns ErrRestartPending without deletion. A second Restart returns ErrRestartPending. Failed helper startup and failed readiness reclaim staging. The added tests cover concurrent download, failed handoff, helper-path preservation, and duplicate restart behavior.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue [#6349]. The updater changes implement staging ownership and duplicate-restart protection. The tests verify those behaviors. The unreleased changelog entry documents the fix. No unrelated change is shown.

Full details: Docstring Coverage

Explanation

Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit guards the payload tight,
While helpers wait through shutdown night.
No second restart joins the queue,
Failed starts release the claim anew.
Safe files wait for the swap to bloom.

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added Bug Something isn't working v3 Documentation Improvements or additions to documentation labels Oct 25, 2026

@leaanthony leaanthony left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick fix!

@leaanthony
leaanthony merged commit c8f507b into wailsapp:master Oct 26, 2026
34 of 36 checks passed
@julianstorer
julianstorer deleted the fix/updater-staging-handoff branch October 27, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working Documentation Improvements or additions to documentation v3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3] updater: a download during a pending Restart deletes the staged payload, installing an empty app

2 participants