Fix the two remaining UI-thread elevation freezes (profile save + installer)
Review finding, same class as the shipped install-hang fix: two paths still ran the elevated service helper synchronously on the UI thread, freezing the window and live audio for the duration (worst case minutes across two UAC prompts). 1) Service-profile save (MainForm.ConfigureServiceProfile): saving while the service runs did RunElevated(stop) + RunElevated(start) inline. Now: the restart runs on a background task, and tries a NO-UAC restart first - ServiceControl.TryRestartNoAdmin uses the start/stop rights the installer grants the installing account, so the normal case has no elevation prompt at all. Elevated verbs remain the fallback (service installed by a different account). Success is silent; only a failed restart reports back. The save popup now says the service is restarting. 2) App installer's optional service step (AppInstaller): the install + start-now calls ran RunElevated inline. The flow is sequential (can't fire-and-forget - the installer relaunches and exits afterwards), so RunElevatedResponsive runs the helper on a worker while a small modal "working..." shell pumps messages: UI and audio stay live, nothing can be double-triggered, NVDA announces the step, and the exit code still returns inline. Test: "No-admin service restart fails safe" - TryRestartNoAdmin against a missing service returns false promptly without throwing (that false routes callers onto the elevated fallback). The success path needs the real SCM + grant, covered by hand-test. Gate 47/47. Part of the review-fix batch; no release until the whole plan lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Service app-yield token", ServiceInteractivePresence);
|
||||
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
||||
RunStep(results, "Elevated helper: no pipe deadlock on flooded output", ServiceProcessCaptureNoDeadlock);
|
||||
RunStep(results, "No-admin service restart fails safe (missing service)", ServiceRestartNoAdminFailsSafe);
|
||||
RunStep(results, "Default-output follower (service follows Windows default)", DefaultOutputFollower);
|
||||
RunStep(results, "Default follower exclusivity (locks out specific cards)", DefaultFollowerExclusivity);
|
||||
RunStep(results, "Service profile isolation (location + hidden from pickers)", ServiceProfileIsolation);
|
||||
@@ -945,6 +946,20 @@ internal static class SelfTest
|
||||
return "follower flagged + sentinel shared with the app; service resolves it to the live default render endpoint";
|
||||
}
|
||||
|
||||
/// <summary>The no-UAC restart used after a service-profile save (TryRestartNoAdmin) must FAIL SAFE:
|
||||
/// against a service that doesn't exist it returns false, promptly, and never throws — that false is
|
||||
/// what routes the caller onto the elevated fallback. (The success path needs the real installed
|
||||
/// service + granted rights, so it's covered by hand-testing, not the gate.)</summary>
|
||||
private static string? ServiceRestartNoAdminFailsSafe()
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var ok = ServiceControl.TryRestartNoAdmin("RemSoundSelfTestNoSuchService");
|
||||
sw.Stop();
|
||||
Check(!ok, "restarting a non-existent service must report false, not throw");
|
||||
Check(sw.ElapsedMilliseconds < 5000, $"the failure must be prompt (took {sw.ElapsedMilliseconds} ms)");
|
||||
return $"missing service → false in {sw.ElapsedMilliseconds} ms, no throw";
|
||||
}
|
||||
|
||||
/// <summary>Reproduces the install-hang condition and proves it's fixed: a child that floods BOTH
|
||||
/// stdout and stderr far past the ~4 KB pipe buffer (a big directory listing plus a failing dir). The
|
||||
/// old "read stderr to end, then stdout" order deadlocked exactly here (icacls /T over the 100-file
|
||||
|
||||
Reference in New Issue
Block a user