Service: always-on update log + "View service update log" menu item
Local checkpoint - NOT for public release. Ed wants logging for the service updating.
- New ALWAYS-ON update log at ProgramData\RemSound\service\update.log (not gated on the
service-logging toggle - updates are rare but important). It records the full sequence:
* "update detected: newer RemSound.exe (5.3) found, running 5.2 - restarting"
* the restarter's own "stopping" / "service started" (or "START FAILED - <reason>")
* "update complete: now running version 5.3"
The restart PowerShell writes its stop/start outcome itself, so the part that runs AFTER
the old service process is gone - and any failed start - is still captured. A pending
marker set at detection and consumed on the next start closes the loop (its absence flags
a stuck update).
- Service menu gains "View service update log" to open it (friendly message if none yet).
Update log + pending-marker round-trip covered by the isolation self-test. Gate 27/27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d5767050f2
commit
4b2ecca960
@@ -2194,12 +2194,15 @@ public sealed class MainForm : Form
|
|||||||
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" };
|
||||||
stop.Click += (_, _) => ServiceAction(ServiceControl.StopVerb, "stop", confirm: false);
|
stop.Click += (_, _) => ServiceAction(ServiceControl.StopVerb, "stop", confirm: false);
|
||||||
|
var updateLog = new ToolStripMenuItem("View service update &log") { AccessibleName = "View service update log" };
|
||||||
|
updateLog.Click += (_, _) => OpenServiceUpdateLog();
|
||||||
|
|
||||||
serviceMenu.DropDownItems.AddRange(new ToolStripItem[]
|
serviceMenu.DropDownItems.AddRange(new ToolStripItem[]
|
||||||
{
|
{
|
||||||
status, new ToolStripSeparator(),
|
status, new ToolStripSeparator(),
|
||||||
configure, new ToolStripSeparator(),
|
configure, new ToolStripSeparator(),
|
||||||
install, uninstall, start, stop,
|
install, uninstall, start, stop, new ToolStripSeparator(),
|
||||||
|
updateLog,
|
||||||
});
|
});
|
||||||
serviceMenu.DropDownOpening += (_, _) =>
|
serviceMenu.DropDownOpening += (_, _) =>
|
||||||
{
|
{
|
||||||
@@ -2220,6 +2223,18 @@ public sealed class MainForm : Form
|
|||||||
return serviceMenu;
|
return serviceMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenServiceUpdateLog()
|
||||||
|
{
|
||||||
|
var path = ServiceStore.UpdateLogPath;
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "No service update log yet — it's written the first time the service updates itself.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); }
|
||||||
|
catch (Exception ex) { MessageBox.Show(this, $"Could not open the update log ({path}): {ex.Message}", AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning); }
|
||||||
|
}
|
||||||
|
|
||||||
private static string DescribeAgo(DateTime utc)
|
private static string DescribeAgo(DateTime utc)
|
||||||
{
|
{
|
||||||
var span = DateTime.UtcNow - utc;
|
var span = DateTime.UtcNow - utc;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ public sealed class RemSoundService : ServiceBase
|
|||||||
// Record the running version + start time so the app's Service menu can show them — this is how a
|
// Record the running version + start time so the app's Service menu can show them — this is how a
|
||||||
// self-update is visible (the version bumps and the start time is recent).
|
// self-update is visible (the version bumps and the start time is recent).
|
||||||
try { ServiceStore.SaveStatus(new ServiceStore.ServiceStatus { Version = versionText, StartedUtc = DateTime.UtcNow }); } catch { }
|
try { ServiceStore.SaveStatus(new ServiceStore.ServiceStatus { Version = versionText, StartedUtc = DateTime.UtcNow }); } catch { }
|
||||||
|
// If this start is the completion of a self-update restart, close the loop in the update log.
|
||||||
|
try { if (ServiceStore.ConsumeUpdatePending()) ServiceStore.AppendUpdateLog($"update complete: now running version {versionText}"); } catch { }
|
||||||
host = ServiceSendHost.FromConfig(msg => log?.Event(msg));
|
host = ServiceSendHost.FromConfig(msg => log?.Event(msg));
|
||||||
worker = new Thread(() =>
|
worker = new Thread(() =>
|
||||||
{
|
{
|
||||||
@@ -66,6 +68,11 @@ public sealed class RemSoundService : ServiceBase
|
|||||||
if (restartScheduled) return;
|
if (restartScheduled) return;
|
||||||
if (!ServiceUpdate.UpdateLanded()) return;
|
if (!ServiceUpdate.UpdateLanded()) return;
|
||||||
restartScheduled = true;
|
restartScheduled = true;
|
||||||
|
var running = ServiceUpdate.RunningVersion();
|
||||||
|
var runningText = running is null ? "?" : $"{running.Major}.{running.Minor}";
|
||||||
|
// Always-on update log (not gated on the service-logging toggle) — updates are rare + important.
|
||||||
|
ServiceStore.AppendUpdateLog($"update detected: newer RemSound.exe ({ServiceUpdate.OnDiskVersion()}) found, running {runningText} — restarting to update");
|
||||||
|
ServiceStore.SetUpdatePending();
|
||||||
log?.Event("service: a newer RemSound version was installed — restarting to update");
|
log?.Event("service: a newer RemSound version was installed — restarting to update");
|
||||||
ServiceUpdate.RestartSelf();
|
ServiceUpdate.RestartSelf();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -736,6 +736,14 @@ internal static class SelfTest
|
|||||||
// Running status (version + start time) round-trips — this is what the Service menu shows.
|
// Running status (version + start time) round-trips — this is what the Service menu shows.
|
||||||
ServiceStore.SaveStatus(new ServiceStore.ServiceStatus { Version = "5.3", StartedUtc = DateTime.UtcNow });
|
ServiceStore.SaveStatus(new ServiceStore.ServiceStatus { Version = "5.3", StartedUtc = DateTime.UtcNow });
|
||||||
Check(ServiceStore.LoadStatus()?.Version == "5.3", "the service running-status version must round-trip");
|
Check(ServiceStore.LoadStatus()?.Version == "5.3", "the service running-status version must round-trip");
|
||||||
|
|
||||||
|
// Update log + pending marker: the always-on trail of a self-update.
|
||||||
|
ServiceStore.AppendUpdateLog("update detected: test");
|
||||||
|
Check(File.Exists(ServiceStore.UpdateLogPath) && File.ReadAllText(ServiceStore.UpdateLogPath).Contains("update detected: test"),
|
||||||
|
"the update log must be written");
|
||||||
|
ServiceStore.SetUpdatePending();
|
||||||
|
Check(ServiceStore.ConsumeUpdatePending(), "a set update-pending marker must be consumed once");
|
||||||
|
Check(!ServiceStore.ConsumeUpdatePending(), "the update-pending marker must not be consumed twice");
|
||||||
}
|
}
|
||||||
finally { ServiceStore.TestDirectoryOverride = saved; }
|
finally { ServiceStore.TestDirectoryOverride = saved; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using RemSound.Core;
|
||||||
|
|
||||||
namespace RemSound.App;
|
namespace RemSound.App;
|
||||||
|
|
||||||
@@ -25,32 +26,46 @@ internal static class ServiceUpdate
|
|||||||
|
|
||||||
/// <summary>True when a strictly-newer RemSound.exe sits next to the running service binary (i.e. an
|
/// <summary>True when a strictly-newer RemSound.exe sits next to the running service binary (i.e. an
|
||||||
/// update landed). Reads the on-disk exe's file version; never throws.</summary>
|
/// update landed). Reads the on-disk exe's file version; never throws.</summary>
|
||||||
public static bool UpdateLanded()
|
public static bool UpdateLanded() => IsNewer(RunningVersion(), OnDiskVersion());
|
||||||
|
|
||||||
|
/// <summary>The version string of the RemSound.exe sitting next to the service, or null if unreadable.</summary>
|
||||||
|
public static string? OnDiskVersion()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var onDiskExe = Path.Combine(AppContext.BaseDirectory, "RemSound.exe");
|
var onDiskExe = Path.Combine(AppContext.BaseDirectory, "RemSound.exe");
|
||||||
if (!File.Exists(onDiskExe)) return false;
|
return File.Exists(onDiskExe) ? FileVersionInfo.GetVersionInfo(onDiskExe).FileVersion : null;
|
||||||
var running = Assembly.GetExecutingAssembly().GetName().Version;
|
|
||||||
var onDisk = FileVersionInfo.GetVersionInfo(onDiskExe).FileVersion;
|
|
||||||
return IsNewer(running, onDisk);
|
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Version? RunningVersion() => Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
|
||||||
/// <summary>Restart the service onto the new binary. Spawns a DETACHED PowerShell (as SYSTEM, inherited
|
/// <summary>Restart the service onto the new binary. Spawns a DETACHED PowerShell (as SYSTEM, inherited
|
||||||
/// from the service) that stops this service — which exits this process — then starts it again, so the
|
/// from the service) that stops this service — which exits this process — then starts it again, so the
|
||||||
/// SCM launches the freshly-installed exe. Never throws.</summary>
|
/// SCM launches the freshly-installed exe. The script LOGS its own stop/start outcome to the update log,
|
||||||
|
/// so even the part that runs after this process is gone (and any failed start) is recorded. Never
|
||||||
|
/// throws; worst case the service picks up the update on the next reboot.</summary>
|
||||||
public static void RestartSelf()
|
public static void RestartSelf()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var dir = ServiceStore.Directory;
|
||||||
|
System.IO.Directory.CreateDirectory(dir);
|
||||||
|
var script = Path.Combine(dir, "restart.ps1");
|
||||||
|
var log = ServiceStore.UpdateLogPath;
|
||||||
|
var svc = ServiceControl.ServiceName;
|
||||||
|
var content =
|
||||||
|
"$ts = { (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') }\r\n" +
|
||||||
|
$"Add-Content -LiteralPath '{log}' -Value \"$(& $ts) restarter: stopping {svc}\"\r\n" +
|
||||||
|
$"Stop-Service -Name {svc} -Force -ErrorAction SilentlyContinue\r\n" +
|
||||||
|
$"try {{ Start-Service -Name {svc} -ErrorAction Stop; $r = 'restart: service started' }} catch {{ $r = 'restart: START FAILED - ' + $_.Exception.Message }}\r\n" +
|
||||||
|
$"Add-Content -LiteralPath '{log}' -Value \"$(& $ts) $r\"\r\n";
|
||||||
|
File.WriteAllText(script, content);
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "powershell.exe",
|
FileName = "powershell.exe",
|
||||||
Arguments = "-NonInteractive -WindowStyle Hidden -Command " +
|
Arguments = $"-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"{script}\"",
|
||||||
$"\"Stop-Service -Name {ServiceControl.ServiceName} -Force -ErrorAction SilentlyContinue; " +
|
|
||||||
$"Start-Service -Name {ServiceControl.ServiceName} -ErrorAction SilentlyContinue\"",
|
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,5 +89,36 @@ public static class ServiceStore
|
|||||||
catch { return null; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Update log (ALWAYS written, not gated on the service-logging toggle) ===
|
||||||
|
// Updates are rare but important, so we always keep a small trail of them where the user can find it.
|
||||||
|
public static string UpdateLogPath => Path.Combine(Directory, "update.log");
|
||||||
|
private static string UpdatePendingPath => Path.Combine(Directory, "update-pending");
|
||||||
|
|
||||||
|
/// <summary>Append a timestamped line to the service update log. Never throws. Truncates if it ever
|
||||||
|
/// grows large (update events are infrequent, so it normally stays tiny).</summary>
|
||||||
|
public static void AppendUpdateLog(string line)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.Directory.CreateDirectory(Directory);
|
||||||
|
try { if (File.Exists(UpdateLogPath) && new FileInfo(UpdateLogPath).Length > 200_000) File.Delete(UpdateLogPath); } catch { }
|
||||||
|
File.AppendAllText(UpdateLogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {line}{Environment.NewLine}");
|
||||||
|
}
|
||||||
|
catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Marker dropped when a self-update restart is triggered; the next start consumes it and logs
|
||||||
|
/// completion — so the update log shows the update actually finished (and its absence flags a stuck one).</summary>
|
||||||
|
public static void SetUpdatePending()
|
||||||
|
{
|
||||||
|
try { System.IO.Directory.CreateDirectory(Directory); File.WriteAllText(UpdatePendingPath, ""); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ConsumeUpdatePending()
|
||||||
|
{
|
||||||
|
try { if (File.Exists(UpdatePendingPath)) { File.Delete(UpdatePendingPath); return true; } } catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class ServiceSettings { public bool LoggingEnabled { get; set; } }
|
private sealed class ServiceSettings { public bool LoggingEnabled { get; set; } }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user