Always-on service events log + show failure reasons on screen
So that if the service breaks on a machine where nobody enabled logging first (e.g. a Win7 tester), we still learn WHY: - New always-on ServiceStore.AppendServiceEvent -> service-events.log in ProgramData, independent of any logging toggle (like the update log). - Service menu status-query failure now shows the actual reason IN the status line (a screen reader reads it) AND records it to the events log, instead of a bare 'status unavailable' whose reason only went to the off-by-default app log. - ServiceAction (install/uninstall/start/stop) records request + outcome to the events log. - ServiceEntry (the elevated child) logs each verb's start, and CATCHES a System.ServiceProcess load failure to record its reason before rethrowing (so the crash file still gets the full stack) — this is the likely Win7 failure. - 'View service log' falls back to the events log when no runtime diagnostic log exists, so there's always something to view after any service interaction. Recap of coverage: hard crashes -> crash-*.txt (always, no setup). Graceful service failures -> now the events log + on-screen reason (no setup). Gate 35/35. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4d1815e071
commit
5129aa2a39
@@ -2254,8 +2254,12 @@ public sealed class MainForm : Form
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.Text = "Service: status unavailable";
|
||||
// Show the reason right in the status line (a screen reader reads it), AND record it to the
|
||||
// always-on service events log — so even with app logging off (e.g. a Win7 tester) we learn
|
||||
// WHY the service machinery couldn't load, not just that it didn't.
|
||||
status.Text = $"Service: unavailable — {ex.GetType().Name}: {ex.Message}";
|
||||
logFile.Event($"service menu: status query failed {ex.GetType().Name}: {ex.Message}");
|
||||
ServiceStore.AppendServiceEvent($"menu status query FAILED: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
};
|
||||
return serviceMenu;
|
||||
@@ -2268,12 +2272,16 @@ public sealed class MainForm : Form
|
||||
/// there's nothing here yet, that's the first thing to turn on.</summary>
|
||||
private void OpenServiceLog()
|
||||
{
|
||||
// Prefer the service's runtime diagnostic log (only exists if the service actually ran with logging
|
||||
// on); otherwise fall back to the ALWAYS-ON service events log, which records every menu/install/
|
||||
// start/stop and any failure reason — so there's a trail to view even if nobody enabled logging.
|
||||
var path = ServiceStore.NewestLogFile();
|
||||
if (path is null && File.Exists(ServiceStore.ServiceEventsLogPath)) path = ServiceStore.ServiceEventsLogPath;
|
||||
if (path is null)
|
||||
{
|
||||
var msg = ServiceStore.LoadLoggingEnabled()
|
||||
? "No service log yet. The service writes one once it starts with logging on — start (or restart) the service, then check back here."
|
||||
: "No service log yet. Turn on logging first: Service menu → Configure service profile → Logging tab → enable service logging, then start (or restart) the service. The log records what the service does and why it is or isn't sending.";
|
||||
? "No service log yet. The service writes one once it starts with logging on — start (or restart) the service, then check back here. (The service events log appears here too once you install/start it.)"
|
||||
: "No service log yet. Once you install or start the service, its events (and any failure reason) are recorded here automatically. For the fuller runtime log, also turn on logging: Service menu → Configure service profile → Logging tab.";
|
||||
MessageBox.Show(this, msg, AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
@@ -2359,8 +2367,11 @@ public sealed class MainForm : Form
|
||||
if (MessageBox.Show(this, msg, AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
|
||||
}
|
||||
logFile.Event($"service: {label} requested (elevated)");
|
||||
ServiceStore.AppendServiceEvent($"{label} requested (elevated)");
|
||||
var rc = ServiceControl.RunElevated(verb);
|
||||
logFile.Event($"service: {label} finished with code {rc} ({(rc == 0 ? "success" : rc == -1 ? "cancelled/declined" : "failed")})");
|
||||
var outcome = rc == 0 ? "success" : rc == -1 ? "cancelled/declined" : "failed";
|
||||
logFile.Event($"service: {label} finished with code {rc} ({outcome})");
|
||||
ServiceStore.AppendServiceEvent($"{label} finished: code {rc} ({outcome})");
|
||||
if (rc == 0)
|
||||
MessageBox.Show(this, $"Service {label} succeeded.", AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
else if (rc == -1)
|
||||
|
||||
@@ -1340,7 +1340,12 @@ internal static class SelfTest
|
||||
File.SetLastWriteTimeUtc(older, new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
File.SetLastWriteTimeUtc(newer, new DateTime(2020, 1, 2, 0, 0, 0, DateTimeKind.Utc));
|
||||
Check(ServiceStore.NewestLogFile() == newer, "NewestLogFile must return the most recently written .log");
|
||||
return "log folder resolves under the service data dir; newest .log is found; empty case handled";
|
||||
|
||||
// Always-on service events log: writes without any toggle, and is where a Win7 failure reason lands.
|
||||
ServiceStore.AppendServiceEvent("selftest: install THREW FileLoadException: could not load System.ServiceProcess");
|
||||
Check(File.Exists(ServiceStore.ServiceEventsLogPath), "AppendServiceEvent must write even with no logging enabled");
|
||||
Check(File.ReadAllText(ServiceStore.ServiceEventsLogPath).Contains("install THREW"), "the service events log must contain the recorded event");
|
||||
return "log folder resolves; newest .log found; empty case handled; always-on events log records without a toggle";
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -28,14 +28,42 @@ internal static class ServiceEntry
|
||||
// profile — otherwise a headless SYSTEM service logs into the SYSTEM account's AppData, which
|
||||
// is near-impossible to find. (The profile itself always comes from ServiceStore.)
|
||||
AppConfig.SetUserDataDirectoryOverride(ServiceStore.Directory);
|
||||
RemSoundService.RunAsService();
|
||||
ServiceStore.AppendServiceEvent("elevated run-service: starting host");
|
||||
try { RemSoundService.RunAsService(); }
|
||||
catch (Exception ex) { ServiceStore.AppendServiceEvent($"elevated run-service: THREW {ex.GetType().Name}: {ex.Message}"); throw; }
|
||||
return 0;
|
||||
}
|
||||
if (Has(args, ServiceControl.InstallVerb)) return ServiceControl.DoInstall();
|
||||
if (Has(args, ServiceControl.UninstallVerb)) return ServiceControl.DoUninstall();
|
||||
if (Has(args, ServiceControl.StartVerb)) return ServiceControl.DoStart();
|
||||
if (Has(args, ServiceControl.StopVerb)) return ServiceControl.DoStop();
|
||||
return 0;
|
||||
|
||||
var verb = Has(args, ServiceControl.InstallVerb) ? "install"
|
||||
: Has(args, ServiceControl.UninstallVerb) ? "uninstall"
|
||||
: Has(args, ServiceControl.StartVerb) ? "start"
|
||||
: Has(args, ServiceControl.StopVerb) ? "stop"
|
||||
: null;
|
||||
if (verb is null) return 0;
|
||||
|
||||
// Log to the always-on events log BEFORE touching the service machinery, and wrap the call: if
|
||||
// System.ServiceProcess can't load on this OS (the Win7 unknown), the exception is caught HERE and
|
||||
// its reason recorded — then rethrown so the crash file also captures the full stack. Either way we
|
||||
// learn WHY, with no logging toggle needed.
|
||||
ServiceStore.AppendServiceEvent($"elevated {verb}: starting (loading service machinery)");
|
||||
try
|
||||
{
|
||||
var rc = verb switch
|
||||
{
|
||||
"install" => ServiceControl.DoInstall(),
|
||||
"uninstall" => ServiceControl.DoUninstall(),
|
||||
"start" => ServiceControl.DoStart(),
|
||||
"stop" => ServiceControl.DoStop(),
|
||||
_ => 0,
|
||||
};
|
||||
ServiceStore.AppendServiceEvent($"elevated {verb}: finished with code {rc}");
|
||||
return rc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ServiceStore.AppendServiceEvent($"elevated {verb}: THREW {ex.GetType().Name}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Has(string[] args, string flag) =>
|
||||
|
||||
@@ -130,6 +130,25 @@ public static class ServiceStore
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
// === Service events log (ALWAYS written, not gated on any logging toggle) ===
|
||||
// The trail of what the app did to the service — open the menu, install, start, stop, configure — AND
|
||||
// why any of it failed. Always on, so if the service breaks on a machine where nobody thought to turn
|
||||
// logging on first (e.g. a Win7 tester), the reason is still captured. Distinct from the service's own
|
||||
// runtime diagnostic log (that one only exists while the service is actually running with logging on).
|
||||
public static string ServiceEventsLogPath => Path.Combine(Directory, "service-events.log");
|
||||
|
||||
/// <summary>Append a timestamped line to the always-on service events log. Never throws; caps its size.</summary>
|
||||
public static void AppendServiceEvent(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(Directory);
|
||||
try { if (File.Exists(ServiceEventsLogPath) && new FileInfo(ServiceEventsLogPath).Length > 200_000) File.Delete(ServiceEventsLogPath); } catch { }
|
||||
File.AppendAllText(ServiceEventsLogPath, $"{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()
|
||||
|
||||
Reference in New Issue
Block a user