Offer the service on Win7+ by feature-detecting, not version-gating
Replaces the hardcoded "Windows 10 or newer" gate on the Service menu with a
runtime capability probe (ServiceCapability). It tries to load the .NET
service machinery (System.ServiceProcess) and offers the Service menu wherever
that succeeds -- including Windows 7 if the service layer loads there, which is
what Ed asked for ("install on Win7 or above"). If the assembly can't load on
an older/unsupported Windows, the probe catches it and the menu is simply
hidden, so such a machine can never be crashed by it.
Safety by construction: the reference to the service types lives only in the
NoInlining Probe method, so the assembly load is triggered by the CALL from
IsAvailable (inside its try/catch) and is catchable -- not during JIT of the
caller, which would be fatal. The startup path stays service-type-free via
ServiceEntry, so this probe runs only at window construction, never at launch.
Probe uses the parameterless ServiceController ctor: it forces the assembly to
load but touches no service and no SCM. (First cut read .ServiceName on a
made-up name, which actually queries the SCM and threw "service not found" --
the self-test caught that it was hiding the menu on Windows 11 too.)
New self-test "Service capability probe": available + cached + never throws on
the Win10/11 gate runner. Gate 30/30.
Note: this makes the service INSTALLABLE on Win7 wherever the layer loads; it
does not prove the service RUNS there (Session-0 capture on an unsupported
runtime) -- only a real test on the Win7 box can confirm that.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9392cc1fcc
commit
2dad00c42f
@@ -2182,12 +2182,14 @@ public sealed class MainForm : Form
|
||||
// and the arrow keys.
|
||||
menu.Items.Add(fileMenu);
|
||||
menu.Items.Add(recordMenu);
|
||||
// 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))
|
||||
// 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())
|
||||
menu.Items.Add(BuildServiceMenu());
|
||||
menu.Items.Add(optionsMenu);
|
||||
menu.Items.Add(helpMenu);
|
||||
|
||||
@@ -80,6 +80,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);
|
||||
|
||||
var failed = results.Count(r => r.Status == "FAIL");
|
||||
var skipped = results.Count(r => r.Status == "SKIP");
|
||||
@@ -1313,6 +1314,21 @@ internal static class SelfTest
|
||||
private static bool IsAssemblyLoaded(string simpleName) =>
|
||||
AppDomain.CurrentDomain.GetAssemblies().Any(a => string.Equals(a.GetName().Name, simpleName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>The Service menu is shown by FEATURE-DETECTING the Windows service machinery (so it can
|
||||
/// appear on Win7 too if the .NET service layer loads there), not by a hardcoded Windows version. The
|
||||
/// probe must be stable/cached and must never throw. On this Win10/11 gate runner the machinery loads,
|
||||
/// 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.</summary>
|
||||
private static string? ServiceCapabilityProbe()
|
||||
{
|
||||
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";
|
||||
}
|
||||
|
||||
private static int CountControls(Control root, Func<Control, bool> predicate)
|
||||
{
|
||||
var n = 0;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>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 (<c>System.ServiceProcess</c>) 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.</para>
|
||||
///
|
||||
/// <para>The load attempt is isolated in <see cref="Probe"/> and marked NoInlining on purpose: the reference
|
||||
/// to the <c>System.ServiceProcess</c> types lives ONLY there, so the assembly load is triggered by the
|
||||
/// <em>call</em> to Probe (inside <see cref="IsAvailable"/>'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.</para>
|
||||
/// </summary>
|
||||
internal static class ServiceCapability
|
||||
{
|
||||
private static bool? cached;
|
||||
|
||||
/// <summary>True when the Windows service machinery loads and is usable on this OS/runtime. Cached after
|
||||
/// the first check; never throws.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user