Service auto-updates itself when the app updates (no UAC, no menu click)

The self-contained-service change broke the old self-update: the service used to run in
the app's own folder and restart onto a newer RemSound.exe that landed "next to it";
now it runs from its own ProgramData copy, so it never saw the app's new build.

Rework: at install/update the service records the app's folder (ServiceStore app-source
path, written elevated so the SYSTEM account can read it). The service's existing 45s
update poll now watches THAT folder; when the app's auto-updater drops a strictly-newer
RemSound.exe there, the service (as SYSTEM) copies the new build into its own bin and
restarts onto it via the detached restarter script. All SYSTEM-side: no UAC, no user
action. Loop-safe (strictly-newer only; bin == app version after the copy).

So a real release (version bump) propagates to the service automatically. Same-version
dev rebuilds don't trip the strictly-newer check -- the Service menu "Update service to
this version" forces those.

Trust posture unchanged from the old in-place scheme (SYSTEM copies from a user-writable
folder); noted in the class doc for a future code-signed hardening.

New self-test coverage: app-source path round-trip + "no readable app version => no
update" (never act on uncertainty). Gate: 40/40.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-17 15:03:37 +01:00
co-authored by Claude Opus 4.8
parent fa554cbe35
commit 9e75331daf
4 changed files with 71 additions and 10 deletions
+16
View File
@@ -768,6 +768,22 @@ internal static class SelfTest
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");
// 2b. The app-source path (which the SYSTEM service watches for auto-updates) round-trips, and drives
// the update check: unknown/empty source => no update, so the service never acts on uncertainty.
var savedOverride = ServiceStore.TestDirectoryOverride;
var storeTmp = Path.Combine(Path.GetTempPath(), "remsound-appsrc-" + Guid.NewGuid().ToString("N"));
try
{
ServiceStore.TestDirectoryOverride = storeTmp;
Check(ServiceStore.LoadAppSourcePath() is null, "no app-source recorded yet must read back null");
ServiceStore.SaveAppSourcePath(@"C:\Some\App\Folder");
Check(ServiceStore.LoadAppSourcePath() == @"C:\Some\App\Folder", "the app-source path must round-trip");
// Points at a folder with no RemSound.exe => version unreadable => no update landed (never act on uncertainty).
Check(ServiceUpdate.OnDiskVersion() is null, "an app-source folder with no RemSound.exe must yield no version");
Check(!ServiceUpdate.UpdateLanded(), "with no readable app version, no update must be detected");
}
finally { ServiceStore.TestDirectoryOverride = savedOverride; try { Directory.Delete(storeTmp, recursive: true); } catch { } }
// 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");