Tests: opt-in real Windows-service lifecycle (install/start/stop/uninstall)
Local checkpoint - NOT for public release. New self-test "Service real lifecycle" drives the ACTUAL SCM end to end: install -> report installed -> start -> report running -> stop -> report stopped -> uninstall -> report gone. Needs admin, so it's OPT-IN via REMSOUND_TEST_SERVICE=1 and skips cleanly in the normal unprivileged gate. It clears any leftover registration first (DoUninstall stops a running copy, so a stray from an aborted run can't fail it) and always removes the service afterwards, even on failure - so it never leaves a service pointing at a throwaway exe. Run elevated: set REMSOUND_TEST_SERVICE=1 then RemSound.exe --selftest. The app-yield takeover is already covered headlessly by "Service send host (stream + yield)"; a full real end-to-end (installed service goes quiet when the app opens) needs a live peer to observe and stays a manual check. Gate 26 passed, 1 skipped of 27. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ce45489b30
commit
4ee9cf6907
@@ -66,6 +66,7 @@ internal static class SelfTest
|
|||||||
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
||||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||||
|
RunStep(results, "Service real lifecycle (install/start/stop/uninstall)", ServiceRealLifecycle);
|
||||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||||
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
||||||
RunStep(results, "Recording churn / soak", RecordingChurn);
|
RunStep(results, "Recording churn / soak", RecordingChurn);
|
||||||
@@ -661,6 +662,59 @@ internal static class SelfTest
|
|||||||
return File.Exists(path) ? new FileInfo(path).Length : 0;
|
return File.Exists(path) ? new FileInfo(path).Length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The REAL Windows-service lifecycle end to end: install → started → stopped → uninstalled,
|
||||||
|
/// driving the actual SCM (sc.exe + ServiceController). Needs administrator rights, so it's OPT-IN via
|
||||||
|
/// REMSOUND_TEST_SERVICE=1 and skips cleanly otherwise — the normal gate runs unprivileged. It clears
|
||||||
|
/// any leftover registration first (so a stray copy from an aborted run can't fail it) and always
|
||||||
|
/// removes the service afterwards, even on failure. Run it elevated:
|
||||||
|
/// set REMSOUND_TEST_SERVICE=1 && RemSound.exe --selftest (from an elevated prompt)</summary>
|
||||||
|
private static string? ServiceRealLifecycle()
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("REMSOUND_TEST_SERVICE"), "1", StringComparison.Ordinal))
|
||||||
|
return Skip("set REMSOUND_TEST_SERVICE=1 (elevated) to run the real install/start/stop/uninstall lifecycle");
|
||||||
|
if (!IsAdministrator())
|
||||||
|
return Skip("the real service lifecycle needs administrator rights");
|
||||||
|
|
||||||
|
// Start from a known state — remove any service left over from a previous aborted run. DoUninstall
|
||||||
|
// stops it first, so this also clears a leftover RUNNING copy.
|
||||||
|
if (ServiceControl.IsInstalled()) ServiceControl.DoUninstall();
|
||||||
|
Check(!ServiceControl.IsInstalled(), "pre-clean: no leftover RemSound service should remain");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Check(ServiceControl.DoInstall() == 0, "DoInstall should succeed");
|
||||||
|
Check(ServiceControl.IsInstalled(), "the service should report installed after DoInstall");
|
||||||
|
Check(ServiceControl.Query() == ServiceState.Stopped, "an auto-start service is Stopped until started");
|
||||||
|
|
||||||
|
Check(ServiceControl.DoStart() == 0, "DoStart should succeed");
|
||||||
|
Check(ServiceControl.Query() == ServiceState.Running, "the service should be Running after DoStart");
|
||||||
|
|
||||||
|
Check(ServiceControl.DoStop() == 0, "DoStop should succeed");
|
||||||
|
Check(ServiceControl.Query() == ServiceState.Stopped, "the service should be Stopped after DoStop");
|
||||||
|
|
||||||
|
Check(ServiceControl.DoUninstall() == 0, "DoUninstall should succeed");
|
||||||
|
Check(!ServiceControl.IsInstalled(), "the service should be gone after DoUninstall");
|
||||||
|
|
||||||
|
return "installed → started → stopped → uninstalled (real SCM)";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Safety net: never leave a service registered (its binPath would point at a throwaway exe).
|
||||||
|
try { if (ServiceControl.IsInstalled()) ServiceControl.DoUninstall(); } catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdministrator()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var id = System.Security.Principal.WindowsIdentity.GetCurrent();
|
||||||
|
return new System.Security.Principal.WindowsPrincipal(id)
|
||||||
|
.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The sc.exe "create" argument string quotes a spaced exe path correctly — a real footgun
|
/// <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
|
/// (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>
|
/// never touches the SCM or needs admin.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user