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
+66
View File
@@ -0,0 +1,66 @@
using System.ServiceProcess;
using RemSound.Core;
namespace RemSound.App;
/// <summary>
/// The RemSound Windows service (send-only lock-screen streaming). Hosts <see cref="ServiceSendHost"/>
/// on a background thread; the host's own RunLoop yields to the interactive app via
/// <see cref="InteractivePresence"/>. Started by the SCM when Program.cs is launched with
/// <see cref="ServiceControl.RunVerb"/>.
/// </summary>
public sealed class RemSoundService : ServiceBase
{
private readonly CancellationTokenSource cts = new();
private Thread? worker;
private ServiceSendHost? host;
private RemSoundLog? log;
public RemSoundService()
{
ServiceName = ServiceControl.ServiceName;
CanStop = true;
CanShutdown = true;
CanPauseAndContinue = false;
}
protected override void OnStart(string[] args)
{
log = new RemSoundLog { Enabled = SafeServiceLogging() };
log.Event("service: OnStart");
host = ServiceSendHost.FromConfig(msg => log?.Event(msg));
worker = new Thread(() =>
{
try { host.RunLoop(cts.Token); }
catch (Exception ex) { log?.Event($"service: run loop crashed {ex.GetType().Name}: {ex.Message}"); }
})
{ IsBackground = true, Name = "remsound-service" };
worker.Start();
}
protected override void OnStop()
{
log?.Event("service: OnStop");
try { cts.Cancel(); } catch { }
try { worker?.Join(5000); } catch { }
try { host?.Dispose(); } catch { }
}
protected override void OnShutdown() => OnStop();
private static bool SafeServiceLogging()
{
try { return AppConfig.Load().ServiceLoggingEnabled; }
catch { return false; }
}
protected override void Dispose(bool disposing)
{
if (disposing) { try { cts.Dispose(); } catch { } }
base.Dispose(disposing);
}
/// <summary>Blocks in the SCM dispatcher until the service is stopped. Called from Program.cs
/// when launched with the run verb.</summary>
public static void RunAsService() => ServiceBase.Run(new RemSoundService());
}