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.