Service owns its binaries + is stoppable without admin; installer offers the service
The service was registered to run from wherever it was installed FROM (Environment .ProcessPath), so installing from a dev/test folder pinned it there: it locked those files (blocking every rebuild) and, for a real user installing from the app folder, would lock the app's own binaries and break the auto-updater. Stopping it also needed admin, so only the app's UAC-prompting Service menu could do it. Fixes: - The service now installs its OWN copy of the program into ProgramData\RemSound\ service\bin and is registered to run from there. Never touches the install/dev folder again. CopyProgramTo copies the exe + DLLs + runtimes + default sounds but excludes user-state folders; uninstall removes the bin copy. - Install grants Authenticated Users start/stop/query on the service (sc sdset, ACE merged into the existing DACL) so it can be stopped with a plain `sc stop RemSoundService` or the Service menu -- no admin, no app. Pure SDDL-amend helper is unit-tested (placement + idempotency). - The app installer now asks, after install, whether to also install the service (optional, its own UAC step; declining is fine -- Service menu installs it later). - deploy-test.ps1: only a copy running FROM the publish folder locks its binaries, so only that forces a sounds-only deploy. The service (ProgramData) and an installed app no longer make the script silently skip the binary publish -- the bug that had us testing stale builds for rounds. New self-test "Service self-contained install" (runs-from-own-bin, SDDL amend, copy exclusions). Gate: 40/40. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a450cae66a
commit
4e15451e1d
@@ -73,6 +73,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Remembered applications list is global + clearable", RememberedApplicationsGlobal);
|
||||
RunStep(results, "Send-app lists semantics (ticked → Active, out of Remembered)", SendAppListSemantics);
|
||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||
RunStep(results, "Service self-contained install (own bin + user stop rights)", ServiceSelfContainedInstall);
|
||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||
RunStep(results, "Recording split tracks (per-peer + own)", RecordingSplitTracks);
|
||||
RunStep(results, "Recording churn / soak", RecordingChurn);
|
||||
@@ -742,6 +743,61 @@ internal static class SelfTest
|
||||
return "sc create + failure args well-formed; self-update comparison loop-safe";
|
||||
}
|
||||
|
||||
/// <summary>The service installs and runs from its OWN copy of the program under ProgramData, never the
|
||||
/// folder it was installed from — so it can't lock the app's install folder / a dev working copy or
|
||||
/// block the auto-updater. And it grants authenticated users start/stop so it's stoppable without admin.
|
||||
/// Tests the pure pieces: the run-from path, the SDDL amendment, and the program-copy exclusions.</summary>
|
||||
private static string? ServiceSelfContainedInstall()
|
||||
{
|
||||
// 1. The service runs from ProgramData\RemSound\service\bin\RemSound.exe, and BuildCreateArgs points there.
|
||||
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
Check(ServiceStore.BinExePath.StartsWith(programData, StringComparison.OrdinalIgnoreCase)
|
||||
&& ServiceStore.BinExePath.EndsWith(@"\bin\RemSound.exe", StringComparison.OrdinalIgnoreCase),
|
||||
$"the service must run from its own ProgramData bin copy (got {ServiceStore.BinExePath})");
|
||||
var createArgs = ServiceControl.BuildCreateArgs(ServiceStore.BinExePath);
|
||||
Check(createArgs.Contains("\\\"" + ServiceStore.BinExePath + "\\\" " + ServiceControl.RunVerb),
|
||||
"the create command must register the ProgramData bin exe as the service binary");
|
||||
|
||||
// 2. AddUserStartStopAce inserts the AU start/stop ACE into the DACL, ahead of the SACL, and is idempotent.
|
||||
const string sample = "D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCLCSWLOCRRC;;;IU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)";
|
||||
var amended = ServiceControl.AddUserStartStopAce(sample);
|
||||
Check(amended is not null && amended.Contains(ServiceControl.UserStartStopAce), "the AU start/stop ACE must be added");
|
||||
Check(amended!.IndexOf(ServiceControl.UserStartStopAce, StringComparison.Ordinal) < amended.IndexOf("S:", StringComparison.Ordinal),
|
||||
"the ACE must sit inside the DACL, before the SACL");
|
||||
Check(amended.StartsWith("D:", StringComparison.Ordinal), "the result must still be a valid DACL-first SDDL");
|
||||
Check(ServiceControl.AddUserStartStopAce(amended) == amended, "adding the ACE twice must be a no-op (idempotent)");
|
||||
Check(ServiceControl.AddUserStartStopAce("garbage") is null, "a non-DACL SDDL must be rejected");
|
||||
|
||||
// 3. CopyProgramTo copies program files but NEVER the user-state folders.
|
||||
var root = Path.Combine(Path.GetTempPath(), "remsound-svccopy-" + Guid.NewGuid().ToString("N"));
|
||||
var src = Path.Combine(root, "src");
|
||||
var dst = Path.Combine(root, "dst");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(src, "runtimes", "win-x64", "native"));
|
||||
Directory.CreateDirectory(Path.Combine(src, "default sounds"));
|
||||
Directory.CreateDirectory(Path.Combine(src, "user settings and logs", "logs"));
|
||||
Directory.CreateDirectory(Path.Combine(src, "logs"));
|
||||
File.WriteAllText(Path.Combine(src, "RemSound.exe"), "exe");
|
||||
File.WriteAllText(Path.Combine(src, "RemSound.Sender.dll"), "dll");
|
||||
File.WriteAllText(Path.Combine(src, "runtimes", "win-x64", "native", "opus.dll"), "opus");
|
||||
File.WriteAllText(Path.Combine(src, "default sounds", "connect.wav"), "wav");
|
||||
File.WriteAllText(Path.Combine(src, "user settings and logs", "logs", "secret.log"), "log");
|
||||
File.WriteAllText(Path.Combine(src, "logs", "stray.log"), "log");
|
||||
|
||||
ServiceControl.CopyProgramTo(src, dst);
|
||||
|
||||
Check(File.Exists(Path.Combine(dst, "RemSound.exe")), "the exe must be copied");
|
||||
Check(File.Exists(Path.Combine(dst, "RemSound.Sender.dll")), "sibling DLLs must be copied");
|
||||
Check(File.Exists(Path.Combine(dst, "runtimes", "win-x64", "native", "opus.dll")), "native runtimes must be copied");
|
||||
Check(File.Exists(Path.Combine(dst, "default sounds", "connect.wav")), "bundled default sounds must be copied");
|
||||
Check(!Directory.Exists(Path.Combine(dst, "user settings and logs")), "user settings/logs must NOT be copied");
|
||||
Check(!Directory.Exists(Path.Combine(dst, "logs")), "stray logs folder must NOT be copied");
|
||||
return "runs from own ProgramData bin; AU start/stop ACE added idempotently; program copy excludes user state";
|
||||
}
|
||||
finally { try { Directory.Delete(root, recursive: true); } catch { /* temp */ } }
|
||||
}
|
||||
|
||||
/// <summary>The service profile is fully isolated from the normal profile machinery: it lives in a
|
||||
/// MACHINE-WIDE ProgramData location (readable by the SYSTEM service, outside the user's profiles
|
||||
/// folder), and the reserved title never shows up in the profile listing that backs the startup
|
||||
|
||||
Reference in New Issue
Block a user