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()