From b8280a146599c03ac704be2f5a9712e8148b345a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 4 May 2026 22:27:57 -0400 Subject: [PATCH] Drop SupportedOSPlatform gates; SDK now runs on Linux The dialect / orchestrators were defensively gated on Windows because HistorianSspiClient previously P/Invoked InitializeSecurityContextW. With that replaced by NegotiateAuthentication (cross-platform), the gates are unnecessary. Removed them from: - Historian2020ProtocolDialect (4 read paths + 3 status helpers) - HistorianClient.EnsureTagAsync / DeleteTagAsync - HistorianWcf{Auth,Read,Event,Status,TagWrite}Orchestrator/Helper - HistorianWcf{HistAddressing,MessageCapture}Behavior - HistorianWcfBindingFactory (with #pragma on the Named-Pipe builder which still requires Windows at the BCL level) Runtime constraint: LocalPipe and RemoteTcpIntegrated transports still require Windows because NetNamedPipeBinding and the Windows transport security binding are Windows-only at the BCL level. RemoteTcpCertificate is now usable from Linux, and ProbeAsync is verified working from a Debian client (10.100.0.35) against the Windows Historian (10.100.0.48). 171/171 tests still pass on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AVEVA.Historian.Client/HistorianClient.cs | 8 --- .../Protocol/Historian2020ProtocolDialect.cs | 57 ------------------- .../Wcf/HistorianWcfAuthChainHelper.cs | 1 - .../Wcf/HistorianWcfBindingFactory.cs | 12 ++-- .../Wcf/HistorianWcfEventOrchestrator.cs | 1 - .../Wcf/HistorianWcfHistAddressingBehavior.cs | 1 - .../Wcf/HistorianWcfMessageCaptureBehavior.cs | 1 - .../Wcf/HistorianWcfReadOrchestrator.cs | 1 - .../Wcf/HistorianWcfStatusClient.cs | 1 - .../Wcf/HistorianWcfTagWriteOrchestrator.cs | 1 - 10 files changed, 8 insertions(+), 76 deletions(-) diff --git a/src/AVEVA.Historian.Client/HistorianClient.cs b/src/AVEVA.Historian.Client/HistorianClient.cs index b2d3092..05e68b3 100644 --- a/src/AVEVA.Historian.Client/HistorianClient.cs +++ b/src/AVEVA.Historian.Client/HistorianClient.cs @@ -132,10 +132,6 @@ public sealed class HistorianClient : IAsyncDisposable public Task EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(definition); - if (!OperatingSystem.IsWindows()) - { - throw new ProtocolEvidenceMissingException("EnsureTagAsync requires Windows for the SSPI auth path"); - } return new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken); } @@ -150,10 +146,6 @@ public sealed class HistorianClient : IAsyncDisposable public Task DeleteTagAsync(string tagName, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(tagName); - if (!OperatingSystem.IsWindows()) - { - throw new ProtocolEvidenceMissingException("DeleteTagAsync requires Windows for the SSPI auth path"); - } return new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken); } diff --git a/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs b/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs index 8c5bb04..68bd587 100644 --- a/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs +++ b/src/AVEVA.Historian.Client/Protocol/Historian2020ProtocolDialect.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using AVEVA.Historian.Client.Models; using AVEVA.Historian.Client.Wcf; @@ -15,53 +14,21 @@ internal sealed class Historian2020ProtocolDialect public IAsyncEnumerable ReadRawAsync(string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken cancellationToken) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Missing("StartDataRetrievalQuery/Full requires Windows for the SSPI path", cancellationToken); - } - - return ReadRawWindowsAsync(tag, startUtc, endUtc, maxValues, cancellationToken); - } - - private IAsyncEnumerable ReadRawWindowsAsync(string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken cancellationToken) - { -#pragma warning disable CA1416 // Validated by RuntimeInformation.IsOSPlatform check above. HistorianWcfReadOrchestrator orchestrator = new(_options); return orchestrator.ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken); -#pragma warning restore CA1416 } public IAsyncEnumerable ReadAggregateAsync(string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken cancellationToken) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Missing($"StartDataRetrievalQuery/{mode} requires Windows for the SSPI path", cancellationToken); - } - - return ReadAggregateWindowsAsync(tag, startUtc, endUtc, mode, interval, cancellationToken); - } - - private IAsyncEnumerable ReadAggregateWindowsAsync(string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken cancellationToken) - { -#pragma warning disable CA1416 HistorianWcfReadOrchestrator orchestrator = new(_options); return orchestrator.ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken); -#pragma warning restore CA1416 } public Task> ReadAtTimeAsync(string tag, IReadOnlyList timestampsUtc, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - throw new ProtocolEvidenceMissingException("StartDataRetrievalQuery/Interpolated at-time requires Windows for the SSPI path"); - } - -#pragma warning disable CA1416 HistorianWcfReadOrchestrator orchestrator = new(_options); return orchestrator.ReadAtTimeAsync(tag, timestampsUtc, cancellationToken); -#pragma warning restore CA1416 } public IAsyncEnumerable ReadBlocksAsync(string tag, DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) @@ -71,39 +38,19 @@ internal sealed class Historian2020ProtocolDialect public IAsyncEnumerable ReadEventsAsync(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Missing("StartEventDataRetrievalQuery requires Windows for the SSPI path", cancellationToken); - } - - return ReadEventsWindowsAsync(startUtc, endUtc, cancellationToken); - } - - private IAsyncEnumerable ReadEventsWindowsAsync(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) - { -#pragma warning disable CA1416 HistorianWcfEventOrchestrator orchestrator = new(_options); return orchestrator.ReadEventsAsync(startUtc, endUtc, cancellationToken); -#pragma warning restore CA1416 } public Task GetConnectionStatusAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (!OperatingSystem.IsWindows()) - { - throw new ProtocolEvidenceMissingException("GetConnectionStatus on non-Windows"); - } return Wcf.HistorianWcfStatusClient.GetConnectionStatusAsync(_options, cancellationToken); } public Task GetStoreForwardStatusAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (!OperatingSystem.IsWindows()) - { - throw new ProtocolEvidenceMissingException("GetStoreForwardStatus on non-Windows"); - } return Wcf.HistorianWcfStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken); } @@ -111,10 +58,6 @@ internal sealed class Historian2020ProtocolDialect { cancellationToken.ThrowIfCancellationRequested(); ArgumentException.ThrowIfNullOrWhiteSpace(name); - if (!OperatingSystem.IsWindows()) - { - throw new ProtocolEvidenceMissingException("GetSystemParameter on non-Windows"); - } return Wcf.HistorianWcfStatusClient.GetSystemParameterAsync(_options, name, cancellationToken); } diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs index 8c32855..02a0e59 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfAuthChainHelper.cs @@ -7,7 +7,6 @@ using AVEVA.Historian.Client.Wcf.Contracts; namespace AVEVA.Historian.Client.Wcf; -[SupportedOSPlatform("windows")] internal static class HistorianWcfAuthChainHelper { private const int OpenConnection3MinResponseLength = 5; diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs index dba9f31..9838178 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfBindingFactory.cs @@ -1,5 +1,4 @@ using System.Net.Security; -using System.Runtime.Versioning; using System.ServiceModel; using System.ServiceModel.Channels; @@ -92,7 +91,10 @@ internal static class HistorianWcfBindingFactory }; } - [SupportedOSPlatform("windows")] + // NetNamedPipeBinding is Windows-only at the BCL level; calling this on Linux + // throws PlatformNotSupportedException at runtime. Cross-platform callers should + // choose Transport = RemoteTcpCertificate (or RemoteTcpIntegrated on Windows). +#pragma warning disable CA1416 // Documented Windows-only entry point public static Binding CreateMdasNetNamedPipeBinding(TimeSpan timeout, int maxBufferSize = 64 * 1024 * 1024) { NetNamedPipeBinding nativeShape = new() @@ -121,8 +123,8 @@ internal static class HistorianWcfBindingFactory SendTimeout = timeout }; } +#pragma warning restore CA1416 - [SupportedOSPlatform("windows")] public static (Binding HistoryBinding, EndpointAddress HistoryEndpoint, Binding RetrievalBinding, EndpointAddress RetrievalEndpoint) CreateBindingPair( HistorianClientOptions options) { @@ -188,7 +190,9 @@ internal static class HistorianWcfBindingFactory /// transport-security upgrade that the History service negotiates; the established session /// authenticates the client already. /// - [SupportedOSPlatform("windows")] + // NetNamedPipeBinding / WindowsStreamSecurityBindingElement are Windows-only at the + // BCL level; calling this on Linux throws PlatformNotSupportedException at runtime. + // Cross-platform callers should choose Transport = RemoteTcpCertificate. public static Binding CreateAuxiliaryBinding(HistorianClientOptions options) { ArgumentNullException.ThrowIfNull(options); diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs index c4d34f6..e37d036 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfEventOrchestrator.cs @@ -14,7 +14,6 @@ namespace AVEVA.Historian.Client.Wcf; /// orchestrator returns an empty enumeration but logs the row-buffer length via the /// diagnostic so a follow-up capture can decode the wire shape. /// -[SupportedOSPlatform("windows")] internal sealed class HistorianWcfEventOrchestrator { private const int OpenConnection3MinResponseLength = 5; diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfHistAddressingBehavior.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfHistAddressingBehavior.cs index 70b4c0a..8293419 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfHistAddressingBehavior.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfHistAddressingBehavior.cs @@ -14,7 +14,6 @@ namespace AVEVA.Historian.Client.Wcf; /// endpoint address, but the captured SDK bytes show it absent — re-asserting it /// here closes the gap. /// -[SupportedOSPlatform("windows")] internal sealed class HistorianWcfHistAddressingBehavior : IEndpointBehavior { public void Validate(ServiceEndpoint endpoint) { } diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfMessageCaptureBehavior.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfMessageCaptureBehavior.cs index 30e9ffe..714e1d1 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfMessageCaptureBehavior.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfMessageCaptureBehavior.cs @@ -14,7 +14,6 @@ namespace AVEVA.Historian.Client.Wcf; /// instrument-wcf-{write,read}message native captures and diff offset-by-offset to /// isolate SDK-vs-native differences. NEVER enable in production. /// -[SupportedOSPlatform("windows")] internal sealed class HistorianWcfMessageCaptureBehavior : IEndpointBehavior { public const string CapturePathEnvVar = "AVEVA_HISTORIAN_SDK_WIRE_CAPTURE"; diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs index 721a7a9..aa147ed 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfReadOrchestrator.cs @@ -7,7 +7,6 @@ using AVEVA.Historian.Client.Wcf.Contracts; namespace AVEVA.Historian.Client.Wcf; -[SupportedOSPlatform("windows")] internal sealed class HistorianWcfReadOrchestrator { private const ushort StartQueryRequestType = HistorianDataQueryProtocol.QueryRequestTypeData; diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfStatusClient.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfStatusClient.cs index 591fa12..44af582 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfStatusClient.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfStatusClient.cs @@ -6,7 +6,6 @@ using AVEVA.Historian.Client.Wcf.Contracts; namespace AVEVA.Historian.Client.Wcf; -[SupportedOSPlatform("windows")] internal static class HistorianWcfStatusClient { public static Task GetSystemParameterAsync( diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs index 0f17a45..ba62a9b 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfTagWriteOrchestrator.cs @@ -16,7 +16,6 @@ namespace AVEVA.Historian.Client.Wcf; /// AddS2 is intentionally NOT here — it is blocked architecturally per /// docs/plans/write-commands-reverse-engineering.md Phase 2 findings. /// -[SupportedOSPlatform("windows")] internal sealed class HistorianWcfTagWriteOrchestrator { private readonly HistorianClientOptions _options;