fix: serialize Gaming Profile crash-recovery on the service gate#1436
Merged
Conversation
On launch, if a previous run closed with game-mode tweaks still applied, the tab offers to revert them via RecoverPendingAsync. Unlike ApplyAsync and RevertAsync (both serialize on the service SemaphoreSlim _gate), RecoverPendingAsync ran its RunRevertAsync and its LoadStore->SaveStore completely ungated. After the user answers the "restore?" dialog the UI is live again, so clicking Start launches ApplyAsync concurrently with the still-running recovery revert: two paths reverting/applying the same machine-wide tweaks and doing an unsynchronized read-modify-write of the on-disk store -- which can lose the ActiveSession=null clear (resurrecting the leftover marker so it re-prompts forever) or interleave conflicting tweak steps. RecoverPendingAsync now acquires _gate for its whole body (mirroring RevertAsync, including the ConfigureAwait(false) that keeps the gate-release continuation off the UI thread so Dispose's shutdown _gate.Wait() can't deadlock) and re-reads the store inside the gate so the read-modify-write is atomic against a concurrent SaveStore. Regression test: with a revert suspended holding the gate (via the existing GatedRevertTweak seam), RecoverPendingAsync's returned Task is deterministically NOT completed until the gate is released -- proving it serializes on _gate. Pins the marker is cleared exactly once after recovery.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (P2 — startup race, non-destructive but user-visible)
On launch, if a previous run closed/crashed with game-mode tweaks still applied, the Gaming Profile tab offers to revert them via
GamingProfileService.RecoverPendingAsync. UnlikeApplyAsyncandRevertAsync— which both serialize on the service'sSemaphoreSlim(_gate) —RecoverPendingAsyncran itsRunRevertAsyncand itsLoadStore→SaveStorecompletely ungated.After the user answers the "restore leftover changes?" dialog, the UI is live again. Clicking Start then launches
ApplyAsyncconcurrently with the still-running recovery revert:LoadStore … SaveStore(store with { ActiveSession = null })can interleave with Apply'sSaveStore(… with { ActiveSession = <new> }), losing one write. If theActiveSession = nullclear is the one lost, the leftover marker resurrects and the app re-prompts for recovery on every launch.Fix
RecoverPendingAsyncnow acquires_gatefor its whole body, mirroringRevertAsync:ApplyAsync/RevertAsync(a concurrent Start now waits for recovery to finish, and vice-versa).SaveStore.ConfigureAwait(false)under the gate for the same anti-deadlock reason the other two do (the gate-release continuation must stay off the UI thread soDispose's shutdown_gate.Wait()can't deadlock against it).All three gate consumers (
ApplyAsync,RevertAsync,RecoverPendingAsync) are now consistent.Test
RecoverPendingAsync_WaitsForGate_WhenARevertHoldsIt— reuses the file's existing deterministic seam (GatedRevertTweak, which suspends on a signal the test controls — no wall-clock): with a revert suspended holding_gate,RecoverPendingAsync()'s returned Task is asserted not completed (it's parked on_gate.WaitAsync); afterResume(), both complete and theActiveSessionmarker is cleared exactly once. The pending session uses an all-false profile so recovery's own revert is an inert no-op (no power/registry/service call) — fully deterministic, touches nothing on the machine.Red→green: pre-fix (ungated)
RecoverPendingAsynccompletes immediately even while the revert holds the gate, soAssert.False(recover.IsCompleted)fails; post-fix it blocks on the gate → passes. (dotnet testnot run locally per repo policy; verified via CI.)Regression sweep
dotnet buildmain + tests: 0 warnings / 0 errors.