diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index 6e241d3..e6e8fa4 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -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. 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) diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs index 8a04fdc..dce7e8a 100644 --- a/src/RemSound.App/SelfTest.cs +++ b/src/RemSound.App/SelfTest.cs @@ -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 { diff --git a/src/RemSound.App/ServiceEntry.cs b/src/RemSound.App/ServiceEntry.cs index 3dd96e0..6e4948a 100644 --- a/src/RemSound.App/ServiceEntry.cs +++ b/src/RemSound.App/ServiceEntry.cs @@ -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) => diff --git a/src/RemSound.Core/ServiceStore.cs b/src/RemSound.Core/ServiceStore.cs index 0506172..e06b3e6 100644 --- a/src/RemSound.Core/ServiceStore.cs +++ b/src/RemSound.Core/ServiceStore.cs @@ -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"); + + /// Append a timestamped line to the always-on service events log. Never throws; caps its size. + 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 */ } + } + /// 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). public static void SetUpdatePending()