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:
Ednunp
2026-07-23 15:40:30 +01:00
co-authored by Claude Fable 5
parent 97c96b4fcb
commit cf1eb92f11
4 changed files with 117 additions and 6 deletions
+34 -4
View File
@@ -2367,12 +2367,42 @@ public sealed class MainForm : Form
logFile.Event($"service: profile saved (service logging {(dlg.ServiceLoggingEnabled ? "on" : "off")})");
// Remove any stray copy the old design left in the user's profiles folder.
try { profileStore?.Delete(ServiceControl.ServiceProfileTitle); } catch { /* best-effort */ }
if (ServiceControl.Query() == ServiceState.Running)
var restartNeeded = ServiceControl.Query() == ServiceState.Running;
if (restartNeeded)
{
ServiceControl.RunElevated(ServiceControl.StopVerb);
ServiceControl.RunElevated(ServiceControl.StartVerb);
// Restart OFF the UI thread — the old inline RunElevated stop+start pair here blocked the
// window (and its audio) through two UAC prompts; same bug class as the install hang.
// No-UAC first: the installer granted this account start/stop rights, so a plain SCM
// restart normally needs no elevation at all. Elevated verbs are the fallback (service
// installed by a different account, grant missing). Only a FAILURE is reported back;
// success needs no second popup.
ServiceStore.AppendServiceEvent("restart requested (service profile changed)");
Task.Run(() =>
{
var ok = ServiceControl.TryRestartNoAdmin();
if (!ok)
{
ServiceControl.RunElevated(ServiceControl.StopVerb);
ok = ServiceControl.RunElevated(ServiceControl.StartVerb) == 0;
}
ServiceStore.AppendServiceEvent(ok
? "restart finished (new service profile is live)"
: "restart FAILED after profile change");
if (ok || IsDisposed) return;
try
{
BeginInvoke(new Action(() => MessageBox.Show(this,
"The service profile was saved, but the running service could not be restarted to pick it up. Use the Service menu to stop and start it.",
AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning)));
}
catch { /* window closing */ }
});
}
MessageBox.Show(this, "Service profile saved.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
MessageBox.Show(this,
restartNeeded
? "Service profile saved. The running service is restarting to pick it up."
: "Service profile saved.",
AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{