Fix Win7 launch crash: keep service types off the startup path

Reported: RemSound no longer launches at all on Windows 7 since the
send-only service was added. Cause: RemSoundService derives from
ServiceBase (System.ServiceProcess), and Program.Main called
RemSoundService.RunAsService() directly in its body. The runtime resolves
every type a method names when it JIT-compiles that method -- so the moment
Main was compiled, at the very start of every launch and before any argument
was read, it force-loaded System.ServiceProcess. That assembly won't load on
Windows 7 under the .NET 10 runtime, so Main failed to compile and the app
died with no window. net10 has always been the target, so this was a pure
regression from the service work, not a framework change.

Fix:
- Move the whole service-verb dispatch into a separate ServiceEntry class.
  Program.Main now only calls it (a) after a cheap check that uses only
  inlined const verb strings, and (b) only when a service verb is actually
  present. A normal launch never JIT-compiles anything that names a service
  type, so System.ServiceProcess is never loaded. Verified empirically:
  a normal launch loads 107 modules, none of them System.ServiceProcess.
- Gate the Service menu to Windows 10+ (OperatingSystem.IsWindowsVersionAtLeast),
  mirroring how the "capture individual apps" feature is gated. On Win7/8 the
  menu isn't shown and no service code is reachable. Made the status-query
  handler defensive too, so a query failure can never crash the menu.

New self-test "Service verb gate": normal launches (no args, --silent,
--profile, --connect, --minimized, --config-dir) are never treated as a
service invocation; all five service verbs are recognised case-insensitively;
and deciding a normal launch loads no service assembly. Gate 29/29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-14 08:41:56 +01:00
co-authored by Claude Opus 4.8
parent 26903950f7
commit 9392cc1fcc
4 changed files with 142 additions and 26 deletions
+28 -12
View File
@@ -2182,7 +2182,13 @@ public sealed class MainForm : Form
// and the arrow keys.
menu.Items.Add(fileMenu);
menu.Items.Add(recordMenu);
menu.Items.Add(BuildServiceMenu());
// The send-only Windows service is a Windows 10+ feature: it was built and tested there, and on
// older Windows the System.ServiceProcess assembly it relies on won't even load under .NET 10. Only
// offer the Service menu where it can actually work — on Win7/8 it simply isn't shown and no service
// code is ever reached. (Mirrors how "capture individual apps" is gated to the versions that support
// it.) The startup path is already load-safe via ServiceEntry; this keeps the menu load-safe too.
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0))
menu.Items.Add(BuildServiceMenu());
menu.Items.Add(optionsMenu);
menu.Items.Add(helpMenu);
return menu;
@@ -2218,19 +2224,29 @@ public sealed class MainForm : Form
});
serviceMenu.DropDownOpening += (_, _) =>
{
var state = ServiceControl.Query();
status.Text = "Service: " + DescribeServiceState(state);
// Surface the running version + when it (re)started, so a self-update is visible at a glance.
if (state is ServiceState.Running or ServiceState.Stopped && ServiceStore.LoadStatus() is { Version: { } ver } st)
// Never let a status-query failure crash the menu (and with it the app). The menu only appears
// on Win10+ where the service assembly loads fine, but a defensive net here is cheap insurance.
try
{
status.Text += $" — version {ver}";
if (state == ServiceState.Running && st.StartedUtc != default) status.Text += $", running since {DescribeAgo(st.StartedUtc)}";
var state = ServiceControl.Query();
status.Text = "Service: " + DescribeServiceState(state);
// Surface the running version + when it (re)started, so a self-update is visible at a glance.
if (state is ServiceState.Running or ServiceState.Stopped && ServiceStore.LoadStatus() is { Version: { } ver } st)
{
status.Text += $" — version {ver}";
if (state == ServiceState.Running && st.StartedUtc != default) status.Text += $", running since {DescribeAgo(st.StartedUtc)}";
}
var installed = state != ServiceState.NotInstalled;
install.Enabled = !installed;
uninstall.Enabled = installed;
start.Enabled = installed && state is ServiceState.Stopped;
stop.Enabled = installed && state is ServiceState.Running;
}
catch (Exception ex)
{
status.Text = "Service: status unavailable";
logFile.Event($"service menu: status query failed {ex.GetType().Name}: {ex.Message}");
}
var installed = state != ServiceState.NotInstalled;
install.Enabled = !installed;
uninstall.Enabled = installed;
start.Enabled = installed && state is ServiceState.Stopped;
stop.Enabled = installed && state is ServiceState.Running;
};
return serviceMenu;
}