Service: one-click Update-service-to-this-version (no uninstall/reinstall)
Because the service now runs from its own admin-only copy, getting a new build into it meant uninstall+reinstall (two UAC prompts). Add a DoUpdate path + Service-menu item that does it in one: stop -> overwrite the ProgramData bin copy from the running app -> start. Wired as the --update-service verb (elevated), recognised by the service-verb gate. Enabled only when the service is installed; falls back to a plain install if not. Note: the service bin folder stays admin-only writable on purpose (a user-writable SYSTEM binary would be a privilege-escalation hole), so the copy is done elevated via the one UAC prompt rather than in-process. 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
b8d0fa5a65
commit
fa554cbe35
@@ -2228,6 +2228,8 @@ public sealed class MainForm : Form
|
|||||||
install.Click += (_, _) => ServiceAction(ServiceControl.InstallVerb, "install", confirm: true);
|
install.Click += (_, _) => ServiceAction(ServiceControl.InstallVerb, "install", confirm: true);
|
||||||
var uninstall = new ToolStripMenuItem("&Uninstall service") { AccessibleName = "Uninstall service" };
|
var uninstall = new ToolStripMenuItem("&Uninstall service") { AccessibleName = "Uninstall service" };
|
||||||
uninstall.Click += (_, _) => ServiceAction(ServiceControl.UninstallVerb, "uninstall", confirm: true);
|
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" };
|
var start = new ToolStripMenuItem("S&tart service") { AccessibleName = "Start service" };
|
||||||
start.Click += (_, _) => ServiceAction(ServiceControl.StartVerb, "start", confirm: false);
|
start.Click += (_, _) => ServiceAction(ServiceControl.StartVerb, "start", confirm: false);
|
||||||
var stop = new ToolStripMenuItem("Sto&p service") { AccessibleName = "Stop service" };
|
var stop = new ToolStripMenuItem("Sto&p service") { AccessibleName = "Stop service" };
|
||||||
@@ -2241,7 +2243,7 @@ public sealed class MainForm : Form
|
|||||||
{
|
{
|
||||||
status, new ToolStripSeparator(),
|
status, new ToolStripSeparator(),
|
||||||
configure, new ToolStripSeparator(),
|
configure, new ToolStripSeparator(),
|
||||||
install, uninstall, start, stop, new ToolStripSeparator(),
|
install, uninstall, update, start, stop, new ToolStripSeparator(),
|
||||||
activityLog, updateLog,
|
activityLog, updateLog,
|
||||||
});
|
});
|
||||||
serviceMenu.DropDownOpening += (_, _) =>
|
serviceMenu.DropDownOpening += (_, _) =>
|
||||||
@@ -2261,6 +2263,7 @@ public sealed class MainForm : Form
|
|||||||
var installed = state != ServiceState.NotInstalled;
|
var installed = state != ServiceState.NotInstalled;
|
||||||
install.Enabled = !installed;
|
install.Enabled = !installed;
|
||||||
uninstall.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;
|
start.Enabled = installed && state is ServiceState.Stopped;
|
||||||
stop.Enabled = installed && state is ServiceState.Running;
|
stop.Enabled = installed && state is ServiceState.Running;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ internal static class Program
|
|||||||
/// never reached.</summary>
|
/// never reached.</summary>
|
||||||
internal static bool IsServiceInvocation(string[] args) =>
|
internal static bool IsServiceInvocation(string[] args) =>
|
||||||
HasArg(args, ServiceControl.RunVerb) || HasArg(args, ServiceControl.InstallVerb)
|
HasArg(args, ServiceControl.RunVerb) || HasArg(args, ServiceControl.InstallVerb)
|
||||||
|| HasArg(args, ServiceControl.UninstallVerb) || HasArg(args, ServiceControl.StartVerb)
|
|| HasArg(args, ServiceControl.UninstallVerb) || HasArg(args, ServiceControl.UpdateVerb)
|
||||||
|| HasArg(args, ServiceControl.StopVerb);
|
|| HasArg(args, ServiceControl.StartVerb) || HasArg(args, ServiceControl.StopVerb);
|
||||||
|
|
||||||
// Writes an otherwise-fatal exception to a timestamped crash file in the logs folder, so a
|
// 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
|
// "RemSound just disappeared, no dialog" report (#16) leaves a stack behind to diagnose instead
|
||||||
|
|||||||
@@ -1399,7 +1399,7 @@ internal static class SelfTest
|
|||||||
foreach (var verb in new[]
|
foreach (var verb in new[]
|
||||||
{
|
{
|
||||||
ServiceControl.RunVerb, ServiceControl.InstallVerb, ServiceControl.UninstallVerb,
|
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");
|
Check(Program.IsServiceInvocation(new[] { verb }), $"'{verb}' must be recognised as a service invocation");
|
||||||
@@ -1417,7 +1417,7 @@ internal static class SelfTest
|
|||||||
if (!loadedBefore)
|
if (!loadedBefore)
|
||||||
Check(!IsAssemblyLoaded(svcAsm), "deciding a normal launch must not load the Windows-service assembly");
|
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)";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The Service menu's "View service log" opens the newest diagnostic log — the log that says
|
/// <summary>The Service menu's "View service log" opens the newest diagnostic log — the log that says
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public static class ServiceControl
|
|||||||
/// the Program.cs dispatcher agree.</summary>
|
/// the Program.cs dispatcher agree.</summary>
|
||||||
public const string InstallVerb = "--install-service";
|
public const string InstallVerb = "--install-service";
|
||||||
public const string UninstallVerb = "--uninstall-service";
|
public const string UninstallVerb = "--uninstall-service";
|
||||||
|
public const string UpdateVerb = "--update-service";
|
||||||
public const string StartVerb = "--start-service";
|
public const string StartVerb = "--start-service";
|
||||||
public const string StopVerb = "--stop-service";
|
public const string StopVerb = "--stop-service";
|
||||||
public const string RunVerb = "--run-service";
|
public const string RunVerb = "--run-service";
|
||||||
@@ -201,6 +202,29 @@ public static class ServiceControl
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Refreshes the service's OWN copy of the program with the CURRENTLY-running build, without
|
||||||
|
/// an uninstall/reinstall: stop → overwrite the binaries in <see cref="ServiceStore.BinDirectory"/>
|
||||||
|
/// 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.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Starts the service. Must be run elevated. Returns 0 on success.</summary>
|
/// <summary>Starts the service. Must be run elevated. Returns 0 on success.</summary>
|
||||||
public static int DoStart()
|
public static int DoStart()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ internal static class ServiceEntry
|
|||||||
|
|
||||||
var verb = Has(args, ServiceControl.InstallVerb) ? "install"
|
var verb = Has(args, ServiceControl.InstallVerb) ? "install"
|
||||||
: Has(args, ServiceControl.UninstallVerb) ? "uninstall"
|
: Has(args, ServiceControl.UninstallVerb) ? "uninstall"
|
||||||
|
: Has(args, ServiceControl.UpdateVerb) ? "update"
|
||||||
: Has(args, ServiceControl.StartVerb) ? "start"
|
: Has(args, ServiceControl.StartVerb) ? "start"
|
||||||
: Has(args, ServiceControl.StopVerb) ? "stop"
|
: Has(args, ServiceControl.StopVerb) ? "stop"
|
||||||
: null;
|
: null;
|
||||||
@@ -52,6 +53,7 @@ internal static class ServiceEntry
|
|||||||
{
|
{
|
||||||
"install" => ServiceControl.DoInstall(),
|
"install" => ServiceControl.DoInstall(),
|
||||||
"uninstall" => ServiceControl.DoUninstall(),
|
"uninstall" => ServiceControl.DoUninstall(),
|
||||||
|
"update" => ServiceControl.DoUpdate(),
|
||||||
"start" => ServiceControl.DoStart(),
|
"start" => ServiceControl.DoStart(),
|
||||||
"stop" => ServiceControl.DoStop(),
|
"stop" => ServiceControl.DoStop(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user