diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs
index 08c040e..1300923 100644
--- a/src/RemSound.App/MainForm.cs
+++ b/src/RemSound.App/MainForm.cs
@@ -2228,6 +2228,8 @@ 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" };
@@ -2241,7 +2243,7 @@ public sealed class MainForm : Form
{
status, new ToolStripSeparator(),
configure, new ToolStripSeparator(),
- install, uninstall, start, stop, new ToolStripSeparator(),
+ install, uninstall, update, start, stop, new ToolStripSeparator(),
activityLog, updateLog,
});
serviceMenu.DropDownOpening += (_, _) =>
@@ -2261,6 +2263,7 @@ 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/Program.cs b/src/RemSound.App/Program.cs
index 783d4df..7ed2234 100644
--- a/src/RemSound.App/Program.cs
+++ b/src/RemSound.App/Program.cs
@@ -22,8 +22,8 @@ internal static class Program
/// never reached.
internal static bool IsServiceInvocation(string[] args) =>
HasArg(args, ServiceControl.RunVerb) || HasArg(args, ServiceControl.InstallVerb)
- || HasArg(args, ServiceControl.UninstallVerb) || HasArg(args, ServiceControl.StartVerb)
- || HasArg(args, ServiceControl.StopVerb);
+ || HasArg(args, ServiceControl.UninstallVerb) || HasArg(args, ServiceControl.UpdateVerb)
+ || HasArg(args, ServiceControl.StartVerb) || HasArg(args, ServiceControl.StopVerb);
// Writes an otherwise-fatal exception to a timestamped crash file in the logs folder, so a
// "RemSound just disappeared, no dialog" report (#16) leaves a stack behind to diagnose instead
diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs
index 3aebc36..7ba7cdc 100644
--- a/src/RemSound.App/SelfTest.cs
+++ b/src/RemSound.App/SelfTest.cs
@@ -1399,7 +1399,7 @@ internal static class SelfTest
foreach (var verb in new[]
{
ServiceControl.RunVerb, ServiceControl.InstallVerb, ServiceControl.UninstallVerb,
- ServiceControl.StartVerb, ServiceControl.StopVerb,
+ ServiceControl.UpdateVerb, ServiceControl.StartVerb, ServiceControl.StopVerb,
})
{
Check(Program.IsServiceInvocation(new[] { verb }), $"'{verb}' must be recognised as a service invocation");
@@ -1417,7 +1417,7 @@ internal static class SelfTest
if (!loadedBefore)
Check(!IsAssemblyLoaded(svcAsm), "deciding a normal launch must not load the Windows-service assembly");
- return "normal launches stay load-safe; all five service verbs recognised (case-insensitive)";
+ return "normal launches stay load-safe; all six service verbs recognised (case-insensitive)";
}
/// The Service menu's "View service log" opens the newest diagnostic log — the log that says
diff --git a/src/RemSound.App/ServiceControl.cs b/src/RemSound.App/ServiceControl.cs
index 33fe470..86365ff 100644
--- a/src/RemSound.App/ServiceControl.cs
+++ b/src/RemSound.App/ServiceControl.cs
@@ -33,6 +33,7 @@ public static class ServiceControl
/// the Program.cs dispatcher agree.
public const string InstallVerb = "--install-service";
public const string UninstallVerb = "--uninstall-service";
+ public const string UpdateVerb = "--update-service";
public const string StartVerb = "--start-service";
public const string StopVerb = "--stop-service";
public const string RunVerb = "--run-service";
@@ -201,6 +202,29 @@ public static class ServiceControl
return rc;
}
+ /// Refreshes the service's OWN copy of the program with the CURRENTLY-running build, without
+ /// an uninstall/reinstall: stop → overwrite the binaries in
+ /// from this exe's folder → start. Must be run elevated (writing into the admin-only service bin
+ /// folder, and stop/start). Returns 0 on success. Used by the Service menu's "Update service" so a new
+ /// build reaches the service in one UAC prompt.
+ public static int DoUpdate()
+ {
+ if (!IsInstalled()) return DoInstall(); // not installed yet — a plain install does the copy too
+ var exe = Environment.ProcessPath;
+ var sourceDir = string.IsNullOrEmpty(exe) ? null : Path.GetDirectoryName(exe);
+ if (string.IsNullOrEmpty(sourceDir)) return 2;
+
+ try { DoStop(); } catch { /* best-effort — copy may still fail if files stay locked, handled below */ }
+ try { CopyProgramTo(sourceDir, ServiceStore.BinDirectory); }
+ catch (Exception ex)
+ {
+ ServiceStore.AppendServiceEvent($"update: copy program failed: {ex.GetType().Name}: {ex.Message}");
+ try { DoStart(); } catch { /* leave it stopped rather than half-updated */ }
+ return 5;
+ }
+ return DoStart();
+ }
+
/// Starts the service. Must be run elevated. Returns 0 on success.
public static int DoStart()
{
diff --git a/src/RemSound.App/ServiceEntry.cs b/src/RemSound.App/ServiceEntry.cs
index 6e4948a..53ee54e 100644
--- a/src/RemSound.App/ServiceEntry.cs
+++ b/src/RemSound.App/ServiceEntry.cs
@@ -36,6 +36,7 @@ internal static class ServiceEntry
var verb = Has(args, ServiceControl.InstallVerb) ? "install"
: Has(args, ServiceControl.UninstallVerb) ? "uninstall"
+ : Has(args, ServiceControl.UpdateVerb) ? "update"
: Has(args, ServiceControl.StartVerb) ? "start"
: Has(args, ServiceControl.StopVerb) ? "stop"
: null;
@@ -52,6 +53,7 @@ internal static class ServiceEntry
{
"install" => ServiceControl.DoInstall(),
"uninstall" => ServiceControl.DoUninstall(),
+ "update" => ServiceControl.DoUpdate(),
"start" => ServiceControl.DoStart(),
"stop" => ServiceControl.DoStop(),
_ => 0,