Fix the service install hang (pipe deadlock) + stop it from ever freezing the app

Ed uninstalled then reinstalled the service from the app and the app "kind of crashed":
its audio froze while the connection stayed alive. The logs told the whole story - the
app logged "install requested" then never "install finished"; the elevated helper
(child of the app) was still running 40+ minutes later, and the app sat blocked on it,
UI thread frozen, so streaming died but the background heartbeat kept ticking.

Root cause - a classic pipe deadlock in the elevated installer. GrantUsersWriteToBin ran
`icacls /T` over the service bin (100+ files) and read STDERR to end, THEN stdout. icacls
floods stdout far past the ~4 KB pipe buffer, so it blocked writing stdout while we blocked
reading stderr - forever. That hung DoInstall, which hung the app waiting on it.

Fixes (root cause + defence in depth, so a stuck helper can never freeze the app again):
- RunProcessCaptured: one safe process runner that drains stdout AND stderr concurrently
  (async), bounded by a timeout, and kills the child (whole tree) if it overruns. RunSc,
  RunScCapture and GrantUsersWriteToBin all go through it now. This kills the deadlock.
- RunElevated now waits with a 120s cap and returns ElevatedTimedOut instead of blocking
  forever.
- ServiceAction runs the elevated helper OFF the UI thread and reports the result back, so
  even a slow/stuck helper can't stall the window or its audio. New "timed out" message.
- Program.cs Environment.Exit()s after a one-shot service verb, so a helper that finished
  its work can never linger (non-background thread) with the app waiting on it.

Test: "Elevated helper: no pipe deadlock on flooded output" - RunProcessCaptured against a
child that floods both pipes with ~260 KB (a big dir listing + a failing dir); it must
return promptly with the full output. The pre-fix order would have hung. Gate 46/46.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-17 23:56:26 +01:00
co-authored by Claude Opus 4.8
parent 5fc1b5bd75
commit aa5707f8e1
4 changed files with 97 additions and 53 deletions
+20 -2
View File
@@ -2380,14 +2380,32 @@ public sealed class MainForm : Form
}
logFile.Event($"service: {label} requested (elevated)");
ServiceStore.AppendServiceEvent($"{label} requested (elevated)");
var rc = ServiceControl.RunElevated(verb);
var outcome = rc == 0 ? "success" : rc == -1 ? "cancelled/declined" : "failed";
// Run the elevated helper OFF the UI thread. A slow or stuck helper must never freeze the window or
// its audio — that was the install hang: the UI thread sat in WaitForExit while the pipe-deadlocked
// installer never returned, so the app locked up and streaming died. The UI stays live; we report
// the outcome (on the UI thread) when the helper comes back or the wait times out.
Task.Run(() =>
{
var rc = ServiceControl.RunElevated(verb);
if (IsDisposed) return;
try { BeginInvoke(new Action(() => ReportServiceActionResult(label, rc))); } catch { /* window closing */ }
});
}
private void ReportServiceActionResult(string label, int rc)
{
var outcome = rc == 0 ? "success"
: rc == -1 ? "cancelled/declined"
: rc == ServiceControl.ElevatedTimedOut ? "timed out"
: "failed";
logFile.Event($"service: {label} finished with code {rc} ({outcome})");
ServiceStore.AppendServiceEvent($"{label} finished: code {rc} ({outcome})");
if (rc == 0)
MessageBox.Show(this, $"Service {label} succeeded.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
else if (rc == -1)
MessageBox.Show(this, $"Service {label} was cancelled, or administrator rights were declined.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
else if (rc == ServiceControl.ElevatedTimedOut)
MessageBox.Show(this, $"Service {label} is taking longer than expected and hasn't finished yet. It may still complete on its own — check the Service menu status in a moment.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
else
MessageBox.Show(this, $"Service {label} failed (code {rc}).", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
}