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:
co-authored by
Claude Opus 4.8
parent
9e75331daf
commit
5dfc4faec6
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user