diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs
index 073b33c..0b6ccd0 100644
--- a/src/RemSound.App/MainForm.cs
+++ b/src/RemSound.App/MainForm.cs
@@ -2182,14 +2182,16 @@ public sealed class MainForm : Form
// and the arrow keys.
menu.Items.Add(fileMenu);
menu.Items.Add(recordMenu);
- // Offer the Service menu wherever the send-only service can actually run — feature-detected, not
- // gated on a Windows version, so it appears on Windows 7 too if the .NET service layer loads there
- // (Ed's ask: install on "Win7 or above" wherever it's supported). ServiceCapability probes safely:
- // if System.ServiceProcess can't load on this OS, it catches that and the menu is simply hidden, so
- // an older Windows can never be crashed by it. The startup path is already service-type-free via
- // ServiceEntry, so this probe (at window construction, never at launch) is the only place that
- // decides visibility.
- if (ServiceCapability.IsAvailable())
+ // Offer the Service menu on Windows 10+ only. This is a VERSION check on purpose, not a runtime
+ // probe: a probe would have to construct a ServiceController to test it, which loads
+ // System.ServiceProcess — the exact assembly that won't load on Windows 7 — during window
+ // construction on EVERY launch. Deciding by version is free (it touches no service type), so on
+ // Windows 7/8 the menu is simply hidden and that assembly is never referenced at launch at all,
+ // which is what keeps the app launching there. On Windows 10+ the assembly loads only later, if the
+ // user actually opens the Service menu (the DropDownOpening handler, wrapped in try/catch). Feature-
+ // detecting on Win7 and keeping Win7 launch-safe are mutually exclusive — you can't test whether the
+ // assembly loads without loading it — so we choose guaranteed launch safety.
+ if (OperatingSystem.IsWindowsVersionAtLeast(10, 0))
menu.Items.Add(BuildServiceMenu());
menu.Items.Add(optionsMenu);
menu.Items.Add(helpMenu);
diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs
index 145f581..4e196cf 100644
--- a/src/RemSound.App/SelfTest.cs
+++ b/src/RemSound.App/SelfTest.cs
@@ -81,7 +81,7 @@ internal static class SelfTest
RunStep(results, "Main window profile round-trip (controls load + save)", MainWindowProfileRoundTrip);
RunStep(results, "Auto-save non-read-only profiles (options + guard + silent timer)", AutoSaveNonReadOnlyProfiles);
RunStep(results, "Service verb gate (normal launch stays load-safe)", ServiceVerbGate);
- RunStep(results, "Service capability probe (feature-detect, cached, safe)", ServiceCapabilityProbe);
+ RunStep(results, "Main window builds without loading the service assembly (Win7-safe)", MainWindowServiceAssemblyFree);
RunStep(results, "Menu shortcuts don't clash with controls", MenuShortcutsDontClashWithControls);
RunStep(results, "Service log discovery (newest activity log)", ServiceLogDiscovery);
@@ -1356,13 +1356,26 @@ internal static class SelfTest
/// so it must report available; the "can't load → hidden" path can only be exercised on an OS where the
/// assembly genuinely won't load, but the try/catch that guarantees it degrades safely is verified here
/// by the probe never throwing.
- private static string? ServiceCapabilityProbe()
+ /// The Win7 launch guarantee, directly: building the main window (which builds the menu bar)
+ /// must NOT load System.ServiceProcess. The Service menu's visibility is decided by a Windows-VERSION
+ /// check, which touches no service type — so the service assembly is only ever loaded later, if a
+ /// Windows-10+ user opens the Service menu. On Windows 7 that decision hides the menu and the assembly
+ /// (which won't load there) is never referenced at launch, which is what keeps the app starting.
+ private static string? MainWindowServiceAssemblyFree()
{
- bool first = ServiceCapability.IsAvailable();
- bool second = ServiceCapability.IsAvailable();
- Check(first == second, "the capability result must be stable across calls (cached)");
- Check(first, "the Windows service machinery must be detected as available on this Windows 10/11 test runner");
- return "service machinery feature-detected as available, and the result is cached";
+ const string svcAsm = "System.ServiceProcess.ServiceController";
+ bool loadedBefore = IsAssemblyLoaded(svcAsm);
+
+ Form form;
+ try { form = new MainForm(null, RemSound.Core.Profile.NewBlank(), null, null, headless: true); }
+ catch (Exception ex) { return Skip($"headless MainForm could not be constructed: {ex.GetType().Name}: {ex.Message}"); }
+ using (form) { }
+
+ if (loadedBefore)
+ return "service assembly already loaded by an earlier step; main-window load-safety not re-checked this run";
+ Check(!IsAssemblyLoaded(svcAsm),
+ "constructing the main window must NOT load System.ServiceProcess (Service menu is version-gated, not probed) — this is what keeps the app launching on Windows 7");
+ return "the main window builds without loading the Windows-service assembly (Win7 launch-safe)";
}
/// A top-level menu opens on Alt+<its mnemonic> — but a VISIBLE control that owns the same
diff --git a/src/RemSound.App/ServiceCapability.cs b/src/RemSound.App/ServiceCapability.cs
deleted file mode 100644
index d2524e7..0000000
--- a/src/RemSound.App/ServiceCapability.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Runtime.CompilerServices;
-
-namespace RemSound.App;
-
-///
-/// Feature-detects whether the send-only Windows service can be offered on THIS machine — by actually
-/// trying to load the service machinery, not by checking a Windows version number.
-///
-/// Why feature-detect instead of "Windows 10 or newer": Windows 7 has a full Service Control Manager,
-/// so the service could work there IF the .NET service layer (System.ServiceProcess) loads on it.
-/// That's the real unknown — .NET 10 isn't officially supported on Win7 — so we simply try, and offer the
-/// service wherever the attempt succeeds (Win7 included) while hiding it safely wherever it doesn't.
-///
-/// The load attempt is isolated in and marked NoInlining on purpose: the reference
-/// to the System.ServiceProcess types lives ONLY there, so the assembly load is triggered by the
-/// call to Probe (inside 's try/catch) and is therefore catchable — instead
-/// of happening when IsAvailable itself is compiled, which would be an unrecoverable crash on an OS where
-/// that assembly won't load. This is the same failure that took the app down at launch before the startup
-/// path was made service-type-free; here it degrades to "hide the menu" instead.
-///
-internal static class ServiceCapability
-{
- private static bool? cached;
-
- /// True when the Windows service machinery loads and is usable on this OS/runtime. Cached after
- /// the first check; never throws.
- public static bool IsAvailable()
- {
- if (cached is { } c) return c;
- bool ok;
- try { ok = Probe(); }
- catch { ok = false; } // System.ServiceProcess couldn't load here — offer nothing, stay safe
- cached = ok;
- return ok;
- }
-
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static bool Probe()
- {
- // Constructing a ServiceController forces the System.ServiceProcess assembly to load — the exact
- // step that fails on an OS the .NET service layer doesn't support. The PARAMETERLESS ctor touches
- // no service and no Service Control Manager, so it can't throw for a benign reason (naming a made-up
- // service and reading a property WOULD hit the SCM and throw "not found"). The construction succeeding
- // is all we need to know the machinery loads here; a real service is opened later via ServiceControl.
- using var probe = new System.ServiceProcess.ServiceController();
- return true;
- }
-}