Service: --run-service host + registration CLI (install/uninstall/start/stop)

Local checkpoint - NOT for public release.

- RemSoundService (ServiceBase): hosts ServiceSendHost.RunLoop on a worker thread,
  OnStop cancels + joins. Added System.ServiceProcess.ServiceController package.
- ServiceControl: install (sc.exe create, auto-start, careful binPath quoting) /
  uninstall (stop + delete) / start / stop / status. Status query is unprivileged
  (menu can poll it); the mutating verbs self-elevate via ShellExecute runas.
- Program.cs: early guards for --run-service (blocks in the SCM dispatcher) and the
  one-shot elevated verbs, before the single-instance lock (the service is a
  separate role and must never take the interactive lock).
- Self-test "Service registration args" verifies the sc create binPath quoting
  survives a spaced exe path. Gate 20/20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-12 15:19:01 +01:00
co-authored by Claude Opus 4.8
parent e648bee531
commit 5dbbe4dd71
5 changed files with 270 additions and 0 deletions
+18
View File
@@ -64,6 +64,7 @@ internal static class SelfTest
RunStep(results, "Lifecycle churn (modes, sources, pan/EQ, send/receive)", LifecycleChurn);
RunStep(results, "Service app-yield token", ServiceInteractivePresence);
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
RunStep(results, "Service registration args", ServiceRegistrationArgs);
RunStep(results, "v5 settings and shaping round-trip", V5ConfigRoundTrip);
RunStep(results, "Profile save and reload", ProfileRoundTrip);
RunStep(results, "What's-new update marker", WhatsNewMarkerRoundTrip);
@@ -457,6 +458,23 @@ internal static class SelfTest
catch { return 0; }
}
/// <summary>The sc.exe "create" argument string quotes a spaced exe path correctly — a real footgun
/// (a broken binPath silently installs a service that can't start). Pure/side-effect-free, so it
/// never touches the SCM or needs admin.</summary>
private static string? ServiceRegistrationArgs()
{
const string exe = @"C:\Program Files\RemSound\RemSound.exe";
var args = ServiceControl.BuildCreateArgs(exe);
Check(args.StartsWith($"create {ServiceControl.ServiceName} "), "must be a create for the named service");
Check(args.Contains("start= auto"), "service must be auto-start");
// The exe path must be wrapped in ESCAPED quotes inside the binPath value, followed by the run
// verb, so a path with spaces survives sc.exe's parsing.
Check(args.Contains("\\\"" + exe + "\\\" " + ServiceControl.RunVerb),
$"exe path must be escaped-quoted with the run verb (got: {args})");
Check(args.Contains($"DisplayName= \"{ServiceControl.DisplayName}\""), "must set the display name");
return "sc create args quoted correctly for a spaced path";
}
/// <summary>The lock-screen service's app-yield token: while a hold is active the service must see an
/// interactive app present; once released (or on crash — the OS frees the mutex) it must see none.
/// Uses a unique token name so the test is immune to a real RemSound running alongside the gate.</summary>