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.