diff --git a/adapter/MTConnect.NET-Applications-Adapter/Service.cs b/adapter/MTConnect.NET-Applications-Adapter/Service.cs index c24ea8d09..8596deac8 100644 --- a/adapter/MTConnect.NET-Applications-Adapter/Service.cs +++ b/adapter/MTConnect.NET-Applications-Adapter/Service.cs @@ -3,7 +3,9 @@ using MTConnect.Services; using NLog; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif namespace MTConnect.Applications { diff --git a/agent/MTConnect.NET-Applications-Agents/Service.cs b/agent/MTConnect.NET-Applications-Agents/Service.cs index ea8032ebc..ce3b1925f 100644 --- a/agent/MTConnect.NET-Applications-Agents/Service.cs +++ b/agent/MTConnect.NET-Applications-Agents/Service.cs @@ -3,7 +3,9 @@ using MTConnect.Services; using NLog; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif namespace MTConnect.Applications { diff --git a/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs b/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs index f9c05c9cb..5459864a2 100644 --- a/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs +++ b/libraries/MTConnect.NET-Services/MTConnectAdapterService.cs @@ -5,7 +5,9 @@ using System.Diagnostics; using System.IO; using System.Reflection; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.ServiceProcess; namespace MTConnect.Services diff --git a/libraries/MTConnect.NET-Services/MTConnectAgentService.cs b/libraries/MTConnect.NET-Services/MTConnectAgentService.cs index 7bb3e92dd..4134627f1 100644 --- a/libraries/MTConnect.NET-Services/MTConnectAgentService.cs +++ b/libraries/MTConnect.NET-Services/MTConnectAgentService.cs @@ -5,7 +5,9 @@ using System.Diagnostics; using System.IO; using System.Reflection; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.ServiceProcess; namespace MTConnect.Services diff --git a/libraries/MTConnect.NET-Services/WindowsService.cs b/libraries/MTConnect.NET-Services/WindowsService.cs index 9c2ac4c28..cc43cf424 100644 --- a/libraries/MTConnect.NET-Services/WindowsService.cs +++ b/libraries/MTConnect.NET-Services/WindowsService.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Runtime.InteropServices; +#if NET5_0_OR_GREATER using System.Runtime.Versioning; +#endif using System.Security.Principal; using System.ServiceProcess; diff --git a/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj b/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj index 05dc9efdd..1b09a0fdd 100644 --- a/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj +++ b/tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj @@ -22,7 +22,10 @@ (tests/MTConnect.NET-Common-Tests/Http/), which reflection-inspect MTConnectPostResponseHandler.ReadRequestBytes and drive the Ceen LimitedBodyStream drain path to pin the CA2022 short-read handling - contract across every TFM. + contract across every TFM. It is also referenced from + SupportedOSPlatformAttributePresenceTests + (tests/MTConnect.NET-Common-Tests/Platform/) to reflection-assert the + [SupportedOSPlatform("windows")] attribute on the HTTP server surface. --> + + + + diff --git a/tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs b/tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs new file mode 100644 index 000000000..5dba0514c --- /dev/null +++ b/tests/MTConnect.NET-Common-Tests/Platform/SupportedOSPlatformAttributePresenceTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) 2026 TrakHound Inc., All Rights Reserved. +// TrakHound Inc. licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using NUnit.Framework; + +namespace MTConnect.NET_Common_Tests.Platform +{ + // Pins the [SupportedOSPlatform("windows")] decoration on every type or + // method that was annotated by commit dd2eb424 (2026-05-22) to silence + // the CA1416 platform-compatibility analyzer on .NET 8. The attribute + // type ships in System.Runtime on .NET 5.0+ but is absent from net4x + // and netstandard2.0; PR #218 wraps each declaration plus its + // using-directive in `#if NET5_0_OR_GREATER ... #endif` so the + // Release pack survives the full TFM matrix. + // + // This fixture only exercises the net8.0 build path (the only TFM + // every test project targets), and so cannot directly fail under the + // pre-fix code (net8.0 keeps the attribute regardless of the wrap). + // Its value is REGRESSION-PREVENTIVE: a future contributor who + // removes the attribute outright — or who narrows the wrap to a + // condition that excludes net8.0 — fails this test, and the + // protection the original commit added stays in place. + /// Pins SupportedOSPlatform("windows") on every site PR #218 wraps. + [TestFixture] + [Category("MultiTfmCompat")] + public class SupportedOSPlatformAttributePresenceTests + { + private static void AssertWindowsPlatform(MemberInfo member, string description) + { + var attributes = member + .GetCustomAttributes(typeof(SupportedOSPlatformAttribute), inherit: false) + .Cast() + .ToArray(); + + Assert.That(attributes, Is.Not.Empty, + $"{description}: expected [SupportedOSPlatform] attribute."); + Assert.That( + attributes.Any(a => string.Equals(a.PlatformName, "windows", StringComparison.Ordinal)), + Is.True, + $"{description}: expected PlatformName == \"windows\" but got " + + $"[{string.Join(", ", attributes.Select(a => a.PlatformName))}]."); + } + + /// MTConnect.Services.WindowsService carries the Windows-only attribute. + [Test] + public void WindowsService_carries_supported_os_platform_windows() + { + // WindowsService is internal — reach it via Assembly.GetType + // rather than typeof(...). + var servicesAssembly = typeof(MTConnect.Services.MTConnectAgentService).Assembly; + var windowsServiceType = servicesAssembly.GetType( + "MTConnect.Services.WindowsService", throwOnError: true); + AssertWindowsPlatform(windowsServiceType!, + "MTConnect.Services.WindowsService"); + } + + /// MTConnect.Services.MTConnectAgentService carries the Windows-only attribute. + [Test] + public void MTConnectAgentService_carries_supported_os_platform_windows() + { + AssertWindowsPlatform(typeof(MTConnect.Services.MTConnectAgentService), + "MTConnect.Services.MTConnectAgentService"); + } + + /// MTConnect.Services.MTConnectAdapterService carries the Windows-only attribute. + [Test] + public void MTConnectAdapterService_carries_supported_os_platform_windows() + { + AssertWindowsPlatform(typeof(MTConnect.Services.MTConnectAdapterService), + "MTConnect.Services.MTConnectAdapterService"); + } + + /// The agent-application Service class carries the Windows-only attribute. + [Test] + public void AgentApplications_Service_carries_supported_os_platform_windows() + { + // Both agent-application and adapter-application Service + // types live under the same MTConnect.Applications + // namespace; distinguish by assembly. + var agentServiceType = typeof(MTConnect.Applications.IMTConnectAgentApplication).Assembly + .GetType("MTConnect.Applications.Service", throwOnError: true); + AssertWindowsPlatform(agentServiceType!, + "MTConnect.Applications.Service (agent application)"); + } + + /// The adapter-application Service class carries the Windows-only attribute. + [Test] + public void AdapterApplications_Service_carries_supported_os_platform_windows() + { + var adapterServiceType = typeof(MTConnect.Applications.IMTConnectAdapterApplication).Assembly + .GetType("MTConnect.Applications.Service", throwOnError: true); + AssertWindowsPlatform(adapterServiceType!, + "MTConnect.Applications.Service (adapter application)"); + } + + /// The Ceen HTTP-server socket-handle sites carry the Windows-only attribute. + [Test] + public void CeenHttpServer_socket_handle_sites_carry_supported_os_platform_windows() + { + // Both annotated members are internal to MTConnect.NET-HTTP: + // * Ceen.Httpd.HttpServer.InterProcessBridge.HandleRequest + // * Ceen.Httpd.HttpServer.RunClient (private static) + // Reach them via Assembly.GetType + reflection. + var httpAssembly = typeof(MTConnect.Servers.Http.MTConnectHttpRequests).Assembly; + var httpServerType = httpAssembly.GetType( + "Ceen.Httpd.HttpServer", throwOnError: true)!; + var socketInformationType = typeof(System.Net.Sockets.SocketInformation); + + // The [SupportedOSPlatform("windows")] attribute lives on + // Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest( + // SocketInformation, EndPoint, string) — AppDomainBridge, + // not InterProcessBridge, is the AppDomain-marshallable + // sibling that wraps the duplicated socket handle. The + // RunClient(SocketInformation,...) private static method on + // HttpServer itself is the second site. + var bridgeType = httpServerType.GetNestedType( + "AppDomainBridge", BindingFlags.Public | BindingFlags.NonPublic); + Assert.That(bridgeType, Is.Not.Null, + "Ceen.Httpd.HttpServer.AppDomainBridge nested type not found."); + + const BindingFlags allInstance = BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.Instance; + // HandleRequest has two overloads; the SocketInformation-typed + // one is the Windows-only path the attribute decorates. The + // Socket-typed overload is cross-platform. + var handleRequestCandidates = bridgeType!.GetMethods(allInstance) + .Where(m => m.Name == "HandleRequest") + .ToArray(); + var handleRequest = handleRequestCandidates + .FirstOrDefault(m => m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType == socketInformationType); + Assert.That(handleRequest, Is.Not.Null, + "Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest(SocketInformation,...) overload not found among: " + + string.Join("; ", + handleRequestCandidates.Select(m => $"{m.Name}({string.Join(",", m.GetParameters().Select(p => p.ParameterType.FullName))})"))); + + const BindingFlags allStatic = BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.Static; + var runClient = httpServerType.GetMethods(allStatic) + .FirstOrDefault(m => m.Name == "RunClient" + && m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType == socketInformationType); + Assert.That(runClient, Is.Not.Null, + "Ceen.Httpd.HttpServer.RunClient(SocketInformation,...) overload not found."); + + AssertWindowsPlatform(handleRequest!, + "Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest"); + AssertWindowsPlatform(runClient!, + "Ceen.Httpd.HttpServer.RunClient(SocketInformation,...)"); + } + } +}