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
+41 -2
View File
@@ -186,7 +186,7 @@ internal static class AppInstaller
if (wantService == DialogResult.Yes)
{
log?.Invoke("install: user opted to install the service too");
var rc = ServiceControl.RunElevated(ServiceControl.InstallVerb);
var rc = RunElevatedResponsive(owner, ServiceControl.InstallVerb, "Installing the RemSound service...");
if (rc == 0)
{
// Offer to start it now — otherwise it only comes up at the next boot, so a
@@ -199,7 +199,7 @@ internal static class AppInstaller
if (startNow == DialogResult.Yes)
{
log?.Invoke("install: user opted to start the service now");
var startRc = ServiceControl.RunElevated(ServiceControl.StartVerb);
var startRc = RunElevatedResponsive(owner, ServiceControl.StartVerb, "Starting the RemSound service...");
MessageBox.Show(owner,
startRc == 0
? "The RemSound service is running."
@@ -226,6 +226,45 @@ internal static class AppInstaller
Environment.Exit(0);
}
/// <summary>Run an elevated service verb while keeping the UI thread ALIVE. The install flow is
/// sequential (offer → install → offer start → start → relaunch), so a fire-and-forget task doesn't
/// fit — but blocking the UI thread in WaitForExit froze the window and any live audio (the same
/// hang class as the service install-freeze). This runs the elevated helper on a worker while a tiny
/// modal "working…" shell pumps messages: the app stays responsive, the user can't double-trigger
/// anything, NVDA announces what's happening, and the call still returns the exit code in-line.</summary>
private static int RunElevatedResponsive(IWin32Window owner, string verb, string statusText)
{
var rc = -1;
using var wait = new Form
{
Text = "RemSound",
FormBorderStyle = FormBorderStyle.FixedDialog,
StartPosition = FormStartPosition.CenterParent,
MinimizeBox = false,
MaximizeBox = false,
ControlBox = false,
ShowInTaskbar = false,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Padding = new Padding(24),
AccessibleName = statusText,
};
wait.Controls.Add(new Label
{
Text = statusText + Environment.NewLine + "Windows may ask for administrator permission.",
AutoSize = true,
AccessibleName = statusText,
});
wait.Shown += (_, _) => Task.Run(() =>
{
try { rc = ServiceControl.RunElevated(verb); }
catch { rc = -1; }
try { wait.BeginInvoke(new Action(wait.Close)); } catch { /* already closed */ }
});
wait.ShowDialog(owner);
return rc;
}
// ---------------- uninstall ----------------
/// <summary>Options → "Uninstall RemSound from this PC", chosen from inside the running installed