From 5dfc4faec6ad91ac5d646a9d55c89568924cc738 Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:09:48 +0100 Subject: [PATCH] Service: remove the confusing Update-service menu item; make bin folder user-writable Ed: a menu item a user has to know when to click is the wrong answer -- real users get the service via the automatic self-update (version bump), full stop. Remove the menu item. For developer/tester same-version refreshes, honour "stop it and update it": install now grants Authenticated Users Modify on the service bin folder (icacls), so once the service is stopped its binaries can be replaced with no admin -- the exact workflow Ed asked for. Same trust posture as the auto-update copy (user-writable content run as SYSTEM); noted for a future code-signed hardening. The --update-service verb / DoUpdate stay as internal plumbing (no user-facing entry). Gate: 40/40. Co-Authored-By: Claude Opus 4.8 --- src/RemSound.App/MainForm.cs | 5 +---- src/RemSound.App/ServiceControl.cs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index 1300923..08c040e 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -2228,8 +2228,6 @@ public sealed class MainForm : Form install.Click += (_, _) => ServiceAction(ServiceControl.InstallVerb, "install", confirm: true); var uninstall = new ToolStripMenuItem("&Uninstall service") { AccessibleName = "Uninstall service" }; uninstall.Click += (_, _) => ServiceAction(ServiceControl.UninstallVerb, "uninstall", confirm: true); - var update = new ToolStripMenuItem("Up&date service to this version") { AccessibleName = "Update service to this version — copies the currently-running RemSound build into the service" }; - update.Click += (_, _) => ServiceAction(ServiceControl.UpdateVerb, "update", confirm: false); var start = new ToolStripMenuItem("S&tart service") { AccessibleName = "Start service" }; start.Click += (_, _) => ServiceAction(ServiceControl.StartVerb, "start", confirm: false); var stop = new ToolStripMenuItem("Sto&p service") { AccessibleName = "Stop service" }; @@ -2243,7 +2241,7 @@ public sealed class MainForm : Form { status, new ToolStripSeparator(), configure, new ToolStripSeparator(), - install, uninstall, update, start, stop, new ToolStripSeparator(), + install, uninstall, start, stop, new ToolStripSeparator(), activityLog, updateLog, }); serviceMenu.DropDownOpening += (_, _) => @@ -2263,7 +2261,6 @@ public sealed class MainForm : Form var installed = state != ServiceState.NotInstalled; install.Enabled = !installed; uninstall.Enabled = installed; - update.Enabled = installed; // refresh the service's copy with the running build (stop→copy→start) start.Enabled = installed && state is ServiceState.Stopped; stop.Enabled = installed && state is ServiceState.Running; } diff --git a/src/RemSound.App/ServiceControl.cs b/src/RemSound.App/ServiceControl.cs index 27a0e91..29e915a 100644 --- a/src/RemSound.App/ServiceControl.cs +++ b/src/RemSound.App/ServiceControl.cs @@ -120,9 +120,38 @@ public static class ServiceControl RunSc(BuildFailureArgs()); // Let a normal (non-admin) user start/stop it — otherwise stopping needs the app's UAC prompt. GrantUserStartStop(); + // Let a normal user REPLACE the binaries in the service's bin folder too (once the service is + // stopped), so a new build can be dropped in without admin — the auto-updater does this as SYSTEM, + // but it also makes "stop the service, copy the new files in, start it" work for a developer/tester + // with no UAC. (Trust note: a user-writable folder whose contents run as SYSTEM is the same posture + // as the auto-update copy; fine for this app, a hardened build would code-sign instead.) + GrantUsersWriteToBin(); return 0; } + /// Grant Authenticated Users Modify rights on the service's bin folder (via icacls), so a + /// stopped service's binaries can be refreshed without administrator rights. Best-effort. + private static void GrantUsersWriteToBin() + { + try + { + // *S-1-5-11 = Authenticated Users (locale-independent). (OI)(CI) = inherit to files+subfolders; M = Modify. + var psi = new ProcessStartInfo + { + FileName = "icacls.exe", + Arguments = $"\"{ServiceStore.BinDirectory}\" /grant \"*S-1-5-11:(OI)(CI)M\" /T /C /Q", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + using var p = Process.Start(psi); + p?.WaitForExit(20000); + if (p is { ExitCode: not 0 }) ServiceStore.AppendServiceEvent($"install: icacls grant-write on bin returned {p.ExitCode}"); + } + catch (Exception ex) { ServiceStore.AppendServiceEvent($"install: grant-write on bin failed: {ex.GetType().Name}: {ex.Message}"); } + } + /// Copies the program files from to , /// recursively, but NEVER the user-state folders (logs, profiles, config, recordings) — the service /// keeps its own state in ProgramData. Overwrites so a re-install refreshes the binaries.