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 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-17 15:09:48 +01:00
co-authored by Claude Opus 4.8
parent 9e75331daf
commit 5dfc4faec6
2 changed files with 30 additions and 4 deletions
+1 -4
View File
@@ -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;
}
+29
View File
@@ -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;
}
/// <summary>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.</summary>
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}"); }
}
/// <summary>Copies the program files from <paramref name="sourceDir"/> to <paramref name="destDir"/>,
/// 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.</summary>