diff --git a/ZB.MOM.WW.SPHistorianClient/.gitignore b/ZB.MOM.WW.SPHistorianClient/.gitignore deleted file mode 100644 index 894d9d5..0000000 --- a/ZB.MOM.WW.SPHistorianClient/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -bin/ -obj/ -# identity-bearing / non-redistributable — never commit -*.ndjson -current/ -aveva-install-*/ diff --git a/ZB.MOM.WW.SPHistorianClient/CLAUDE.md b/ZB.MOM.WW.SPHistorianClient/CLAUDE.md deleted file mode 100644 index e5fed13..0000000 --- a/ZB.MOM.WW.SPHistorianClient/CLAUDE.md +++ /dev/null @@ -1,173 +0,0 @@ -# ZB.MOM.WW.SPHistorianClient - -Pure-managed .NET 10 client for **AVEVA System Platform Historian** (Wonderware), for the -ZB.MOM.WW SCADA family. This is a **library, not a service** — it is linked directly into the -consuming application and runs in-process alongside it. - -The wire protocol is reverse-engineered and re-implemented in C#. There is **no native AVEVA -runtime dependency** — `aahClientManaged.dll` / `aahClient.dll` are not referenced or loaded. -The library runs on any OS for offline/unit testing; live WCF transports require Windows. - -**Status: ported and rebranded into this repo; builds and 191 tests pass on macOS. Version 0.1.0. -The `RemoteGrpc` (2023 R2) read path is live-verified (2026-06-19). NOT yet packed/published to any -NuGet feed beyond the local `artifacts/` nupkg. NOT yet adopted by any consumer.** - ---- - -## Supported operation surface - -All operations are exposed via the public façade `HistorianClient`. - -| Operation | Status | -|---|---| -| `ProbeAsync` | live-verified | -| `ReadRawAsync` | live-verified | -| `ReadAggregateAsync` | live-verified across the `RetrievalMode` enum (15 modes) | -| `ReadAtTimeAsync` | live-verified | -| `ReadBlocksAsync` | block history read | -| `ReadEventsAsync` | live-verified (typed event + property bag) | -| `BrowseTagNamesAsync` | live-verified | -| `GetTagMetadataAsync` | live-verified across many native data-type codes | -| `GetConnectionStatusAsync` | synthesized from authenticated probe | -| `GetStoreForwardStatusAsync` | synthesized defaults | -| `GetSystemParameterAsync` | live-verified | -| `EnsureTagAsync` | live-verified for analog `Float`; `Double`/`Int2`/`Int4`/`UInt4` supported (optional `ApplyScaling` persists distinct MinRaw/MaxRaw) | -| `DeleteTagAsync` | live-verified (known issue: server-side cascade may not always complete; use SMC as fallback to clean up sandbox tags) | - -### Out of scope - -- **Writing sample values** (`AddS2`) is architecturally blocked — the server runtime cache only - ingests from configured IOServer / Application Server pipelines, not from a standalone AddTag - client flow. -- Store-forward write, historian configuration changes, discrete/string tag creation (native - AddTag rejects them). - ---- - -## Transport matrix - -Configured via `HistorianClientOptions.Transport` (`HistorianTransport` enum). - -| Transport | Protocol | Platform | Verification | -|---|---|---|---| -| `LocalPipe` | WCF/MDAS over Net.NamedPipe (local) | Windows-only | live-verified (read / browse / metadata / event / status) | -| `RemoteTcpIntegrated` | WCF/MDAS over Net.TCP + Windows transport auth | Windows-only | live-verified (full read / browse / metadata / event / status surface) | -| `RemoteTcpCertificate` | WCF/MDAS over Net.TCP + server-cert TLS | Windows-only | `ProbeAsync` live-verified; deeper coverage pending | -| `RemoteGrpc` | gRPC (2023 R2), Grpc.Net.Client/.Web | cross-platform | **live-verified 2026-06-19** against a 2023 R2 server — TLS + `StorageService.ValidateClientCredential` NTLM handshake + raw read returning correct values | - ---- - -## DI registration - -```csharp -services.AddZbSpHistorianClient(new HistorianClientOptions -{ - Host = "localhost", - IntegratedSecurity = true, - Transport = HistorianTransport.RemoteTcpIntegrated, -}); -``` - -`AddZbSpHistorianClient` registers the options instance (singleton) + `HistorianClient` -(transient). Because `HistorianClientOptions` uses `required`/`init`-only properties, the -consumer passes a fully-built instance. In a real app, bind it from configuration: - -```csharp -services.AddZbSpHistorianClient( - config.GetSection("Historian").Get()!); -``` - -The package depends only on `Microsoft.Extensions.DependencyInjection.Abstractions` — no -ASP.NET Core or framework reference required. - ---- - -## Architecture - -Three decoupled subsystems under `src/ZB.MOM.WW.SPHistorianClient/`: - -| Subsystem | Path | Responsibility | -|---|---|---| -| Public façade | `HistorianClient.cs`, `HistorianClientOptions.cs` | Entry point; delegates to the transport layer | -| WCF/MDAS layer | `Wcf/` | Managed WCF transport; custom `MdasMessageEncoder`, binding factory, versioned service contracts | -| Binary frame layer | `Protocol/` | `Historian2020ProtocolDialect`; methods without protocol evidence throw `ProtocolEvidenceMissingException` | -| Public models | `Models/` | Public DTOs and enums (`HistorianSample`, `HistorianTagMetadata`, `RetrievalMode`, …) | -| gRPC transport | `Grpc/` | 2023 R2 gRPC transport; recovered `.proto` compiled by Grpc.Tools at build; wire contracts keep AVEVA's `ArchestrA.Grpc.Contract.*` namespaces | - ---- - -## Build, test, and pack commands - -```bash -# From ZB.MOM.WW.SPHistorianClient/ - -# Build -dotnet build ZB.MOM.WW.SPHistorianClient.slnx -dotnet build ZB.MOM.WW.SPHistorianClient.slnx -c Release - -# Test (offline unit/golden-byte tests run on any OS; -# Windows-only WCF tests no-op off Windows; -# live integration tests skip when env vars are unset — see below) -dotnet test ZB.MOM.WW.SPHistorianClient.slnx - -# Run a single test class -dotnet test ZB.MOM.WW.SPHistorianClient.slnx --filter "FullyQualifiedName~WcfDataQueryProtocolTests" - -# Pack (one .nupkg lands in artifacts/) -dotnet pack ZB.MOM.WW.SPHistorianClient.slnx -c Release -o ./artifacts -``` - -### Test posture - -All tests run offline by default; live integration tests are gated by environment variables -and skip cleanly when unset. - -| Test type | Count | -|---|---| -| Offline unit / golden-byte tests | the bulk of the 191 total | -| WCF live integration (gated) | skipped off Windows or when `HISTORIAN_HOST` is unset | -| gRPC live integration (gated) | skipped when `HISTORIAN_GRPC_HOST` is unset | -| **Total** | **191** | - -`GeneratePackageOnBuild` is off — pack explicitly with the command above. - -### Live integration test environment variables - -**WCF transports:** - -| Variable | Required | Notes | -|---|---|---| -| `HISTORIAN_HOST` | yes (gates WCF tests) | Historian server hostname or IP | -| `HISTORIAN_PORT` | optional | Override the WCF TCP port (default 32568) | -| `HISTORIAN_TEST_TAG` | yes | A historized tag that exists on the server (use a system tag such as `SysTimeSec` for safe testing) | -| `HISTORIAN_USER` | optional | Omit to use Windows integrated security | -| `HISTORIAN_PASSWORD` | optional | Only used when `HISTORIAN_USER` is set | -| `HISTORIAN_TAG_FILTER` | optional | Browse filter pattern passed to `BrowseTagNamesAsync` | - -**gRPC transport:** - -| Variable | Required | Notes | -|---|---|---| -| `HISTORIAN_GRPC_HOST` | yes (gates gRPC tests) | 2023 R2 gRPC endpoint host | -| `HISTORIAN_GRPC_PORT` | optional | Default 32565 | -| `HISTORIAN_GRPC_TLS` | optional | Set `true` to enable TLS | -| `HISTORIAN_GRPC_DNSID` | optional | Override DNS identity for certificate validation | - ---- - -## Status and provenance - -**Version 0.1.0.** Ported from a reverse-engineering migration bundle and rebranded into this -repo. Builds and all 191 tests pass on macOS. NOT yet packed/published to the Gitea NuGet feed. -NOT yet adopted by any consumer (OtOpcUa, MxAccessGateway, ScadaBridge). - -Production code is pure-managed .NET 10 with no native AVEVA reference. Reverse-engineering -tooling and proprietary decompilations from the source bundle were intentionally excluded from -this repo. - -**Safety rules for this library (hard — never violate):** -- Never commit real server hostnames, IP addresses, or credentials. -- Never commit customer tag names or live capture data (`.gitignore` blocks `*.ndjson` and - similar raw-capture extensions). -- Use only generic placeholders (`localhost`, ``) and built-in AVEVA - system tags (e.g., `SysTimeSec`) in all documentation and test defaults. diff --git a/ZB.MOM.WW.SPHistorianClient/Directory.Build.props b/ZB.MOM.WW.SPHistorianClient/Directory.Build.props deleted file mode 100644 index c4755a6..0000000 --- a/ZB.MOM.WW.SPHistorianClient/Directory.Build.props +++ /dev/null @@ -1,12 +0,0 @@ - - - - net10.0 - enable - enable - latest - 0.1.0 - true - - - diff --git a/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props b/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props deleted file mode 100644 index ead2913..0000000 --- a/ZB.MOM.WW.SPHistorianClient/Directory.Packages.props +++ /dev/null @@ -1,31 +0,0 @@ - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ZB.MOM.WW.SPHistorianClient/README.md b/ZB.MOM.WW.SPHistorianClient/README.md deleted file mode 100644 index bcf0cdb..0000000 --- a/ZB.MOM.WW.SPHistorianClient/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# ZB.MOM.WW.SPHistorianClient - -Pure-managed .NET 10 client library for **AVEVA System Platform Historian** (Wonderware). -Part of the ZB.MOM.WW SCADA family. - -No native AVEVA runtime dependency — `aahClientManaged.dll` / `aahClient.dll` are **not** -required. The wire protocol is re-implemented in managed C#. Live WCF transports require -Windows; offline tests and gRPC run cross-platform. - ---- - -## Quick start - -```csharp -using ZB.MOM.WW.SPHistorianClient; -using ZB.MOM.WW.SPHistorianClient.Models; - -await using HistorianClient client = new(new HistorianClientOptions -{ - Host = "localhost", - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe, -}); - -DateTime endUtc = DateTime.UtcNow; -DateTime startUtc = endUtc - TimeSpan.FromMinutes(10); - -await foreach (HistorianSample sample in client.ReadRawAsync( - "SysTimeSec", startUtc, endUtc, maxValues: 100)) -{ - Console.WriteLine($"{sample.TimestampUtc:o} {sample.NumericValue} Q={sample.Quality}"); -} -``` - -### DI registration - -```csharp -// In Program.cs / Startup -services.AddZbSpHistorianClient( - config.GetSection("Historian").Get()!); - -// Resolves HistorianClient (transient) from the container -``` - ---- - -## Supported operations - -| Operation | Status | -|---|---| -| `ProbeAsync` | live-verified | -| `ReadRawAsync` | live-verified | -| `ReadAggregateAsync` | live-verified across the `RetrievalMode` enum (15 modes) | -| `ReadAtTimeAsync` | live-verified | -| `ReadBlocksAsync` | block history read | -| `ReadEventsAsync` | live-verified (typed event + property bag) | -| `BrowseTagNamesAsync` | live-verified | -| `GetTagMetadataAsync` | live-verified across many native data-type codes | -| `GetConnectionStatusAsync` | synthesized from authenticated probe | -| `GetStoreForwardStatusAsync` | synthesized defaults | -| `GetSystemParameterAsync` | live-verified | -| `EnsureTagAsync` | live-verified for analog `Float`; `Double`/`Int2`/`Int4`/`UInt4` supported | -| `DeleteTagAsync` | live-verified (see note below) | - -> **Note:** Writing sample values is architecturally blocked — the Historian server cache only -> ingests from configured IOServer / Application Server pipelines. `DeleteTagAsync` server-side -> cascade may not always complete; use SMC as a fallback to clean up sandbox tags. - ---- - -## Transport matrix - -| Transport | Protocol | Platform | Verification | -|---|---|---|---| -| `LocalPipe` | WCF/MDAS over Net.NamedPipe | Windows-only | live-verified | -| `RemoteTcpIntegrated` | WCF/MDAS over Net.TCP + Windows auth | Windows-only | live-verified | -| `RemoteTcpCertificate` | WCF/MDAS over Net.TCP + TLS | Windows-only | `ProbeAsync` live-verified; deeper coverage pending | -| `RemoteGrpc` | gRPC (2023 R2) | cross-platform | live-verified 2026-06-19 | - ---- - -## Build and test - -```bash -# From ZB.MOM.WW.SPHistorianClient/ - -dotnet build ZB.MOM.WW.SPHistorianClient.slnx -dotnet test ZB.MOM.WW.SPHistorianClient.slnx - -# Pack -dotnet pack ZB.MOM.WW.SPHistorianClient.slnx -c Release -o ./artifacts -``` - -Offline unit tests (191 total) run on any OS. Live integration tests are gated by environment -variables (`HISTORIAN_HOST` for WCF, `HISTORIAN_GRPC_HOST` for gRPC) and skip cleanly when unset. -See `CLAUDE.md` for the full environment variable reference. diff --git a/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.slnx b/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.slnx deleted file mode 100644 index a4d8a21..0000000 --- a/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.slnx +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/ZB.MOM.WW.SPHistorianClient/artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg b/ZB.MOM.WW.SPHistorianClient/artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg deleted file mode 100644 index 81f0973..0000000 Binary files a/ZB.MOM.WW.SPHistorianClient/artifacts/ZB.MOM.WW.SPHistorianClient.0.1.0.nupkg and /dev/null differ diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs deleted file mode 100644 index a8e231d..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/DependencyInjection/ZbSpHistorianClientServiceCollectionExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace ZB.MOM.WW.SPHistorianClient; - -/// -/// ZB.MOM.WW DI registration for . Mirrors the family's -/// AddZb* convention. Because is required/ -/// init-only, callers pass a fully-built options instance (bind it from configuration in the -/// consuming app, e.g. config.GetSection("Historian").Get<HistorianClientOptions>()). -/// -public static class ZbSpHistorianClientServiceCollectionExtensions -{ - public static IServiceCollection AddZbSpHistorianClient( - this IServiceCollection services, - HistorianClientOptions options) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(options); - if (string.IsNullOrWhiteSpace(options.Host)) - { - throw new ArgumentException( - "HistorianClientOptions.Host must be set.", nameof(options)); - } - - services.AddSingleton(options); - // HistorianClient opens a fresh channel per operation and has a no-op DisposeAsync, - // so transient is safe and avoids assuming the shared dialect is concurrency-safe. - services.AddTransient(); - return services; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/HistorianGrpcChannelFactory.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/HistorianGrpcChannelFactory.cs deleted file mode 100644 index 9e7225c..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/HistorianGrpcChannelFactory.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using Grpc.Core; -using Grpc.Net.Client; -using Grpc.Net.Client.Web; - -namespace ZB.MOM.WW.SPHistorianClient.Grpc; - -/// -/// Builds a for the 2023 R2 Historian Client Access Point, -/// replicating the stock Archestra.Historian.GrpcClient.GrpcClientBase.InitializeBase -/// transport shape: gRPC-Web (binary) over HTTP/1.1, optional TLS with an -/// untrusted-certificate bypass, and gzip request encoding. -/// -internal static class HistorianGrpcChannelFactory -{ - /// - /// Resolves the effective gRPC port: when the caller left - /// at the WCF default (32568), the 2023 R2 gRPC default (32565) is substituted; otherwise the - /// explicit value is honoured. - /// - internal static int ResolvePort(HistorianClientOptions options) => - options.Port == HistorianClientOptions.DefaultPort ? HistorianClientOptions.DefaultGrpcPort : options.Port; - - /// - /// Builds the channel address. TLS uses https://{ServerDnsIdentity|Host}:{port} (the - /// DNS-identity override lets the URL match the server certificate name when connecting by IP); - /// plaintext uses http://{Host}:{port}. - /// - internal static string ResolveAddress(HistorianClientOptions options) - { - int port = ResolvePort(options); - if (options.GrpcUseTls) - { - string tlsHost = !string.IsNullOrEmpty(options.ServerDnsIdentity) ? options.ServerDnsIdentity! : options.Host; - return $"https://{tlsHost}:{port}"; - } - - return $"http://{options.Host}:{port}"; - } - - public static HistorianGrpcConnection Create(HistorianClientOptions options) - { - string address = ResolveAddress(options); - - var httpHandler = new HttpClientHandler(); - if (options.AllowUntrustedServerCertificate) - { - httpHandler.ServerCertificateCustomValidationCallback = AcceptAnyCertificate; - } - - // gRPC-Web binary mode over HTTP/1.1 — matches the stock client (GrpcWebMode.GrpcWeb, - // HttpVersion 1.1). The 2023 R2 HCAP endpoint speaks gRPC-Web, not bare HTTP/2 gRPC. - var webHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, httpHandler) - { - HttpVersion = new Version(1, 1) - }; - - var channelOptions = new GrpcChannelOptions - { - HttpHandler = webHandler - }; - - GrpcChannel channel = GrpcChannel.ForAddress(address, channelOptions); - - // The stock client always advertises gzip request encoding; honour the option so - // bandwidth-limited links can disable it. - var metadata = new Metadata(); - if (options.Compression) - { - metadata.Add("grpc-internal-encoding-request", "gzip"); - } - - return new HistorianGrpcConnection(channel, metadata); - } - - private static bool AcceptAnyCertificate( - HttpRequestMessage request, - X509Certificate2? certificate, - X509Chain? chain, - SslPolicyErrors errors) => true; -} - -/// A live gRPC channel plus the per-call metadata header set. -internal sealed class HistorianGrpcConnection(GrpcChannel channel, Metadata metadata) : IDisposable -{ - public GrpcChannel Channel { get; } = channel; - - public Metadata Metadata { get; } = metadata; - - public void Dispose() => Channel.Dispose(); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/HistorianGrpcReadOrchestrator.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/HistorianGrpcReadOrchestrator.cs deleted file mode 100644 index d040949..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/HistorianGrpcReadOrchestrator.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System.Runtime.CompilerServices; -using Google.Protobuf; -using Grpc.Core; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using GrpcHistory = ArchestrA.Grpc.Contract.History; -using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval; -using GrpcStorage = ArchestrA.Grpc.Contract.Storage; - -namespace ZB.MOM.WW.SPHistorianClient.Grpc; - -/// -/// 2023 R2 gRPC read orchestrator. Mirrors over the -/// gRPC transport: the same native binary buffers travel inside protobuf bytes fields, -/// and the same serializers/parsers (, -/// ) are reused unchanged. -/// -/// Operation mapping (2020 WCF → 2023 R2 gRPC): -/// Hist.GetInterfaceVersion → HistoryService.GetInterfaceVersion -/// Hist.ValidateClientCredential (loop) → StorageService.ValidateClientCredential (loop) -/// Hist.OpenConnection2 → HistoryService.OpenConnection -/// Retr.StartQuery2 → RetrievalService.StartQuery -/// Retr.GetNextQueryResultBuffer2 (loop) → RetrievalService.GetNextQueryResultBuffer (loop) -/// Retr.EndQuery2 → RetrievalService.EndQuery -/// -/// AUTH: the SSPI/Negotiate token loop runs through StorageService.ValidateClientCredential -/// (Handle + InBuff → Status + OutBuff) — per the 2023 R2 contract analysis, that op carries the -/// NTLM/SSPI tokens (the field names inBuff/outBuff match the 2020 native contract), whereas -/// HistoryService.ExchangeKey is a separate key-exchange/cert op (NOT the credential handshake). -/// OpenConnection and the retrieval chain stay on their original services; the server correlates -/// the validated context by the handshake GUID handle. Live-verified 2026-06-19 against a 2023 R2 -/// server (wonder-sql-vd03) — earlier ExchangeKey wiring was rejected at token round 0. -/// -internal sealed class HistorianGrpcReadOrchestrator -{ - private const ushort StartQueryRequestType = HistorianDataQueryProtocol.QueryRequestTypeData; - - private readonly HistorianClientOptions _options; - - public HistorianGrpcReadOrchestrator(HistorianClientOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public async IAsyncEnumerable ReadRawAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - int maxValues, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - ValidateAuth(); - cancellationToken.ThrowIfCancellationRequested(); - - IReadOnlyList rows = await Task.Run( - () => RunRawChain(tag, startUtc, endUtc, maxValues, cancellationToken), cancellationToken).ConfigureAwait(false); - foreach (HistorianSample sample in rows) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return sample; - } - } - - public async IAsyncEnumerable ReadAggregateAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - RetrievalMode mode, - TimeSpan interval, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - ValidateAuth(); - cancellationToken.ThrowIfCancellationRequested(); - - IReadOnlyList rows = await Task.Run( - () => RunAggregateChain(tag, startUtc, endUtc, mode, interval, cancellationToken), cancellationToken).ConfigureAwait(false); - foreach (HistorianAggregateSample sample in rows) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return sample; - } - } - - public Task> ReadAtTimeAsync( - string tag, - IReadOnlyList timestampsUtc, - CancellationToken cancellationToken) - { - ValidateAuth(); - cancellationToken.ThrowIfCancellationRequested(); - return Task.Run>(() => RunAtTimeChain(tag, timestampsUtc, cancellationToken), cancellationToken); - } - - private void ValidateAuth() - { - if (!_options.IntegratedSecurity && string.IsNullOrEmpty(_options.UserName)) - { - throw new ProtocolEvidenceMissingException( - "Managed gRPC read flow currently requires IntegratedSecurity or an explicit UserName + Password."); - } - } - - private List RunRawChain(string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken cancellationToken) - { - using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); - uint clientHandle = OpenAuthenticatedConnection(connection, cancellationToken); - HistorianDataQueryRequest request = HistorianWcfReadOrchestrator.BuildDataQueryRequest(tag, startUtc, endUtc, maxValues); - return RunQuery(connection, clientHandle, request, maxValues, cancellationToken); - } - - private List RunAggregateChain( - string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken cancellationToken) - { - using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); - uint clientHandle = OpenAuthenticatedConnection(connection, cancellationToken); - return RunAggregateQuery(connection, clientHandle, tag, startUtc, endUtc, mode, interval, cancellationToken); - } - - private List RunAtTimeChain(string tag, IReadOnlyList timestampsUtc, CancellationToken cancellationToken) - { - if (timestampsUtc.Count == 0) - { - return []; - } - - using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); - uint clientHandle = OpenAuthenticatedConnection(connection, cancellationToken); - - List results = new(timestampsUtc.Count); - foreach (DateTime ts in timestampsUtc) - { - cancellationToken.ThrowIfCancellationRequested(); - DateTime tsUtc = ts.ToUniversalTime(); - List aggregates = RunAggregateQuery( - connection, - clientHandle, - tag, - tsUtc - TimeSpan.FromTicks(1), - tsUtc + TimeSpan.FromTicks(1), - RetrievalMode.Interpolated, - TimeSpan.FromTicks(2), - cancellationToken); - - if (aggregates.Count == 0) - { - continue; - } - - HistorianAggregateSample chosen = aggregates[0]; - results.Add(new HistorianSample( - TagName: chosen.TagName, - TimestampUtc: tsUtc, - NumericValue: chosen.Value, - StringValue: null, - Quality: chosen.Quality, - QualityDetail: chosen.QualityDetail, - OpcQuality: chosen.OpcQuality, - PercentGood: 100)); - } - - return results; - } - - private uint OpenAuthenticatedConnection(HistorianGrpcConnection connection, CancellationToken cancellationToken) - { - Guid contextKey = Guid.NewGuid(); - var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); - var storageClient = new GrpcStorage.StorageService.StorageServiceClient(connection.Channel); - - historyClient.GetInterfaceVersion(new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken); - - HistorianNativeHandshake.RunTokenRounds( - (handle, wrapped, _) => - { - GrpcStorage.ValidateClientCredentialResponse response = storageClient.ValidateClientCredential( - new GrpcStorage.ValidateClientCredentialRequest { Handle = handle, InBuff = ByteString.CopyFrom(wrapped) }, - connection.Metadata, - Deadline(), - cancellationToken); - byte[] serverOutput = response.OutBuff?.ToByteArray() ?? []; - byte[] error = response.Status?.BtError?.ToByteArray() ?? []; - bool success = response.Status?.BSuccess ?? false; - return new HistorianNativeHandshake.TokenExchangeResult(success, serverOutput, error); - }, - contextKey, - _options, - cancellationToken); - - byte[] open2Request = HistorianNativeHandshake.BuildOpenConnection3Request( - _options.Host, contextKey, HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode); - - GrpcHistory.OpenConnectionResponse open2 = historyClient.OpenConnection( - new GrpcHistory.OpenConnectionRequest { BtConnectionRequest = ByteString.CopyFrom(open2Request) }, - connection.Metadata, - Deadline(), - cancellationToken); - - byte[] open2Response = open2.BtConnectionResponse?.ToByteArray() ?? []; - if (!(open2.Status?.BSuccess ?? false)) - { - byte[] err = open2.Status?.BtError?.ToByteArray() ?? []; - throw new InvalidOperationException($"gRPC OpenConnection failed (errorLen={err.Length}, responseLen={open2Response.Length})."); - } - - (uint clientHandle, _) = HistorianNativeHandshake.ParseOpenConnectionResponse(open2Response); - return clientHandle; - } - - private List RunQuery( - HistorianGrpcConnection connection, - uint clientHandle, - HistorianDataQueryRequest request, - int maxValues, - CancellationToken cancellationToken) - { - var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); - retrievalClient.GetRetrievalInterfaceVersion(new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), null, Deadline(), cancellationToken); - - byte[] requestBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(request); - uint queryHandle = StartQuery(retrievalClient, clientHandle, requestBuffer, "raw", cancellationToken); - - try - { - List samples = []; - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - (byte[] resultBuffer, byte[] errorBuffer) = GetNextResultBuffer(retrievalClient, clientHandle, queryHandle, "raw", cancellationToken); - - if (!HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows(resultBuffer, errorBuffer, out IReadOnlyList rows, out bool hasMoreData)) - { - throw new InvalidOperationException($"gRPC GetNextQueryResultBuffer returned an unparsable result buffer (length={resultBuffer.Length})."); - } - - foreach (HistorianSample sample in rows) - { - samples.Add(sample); - if (samples.Count >= maxValues) - { - return samples; - } - } - - if (!hasMoreData) - { - return samples; - } - } - } - finally - { - EndQuerySafely(retrievalClient, clientHandle, queryHandle); - } - } - - private List RunAggregateQuery( - HistorianGrpcConnection connection, - uint clientHandle, - string tag, - DateTime startUtc, - DateTime endUtc, - RetrievalMode mode, - TimeSpan interval, - CancellationToken cancellationToken) - { - var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); - retrievalClient.GetRetrievalInterfaceVersion(new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), null, Deadline(), cancellationToken); - - HistorianDataQueryRequest request = HistorianWcfReadOrchestrator.BuildAggregateQueryRequest(tag, startUtc, endUtc, mode, interval); - byte[] requestBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(request); - uint queryHandle = StartQuery(retrievalClient, clientHandle, requestBuffer, $"aggregate {mode}", cancellationToken); - - try - { - List samples = []; - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - (byte[] resultBuffer, byte[] errorBuffer) = GetNextResultBuffer(retrievalClient, clientHandle, queryHandle, $"aggregate {mode}", cancellationToken); - - if (!HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferAggregateRows( - resultBuffer, errorBuffer, mode, interval, out IReadOnlyList rows, out bool hasMoreData)) - { - throw new InvalidOperationException($"gRPC GetNextQueryResultBuffer (aggregate {mode}) returned an unparsable buffer (length={resultBuffer.Length})."); - } - - samples.AddRange(rows); - if (!hasMoreData) - { - return samples; - } - } - } - finally - { - EndQuerySafely(retrievalClient, clientHandle, queryHandle); - } - } - - private uint StartQuery( - GrpcRetrieval.RetrievalService.RetrievalServiceClient client, - uint clientHandle, - byte[] requestBuffer, - string label, - CancellationToken cancellationToken) - { - GrpcRetrieval.StartQueryResponse response = client.StartQuery( - new GrpcRetrieval.StartQueryRequest - { - UiHandle = clientHandle, - UiQueryRequestType = StartQueryRequestType, - BtRequestBuffer = ByteString.CopyFrom(requestBuffer) - }, - null, - Deadline(), - cancellationToken); - - if (!(response.Status?.BSuccess ?? false)) - { - byte[] err = response.Status?.BtError?.ToByteArray() ?? []; - throw new InvalidOperationException($"gRPC StartQuery ({label}) failed (errorLen={err.Length})."); - } - - return response.UiQueryHandle; - } - - private (byte[] ResultBuffer, byte[] ErrorBuffer) GetNextResultBuffer( - GrpcRetrieval.RetrievalService.RetrievalServiceClient client, - uint clientHandle, - uint queryHandle, - string label, - CancellationToken cancellationToken) - { - GrpcRetrieval.GetNextQueryResultBufferResponse response = client.GetNextQueryResultBuffer( - new GrpcRetrieval.GetNextQueryResultBufferRequest { UiHandle = clientHandle, UiQueryHandle = queryHandle }, - null, - Deadline(), - cancellationToken); - - byte[] errorBuffer = response.Status?.BtError?.ToByteArray() ?? []; - if (!(response.Status?.BSuccess ?? false)) - { - throw new InvalidOperationException($"gRPC GetNextQueryResultBuffer ({label}) failed (errorLen={errorBuffer.Length})."); - } - - byte[] resultBuffer = response.BtQueryResult?.ToByteArray() ?? []; - return (resultBuffer, errorBuffer); - } - - private void EndQuerySafely(GrpcRetrieval.RetrievalService.RetrievalServiceClient client, uint clientHandle, uint queryHandle) - { - try - { - client.EndQuery( - new GrpcRetrieval.EndQueryRequest { UiHandle = clientHandle, UiQueryHandle = queryHandle }, - null, - Deadline(), - CancellationToken.None); - } - catch - { - // Best-effort cleanup; the read result is already collected. - } - } - - private DateTime Deadline() => DateTime.UtcNow.Add(_options.RequestTimeout); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/HistoryService.proto b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/HistoryService.proto deleted file mode 100644 index 5207efe..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/HistoryService.proto +++ /dev/null @@ -1,209 +0,0 @@ -// Recovered from HistoryService.proto (AVEVA Historian SDK 2023 R2, Archestra.Grpc.Contract). -// Reconstructed from the embedded protobuf FileDescriptor; field numbers are authoritative. -syntax = "proto3"; - -import "Status.proto"; - -option csharp_namespace = "ArchestrA.Grpc.Contract.History"; - -message CreateTagResponse { - bool bSuccess = 1; - bytes tagid = 2; -} - -message GetInterfaceVersionRequest { -} - -message GetInterfaceVersionResponse { - uint32 uiError = 1; - uint32 uiVersion = 2; -} - -message OpenConnectionRequest { - bytes btConnectionRequest = 1; -} - -message OpenConnectionResponse { - .Status status = 1; - bytes btConnectionResponse = 2; -} - -message CloseConnectionRequest { - string strHandle = 1; -} - -message CloseConnectionResponse { - .Status status = 1; -} - -message UpdateClientStatusRequest { - string strHandle = 1; - bytes btClientStatus = 2; -} - -message UpdateClientStatusResponse { - .Status status = 1; - bytes btServerStatus = 2; -} - -message RegisterTagsRequest { - string strHandle = 1; - bytes btTagInfos = 2; -} - -message RegisterTagsResponse { - .Status status = 1; - bytes btTagStatus = 2; -} - -message EnsureTagsRequest { - string strHandle = 1; - bytes btTagInfos = 2; - uint32 elementCount = 3; -} - -message EnsureTagsResponse { - .Status status = 1; - bytes btTagStatus = 2; -} - -message AddStreamValuesRequest { - string strHandle = 1; - bytes btValues = 2; -} - -message AddStreamValuesResponse { - .Status status = 1; -} - -message TagExtendedProperty { - enum TagExtendedPropertyDataType { - String = 0; - Int16 = 1; - Int32 = 2; - Int64 = 3; - Double = 4; - Boolean = 5; - DateTimeOffset = 6; - Guid = 7; - Geography = 8; - Geometry = 9; - } - - string PropertyName = 1; - .TagExtendedProperty.TagExtendedPropertyDataType type = 2; - bytes value = 3; - bool Facetable = 4; - bool Searchable = 5; - bool SubstringSearchable = 6; -} - -message TagExtendedPropertyGroup { - string tagname = 1; - repeated .TagExtendedProperty TagExtendedProperties = 2; -} - -message AddTagExtendedPropertyRequest { - string strHandle = 1; - repeated .TagExtendedPropertyGroup TagExtendedPropertyGroups = 2; -} - -message AddTagExtendedPropertyResponse { - .Status status = 1; -} - -message ExchangeKeyRequest { - string strHandle = 1; - bytes btInput = 2; -} - -message ExchangeKeyResponse { - .Status status = 1; - bytes btOutput = 2; -} - -message StartJobRequest { - string strHandle = 1; - bytes btInput = 2; -} - -message StartJobResponse { - .Status status = 1; - string strJobid = 2; -} - -message GetJobStatusRequest { - string strHandle = 1; - string strJobid = 2; -} - -message GetJobStatusResponse { - .Status status = 1; - bytes btJobStatus = 2; -} - -message AddTagExtendedPropertiesRequest { - string strHandle = 1; - bytes btTeps = 2; -} - -message AddTagExtendedPropertiesResponse { - .Status status = 1; -} - -message DeleteTagExtendedPropertiesRequest { - string strHandle = 1; - bytes btInput = 2; -} - -message DeleteTagExtendedPropertiesResponse { - .Status status = 1; -} - -message DeleteTagsRequest { - uint32 uiHandle = 1; - bytes btTagnames = 2; -} - -message DeleteTagsResponse { - .Status status = 1; - bytes btDeleteTagStatus = 2; -} - -message AddTagLocalizedPropertiesRequest { - string strHandle = 1; - bytes btInput = 2; -} - -message AddTagLocalizedPropertiesResponse { - .Status status = 1; -} - -message DeleteTagLocalizedPropertiesRequest { - string strHandle = 1; - bytes btInput = 2; -} - -message DeleteTagLocalizedPropertiesResponse { - .Status status = 1; -} - -service HistoryService { - rpc GetInterfaceVersion (.GetInterfaceVersionRequest) returns (.GetInterfaceVersionResponse); - rpc ExchangeKey (.ExchangeKeyRequest) returns (.ExchangeKeyResponse); - rpc OpenConnection (.OpenConnectionRequest) returns (.OpenConnectionResponse); - rpc CloseConnection (.CloseConnectionRequest) returns (.CloseConnectionResponse); - rpc UpdateClientStatus (.UpdateClientStatusRequest) returns (.UpdateClientStatusResponse); - rpc RegisterTags (.RegisterTagsRequest) returns (.RegisterTagsResponse); - rpc EnsureTags (.EnsureTagsRequest) returns (.EnsureTagsResponse); - rpc AddStreamValues (.AddStreamValuesRequest) returns (.AddStreamValuesResponse); - rpc AddTagExtendedPropertyGroups (.AddTagExtendedPropertyRequest) returns (.AddTagExtendedPropertyResponse); - rpc AddTagExtendedProperties (.AddTagExtendedPropertiesRequest) returns (.AddTagExtendedPropertiesResponse); - rpc StartJob (.StartJobRequest) returns (.StartJobResponse); - rpc GetJobStatus (.GetJobStatusRequest) returns (.GetJobStatusResponse); - rpc DeleteTagExtendedProperties (.DeleteTagExtendedPropertiesRequest) returns (.DeleteTagExtendedPropertiesResponse); - rpc DeleteTags (.DeleteTagsRequest) returns (.DeleteTagsResponse); - rpc AddTagLocalizedProperties (.AddTagLocalizedPropertiesRequest) returns (.AddTagLocalizedPropertiesResponse); - rpc DeleteTagLocalizedProperties (.DeleteTagLocalizedPropertiesRequest) returns (.DeleteTagLocalizedPropertiesResponse); -} - diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/RetrievalService.proto b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/RetrievalService.proto deleted file mode 100644 index 8f50c13..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/RetrievalService.proto +++ /dev/null @@ -1,186 +0,0 @@ -// Recovered from RetrievalService.proto (AVEVA Historian SDK 2023 R2, Archestra.Grpc.Contract). -// Reconstructed from the embedded protobuf FileDescriptor; field numbers are authoritative. -syntax = "proto3"; - -import "Status.proto"; - -option csharp_namespace = "ArchestrA.Grpc.Contract.Retrieval"; - -message GetRetrievalInterfaceVersionRequest { -} - -message GetRetrievalInterfaceVersionResponse { - uint32 uiError = 1; - uint32 uiVersion = 2; -} - -message StartQueryRequest { - uint32 uiHandle = 1; - uint32 uiQueryRequestType = 2; - bytes btRequestBuffer = 3; -} - -message StartQueryResponse { - .Status status = 1; - uint32 uiQueryHandle = 2; - bytes btResponseBuffer = 3; -} - -message GetNextQueryResultBufferRequest { - uint32 uiHandle = 1; - uint32 uiQueryHandle = 2; -} - -message GetNextQueryResultBufferResponse { - .Status status = 1; - bytes btQueryResult = 2; -} - -message EndQueryRequest { - uint32 uiHandle = 1; - uint32 uiQueryHandle = 2; -} - -message EndQueryResponse { - .Status status = 1; -} - -message GetShardTagidsByTagnameAndSourceRequest { - string strHandle = 1; - bytes btTagnameAndSource = 2; -} - -message GetShardTagidsByTagnameAndSourceResponse { - .Status status = 1; - bytes btShardTagids = 2; -} - -message GetTagInfosFromNameRequest { - string strHandle = 1; - bytes btTagNames = 2; - uint32 uiSequence = 3; -} - -message GetTagInfosFromNameResponse { - .Status status = 1; - bytes btTagInfos = 2; - uint32 uiSequence = 3; -} - -message GetTagExtendedPropertiesFromNameRequest { - string strHandle = 1; - bytes btTagNames = 2; - uint32 uiSequence = 3; -} - -message GetTagExtendedPropertiesFromNameResponse { - .Status status = 1; - bytes btTeps = 2; - uint32 uiSequence = 3; -} - -message ExecuteSqlCommandRequest { - string strHandle = 1; - string StrCommand = 2; - uint32 uiOption = 3; - uint32 uiQueryHandle = 4; -} - -message ExecuteSqlCommandResponse { - .Status status = 1; - int32 iRetValue = 2; - uint32 uiQueryHandle = 3; -} - -message StartEventQueryRequest { - uint32 uiHandle = 1; - uint32 uiQueryRequestType = 2; - bytes btRequest = 3; - uint32 uiQueryHandle = 4; -} - -message StartEventQueryResponse { - .Status status = 1; - uint32 uiQueryHandle = 2; - bytes btResonse = 3; -} - -message GetNextEventQueryResultBufferRequest { - uint32 uiHandle = 1; - uint32 uiQueryHandle = 2; -} - -message GetNextEventQueryResultBufferResponse { - .Status status = 1; - bytes btResult = 2; -} - -message EndEventQueryRequest { - uint32 uiHandle = 1; - uint32 uiQueryHandle = 2; -} - -message EndEventQueryResponse { - .Status status = 1; -} - -message StartTagQueryRequest { - string strHandle = 1; - bytes btRequest = 2; -} - -message StartTagQueryResponse { - .Status status = 1; - bytes btResponse = 2; -} - -message QueryTagRequest { - string strHandle = 1; - uint32 uiQueryHandle = 2; - bytes btRequest = 3; -} - -message QueryTagResponse { - .Status status = 1; - bytes btResonse = 2; -} - -message EndTagQueryRequest { - string strHandle = 1; - uint32 uiQueryHandle = 2; -} - -message EndTagQueryResponse { - .Status status = 1; -} - -message GetTagLocalizedPropertiesFromNameRequest { - string strHandle = 1; - bytes btTagNames = 2; - uint32 uiSequence = 3; -} - -message GetTagLocalizedPropertiesFromNameResponse { - .Status status = 1; - uint32 uiSequence = 2; - bytes btOutBuffer = 3; -} - -service RetrievalService { - rpc GetRetrievalInterfaceVersion (.GetRetrievalInterfaceVersionRequest) returns (.GetRetrievalInterfaceVersionResponse); - rpc StartQuery (.StartQueryRequest) returns (.StartQueryResponse); - rpc GetNextQueryResultBuffer (.GetNextQueryResultBufferRequest) returns (.GetNextQueryResultBufferResponse); - rpc EndQuery (.EndQueryRequest) returns (.EndQueryResponse); - rpc GetShardTagidsByTagnameAndSource (.GetShardTagidsByTagnameAndSourceRequest) returns (.GetShardTagidsByTagnameAndSourceResponse); - rpc GetTagInfosFromName (.GetTagInfosFromNameRequest) returns (.GetTagInfosFromNameResponse); - rpc GetTagExtendedPropertiesFromName (.GetTagExtendedPropertiesFromNameRequest) returns (.GetTagExtendedPropertiesFromNameResponse); - rpc ExecuteSqlCommand (.ExecuteSqlCommandRequest) returns (.ExecuteSqlCommandResponse); - rpc StartEventQuery (.StartEventQueryRequest) returns (.StartEventQueryResponse); - rpc GetNextEventQueryResultBuffer (.GetNextEventQueryResultBufferRequest) returns (.GetNextEventQueryResultBufferResponse); - rpc EndEventQuery (.EndEventQueryRequest) returns (.EndEventQueryResponse); - rpc StartTagQuery (.StartTagQueryRequest) returns (.StartTagQueryResponse); - rpc QueryTag (.QueryTagRequest) returns (.QueryTagResponse); - rpc EndTagQuery (.EndTagQueryRequest) returns (.EndTagQueryResponse); - rpc GetTagLocalizedPropertiesFromName (.GetTagLocalizedPropertiesFromNameRequest) returns (.GetTagLocalizedPropertiesFromNameResponse); -} - diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/Status.proto b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/Status.proto deleted file mode 100644 index 4623094..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/Status.proto +++ /dev/null @@ -1,12 +0,0 @@ -// Recovered from Status.proto (AVEVA Historian SDK 2023 R2, Archestra.Grpc.Contract). -// Reconstructed from the embedded protobuf FileDescriptor; field numbers are authoritative. -syntax = "proto3"; - - -option csharp_namespace = "ArchestrA.Grpc.Contract.RequestStatus"; - -message Status { - bool bSuccess = 1; - bytes btError = 2; -} - diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/StatusService.proto b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/StatusService.proto deleted file mode 100644 index 6f98388..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/StatusService.proto +++ /dev/null @@ -1,215 +0,0 @@ -// Recovered from StatusService.proto (AVEVA Historian SDK 2023 R2, Archestra.Grpc.Contract). -// Reconstructed from the embedded protobuf FileDescriptor; field numbers are authoritative. -syntax = "proto3"; - -import "Status.proto"; - -option csharp_namespace = "ArchestrA.Grpc.Contract.Status"; - -message GetStatusInterfaceVersionRequest { -} - -message GetStatusInterfaceVersionResponse { - uint32 uiError = 1; - uint32 uiVersion = 2; -} - -message GetSystemParameterRequest { - uint32 uiHandle = 1; - string strParameterName = 2; -} - -message GetSystemParameterResponse { - .Status status = 1; - string strParameterValue = 2; -} - -message SendInfoRequest { - string strHandle = 1; - string strPipeName = 2; - uint32 uiOption = 3; - bytes btReqBuff = 4; - string strInfoID = 5; -} - -message SendInfoResponse { - .Status status = 1; - string strInfoID = 2; - bytes btRespBuff = 3; -} - -message RequestInfoRequest { - string strHandle = 1; - string strInfoID = 2; - uint32 uiOffset = 3; -} - -message RequestInfoResponse { - .Status status = 1; - bytes btRespBuff = 2; -} - -message DeleteInfoRequest { - string strHandle = 1; - string strInfoID = 2; -} - -message DeleteInfoResponse { - .Status status = 1; -} - -message GetHistorianInfoRequest { - string strHandle = 1; - bytes btRequest = 2; -} - -message GetHistorianInfoResponse { - .Status status = 1; - bytes btHistorianInfo = 2; -} - -message StartProcessRequest { - string strHandle = 1; - string strPipeName = 2; - string strPath = 3; - string strAuguments = 4; - uint32 uiKeepAliveInterval = 5; - uint32 uiKeepAliveMethod = 6; -} - -message StartProcessResponse { - .Status status = 1; -} - -message StopProcessRequest { - string strHandle = 1; - string StrPipeName = 2; -} - -message StopProcessResponse { - .Status status = 1; -} - -message PingServerRequest { - string strHandle = 1; - string strPipeName = 2; - uint32 uiTimeout = 3; -} - -message PingServerResponse { - .Status status = 1; -} - -message PingPipeRequest { - string strHandle = 1; - string strPipeName = 2; -} - -message PingPipeResponse { - .Status status = 1; -} - -message ConfigureAutoStartProcessRequest { - string strHandle = 1; - string strPipeName = 2; - string strPath = 3; - string strAuguments = 4; - uint32 uiKeepAliveInterval = 5; - uint32 uiKeepAliveMethod = 6; - uint32 uiStartupFlags = 7; -} - -message ConfigureAutoStartProcessResponse { - .Status status = 1; -} - -message GetHistorianConsoleStatusRequest { - string strHandle = 1; -} - -message GetHistorianConsoleStatusResponse { - .Status status = 1; - uint32 uiConsoleStatus = 2; -} - -message GetRuntimeParameterRequest { - string strHandle = 1; - bytes btRequest = 2; -} - -message GetRuntimeParameterResponse { - .Status status = 1; - bytes btResponse = 2; -} - -message GetSystemTimeZoneNameRequest { - uint32 uiHandle = 1; -} - -message GetSystemTimeZoneNameResponse { - .Status status = 1; - string strSystemTimeZoneName = 2; -} - -message SetHistorianConsoleStatusRequest { - string strHandle = 1; - uint32 uiStatus = 2; - uint32 uiOption = 3; -} - -message SetHistorianConsoleStatusResponse { - .Status status = 1; -} - -message CanUpdateAreaHierarchyRequest { - uint32 uiHandle = 1; -} - -message CanUpdateAreaHierarchyResponse { - .Status status = 1; - bool canUpdate = 2; -} - -message UpdateAreaHierarchyRequest { - uint32 uiHandle = 1; - string guid = 2; - uint32 sequence = 3; - bytes buffer = 4; -} - -message UpdateAreaHierarchyResponse { - .Status status = 1; -} - -message UpdateObjectHierarchyRequest { - uint32 uiHandle = 1; - string guid = 2; - uint32 sequence = 3; - bytes buffer = 4; -} - -message UpdateObjectHierarchyResponse { - .Status status = 1; -} - -service StatusService { - rpc GetStatusInterfaceVersion (.GetStatusInterfaceVersionRequest) returns (.GetStatusInterfaceVersionResponse); - rpc GetSystemParameter (.GetSystemParameterRequest) returns (.GetSystemParameterResponse); - rpc SendInfo (.SendInfoRequest) returns (.SendInfoResponse); - rpc RequestInfo (.RequestInfoRequest) returns (.RequestInfoResponse); - rpc DeleteInfo (.DeleteInfoRequest) returns (.DeleteInfoResponse); - rpc GetHistorianInfo (.GetHistorianInfoRequest) returns (.GetHistorianInfoResponse); - rpc StartProcess (.StartProcessRequest) returns (.StartProcessResponse); - rpc StopProcess (.StopProcessRequest) returns (.StopProcessResponse); - rpc PingServer (.PingServerRequest) returns (.PingServerResponse); - rpc PingPipe (.PingPipeRequest) returns (.PingPipeResponse); - rpc ConfigureAutoStartProcess (.ConfigureAutoStartProcessRequest) returns (.ConfigureAutoStartProcessResponse); - rpc GetHistorianConsoleStatus (.GetHistorianConsoleStatusRequest) returns (.GetHistorianConsoleStatusResponse); - rpc GetRuntimeParameter (.GetRuntimeParameterRequest) returns (.GetRuntimeParameterResponse); - rpc GetSystemTimeZoneName (.GetSystemTimeZoneNameRequest) returns (.GetSystemTimeZoneNameResponse); - rpc SetHistorianConsoleStatus (.SetHistorianConsoleStatusRequest) returns (.SetHistorianConsoleStatusResponse); - rpc CanUpdateAreaHierarchy (.CanUpdateAreaHierarchyRequest) returns (.CanUpdateAreaHierarchyResponse); - rpc UpdateAreaHierarchy (.UpdateAreaHierarchyRequest) returns (.UpdateAreaHierarchyResponse); - rpc UpdateObjectHierarchy (.UpdateObjectHierarchyRequest) returns (.UpdateObjectHierarchyResponse); -} - diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/StorageService.proto b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/StorageService.proto deleted file mode 100644 index 352d149..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/StorageService.proto +++ /dev/null @@ -1,417 +0,0 @@ -// Recovered from StorageService.proto (AVEVA Historian SDK 2023 R2, Archestra.Grpc.Contract). -// Reconstructed from the embedded protobuf FileDescriptor; field numbers are authoritative. -syntax = "proto3"; - -import "Status.proto"; - -option csharp_namespace = "ArchestrA.Grpc.Contract.Storage"; - -message GetInterfaceVersionRequest { -} - -message GetInterfaceVersionResponse { - uint32 uiError = 1; - uint32 uiVersion = 2; -} - -message OpenStorageConnectionRequest { - string HostName = 1; - string EnginePath = 2; - uint32 FreeDiskSpace = 3; - string ProcessName = 4; - uint32 ProcessId = 5; - string UserName = 6; - bytes Password = 7; - uint32 PwdLength = 8; - uint32 ClientType = 9; - uint32 ClientVersion = 10; - uint32 ConnectionMode = 11; - uint32 ConnectionTimeout = 12; - string StorageSessionId = 13; -} - -message OpenStorageConnectionResponse { - .Status status = 1; - string StorageSessionId = 2; - uint32 Handle = 3; - uint64 ConnectionTime = 4; - uint32 ServerStatus = 5; -} - -message CloseStorageConnectionRequest { - uint32 Handle = 1; -} - -message CloseStorageConnectionResponse { - .Status status = 1; -} - -message PingRequest { - uint32 Handle = 1; -} - -message PingResponse { - .Status status = 1; - uint32 OutByteCount = 2; - bytes OutBuff = 3; -} - -message AddTagsRequest { - uint32 Handle = 1; - uint32 ElementCount = 2; - uint32 InByteCount = 3; - bytes InBuff = 4; -} - -message AddTagsResponse { - .Status status = 1; - uint32 OutByteCount = 2; - bytes OutBuff = 3; -} - -message RegisterTagsRequest { - uint32 Handle = 1; - uint32 ElementCount = 2; - uint32 InByteCount = 3; - bytes InBuff = 4; -} - -message RegisterTagsResponse { - .Status status = 1; - uint32 OutByteCount = 2; - bytes OutBuff = 3; -} - -message AddStreamValuesRequest { - uint32 Handle = 1; - uint32 Size = 2; - bytes Buffer = 3; -} - -message AddStreamValuesResponse { - .Status status = 1; -} - -message GetTagIdsRequest { - uint32 Handle = 1; - uint32 Sequence = 2; -} - -message GetTagIdsResponse { - .Status status = 1; - uint32 Sequence = 2; - uint32 Size = 3; - bytes TagIds = 4; -} - -message GetTagsRequest { - uint32 Handle = 1; - uint32 TagIdsSize = 2; - bytes TagIds = 3; - uint32 Sequence = 4; -} - -message GetTagsResponse { - .Status status = 1; - uint32 Sequence = 2; - uint32 TagInfosSize = 3; - bytes TagInfos = 4; -} - -message FlushMetadataRequest { - uint32 Handle = 1; - uint32 TagIdsSize = 2; - bytes TagIds = 3; -} - -message FlushMetadataResponse { - .Status status = 1; -} - -message FlushDataRequest { - uint32 Handle = 1; -} - -message FlushDataResponse { - .Status status = 1; -} - -message LoadBlocksRequest { - uint32 Handle = 1; - uint32 Sequence = 2; -} - -message LoadBlocksResponse { - .Status status = 1; - uint32 Sequence = 2; - uint32 HistoryBlockSize = 3; - bytes HistoryBlocks = 4; -} - -message GetSnapshotsRequest { - uint32 Handle = 1; - uint64 BlockStartTime = 2; - uint32 Sequence = 3; -} - -message GetSnapshotsResponse { - .Status status = 1; - uint32 Sequence = 2; - uint32 SnapshotSize = 3; - bytes Snapshot = 4; -} - -message StartQuerySnapshotRequest { - uint32 Handle = 1; - uint64 BlockStartTime = 2; - uint32 SnapshotInfoSize = 3; - bytes SnapshotInfo = 4; - uint32 SnapshotQueryId = 5; -} - -message StartQuerySnapshotResponse { - .Status status = 1; - uint32 SnapshotQueryId = 2; -} - -message NextQuerySnapshotRequest { - uint32 Handle = 1; - uint32 SnapshotQueryId = 2; - uint32 Sequence = 3; -} - -message NextQuerySnapshotResponse { - .Status status = 1; - uint32 Sequence = 2; - uint32 SnapshotSize = 3; - bytes Snapshot = 4; -} - -message EndSnapshotRequest { - uint32 Handle = 1; - uint32 SnapshotQueryId = 2; - uint64 BlockStartTime = 3; - uint32 SnapshotInfoSize = 4; - bytes SnapshotInfo = 5; - bool IsDeleteSnapshot = 6; -} - -message EndSnapshotResponse { - .Status status = 1; -} - -message StopRequest { - uint32 Handle = 1; -} - -message StopResponse { - .Status status = 1; -} - -message ClearTagidPairsRequest { - uint32 Handle = 1; -} - -message ClearTagidPairsResponse { - .Status status = 1; -} - -message AddTagidPairsRequest { - uint32 Handle = 1; - uint32 ElementCount = 2; - uint32 InByteCount = 3; - bytes InBuff = 4; -} - -message AddTagidPairsResponse { - .Status status = 1; -} - -message GetSFParameterRequest { - uint32 Handle = 1; - string ParameterName = 2; -} - -message GetSFParameterResponse { - .Status status = 1; - string ParamaterValue = 2; -} - -message SetSFParameterRequest { - uint32 Handle = 1; - string ParamaterName = 2; - string ParamaterValue = 3; -} - -message SetSFParameterResponse { - .Status status = 1; -} - -message SendSnapshotBeginRequest { - uint32 Handle = 1; - uint64 TotalSize = 2; - uint64 StartTime = 3; - uint64 EndTime = 4; - string StorageSessionId = 5; -} - -message SendSnapshotBeginResponse { - .Status status = 1; - string StorageSessionId = 2; - uint32 QueryId = 3; -} - -message SendSnapshotEndRequest { - uint32 Handle = 1; - string StorageSessionId = 2; - uint32 QueryId = 3; - uint32 TimeRangeSize = 4; - bytes TimeRangeBytes = 5; -} - -message SendSnapshotEndResponse { - .Status status = 1; -} - -message SendSnapshotRequest { - uint32 Handle = 1; - string StorageSessionId = 2; - uint32 QueryId = 3; - uint32 Size = 4; - uint64 SnapShotChunkOffset = 5; - bytes Buffer = 6; -} - -message SendSnapshotResponse { - .Status status = 1; -} - -message DeleteSnapshotRequest { - uint32 Handle = 1; - uint64 StartTime = 2; - uint32 SnapshotInfoSize = 3; - bytes SnapshotInfo = 4; -} - -message DeleteSnapshotResponse { - .Status status = 1; -} - -message AddStreamValues2Request { - uint32 Handle = 1; - string ShardId = 2; - bytes Buffer = 3; -} - -message AddStreamValues2Response { - .Status status = 1; -} - -message ClearShardTagidsRequest { - uint32 Handle = 1; -} - -message ClearShardTagidsResponse { - .Status status = 1; -} - -message AddShardTagidsRequest { - uint32 Handle = 1; - bytes Buffer = 2; -} - -message AddShardTagidsResponse { - .Status status = 1; -} - -message SplitUnknownShardsRequest { - uint32 Handle = 1; -} - -message SplitUnknownShardsResponse { - .Status status = 1; -} - -message GetRemainingSnapshotsSizeRequest { - uint32 Handle = 1; -} - -message GetRemainingSnapshotsSizeResponse { - .Status status = 1; - uint64 SnapshotSize = 2; -} - -message DeleteTagsRequest { - uint32 Handle = 1; - bytes Buffer = 2; -} - -message DeleteTagsResponse { - .Status status = 1; -} - -message OpenStorageConnection2Request { - bytes InParameters = 1; -} - -message OpenStorageConnection2Response { - .Status status = 1; - bytes OutParmaters = 2; -} - -message ValidateClientCredentialRequest { - string Handle = 1; - bytes InBuff = 2; -} - -message ValidateClientCredentialResponse { - .Status status = 1; - bytes OutBuff = 2; -} - -message GetInfoRequest { - string Request = 1; -} - -message GetInfoResponse { - .Status status = 1; - bytes info = 2; -} - -service StorageService { - rpc GetInterfaceVersion (.GetInterfaceVersionRequest) returns (.GetInterfaceVersionResponse); - rpc OpenStorageConnection (.OpenStorageConnectionRequest) returns (.OpenStorageConnectionResponse); - rpc CloseStorageConnection (.CloseStorageConnectionRequest) returns (.CloseStorageConnectionResponse); - rpc Ping (.PingRequest) returns (.PingResponse); - rpc AddTags (.AddTagsRequest) returns (.AddTagsResponse); - rpc RegisterTags (.RegisterTagsRequest) returns (.RegisterTagsResponse); - rpc AddStreamValues (.AddStreamValuesRequest) returns (.AddStreamValuesResponse); - rpc GetTagIds (.GetTagIdsRequest) returns (.GetTagIdsResponse); - rpc GetTags (.GetTagsRequest) returns (.GetTagsResponse); - rpc FlushMetadata (.FlushMetadataRequest) returns (.FlushMetadataResponse); - rpc FlushData (.FlushDataRequest) returns (.FlushDataResponse); - rpc LoadBlocks (.LoadBlocksRequest) returns (.LoadBlocksResponse); - rpc GetSnapshots (.GetSnapshotsRequest) returns (.GetSnapshotsResponse); - rpc StartQuerySnapshot (.StartQuerySnapshotRequest) returns (.StartQuerySnapshotResponse); - rpc NextQuerySnapshot (.NextQuerySnapshotRequest) returns (.NextQuerySnapshotResponse); - rpc EndSnapshot (.EndSnapshotRequest) returns (.EndSnapshotResponse); - rpc Stop (.StopRequest) returns (.StopResponse); - rpc ClearTagidPairs (.ClearTagidPairsRequest) returns (.ClearTagidPairsResponse); - rpc AddTagidPairs (.AddTagidPairsRequest) returns (.AddTagidPairsResponse); - rpc GetSFParameter (.GetSFParameterRequest) returns (.GetSFParameterResponse); - rpc SetSFParameter (.SetSFParameterRequest) returns (.SetSFParameterResponse); - rpc SendSnapshotBegin (.SendSnapshotBeginRequest) returns (.SendSnapshotBeginResponse); - rpc SendSnapshotEnd (.SendSnapshotEndRequest) returns (.SendSnapshotEndResponse); - rpc SendSnapshot (.SendSnapshotRequest) returns (.SendSnapshotResponse); - rpc DeleteSnapshot (.DeleteSnapshotRequest) returns (.DeleteSnapshotResponse); - rpc AddStreamValues2 (.AddStreamValues2Request) returns (.AddStreamValues2Response); - rpc ClearShardTagids (.ClearShardTagidsRequest) returns (.ClearShardTagidsResponse); - rpc AddShardTagids (.AddShardTagidsRequest) returns (.AddShardTagidsResponse); - rpc SplitUnknownShards (.SplitUnknownShardsRequest) returns (.SplitUnknownShardsResponse); - rpc GetRemainingSnapshotsSize (.GetRemainingSnapshotsSizeRequest) returns (.GetRemainingSnapshotsSizeResponse); - rpc DeleteTags (.DeleteTagsRequest) returns (.DeleteTagsResponse); - rpc OpenStorageConnection2 (.OpenStorageConnection2Request) returns (.OpenStorageConnection2Response); - rpc ValidateClientCredential (.ValidateClientCredentialRequest) returns (.ValidateClientCredentialResponse); - rpc GetInfo (.GetInfoRequest) returns (.GetInfoResponse); -} - diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/TransactionService.proto b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/TransactionService.proto deleted file mode 100644 index 2c0e02c..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Grpc/Protos/TransactionService.proto +++ /dev/null @@ -1,92 +0,0 @@ -// Recovered from TransactionService.proto (AVEVA Historian SDK 2023 R2, Archestra.Grpc.Contract). -// Reconstructed from the embedded protobuf FileDescriptor; field numbers are authoritative. -syntax = "proto3"; - -import "Status.proto"; - -option csharp_namespace = "ArchestrA.Grpc.Contract.Transaction"; - -message ForwardSnapshotRequest { - string strHandle = 1; - string strSessionID = 2; - uint32 queryID = 3; - uint64 snapShotChunkOffset = 4; - bytes btInput = 5; -} - -message ForwardSnapshotResponse { - .Status status = 1; -} - -message ForwardSnapshotBeginRequest { - string strHandle = 1; - uint64 totalSize = 2; - uint64 startTime = 3; - uint64 endTime = 4; -} - -message ForwardSnapshotBeginResponse { - string strSessionID = 1; - uint32 queryID = 2; - .Status status = 3; -} - -message ForwardSnapshotEndRequest { - string strHandle = 1; - string strSessionID = 2; - uint32 queryID = 3; - bytes timeRange = 4; -} - -message ForwardSnapshotEndResponse { - bytes tagIds = 1; - .Status status = 2; -} - -message GetTransactionInterfaceVersionRequest { -} - -message GetTransactionInterfaceVersionResponse { - uint32 error = 1; - uint32 version = 2; -} - -message AddNonStreamValuesBeginRequest { - string strHandle = 1; -} - -message AddNonStreamValuesBeginResponse { - .Status status = 1; - string strTransactionId = 2; -} - -message AddNonStreamValuesRequest { - string strHandle = 1; - string strTransactionId = 2; - bytes btInput = 3; -} - -message AddNonStreamValuesResponse { - .Status status = 1; -} - -message AddNonStreamValuesEndRequest { - string strHandle = 1; - string strTransactionId = 2; - bool bCommit = 3; -} - -message AddNonStreamValuesEndResponse { - .Status status = 1; -} - -service TransactionService { - rpc ForwardSnapshot (.ForwardSnapshotRequest) returns (.ForwardSnapshotResponse); - rpc ForwardSnapshotBegin (.ForwardSnapshotBeginRequest) returns (.ForwardSnapshotBeginResponse); - rpc ForwardSnapshotEnd (.ForwardSnapshotEndRequest) returns (.ForwardSnapshotEndResponse); - rpc GetTransactionInterfaceVersion (.GetTransactionInterfaceVersionRequest) returns (.GetTransactionInterfaceVersionResponse); - rpc AddNonStreamValuesBegin (.AddNonStreamValuesBeginRequest) returns (.AddNonStreamValuesBeginResponse); - rpc AddNonStreamValues (.AddNonStreamValuesRequest) returns (.AddNonStreamValuesResponse); - rpc AddNonStreamValuesEnd (.AddNonStreamValuesEndRequest) returns (.AddNonStreamValuesEndResponse); -} - diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianClient.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianClient.cs deleted file mode 100644 index 3727fa3..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianClient.cs +++ /dev/null @@ -1,165 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Protocol; -using ZB.MOM.WW.SPHistorianClient.Transport; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient; - -public sealed class HistorianClient : IAsyncDisposable -{ - private readonly HistorianClientOptions _options; - private readonly IHistorianTransportFactory _transportFactory; - private readonly Historian2020ProtocolDialect _protocol; - - public HistorianClient(HistorianClientOptions options) - : this(options, TcpHistorianTransport.Factory) - { - } - - internal HistorianClient(HistorianClientOptions options, IHistorianTransportFactory transportFactory) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _transportFactory = transportFactory ?? throw new ArgumentNullException(nameof(transportFactory)); - _protocol = new Historian2020ProtocolDialect(_options); - } - - public async Task ProbeAsync(CancellationToken cancellationToken = default) - { - return await HistorianWcfProbe.ProbeAsync(_options, cancellationToken).ConfigureAwait(false); - } - - public IAsyncEnumerable ReadRawAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - int maxValues, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tag); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxValues); - ValidateTimeRange(startUtc, endUtc); - - return _protocol.ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken); - } - - public IAsyncEnumerable ReadAggregateAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - RetrievalMode mode, - TimeSpan interval, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tag); - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(interval, TimeSpan.Zero); - ValidateTimeRange(startUtc, endUtc); - - return _protocol.ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken); - } - - public Task> ReadAtTimeAsync( - string tag, - IReadOnlyList timestampsUtc, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tag); - ArgumentNullException.ThrowIfNull(timestampsUtc); - - if (timestampsUtc.Count == 0) - { - return Task.FromResult>(Array.Empty()); - } - - return _protocol.ReadAtTimeAsync(tag, timestampsUtc, cancellationToken); - } - - public IAsyncEnumerable ReadBlocksAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tag); - ValidateTimeRange(startUtc, endUtc); - return _protocol.ReadBlocksAsync(tag, startUtc, endUtc, cancellationToken); - } - - public IAsyncEnumerable ReadEventsAsync( - DateTime startUtc, - DateTime endUtc, - CancellationToken cancellationToken = default) - { - ValidateTimeRange(startUtc, endUtc); - return _protocol.ReadEventsAsync(startUtc, endUtc, cancellationToken); - } - - public IAsyncEnumerable BrowseTagNamesAsync(string filter = "*", CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(filter); - return HistorianWcfTagClient.BrowseTagNamesAsync(_options, filter, cancellationToken); - } - - public Task GetTagMetadataAsync(string tag, CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tag); - return HistorianWcfTagClient.GetTagMetadataAsync(_options, tag, cancellationToken); - } - - public Task GetConnectionStatusAsync(CancellationToken cancellationToken = default) - { - return _protocol.GetConnectionStatusAsync(cancellationToken); - } - - public Task GetStoreForwardStatusAsync(CancellationToken cancellationToken = default) - { - return _protocol.GetStoreForwardStatusAsync(cancellationToken); - } - - public Task GetSystemParameterAsync(string name, CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(name); - return _protocol.GetSystemParameterAsync(name, cancellationToken); - } - - /// - /// Creates or updates the named tag in the Historian Runtime database via - /// EnsureTags2. Currently only is - /// live-verified. Note: writing data values to the new tag (via a separate - /// AddStreamedValue/AddS2 path) is NOT supported by the SDK — see - /// docs/plans/write-commands-reverse-engineering.md for the architectural - /// finding. - /// - public Task EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(definition); - return new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken); - } - - /// - /// Deletes the named tag via DeleteTags. **Known issue (2026-05-04):** - /// the SDK's DelT call returns true but the server-side cascading deletion does - /// not always complete (the row remains in Runtime.dbo.Tag). The - /// captured native flow's DelT removes the tag cleanly, so additional priming - /// or a side call between WCF DelT and server cascade is missing. Use the SMC - /// fallback to clean up sandbox tags until this is resolved. - /// - public Task DeleteTagAsync(string tagName, CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tagName); - return new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken); - } - - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } - - private static void ValidateTimeRange(DateTime startUtc, DateTime endUtc) - { - if (startUtc.ToUniversalTime() > endUtc.ToUniversalTime()) - { - throw new ArgumentException("Start time must be less than or equal to end time."); - } - } - -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianClientOptions.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianClientOptions.cs deleted file mode 100644 index 817d6c7..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianClientOptions.cs +++ /dev/null @@ -1,65 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Models; - -namespace ZB.MOM.WW.SPHistorianClient; - -public sealed class HistorianClientOptions -{ - public const int DefaultPort = 32568; - - /// Default TCP port of the 2023 R2 Historian Client Access Point gRPC endpoint. - public const int DefaultGrpcPort = 32565; - - public required string Host { get; init; } - - public int Port { get; init; } = DefaultPort; - - public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(5); - - public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(30); - - public string UserName { get; init; } = string.Empty; - - public string Password { get; init; } = string.Empty; - - public bool IntegratedSecurity { get; init; } - - public bool Compression { get; init; } - - public HistorianConnectionKind ConnectionKind { get; init; } = HistorianConnectionKind.Process; - - public HistorianTransport Transport { get; init; } = HistorianTransport.LocalPipe; - - public string TargetSpn { get; init; } = @"NT SERVICE\aahClientAccessPoint"; - - /// - /// When true, the WCF channel factories used by the SDK accept the server's - /// X.509 certificate without chain validation. Useful when connecting to a - /// development / on-prem Historian whose /HistCert endpoint presents an - /// installer-generated self-signed cert that isn't in the local trust store - /// (notably .NET WCF on Linux ignores the system CA bundle for its own - /// X509Chain checks). Default false; do not enable in production where the - /// server's identity matters. - /// - public bool AllowUntrustedServerCertificate { get; init; } - - /// - /// Overrides the expected DNS identity in the endpoint address — set this to - /// whatever DNS name the server's certificate actually claims (often - /// localhost on installer-generated AVEVA Historian certificates) when - /// connecting via IP address or a hostname that doesn't match the cert SAN/CN. - /// Without this override WCF rejects the channel with - /// "Identity check failed for outgoing message". Has no effect on transports - /// that don't validate a server certificate. - /// - public string? ServerDnsIdentity { get; init; } - - /// - /// For : when true the channel uses TLS - /// (https://); when false it uses plaintext (http://). Matches the stock - /// 2023 R2 client's securedConnection flag. The TLS host is taken from - /// when set (to match the server certificate's name), - /// otherwise . When is - /// true the server certificate chain is not validated. Default false. - /// - public bool GrpcUseTls { get; init; } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianTransport.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianTransport.cs deleted file mode 100644 index ef819b7..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/HistorianTransport.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient; - -public enum HistorianTransport -{ - LocalPipe = 0, - RemoteTcpIntegrated = 1, - RemoteTcpCertificate = 2, - - /// - /// 2023 R2 gRPC transport (Historian Client Access Point gRPC-Web endpoint, default - /// TCP port 32565). Carries the same native binary payloads as the WCF transports inside - /// protobuf bytes fields. See Grpc/HistorianGrpcReadOrchestrator. - /// - RemoteGrpc = 3 -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/AggregationType.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/AggregationType.cs deleted file mode 100644 index fbe9278..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/AggregationType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public enum AggregationType -{ - Minimum, - Maximum, - Average, - Total, - Percent, - MinContained, - MaxContained, - TotalContained, - AverageContained, - PercentContained -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianAggregateSample.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianAggregateSample.cs deleted file mode 100644 index 62a883a..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianAggregateSample.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianAggregateSample( - string TagName, - DateTime StartTimeUtc, - DateTime EndTimeUtc, - double Value, - ushort Quality, - uint QualityDetail, - ushort OpcQuality, - RetrievalMode RetrievalMode, - TimeSpan Resolution); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianBlock.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianBlock.cs deleted file mode 100644 index 6d681ba..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianBlock.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianBlock( - string TagName, - DateTime StartTimeUtc, - DateTime EndTimeUtc, - IReadOnlyList Samples); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianConnectionKind.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianConnectionKind.cs deleted file mode 100644 index d75e668..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianConnectionKind.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace ZB.MOM.WW.SPHistorianClient.Models; - -[Flags] -public enum HistorianConnectionKind -{ - Process = 1, - Event = 2 -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianConnectionStatus.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianConnectionStatus.cs deleted file mode 100644 index c025c9e..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianConnectionStatus.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianConnectionStatus( - string ServerName, - bool Pending, - bool ErrorOccurred, - string? Error, - bool ConnectedToServer, - bool ConnectedToServerStorage, - bool ConnectedToStoreForward, - HistorianConnectionKind ConnectionKind); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianDataType.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianDataType.cs deleted file mode 100644 index e9e4e32..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianDataType.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -/// -/// AVEVA Historian native tag data types. Existing values (0..10, 13) match the -/// numeric layout the wrapper has historically used; new values (14+) extend the -/// model with types recovered from the native CDataType predicate IL — they aren't -/// part of the original wrapper enum but cover the full native type space. -/// -public enum HistorianDataType -{ - Int1 = 0, - Int2 = 2, - UInt2 = 3, - Int4 = 4, - UInt4 = 5, - Float = 6, - Double = 7, - SingleByteString = 8, - DoubleByteString = 9, - Event = 10, - Structure = 13, - - /// 1-byte unsigned integer (native code 0x08). - UInt1 = 14, - - /// 8-byte signed integer (native code 0x19). - Int8 = 15, - - /// 8-byte unsigned integer (native code 0x39). - UInt8 = 16, - - /// 16-byte GUID (native code 0x10, matches CDataType.IsGuid). - Guid = 17, - - /// Windows FILETIME (8 bytes, 100-ns ticks since 1601-01-01 UTC; native code 0x18, matches CDataType.IsFileTime). - FileTime = 18 -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianDataValue.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianDataValue.cs deleted file mode 100644 index 22fcadf..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianDataValue.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianDataValue( - string TagName, - DateTime TimestampUtc, - double? NumericValue, - string? StringValue, - ushort Quality = 192, - uint QualityDetail = 0); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianEvent.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianEvent.cs deleted file mode 100644 index f0bbbd0..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianEvent.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianEvent( - Guid Id, - DateTime EventTimeUtc, - DateTime ReceivedTimeUtc, - string Type, - string SourceName, - string Namespace, - ushort RevisionVersion, - IReadOnlyDictionary Properties); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianSample.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianSample.cs deleted file mode 100644 index 3e9ab9a..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianSample.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianSample( - string TagName, - DateTime TimestampUtc, - double? NumericValue, - string? StringValue, - ushort Quality, - uint QualityDetail, - ushort OpcQuality, - double PercentGood); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianStorageType.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianStorageType.cs deleted file mode 100644 index 0f45d11..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianStorageType.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -/// -/// Storage strategy for historized samples. Maps to Tag.StorageType in the -/// Runtime DB. Values match the captured native enum and the server-persisted -/// integer column. -/// -public enum HistorianStorageType -{ - /// - /// Sample on a fixed cadence (see HistorianTagDefinition.StorageRateMs). - /// - Cyclic = 1, - - /// - /// Sample only on value change (with optional value/time/rate deadbands). - /// - Delta = 2, -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianStoreForwardStatus.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianStoreForwardStatus.cs deleted file mode 100644 index 041b7c1..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianStoreForwardStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianStoreForwardStatus( - string ServerName, - bool Pending, - bool ErrorOccurred, - string? Error, - bool DataStored, - bool Storing, - HistorianConnectionKind ConnectionKind); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianTagDefinition.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianTagDefinition.cs deleted file mode 100644 index 338883b..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianTagDefinition.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -/// -/// Input model for . Live-verified data -/// types: Float, Double, Int2, Int4, UInt4 (probed 2026-05-04 via instrument-wcf-writemessage). -/// String/Int1/Int8/UInt8 types failed at native AddTag — likely require a different -/// path and are intentionally not supported. MinEU/MaxEU/MinRaw/MaxRaw are now encoded -/// into the wire payload (see HistorianTagWriteProtocol). -/// -/// Semantics: EnsureTagAsync is an upsert. Calling it twice on the same -/// with different fields succeeds both times; the second call -/// updates Description, MinEU, MaxEU, MinRaw, MaxRaw, and AnalogTag.Scaling on the -/// existing row (verified 2026-05-04 by direct SQL inspection after sequential calls). -/// -public sealed record HistorianTagDefinition -{ - /// Tag name (ASCII; up to 255 chars per server limit). - public required string TagName { get; init; } - - /// Tag description (free text; up to 255 chars). - public string? Description { get; init; } - - /// Engineering unit label (e.g. "Seconds", "kPa"). Required for analog tags. - public string? EngineeringUnit { get; init; } - - /// Native data type. Float, Double, Int2, Int4, UInt4 are live-verified. - public HistorianDataType DataType { get; init; } = HistorianDataType.Float; - - /// Engineering-units lower bound. Default 0. - public double MinEU { get; init; } - - /// Engineering-units upper bound. Default 100. - public double MaxEU { get; init; } = 100.0; - - /// - /// Raw lower bound (pre-scaling). Default 0. Persisted distinctly only when - /// is true; with ApplyScaling=false the server mirrors - /// this to MinEU on EnsureTags2 (verified 2026-05-04 against both native and - /// managed clients). - /// - public double MinRaw { get; init; } - - /// - /// Raw upper bound (pre-scaling). Default 100. See for the - /// ApplyScaling caveat. - /// - public double MaxRaw { get; init; } = 100.0; - - /// - /// When true, the server persists / as - /// distinct values from / and sets - /// AnalogTag.Scaling = 1. When false (default), the server mirrors MinRaw - /// to MinEU and MaxRaw to MaxEU and sets AnalogTag.Scaling = 0. - /// - public bool ApplyScaling { get; init; } - - /// - /// Storage rate in milliseconds. Default 1000ms. The server only accepts - /// quantized values (observed valid set: 1000, 5000, 10000, 60000, 300000) — - /// non-quantized values cause to - /// return false. - /// - public uint StorageRateMs { get; init; } = 1000u; - - /// - /// Storage strategy. Default samples - /// on the configured cadence. - /// samples only on value change. The server persists this to Tag.StorageType - /// (Cyclic = 1, Delta = 2). - /// - public HistorianStorageType StorageType { get; init; } = HistorianStorageType.Cyclic; - - /// - /// Divisor applied when storing integral values for trend integration. Default 1.0. - /// Wire bytes flip correctly per the captured native serializer, but live testing - /// 2026-05-05 showed the server stores IntegralDivisor on - /// EngineeringUnit (shared across all tags using that EU) rather than - /// per-tag — so a non-default value sent here is accepted on the wire but does - /// not visibly persist in EngineeringUnit.IntegralDivisor for the test - /// EU. Exposed for completeness and forward-compatibility; check your server's - /// behavior before relying on it. - /// - public double IntegralDivisor { get; init; } = 1.0; -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianTagMetadata.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianTagMetadata.cs deleted file mode 100644 index 61e5e7e..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/HistorianTagMetadata.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public sealed record HistorianTagMetadata( - string Name, - uint? Key, - HistorianDataType DataType, - string? Description = null, - string? EngineeringUnit = null, - double? MinRaw = null, - double? MaxRaw = null); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/InterpolationType.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/InterpolationType.cs deleted file mode 100644 index 0f3365d..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/InterpolationType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public enum InterpolationType -{ - StairStep = 0, - Linear = 1, - SystemDefault = 254, - None = 255 -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/QualityRule.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/QualityRule.cs deleted file mode 100644 index 9e2441a..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/QualityRule.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public enum QualityRule -{ - Extended, - Good, - None, - Optimistic -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/RetrievalMode.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/RetrievalMode.cs deleted file mode 100644 index 25b5602..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/RetrievalMode.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public enum RetrievalMode -{ - Cyclic, - Delta, - Full, - Interpolated, - BestFit, - TimeWeightedAverage, - MinimumWithTime, - MaximumWithTime, - Integral, - Slope, - Counter, - ValueState, - RoundTrip, - StartBound, - EndBound -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/TimestampRule.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/TimestampRule.cs deleted file mode 100644 index d623c45..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/TimestampRule.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public enum TimestampRule -{ - Start, - End, - None -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/ValueSelector.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/ValueSelector.cs deleted file mode 100644 index dbef2ed..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Models/ValueSelector.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Models; - -public enum ValueSelector -{ - Auto = 1, - First, - Last, - Integral, - StandardDeviation, - Minimum, - Maximum, - Average -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/FrameFormatException.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/FrameFormatException.cs deleted file mode 100644 index dd21daa..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/FrameFormatException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal sealed class FrameFormatException : Exception -{ - public FrameFormatException(string message) - : base(message) - { - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/Historian2020ProtocolDialect.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/Historian2020ProtocolDialect.cs deleted file mode 100644 index 4b2a6e4..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/Historian2020ProtocolDialect.cs +++ /dev/null @@ -1,81 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Grpc; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal sealed class Historian2020ProtocolDialect -{ - private readonly HistorianClientOptions _options; - - public Historian2020ProtocolDialect(HistorianClientOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - private bool UseGrpc => _options.Transport == HistorianTransport.RemoteGrpc; - - public IAsyncEnumerable ReadRawAsync(string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken cancellationToken) - { - return UseGrpc - ? new HistorianGrpcReadOrchestrator(_options).ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken) - : new HistorianWcfReadOrchestrator(_options).ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken); - } - - public IAsyncEnumerable ReadAggregateAsync(string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken cancellationToken) - { - return UseGrpc - ? new HistorianGrpcReadOrchestrator(_options).ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken) - : new HistorianWcfReadOrchestrator(_options).ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken); - } - - public Task> ReadAtTimeAsync(string tag, IReadOnlyList timestampsUtc, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return UseGrpc - ? new HistorianGrpcReadOrchestrator(_options).ReadAtTimeAsync(tag, timestampsUtc, cancellationToken) - : new HistorianWcfReadOrchestrator(_options).ReadAtTimeAsync(tag, timestampsUtc, cancellationToken); - } - - public IAsyncEnumerable ReadBlocksAsync(string tag, DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) - { - return Missing("StartBlockRetrievalQuery", cancellationToken); - } - - public IAsyncEnumerable ReadEventsAsync(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) - { - HistorianWcfEventOrchestrator orchestrator = new(_options); - return orchestrator.ReadEventsAsync(startUtc, endUtc, cancellationToken); - } - - public Task GetConnectionStatusAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return Wcf.HistorianWcfStatusClient.GetConnectionStatusAsync(_options, cancellationToken); - } - - public Task GetStoreForwardStatusAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return Wcf.HistorianWcfStatusClient.GetStoreForwardStatusAsync(_options, cancellationToken); - } - - public Task GetSystemParameterAsync(string name, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ArgumentException.ThrowIfNullOrWhiteSpace(name); - return Wcf.HistorianWcfStatusClient.GetSystemParameterAsync(_options, name, cancellationToken); - } - - private static async IAsyncEnumerable Missing( - string operation, - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) - { - await Task.Yield(); - cancellationToken.ThrowIfCancellationRequested(); - throw new ProtocolEvidenceMissingException(operation); -#pragma warning disable CS0162 - yield break; -#pragma warning restore CS0162 - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianBinaryPrimitives.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianBinaryPrimitives.cs deleted file mode 100644 index 4131edb..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianBinaryPrimitives.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Buffers.Binary; -using System.Text; - -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal static class HistorianBinaryPrimitives -{ - public static long ToFileTimeUtc(DateTime value) - { - return value.Kind == DateTimeKind.Unspecified - ? DateTime.SpecifyKind(value, DateTimeKind.Utc).ToFileTimeUtc() - : value.ToUniversalTime().ToFileTimeUtc(); - } - - public static void WriteUInt16LittleEndian(Stream stream, ushort value) - { - Span buffer = stackalloc byte[sizeof(ushort)]; - BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); - stream.Write(buffer); - } - - public static void WriteUInt32LittleEndian(Stream stream, uint value) - { - Span buffer = stackalloc byte[sizeof(uint)]; - BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); - stream.Write(buffer); - } - - public static void WriteUInt64LittleEndian(Stream stream, ulong value) - { - Span buffer = stackalloc byte[sizeof(ulong)]; - BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); - stream.Write(buffer); - } - - public static void WriteFileTimeUtc(Stream stream, DateTime value) - { - WriteUInt64LittleEndian(stream, unchecked((ulong)ToFileTimeUtc(value))); - } - - public static void WriteUtf16NullTerminated(Stream stream, string value) - { - ArgumentNullException.ThrowIfNull(value); - - byte[] bytes = Encoding.Unicode.GetBytes(value); - stream.Write(bytes); - stream.WriteByte(0); - stream.WriteByte(0); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianConnection.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianConnection.cs deleted file mode 100644 index 802e158..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianConnection.cs +++ /dev/null @@ -1,75 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Transport; - -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal sealed class HistorianConnection : IAsyncDisposable -{ - private readonly HistorianClientOptions _options; - private readonly IHistorianTransport _transport; - - public HistorianConnection(HistorianClientOptions options, IHistorianTransport transport) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _transport = transport ?? throw new ArgumentNullException(nameof(transport)); - } - - public async ValueTask ConnectTcpAsync(CancellationToken cancellationToken) - { - using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeout.CancelAfter(_options.ConnectTimeout); - await _transport.ConnectAsync(_options, timeout.Token).ConfigureAwait(false); - } - - public ValueTask OpenProtocolSessionAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - throw new ProtocolEvidenceMissingException("OpenConnection handshake"); - } - - public async ValueTask SendFrameAsync(HistorianFrame frame, CancellationToken cancellationToken) - { - byte[] buffer = HistorianFrameWriter.ToArray(frame); - await _transport.SendAsync(buffer, cancellationToken).ConfigureAwait(false); - } - - public async ValueTask ReceiveFrameAsync(CancellationToken cancellationToken) - { - using MemoryStream frameBytes = new(); - byte[] header = new byte[HistorianFrameReader.HeaderSize]; - await ReadTransportExactlyAsync(header, cancellationToken).ConfigureAwait(false); - frameBytes.Write(header); - - int frameLength = BitConverter.ToInt32(header, 0); - if (frameLength < HistorianFrameReader.HeaderSize || frameLength > HistorianFrameReader.MaxFrameSize) - { - throw new FrameFormatException($"Invalid frame length {frameLength}."); - } - - byte[] payload = new byte[frameLength - HistorianFrameReader.HeaderSize]; - await ReadTransportExactlyAsync(payload, cancellationToken).ConfigureAwait(false); - frameBytes.Write(payload); - frameBytes.Position = 0; - - return await HistorianFrameReader.ReadAsync(frameBytes, cancellationToken).ConfigureAwait(false); - } - - public ValueTask DisposeAsync() - { - return _transport.DisposeAsync(); - } - - private async ValueTask ReadTransportExactlyAsync(Memory buffer, CancellationToken cancellationToken) - { - int offset = 0; - while (offset < buffer.Length) - { - int read = await _transport.ReceiveAsync(buffer[offset..], cancellationToken).ConfigureAwait(false); - if (read == 0) - { - throw new EndOfStreamException("Unexpected end of stream from Historian transport."); - } - - offset += read; - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrame.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrame.cs deleted file mode 100644 index 9621993..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrame.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal readonly record struct HistorianFrame( - HistorianMessageType MessageType, - uint CorrelationId, - ReadOnlyMemory Payload); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrameReader.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrameReader.cs deleted file mode 100644 index 5db0c0e..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrameReader.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Buffers.Binary; - -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal static class HistorianFrameReader -{ - public const int HeaderSize = 10; - public const int MaxFrameSize = 16 * 1024 * 1024; - - public static async ValueTask ReadAsync(Stream stream, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(stream); - - byte[] header = new byte[HeaderSize]; - await ReadExactlyAsync(stream, header, cancellationToken).ConfigureAwait(false); - - int frameLength = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan(0, 4)); - if (frameLength < HeaderSize || frameLength > MaxFrameSize) - { - throw new FrameFormatException($"Invalid frame length {frameLength}."); - } - - ushort messageType = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(4, 2)); - uint correlationId = BinaryPrimitives.ReadUInt32LittleEndian(header.AsSpan(6, 4)); - byte[] payload = new byte[frameLength - HeaderSize]; - await ReadExactlyAsync(stream, payload, cancellationToken).ConfigureAwait(false); - - return new HistorianFrame((HistorianMessageType)messageType, correlationId, payload); - } - - private static async ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) - { - int offset = 0; - while (offset < buffer.Length) - { - int read = await stream.ReadAsync(buffer[offset..], cancellationToken).ConfigureAwait(false); - if (read == 0) - { - throw new EndOfStreamException("Unexpected end of stream while reading Historian frame."); - } - - offset += read; - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrameWriter.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrameWriter.cs deleted file mode 100644 index 6333b7f..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianFrameWriter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Buffers.Binary; - -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal static class HistorianFrameWriter -{ - public static void Write(Stream stream, HistorianFrame frame) - { - ArgumentNullException.ThrowIfNull(stream); - - int frameLength = HistorianFrameReader.HeaderSize + frame.Payload.Length; - Span header = stackalloc byte[HistorianFrameReader.HeaderSize]; - BinaryPrimitives.WriteInt32LittleEndian(header[0..4], frameLength); - BinaryPrimitives.WriteUInt16LittleEndian(header[4..6], (ushort)frame.MessageType); - BinaryPrimitives.WriteUInt32LittleEndian(header[6..10], frame.CorrelationId); - - stream.Write(header); - stream.Write(frame.Payload.Span); - } - - public static byte[] ToArray(HistorianFrame frame) - { - using MemoryStream stream = new(); - Write(stream, frame); - return stream.ToArray(); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianMessageType.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianMessageType.cs deleted file mode 100644 index b385ccd..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianMessageType.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal enum HistorianMessageType : ushort -{ - Unknown = 0 -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianProtocolFacts.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianProtocolFacts.cs deleted file mode 100644 index 08952e8..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Protocol/HistorianProtocolFacts.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Protocol; - -internal static class HistorianProtocolFacts -{ - public const int DefaultTcpPort = 32568; - public const int DataQueryResultRowSizeBytes = 544; - public const int EventQueryFiltersSizeBytes = 72; - public const string QueryTimezone = "UTC"; -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ProtocolEvidenceMissingException.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ProtocolEvidenceMissingException.cs deleted file mode 100644 index 27dac97..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ProtocolEvidenceMissingException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient; - -public sealed class ProtocolEvidenceMissingException : NotSupportedException -{ - public ProtocolEvidenceMissingException(string operation) - : base($"Protocol evidence for '{operation}' has not been captured yet. Add sanitized fixtures before enabling this operation.") - { - Operation = operation; - } - - public string Operation { get; } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ProtocolNotImplementedException.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ProtocolNotImplementedException.cs deleted file mode 100644 index 6098a75..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ProtocolNotImplementedException.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient; - -public sealed class ProtocolNotImplementedException : NotImplementedException -{ - public ProtocolNotImplementedException(string message) - : base(message) - { - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/IHistorianTransport.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/IHistorianTransport.cs deleted file mode 100644 index 3ba0507..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/IHistorianTransport.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Transport; - -internal interface IHistorianTransport : IAsyncDisposable -{ - ValueTask ConnectAsync(HistorianClientOptions options, CancellationToken cancellationToken); - - ValueTask SendAsync(ReadOnlyMemory payload, CancellationToken cancellationToken); - - ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/IHistorianTransportFactory.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/IHistorianTransportFactory.cs deleted file mode 100644 index e942c45..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/IHistorianTransportFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Transport; - -internal interface IHistorianTransportFactory -{ - IHistorianTransport Create(); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/TcpHistorianTransport.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/TcpHistorianTransport.cs deleted file mode 100644 index 088e958..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Transport/TcpHistorianTransport.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Net.Sockets; - -namespace ZB.MOM.WW.SPHistorianClient.Transport; - -internal sealed class TcpHistorianTransport : IHistorianTransport -{ - public static readonly IHistorianTransportFactory Factory = new FactoryImpl(); - - private TcpClient? _client; - private NetworkStream? _stream; - - public async ValueTask ConnectAsync(HistorianClientOptions options, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(options); - - _client = new TcpClient(); - await _client.ConnectAsync(options.Host, options.Port, cancellationToken).ConfigureAwait(false); - _stream = _client.GetStream(); - } - - public async ValueTask SendAsync(ReadOnlyMemory payload, CancellationToken cancellationToken) - { - if (_stream is null) - { - throw new InvalidOperationException("Transport is not connected."); - } - - await _stream.WriteAsync(payload, cancellationToken).ConfigureAwait(false); - } - - public async ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken) - { - if (_stream is null) - { - throw new InvalidOperationException("Transport is not connected."); - } - - return await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - } - - public ValueTask DisposeAsync() - { - _stream?.Dispose(); - _client?.Dispose(); - return ValueTask.CompletedTask; - } - - private sealed class FactoryImpl : IHistorianTransportFactory - { - public IHistorianTransport Create() - { - return new TcpHistorianTransport(); - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IHistoryServiceContract.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IHistoryServiceContract.cs deleted file mode 100644 index 80d9cf8..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IHistoryServiceContract.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.History, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IHistoryServiceContract -{ - [OperationContract(Name = "GetV")] - uint GetInterfaceVersion(out uint version); - - [OperationContract(Name = "Open")] - uint OpenConnection( - string HostName, - string ProcessName, - uint ProcessId, - string UserName, - byte[] Password, - [MessageParameter(Name = "pwdLength")] ushort passwordLength, - byte clientType, - ushort clientVersion, - [MessageParameter(Name = "ConnectionMode")] uint connectionMode, - [MessageParameter(Name = "ConnectionTimeout")] uint connectionTimeout, - ref string StorageSessionId, - out uint Handle, - out long ConnectTime, - out uint ServerStatus); - - [OperationContract(Name = "Close")] - uint CloseConnection([MessageParameter(Name = "handle")] uint clientHandle); - - [OperationContract(Name = "VldC")] - uint ValidateClient( - [MessageParameter(Name = "Handle")] uint handle, - [MessageParameter(Name = "HostName")] string hostName, - [MessageParameter(Name = "ProcessName")] string processName, - [MessageParameter(Name = "ProcessId")] uint processId, - [MessageParameter(Name = "UserName")] string userName, - [MessageParameter(Name = "ConnectTime")] ref long connectTime, - [MessageParameter(Name = "ServerStatus")] out uint serverStatus); - - [OperationContract(Name = "UpdC")] - uint UpdateClientStatus( - [MessageParameter(Name = "Hnd")] uint handle, - [MessageParameter(Name = "Stat")] uint status, - [MessageParameter(Name = "TCnt")] uint tagCount, - [MessageParameter(Name = "VCnt")] long valueCount, - [MessageParameter(Name = "VRate")] float valueRate, - [MessageParameter(Name = "SStat")] out uint serverStatus); - - [OperationContract(Name = "AddT")] - uint AddTags( - [MessageParameter(Name = "Handle")] uint handle, - [MessageParameter(Name = "ElementCount")] uint elementCount, - [MessageParameter(Name = "InByteCount")] uint inByteCount, - [MessageParameter(Name = "pInBuff")] byte[] inputBuffer, - [MessageParameter(Name = "OutByteCount")] out uint outByteCount, - [MessageParameter(Name = "pOutBuff")] out byte[] outputBuffer); - - [OperationContract(Name = "RTag")] - uint RegisterTags( - [MessageParameter(Name = "Handle")] uint handle, - [MessageParameter(Name = "ElementCount")] uint elementCount, - [MessageParameter(Name = "InByteCount")] uint inByteCount, - [MessageParameter(Name = "pInBuff")] byte[] inputBuffer, - [MessageParameter(Name = "OutByteCount")] out uint outByteCount, - [MessageParameter(Name = "pOutBuff")] out byte[] outputBuffer); - - [OperationContract(Name = "AddS")] - uint AddStreamValues( - [MessageParameter(Name = "Handle")] uint handle, - [MessageParameter(Name = "Size")] uint size, - [MessageParameter(Name = "pBuf")] byte[] buffer); - - [OperationContract(Name = "SetT")] - uint SetClientTimeOut( - [MessageParameter(Name = "Handle")] uint handle, - [MessageParameter(Name = "TimeOut")] int timeout, - [MessageParameter(Name = "pRet")] out uint returnValue); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IHistoryServiceContract2.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IHistoryServiceContract2.cs deleted file mode 100644 index b1d99f4..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IHistoryServiceContract2.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Runtime.InteropServices; -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.History, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IHistoryServiceContract2 : IHistoryServiceContract -{ - [OperationContract(Name = "UpdC2")] - uint UpdateClientStatus2(uint handle, uint clientStatus, uint tagCount, long valueCount, float valueRate, out long areaVersion, out uint serverStatus); - - [OperationContract(Name = "EnsT")] - uint EnsureTags( - [MessageParameter(Name = "Handle")] uint handle, - uint elementCount, - [MessageParameter(Name = "InByteCount")] uint inByteCount, - [MessageParameter(Name = "InBuff")] byte[] inBuffer, - [MessageParameter(Name = "OutByteCount")] out uint outByteCount, - [MessageParameter(Name = "OutBuff")] out byte[] outBuffer); - - [OperationContract(Name = "DelT")] - [return: MarshalAs(UnmanagedType.U1)] - bool DeleteTags( - uint handle, - uint tagNamesSize, - byte[] tagNames, - ref uint statusSize, - ref byte[] status, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract(Name = "UpdC3")] - [return: MarshalAs(UnmanagedType.U1)] - bool UpdateClientStatus3( - string handle, - uint clientStatusSize, - ref byte[] clientStatus, - out uint serverStatusSize, - out byte[] serverStatus, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract(Name = "Open2")] - [return: MarshalAs(UnmanagedType.U1)] - bool OpenConnection2( - [MessageParameter(Name = "inParameters")] ref byte[] inParameters, - [MessageParameter(Name = "outParameters")] out byte[] outParameters, - [MessageParameter(Name = "err")] out byte[] err); - - [OperationContract(Name = "Close2")] - [return: MarshalAs(UnmanagedType.U1)] - bool CloseConnection2(string handle, out byte[] errorBuffer); - - [OperationContract(Name = "VldC2")] - [return: MarshalAs(UnmanagedType.U1)] - bool ValidateClient2( - string handle, - [MessageParameter(Name = "HostName")] string hostName, - [MessageParameter(Name = "ProcessName")] string processName, - [MessageParameter(Name = "ProcessId")] uint processId, - [MessageParameter(Name = "UserName")] string userName, - [MessageParameter(Name = "ConnectTime")] ref long connectTime, - [MessageParameter(Name = "ServerStatus")] out uint serverStatus, - out byte[] errorBuffer); - - [OperationContract(Name = "RTag2")] - [return: MarshalAs(UnmanagedType.U1)] - bool RegisterTags2( - string handle, - [MessageParameter(Name = "ElementCount")] uint elementCount, - [MessageParameter(Name = "pInBuff")] byte[] inputBuffer, - [MessageParameter(Name = "outBuff")] out byte[] outputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "AddS2")] - [return: MarshalAs(UnmanagedType.U1)] - bool AddStreamValues2( - string handle, - [MessageParameter(Name = "pBuf")] byte[] buffer, - out byte[] errorBuffer); - - [OperationContract(Name = "EnsT2")] - [return: MarshalAs(UnmanagedType.U1)] - bool EnsureTags2( - [MessageParameter(Name = "Handle")] string handle, - uint elementCount, - [MessageParameter(Name = "InBuff")] byte[] inputBuffer, - [MessageParameter(Name = "OutBuff")] out byte[] outputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "ExKey")] - [return: MarshalAs(UnmanagedType.U1)] - bool ExchangeKey( - string handle, - [MessageParameter(Name = "inBuff")] byte[] inputBuffer, - [MessageParameter(Name = "OutBuff")] out byte[] outputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "AddTEx")] - [return: MarshalAs(UnmanagedType.U1)] - bool AddTagExtendedProperties( - string handle, - [MessageParameter(Name = "inBuff")] byte[] inputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "DelTep")] - [return: MarshalAs(UnmanagedType.U1)] - bool DeleteTagExtendedProperties( - string handle, - [MessageParameter(Name = "inBuff")] byte[] inputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "StJb")] - [return: MarshalAs(UnmanagedType.U1)] - bool StartJob( - string handle, - byte[] jobBuffer, - [MessageParameter(Name = "strJobid")] out string jobId, - out byte[] errorBuffer); - - [OperationContract(Name = "GtJb")] - [return: MarshalAs(UnmanagedType.U1)] - bool GetJobStatus( - string handle, - [MessageParameter(Name = "strJobid")] string jobId, - [MessageParameter(Name = "jobstatus")] out byte[] jobStatus, - out byte[] errorBuffer); - - [OperationContract(Name = "ValCl")] - [return: MarshalAs(UnmanagedType.U1)] - bool ValidateClientCredential( - string handle, - [MessageParameter(Name = "inBuff")] byte[] inputBuffer, - [MessageParameter(Name = "outBuff")] out byte[] outputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "GetI")] - [return: MarshalAs(UnmanagedType.U1)] - bool GetInfo(string request, out byte[] info, out byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract.cs deleted file mode 100644 index 7b469db..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -internal enum InsqlTagType -{ - All = 0 -} - -[ServiceContract(Name = HistorianWcfServiceNames.Retrieval, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IRetrievalServiceContract -{ - [OperationContract(Name = "GetV")] - uint GetInterfaceVersion(out uint version); - - [OperationContract] - uint StartQuery( - uint clientHandle, - ushort queryRequestType, - uint requestSize, - [MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer, - out uint responseSize, - [MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer, - ref uint queryHandle); - - [OperationContract] - uint GetNextQueryResultBuffer( - uint clientHandle, - uint queryHandle, - out uint resultSize, - [MessageParameter(Name = "pResultBuff")] out byte[] resultBuffer, - out uint errorCode); - - [OperationContract] - uint EndQuery(uint clientHandle, uint queryHandle); - - [OperationContract] - uint GetTagTypeFromName(uint clientHandle, string tagName, out uint tagType); - - [OperationContract] - uint IsOriginalAllowed(uint clientHandle, out bool isAllowed); - - [OperationContract] - uint IsManualTag(uint clientHandle, string tagName, out bool isManual); - - [OperationContract] - uint IsTagnameValid(uint clientHandle, string tagName, bool isWide, InsqlTagType tagType, out bool isValid); - - [OperationContract] - uint StartLikeTagNameSearch(uint clientHandle, string tagNameFilter, uint tagType, bool isNotLike); - - [OperationContract] - uint GetLikeTagnames(uint clientHandle, out byte[] tagNameBuffer, out uint tagNameBufferSize, out bool isMore); - - [OperationContract] - uint GetTagInfoFromName(uint clientHandle, string tagName, out uint tagMetadataByteCount, out byte[] tagMetadata); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract2.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract2.cs deleted file mode 100644 index c8c8884..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract2.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Retrieval, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IRetrievalServiceContract2 : IRetrievalServiceContract -{ - [OperationContract(Name = "GetTg")] - uint GetTagInfosFromId(uint handle, uint tagIdsSize, byte[] tagIds, ref uint sequence, out uint tagInfosSize, out byte[] tagInfos); - - [OperationContract(Name = "GetTgByNm")] - uint GetTagInfosFromName(uint handle, uint tagNamesSize, byte[] tagNames, ref uint sequence, out uint tagInfosSize, out byte[] tagInfos); - - [OperationContract] - bool StartQuery2( - uint clientHandle, - ushort queryRequestType, - uint requestSize, - [MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer, - out uint responseSize, - [MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer, - ref uint queryHandle, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract] - bool GetNextQueryResultBuffer2( - uint clientHandle, - uint queryHandle, - out uint resultSize, - [MessageParameter(Name = "pResultBuff")] out byte[] resultBuffer, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract] - bool EndQuery2( - uint clientHandle, - uint queryHandle, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract3.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract3.cs deleted file mode 100644 index 48568fc..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract3.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Retrieval, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IRetrievalServiceContract3 : IRetrievalServiceContract2 -{ - [OperationContract(Name = "ExeC")] - bool ExecuteSqlCommand( - string handle, - string command, - uint option, - ref uint queryHandle, - [MessageParameter(Name = "retValue")] out int returnValue, - out uint errorSize, - out byte[] errorBuffer); - - [OperationContract(Name = "GetR")] - bool GetRecordSetByteStream( - string handle, - uint queryHandle, - ref uint sequence, - out uint resultSize, - [MessageParameter(Name = "pResultBuff")] out byte[] resultBuffer, - out uint errorSize, - out byte[] errorBuffer); - - [OperationContract(Name = "QTB")] - bool StartTagQuery( - string handle, - [MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer, - [MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "QTG")] - bool QueryTag( - string handle, - ref uint queryId, - [MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer, - [MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "QTE")] - bool EndTagQuery(string handle, ref uint queryId, out byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract4.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract4.cs deleted file mode 100644 index 923e29f..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IRetrievalServiceContract4.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Retrieval, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IRetrievalServiceContract4 : IRetrievalServiceContract3 -{ - [OperationContract] - bool StartEventQuery( - uint clientHandle, - ushort queryRequestType, - uint requestSize, - [MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer, - out uint responseSize, - [MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer, - ref uint queryHandle, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract] - bool GetNextEventQueryResultBuffer( - uint clientHandle, - uint queryHandle, - out uint resultSize, - [MessageParameter(Name = "pResultBuff")] out byte[] resultBuffer, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract] - bool EndEventQuery( - uint clientHandle, - uint queryHandle, - [MessageParameter(Name = "errSize")] out uint errorSize, - [MessageParameter(Name = "err")] out byte[] errorBuffer); - - [OperationContract] - bool GetTagidsByTagnameAndSource(string handle, byte[] tagNameIds, out byte[] tagIds, out byte[] errorBuffer); - - [OperationContract] - bool GetShardTagidsByTagnameAndSource( - string handle, - byte[] tagNameIds, - [MessageParameter(Name = "shardTagids")] out byte[] shardTagIds, - out byte[] errorBuffer); - - [OperationContract(Name = "GetTgByNm2")] - bool GetTagInfosFromName2(string handle, byte[] tagNames, ref uint sequence, out byte[] tagInfos, out byte[] errorBuffer); - - [OperationContract(Name = "GetTepByNm")] - bool GetTagExtendedPropertiesFromName(string handle, byte[] tagNames, ref uint sequence, out byte[] tagExtendedProperties, out byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStatusServiceContract.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStatusServiceContract.cs deleted file mode 100644 index 12017a0..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStatusServiceContract.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Status, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IStatusServiceContract -{ - [OperationContract(Name = "GetV")] - uint GetInterfaceVersion(out uint version); - - [OperationContract] - uint GetServerTime(out byte[] systemTime, out uint systemTimeSize); - - [OperationContract] - uint LogError( - uint clientHandle, - int errorLevel, - int destination, - int queueTime, - int errorCode, - int lineNumber, - int hasParam, - int moduleId, - int systemError, - string hostName, - string file, - string stringParameter); - - [OperationContract] - uint GetTimeZoneInfo(uint handle, string timeZoneName, out bool isDaylight, out byte[] timeZoneInfo); - - [OperationContract] - uint IsDBCaseSensitive(uint handle, out bool isCaseSensitive); - - [OperationContract] - uint GetSystemTimeZoneName(uint clientHandle, out string systemTimeZoneName); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStatusServiceContract2.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStatusServiceContract2.cs deleted file mode 100644 index 8cecc78..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStatusServiceContract2.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Runtime.InteropServices; -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Status, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IStatusServiceContract2 : IStatusServiceContract -{ - [OperationContract] - uint GetTimeZoneNames(uint clientHandle, ref uint sequence, out uint bufferSize, out byte[] buffer); - - [OperationContract] - uint IsLicenseFeatureEnabled(uint clientHandle, int feature, out bool isEnabled); - - [OperationContract] - [return: MarshalAs(UnmanagedType.U1)] - bool GetSystemParameter( - uint clientHandle, - string parameterName, - out string parameterValue, - out uint errorSize, - out byte[] errorBuffer); - - [OperationContract(Name = "GETHI")] - [return: MarshalAs(UnmanagedType.U1)] - bool GetHistorianInfo( - string handle, - [MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer, - [MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "PNGS")] - [return: MarshalAs(UnmanagedType.U1)] - bool PingServer(string handle, string pipeName, uint timeout, ref byte[] errorBuffer); - - [OperationContract(Name = "PNGP")] - [return: MarshalAs(UnmanagedType.U1)] - bool PingPipe(string handle, string pipeName, ref byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStorageServiceContract.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStorageServiceContract.cs deleted file mode 100644 index d49d0da..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/IStorageServiceContract.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Storage, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface IStorageServiceContract -{ - [OperationContract(Name = "GetV")] - uint GetInterfaceVersion(out uint version); - - [OperationContract(Name = "Open")] - uint OpenStorageConnection( - string hostName, - string enginePath, - uint freeDiskSpace, - string processName, - uint processId, - string userName, - byte[] password, - ushort passwordLength, - byte clientType, - ushort clientVersion, - uint connectionMode, - uint connectionTimeout, - ref string storageSessionId, - out uint handle, - out long connectTime, - out uint storageStatus); - - [OperationContract(Name = "Close")] - uint CloseStorageConnection(uint handle); - - [OperationContract(Name = "Ping")] - uint Ping(uint handle, out uint outByteCount, out byte[] outputBuffer); - - [OperationContract(Name = "AddT")] - uint AddTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer); - - [OperationContract(Name = "RTag")] - uint RegisterTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer); - - [OperationContract(Name = "AddS")] - uint AddStreamValues(uint handle, uint size, byte[] buffer); - - [OperationContract(Name = "GetId")] - uint GetTagIds(uint handle, ref uint sequence, out uint tagIdsSize, out byte[] tagIds); - - [OperationContract(Name = "GetTg")] - uint GetTags(uint handle, uint tagIdsSize, byte[] tagIds, ref uint sequence, out uint tagInfosSize, out byte[] tagInfos); - - [OperationContract(Name = "FlshMD")] - uint FlushMetadata(uint handle, uint tagIdsSize, byte[] tagIds); - - [OperationContract(Name = "Flush")] - uint FlushData(uint handle); - - [OperationContract(Name = "LoadB")] - uint LoadBlocks(uint handle, ref uint sequence, out uint historyBlocksSize, out byte[] historyBlocks); - - [OperationContract(Name = "GetSS")] - uint GetSnapshots(uint handle, long blockStartTime, ref uint sequence, out uint snapshotSize, out byte[] snapshot); - - [OperationContract(Name = "QSS")] - uint StartQuerySnapshot(uint handle, long blockStartTime, uint snapshotInfoSize, ref byte[] snapshotInfo, ref uint snapshotQueryId); - - [OperationContract(Name = "NxtQSS")] - uint NextQuerySnapshot(uint handle, uint snapshotQueryId, ref uint sequence, out uint snapshotSize, out byte[] snapshot); - - [OperationContract(Name = "EndSS")] - uint EndSnapshot(uint handle, uint snapshotQueryId, long blockStartTime, uint snapshotInfoSize, ref byte[] snapshotInfo, bool isDeleteSnapshot); - - [OperationContract(Name = "Stop")] - uint Stop(uint handle); - - [OperationContract(Name = "ClrTP")] - uint ClearTagIdPairs(uint handle); - - [OperationContract(Name = "AddTP")] - uint AddTagIdPairs(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer); - - [OperationContract(Name = "GetSFP")] - bool GetStoreForwardParameter(uint clientHandle, string parameterName, out string parameterValue, out uint errorSize, out byte[] error); - - [OperationContract(Name = "SetSFP")] - bool SetStoreForwardParameter(uint clientHandle, string parameterName, ref string parameterValue, out uint errorSize, out byte[] error); - - [OperationContract] - bool SendSnapshotBegin(uint handle, ulong totalSize, ulong startTime, ulong endTime, ref string storageSessionIdString, ref uint queryId, out uint errorSize, out byte[] error); - - [OperationContract] - bool SendSnapshotEnd(uint handle, string storageSessionIdString, uint queryId, uint timeRangeSize, byte[] timeRangeBytes, out uint errorSize, out byte[] error); - - [OperationContract] - bool SendSnapshot(uint handle, string storageSessionIdString, uint queryId, uint size, ulong snapshotChunkOffset, byte[] buffer, out uint errorSize, out byte[] error); - - [OperationContract] - bool DeleteSnapshot(uint clientHandle, ulong startTime, uint snapshotInfoSize, ref byte[] snapshotInfo, out uint errorSize, out byte[] errorBuffer); - - [OperationContract(Name = "AddS2")] - bool AddStreamValues2(uint handle, string shardIdString, byte[] buffer, out byte[] errorBuffer); - - [OperationContract(Name = "ClrST")] - bool ClearShardTagIds(uint handle, out byte[] errorBuffer); - - [OperationContract(Name = "AddST")] - bool AddShardTagIds(uint handle, byte[] buffer, out byte[] errorBuffer); - - [OperationContract(Name = "SpltS")] - bool SplitUnknownShards(uint handle, out byte[] errorBuffer); - - [OperationContract(Name = "GetR")] - bool GetRemainingSnapshotsSize(uint handle, ref ulong snapshotSize, out byte[] errorBuffer); - - [OperationContract(Name = "DelT")] - bool DeleteTags(uint handle, byte[] buffer, out byte[] errorBuffer); - - [OperationContract(Name = "Open2")] - bool OpenStorageConnection2(ref byte[] inputParameters, out byte[] outputParameters, out byte[] error); - - [OperationContract(Name = "ValCl")] - bool ValidateClientCredential( - string handle, - [MessageParameter(Name = "inBuff")] byte[] inputBuffer, - [MessageParameter(Name = "outBuff")] out byte[] outputBuffer, - out byte[] errorBuffer); - - [OperationContract(Name = "GetI")] - bool GetInfo(string request, out byte[] info, out byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/ITransactionServiceContract.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/ITransactionServiceContract.cs deleted file mode 100644 index 492e9d6..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/Contracts/ITransactionServiceContract.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Runtime.InteropServices; -using System.ServiceModel; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -[ServiceContract(Name = HistorianWcfServiceNames.Transaction, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface ITransactionServiceContract -{ - [OperationContract(Name = "GetV")] - uint GetInterfaceVersion(out uint version); - - [OperationContract] - uint ForwardSnapshotBegin(uint handle, ulong totalSize, ulong startTime, ulong endTime, ref string storageSessionIdString, ref uint queryId); - - [OperationContract] - uint ForwardSnapshotEnd(uint handle, string storageSessionIdString, uint queryId, uint timeRangeSize, byte[] timeRangeBytes); - - [OperationContract] - uint ForwardSnapshot(uint handle, string storageSessionIdString, uint queryId, uint size, ulong snapshotChunkOffset, byte[] buffer); - - [OperationContract] - uint AddNonStreamValuesBegin(uint handle, out string transactionId); - - [OperationContract] - uint AddNonStreamValues(uint handle, string transactionId, uint size, byte[] buffer); - - [OperationContract] - uint AddNonStreamValuesEnd(uint handle, string transactionId, bool commit); -} - -/// -/// V2 surface — discovered by inspecting CHistoryConnectionWCF.AddNonStreamValuesBegin's -/// IL (token 0x06004051), which calls -/// ITransactionServiceContract2::AddNonStreamValuesBegin2(string, ref string, ref byte[]) -/// before falling back to V1. The V2 ops use the GUID-string handle pattern matching -/// other V2 ops on /Hist (EnsT2, AddS2, RTag2) plus an out-byte[] errorBuffer. -/// -[ServiceContract(Name = HistorianWcfServiceNames.Transaction, Namespace = HistorianWcfServiceNames.Namespace)] -internal interface ITransactionServiceContract2 -{ - [OperationContract(Name = "GetV")] - uint GetInterfaceVersion(out uint version); - - [OperationContract] - [return: MarshalAs(UnmanagedType.U1)] - bool AddNonStreamValuesBegin2( - string handle, - out string transactionId, - out byte[] errorBuffer); - - [OperationContract] - [return: MarshalAs(UnmanagedType.U1)] - bool AddNonStreamValues2( - string handle, - string transactionId, - [MessageParameter(Name = "pBuf")] byte[] buffer, - out byte[] errorBuffer); - - [OperationContract] - [return: MarshalAs(UnmanagedType.U1)] - bool AddNonStreamValuesEnd2( - string handle, - string transactionId, - [MarshalAs(UnmanagedType.U1)] bool commit, - out byte[] errorBuffer); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianAddTagsProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianAddTagsProtocol.cs deleted file mode 100644 index 384363a..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianAddTagsProtocol.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Text; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// CTagMetadata serialiser for the CM_EVENT default-event-tag registration that the AVEVA -/// native wrapper performs via IHistoryServiceContract2.EnsureTags2 (WCF op -/// EnsT2) before any event read can return rows. The action URI on the wire is -/// aa/Hist/EnsT2, not the previously-suspected aa/Hist/AddT. Layout -/// captured byte-for-byte from a successful native event read via the -/// instrument-wcf-writemessage IL-rewrite tooling on -/// aahMDASEncoder.ClientMessageEncoder.WriteMessage: -/// -/// -/// byte version = 3 -/// ushort optional-mask = 0x0086 -/// byte CDataType = 5 -/// 16 bytes tag id GUID = 353b8145-5df0-4d46-a253-871aef49b321 -/// compact ASCII tag name "CM_EVENT" -/// compact ASCII description "AnE Event" -/// 7 bytes 0x02 0x02 0x01 0x00 0x00 0x00 0x01 (storage type 2 + flags; LAST BYTE IS 0x01) -/// uint32 storage rate = 0 -/// int64 created FILETIME UTC -/// 16 bytes common Archestra event type GUID = 5f59ae42-3bb6-4760-91a5-ab0be01f9f02 -/// (note: this differs from the previously-documented ...e01f2f27 — the captured -/// native bytes use ...9f02. The earlier docs were inferred from -/// ConvertEventTagToTagMetadata IL inspection without the wire capture.) -/// 3 trailing bytes 0x2F 0x27 0x01 (purpose unknown; appears stable across captures) -/// -/// -/// Earlier probe attempts via the (wrong) AddT WCF op + a payload with the -/// (wrong) trailer order returned server failures. Routing through EnsT2 with -/// this exact byte layout is the path the native wrapper uses. -/// -internal static class HistorianAddTagsProtocol -{ - public static readonly Guid CmEventTagId = new("353b8145-5df0-4d46-a253-871aef49b321"); - - /// - /// Captured native byte sequence is `42 AE 59 5F B6 3B 60 47 91 A5 AB 0B E0 1F 9F 02`, - /// which decodes to GUID `5f59ae42-3bb6-4760-91a5-ab0be01f9f02`. Prior notes documented - /// `5f59ae42-3bb6-4760-91a5-ab0be01f2f27` from IL inspection — the wire capture is the - /// authoritative value. - /// - public static readonly Guid CommonArchestraEventTypeId = new("5f59ae42-3bb6-4760-91a5-ab0be01f9f02"); - - public static byte[] SerializeCmEventCTagMetadata(DateTime createdUtc) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write((byte)3); - writer.Write((ushort)0x0086); - writer.Write((byte)5); - writer.Write(CmEventTagId.ToByteArray()); - WriteCompressedHistorianString(writer, "CM_EVENT"); - WriteCompressedHistorianString(writer, "AnE Event"); - writer.Write(new byte[] { 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x01 }); - writer.Write(0u); - writer.Write(createdUtc.ToUniversalTime().ToFileTimeUtc()); - writer.Write(CommonArchestraEventTypeId.ToByteArray()); - // 5-byte tail captured byte-for-byte from native: 2F 27 01 01 01. - writer.Write(new byte[] { 0x2F, 0x27, 0x01, 0x01, 0x01 }); - - return stream.ToArray(); - } - - private static void WriteCompressedHistorianString(BinaryWriter writer, string value) - { - if (value.Length == 0) - { - writer.Write((byte)0); - return; - } - - if (value.Length > byte.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(value), "Compact CTagMetadata strings only support short ASCII payloads."); - } - - writer.Write((byte)0x09); - writer.Write((byte)value.Length); - writer.Write((byte)0); - foreach (char character in value) - { - if (character > byte.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(value), "Compact CTagMetadata strings only support ASCII characters."); - } - - writer.Write((byte)character); - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianDataQueryProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianDataQueryProtocol.cs deleted file mode 100644 index b7a5fa8..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianDataQueryProtocol.cs +++ /dev/null @@ -1,379 +0,0 @@ -using System.Buffers.Binary; -using System.Text; -using ZB.MOM.WW.SPHistorianClient.Models; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianDataQueryProtocol -{ - public const ushort QueryRequestTypeData = 1; - private const ushort GetNextResultBufferVersion = 9; - private const int GetNextResultBufferHeaderSize = 6; - private const int GetNextResultRowFixedTailSize = 75; - private const byte TerminalErrorType = 4; - private const uint TerminalErrorCodeNoMoreData = 30; - - /// - /// Walks the WCF GetNextQueryResultBuffer2 result body for raw/Full retrieval. Layout (decoded from - /// the canonical OtOpcUaParityTest_001.Counter capture, 4 rows × 141 bytes inside a 570-byte body): - /// header is UInt16 version=9 + UInt32 rowCount; each row is UInt32 tagKey + UInt32 tagNameLen + - /// (tagNameLen × 2) UTF-16 chars + UInt32 sampleCount + Int64 startUtc FILETIME + UInt32 quality + - /// UInt32 qualityDetail + UInt32 opcQuality + Double numericValue + Double percentGood + 35-byte - /// trailing block. The 5-byte error/terminal buffer accompanying the result decodes as - /// `04 1E 00 00 00` = type 4, code 30 = "no more data"; any other shape leaves - /// true. - /// - /// Trailing 35 bytes (cross-tag verified 2026-05-04 against SysTimeSec — structure is - /// tag-independent, server-internal sample metadata): - /// bytes 0-2 constant 0x00 0x00 0x01 (sample-format marker) - /// bytes 3-10 Int64 FILETIME UTC — duplicate of startTime for raw rows; - /// aggregate parser reads it as the interval start (offset row+tail+43) - /// bytes 11-18 zeros (reserved — likely end-time slot, populated by aggregate variants) - /// bytes 19-26 varies row-to-row even for identical Quality/Value; likely a storage - /// block sequence ID or snapshot offset. No user-facing meaning surfaced. - /// bytes 27,29 flag bytes (0/1 and 0/4 observed); semantics undecoded - /// bytes 28, 30-34 zeros (reserved) - /// No public HistorianSample fields map to bytes 19-34 — they look like server-internal - /// storage metadata. If a customer ever needs them surfaced, capture more rows with - /// known-distinct properties (force-store, backfill, version-replace) to narrow down. - /// - public static bool TryParseGetNextQueryResultBufferRows( - ReadOnlySpan result, - ReadOnlySpan errorTerminal, - out IReadOnlyList rows, - out bool hasMoreData) - { - rows = []; - hasMoreData = !IsTerminalNoMoreData(errorTerminal); - - if (result.Length == 0) - { - return true; - } - - if (result.Length < GetNextResultBufferHeaderSize) - { - return false; - } - - ushort version = BinaryPrimitives.ReadUInt16LittleEndian(result[..2]); - if (version != GetNextResultBufferVersion) - { - return false; - } - - uint rowCount = BinaryPrimitives.ReadUInt32LittleEndian(result.Slice(2, 4)); - int cursor = GetNextResultBufferHeaderSize; - List parsed = new(checked((int)rowCount)); - - for (uint i = 0; i < rowCount; i++) - { - if (cursor + 8 > result.Length) - { - return false; - } - - uint tagNameChars = BinaryPrimitives.ReadUInt32LittleEndian(result.Slice(cursor + 4, 4)); - int tagNameByteLength = checked((int)(tagNameChars * 2)); - int rowSize = checked(8 + tagNameByteLength + GetNextResultRowFixedTailSize); - if (cursor + rowSize > result.Length) - { - return false; - } - - ReadOnlySpan row = result.Slice(cursor, rowSize); - string tagName = Encoding.Unicode.GetString(row.Slice(8, tagNameByteLength)); - int tail = 8 + tagNameByteLength; - long startTimeFileTimeUtc = BinaryPrimitives.ReadInt64LittleEndian(row.Slice(tail + 4, 8)); - uint quality = BinaryPrimitives.ReadUInt32LittleEndian(row.Slice(tail + 12, 4)); - uint qualityDetail = BinaryPrimitives.ReadUInt32LittleEndian(row.Slice(tail + 16, 4)); - uint opcQuality = BinaryPrimitives.ReadUInt32LittleEndian(row.Slice(tail + 20, 4)); - double numericValue = BinaryPrimitives.ReadDoubleLittleEndian(row.Slice(tail + 24, 8)); - double percentGood = BinaryPrimitives.ReadDoubleLittleEndian(row.Slice(tail + 32, 8)); - - parsed.Add(new HistorianSample( - TagName: tagName, - TimestampUtc: DateTime.FromFileTimeUtc(startTimeFileTimeUtc), - NumericValue: numericValue, - StringValue: null, - Quality: checked((ushort)quality), - QualityDetail: qualityDetail, - OpcQuality: checked((ushort)opcQuality), - PercentGood: percentGood)); - - cursor += rowSize; - } - - rows = parsed; - return true; - } - - /// - /// Same wire layout as the raw parser, but interprets FILETIME #1 at row offset - /// `8 + tagNameLen*2 + 4` as the interval END timestamp and FILETIME #2 at trailer - /// offset 2 (row offset `8 + tagNameLen*2 + 43`) as the interval START. Native struct - /// evidence (`getnextrow-interpolated-memory-latest.json` / - /// `getnextrow-timeweightedaverage-memory-latest.json`) maps `+0x28 = EndDateTime` - /// and `+0x150 = StartDateTime`; the wire FILETIME #1 sits in the EndDateTime slot - /// after marshaling. For raw rows where Start == End the two values are equal, which - /// is consistent with the captured fixture. Live aggregate verification will - /// confirm or correct this orientation. - /// - public static bool TryParseGetNextQueryResultBufferAggregateRows( - ReadOnlySpan result, - ReadOnlySpan errorTerminal, - Models.RetrievalMode mode, - TimeSpan resolution, - out IReadOnlyList rows, - out bool hasMoreData) - { - rows = []; - hasMoreData = !IsTerminalNoMoreData(errorTerminal); - - if (result.Length == 0) - { - return true; - } - - if (result.Length < GetNextResultBufferHeaderSize) - { - return false; - } - - ushort version = BinaryPrimitives.ReadUInt16LittleEndian(result[..2]); - if (version != GetNextResultBufferVersion) - { - return false; - } - - uint rowCount = BinaryPrimitives.ReadUInt32LittleEndian(result.Slice(2, 4)); - int cursor = GetNextResultBufferHeaderSize; - List parsed = new(checked((int)rowCount)); - - for (uint i = 0; i < rowCount; i++) - { - if (cursor + 8 > result.Length) - { - return false; - } - - uint tagNameChars = BinaryPrimitives.ReadUInt32LittleEndian(result.Slice(cursor + 4, 4)); - int tagNameByteLength = checked((int)(tagNameChars * 2)); - int rowSize = checked(8 + tagNameByteLength + GetNextResultRowFixedTailSize); - if (cursor + rowSize > result.Length) - { - return false; - } - - ReadOnlySpan row = result.Slice(cursor, rowSize); - string tagName = Encoding.Unicode.GetString(row.Slice(8, tagNameByteLength)); - int tail = 8 + tagNameByteLength; - long endTimeFileTimeUtc = BinaryPrimitives.ReadInt64LittleEndian(row.Slice(tail + 4, 8)); - uint quality = BinaryPrimitives.ReadUInt32LittleEndian(row.Slice(tail + 12, 4)); - uint qualityDetail = BinaryPrimitives.ReadUInt32LittleEndian(row.Slice(tail + 16, 4)); - uint opcQuality = BinaryPrimitives.ReadUInt32LittleEndian(row.Slice(tail + 20, 4)); - double aggregateValue = BinaryPrimitives.ReadDoubleLittleEndian(row.Slice(tail + 24, 8)); - long startTimeFileTimeUtc = BinaryPrimitives.ReadInt64LittleEndian(row.Slice(tail + 43, 8)); - - parsed.Add(new HistorianAggregateSample( - TagName: tagName, - StartTimeUtc: DateTime.FromFileTimeUtc(startTimeFileTimeUtc), - EndTimeUtc: DateTime.FromFileTimeUtc(endTimeFileTimeUtc), - Value: aggregateValue, - Quality: checked((ushort)quality), - QualityDetail: qualityDetail, - OpcQuality: checked((ushort)opcQuality), - RetrievalMode: mode, - Resolution: resolution)); - - cursor += rowSize; - } - - rows = parsed; - return true; - } - - private static bool IsTerminalNoMoreData(ReadOnlySpan errorTerminal) - { - if (errorTerminal.Length != 5 || errorTerminal[0] != TerminalErrorType) - { - return false; - } - - return BinaryPrimitives.ReadUInt32LittleEndian(errorTerminal[1..]) == TerminalErrorCodeNoMoreData; - } - - public static byte[] SerializeFullHistoryRequest(HistorianDataQueryRequest request) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - bool noOption = string.Equals(request.Option, "NoOption", StringComparison.Ordinal); - writer.Write(noOption ? (ushort)3 : (ushort)9); - writer.Write((uint)request.QueryType); - writer.Write(request.QueryFormat); - writer.Write(request.SummaryType); - writer.Write(request.StartUtc.ToFileTimeUtc()); - writer.Write(request.EndUtc.ToFileTimeUtc()); - writer.Write((double)request.Resolution.Ticks); - writer.Write(request.ValueDeadband); - writer.Write(request.TimeDeadband); - WriteHistorianString(writer, request.TimeZone); - writer.Write(request.VersionType); - writer.Write(request.ResultBufferSize); - writer.Write(PackQueryTimeInterpolationFlags(request)); - if (!noOption) - { - WriteHistorianString(writer, request.Option); - } - - WriteHistorianString(writer, request.Filter); - writer.Write((ushort)request.ValueSelector); - writer.Write((ushort)request.AggregationType); - writer.Write((ushort)1); - writer.Write(request.ColumnSelectorFlags); - WriteStringVector(writer, request.TagNames); - writer.Write(request.MaxStates); - WriteMetadataNamespace(writer, request.MetadataNamespace); - writer.Write(request.ClientVersion); - writer.Write(request.SkipRows); - writer.Write(request.ReservedAfterSkipRows); - WriteRedundantEndpoint(writer, request.MdsEndpoint); - WriteRedundantEndpoint(writer, request.StorageEndpoint); - writer.Write(checked(request.Resolution.Ticks * 10_000L)); - WriteStringVector(writer, request.SliceByTagNames); - writer.Write(request.TimeoutQueryProcessingMilliseconds); - WriteAutoSummaryParameters(writer); - return stream.ToArray(); - } - - private static void WriteMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace) - { - writer.Write((byte)1); - WriteScrambledHistorianString(writer, metadataNamespace.Namespace); - WriteScrambledHistorianString(writer, metadataNamespace.TagPrefix); - WriteScrambledHistorianString(writer, metadataNamespace.PropertyPrefix); - } - - private static void WriteStringVector(BinaryWriter writer, IReadOnlyList values) - { - writer.Write((uint)values.Count); - foreach (string value in values) - { - WriteHistorianString(writer, value); - } - } - - private static void WriteRedundantEndpoint(BinaryWriter writer, HistorianRedundantEndpoint endpoint) - { - writer.Write((ushort)1); - WriteHistorianString(writer, endpoint.EndpointName); - checked - { - writer.Write((ushort)endpoint.Endpoints.Count); - } - - foreach (HistorianEndpoint candidate in endpoint.Endpoints) - { - WriteHistorianString(writer, candidate.NodeName); - WriteHistorianString(writer, candidate.PipeName); - } - } - - private static void WriteAutoSummaryParameters(BinaryWriter writer) - { - writer.Write((ushort)1); - writer.Write(0L); - writer.Write(0L); - for (int index = 0; index < 5; index++) - { - writer.Write((byte)0); - } - - writer.Write(0u); - } - - private static ushort PackQueryTimeInterpolationFlags(HistorianDataQueryRequest request) - { - ushort interpolation = request.InterpolationType == 254 ? (ushort)255 : request.InterpolationType; - return checked((ushort)((request.QualityRule << 12) | (request.TimestampRule << 8) | interpolation)); - } - - private static void WriteHistorianString(BinaryWriter writer, string value) - { - writer.Write((uint)value.Length); - if (value.Length > 0) - { - writer.Write(Encoding.Unicode.GetBytes(value)); - } - } - - private static void WriteScrambledHistorianString(BinaryWriter writer, string value) - { - if (value.Length == 0) - { - writer.Write((ushort)1); - writer.Write((byte)0); - return; - } - - ushort scrambleKey = 1; - foreach (char c in value) - { - if (c >= scrambleKey) - { - scrambleKey = checked((ushort)(c + 1)); - } - } - - writer.Write(scrambleKey); - writer.Write((byte)1); - writer.Write((byte)value.Length); - foreach (char c in value) - { - writer.Write((ushort)(c ^ scrambleKey)); - } - } -} - -internal sealed record HistorianDataQueryRequest( - IReadOnlyList TagNames, - DateTime StartUtc, - DateTime EndUtc, - ushort MaxStates, - uint BatchSize, - string Option) -{ - public uint QueryType { get; init; } = 2; - public uint QueryFormat { get; init; } - public uint SummaryType { get; init; } - public TimeSpan Resolution { get; init; } = TimeSpan.Zero; - public float ValueDeadband { get; init; } - public uint TimeDeadband { get; init; } - public string TimeZone { get; init; } = "UTC"; - public uint VersionType { get; init; } = 1; - public uint ResultBufferSize { get; init; } = 65_536; - public ushort InterpolationType { get; init; } = 255; - public ushort TimestampRule { get; init; } = 1; - public ushort QualityRule { get; init; } - public ulong ColumnSelectorFlags { get; init; } = 0x0000_8182_0007_82FF; - public string Filter { get; init; } = "NoFilter"; - public uint ValueSelector { get; init; } = 1; - public uint AggregationType { get; init; } = 3; - public HistorianMetadataNamespace MetadataNamespace { get; init; } = HistorianMetadataNamespace.Empty; - public ushort ClientVersion { get; init; } = 9; - public uint SkipRows { get; init; } - public uint ReservedAfterSkipRows { get; init; } - public HistorianRedundantEndpoint MdsEndpoint { get; init; } = HistorianRedundantEndpoint.Empty; - public HistorianRedundantEndpoint StorageEndpoint { get; init; } = HistorianRedundantEndpoint.Empty; - public IReadOnlyList SliceByTagNames { get; init; } = []; - public uint TimeoutQueryProcessingMilliseconds { get; init; } - public uint MaxQueryMemoryConsumptionInMb { get; init; } -} - -internal sealed record HistorianRedundantEndpoint(string EndpointName, IReadOnlyList Endpoints) -{ - public static HistorianRedundantEndpoint Empty { get; } = new(string.Empty, []); -} - -internal sealed record HistorianEndpoint(string NodeName, string PipeName); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianEventQueryProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianEventQueryProtocol.cs deleted file mode 100644 index ddd7bf9..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianEventQueryProtocol.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianEventQueryProtocol -{ - public const ushort QueryRequestTypeEvent = 3; - - public static IReadOnlyList CreateStartEventQueryAttempts(DateTime startUtc, DateTime endUtc, uint eventCount) - { - List attempts = []; - attempts.Add(CreateNativeEmptyFilterAttempt(startUtc, endUtc, eventCount)); - - return attempts; - } - - private static HistorianEventQueryAttempt CreateNativeEmptyFilterAttempt(DateTime startUtc, DateTime endUtc, uint eventCount) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write((ushort)5); - writer.Write(startUtc.ToFileTimeUtc()); - writer.Write(endUtc.ToFileTimeUtc()); - writer.Write(eventCount); - writer.Write(0u); - writer.Write((ushort)0); - writer.Write((ushort)1); - WriteNativeEmptyFilterBlock(writer); - writer.Write(65_536u); - WriteHistorianString(writer, "UTC"); - WriteMetadataNamespace(writer); - writer.Write(0u); - - byte[] request = stream.ToArray(); - return new HistorianEventQueryAttempt( - "native-empty-filter-version5", - 5, - request, - Convert.ToHexString(SHA256.HashData(request)).ToLowerInvariant()); - } - - private static HistorianEventQueryAttempt CreateAttempt( - string shape, - ushort version, - DateTime startUtc, - DateTime endUtc, - uint eventCount, - Action writeFilters, - bool writeTimeZoneBeforeFilter) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write(version); - writer.Write(startUtc.ToFileTimeUtc()); - writer.Write(endUtc.ToFileTimeUtc()); - writer.Write(eventCount); - writer.Write(0u); - writer.Write((ushort)0); - writer.Write((ushort)1); - - if (writeTimeZoneBeforeFilter) - { - WriteHistorianString(writer, "UTC"); - writeFilters(writer); - } - else - { - writeFilters(writer); - WriteHistorianString(writer, "UTC"); - } - - byte[] request = stream.ToArray(); - return new HistorianEventQueryAttempt( - $"{shape}-version{version}", - version, - request, - Convert.ToHexString(SHA256.HashData(request)).ToLowerInvariant()); - } - - private static void WriteFilterBlockV1(BinaryWriter writer) - { - writer.Write((ushort)1); - writer.Write((byte)0); - writer.Write(0L); - writer.Write(Guid.Empty.ToByteArray()); - writer.Write(0u); - } - - private static void WriteNativeEmptyFilterBlock(BinaryWriter writer) - { - writer.Write((ushort)0); - writer.Write(0u); - writer.Write((byte)0); - } - - private static void WriteMetadataNamespace(BinaryWriter writer) - { - writer.Write((byte)1); - WriteScrambledHistorianString(writer, string.Empty); - WriteScrambledHistorianString(writer, string.Empty); - WriteScrambledHistorianString(writer, string.Empty); - } - - private static void WriteScrambledHistorianString(BinaryWriter writer, string value) - { - if (value.Length == 0) - { - writer.Write((ushort)1); - writer.Write((byte)0); - return; - } - - ushort scrambleKey = 1; - foreach (char c in value) - { - if (c >= scrambleKey) - { - scrambleKey = checked((ushort)(c + 1)); - } - } - - writer.Write(scrambleKey); - writer.Write((byte)1); - writer.Write((byte)value.Length); - foreach (char c in value) - { - writer.Write((ushort)(c ^ scrambleKey)); - } - } - - private static void WriteFilterBlockContinuationOnly(BinaryWriter writer) - { - writer.Write((byte)0); - writer.Write(0L); - writer.Write(Guid.Empty.ToByteArray()); - writer.Write(0u); - } - - private static void WriteFilterBlockCountOnly(BinaryWriter writer) - { - writer.Write(0u); - } - - private static void WriteHistorianString(BinaryWriter writer, string value) - { - writer.Write((uint)value.Length); - if (value.Length > 0) - { - writer.Write(Encoding.Unicode.GetBytes(value)); - } - } -} - -internal sealed record HistorianEventQueryAttempt(string Name, ushort Version, byte[] RequestBuffer, string RequestSha256); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianEventRowProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianEventRowProtocol.cs deleted file mode 100644 index 350d1c0..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianEventRowProtocol.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System.Buffers.Binary; -using System.Text; -using ZB.MOM.WW.SPHistorianClient.Models; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Parser for the version-9 event-row buffer the Historian server returns from -/// /Retr/GetNextEventQueryResultBuffer.pResultBuff. Wire shape decoded from a captured -/// native event read (instrument-wcf-readmessage record 24, two rows for Alarm.Set + Alarm.Clear): -/// -/// -/// UInt16 version = 9 -/// UInt32 rowCount -/// rowCount × Row { -/// UInt32 rowMarker = 0x1E -/// UInt16 rowFormat = 7 -/// Int64 eventTimeUtcFiletime -/// UInt16 × 8 // purpose unclear (slot offsets?) -/// compact ASCII string // event type (Alarm.Set, Alarm.Clear, ...) -/// UInt16 propertyCount -/// propertyCount × Property { -/// compact ASCII string // property name -/// Value { -/// UInt8 typeMarker -/// UInt8 length // bytes of value following status -/// UInt8 status // observed 0x00 in successful captures -/// length × byte // encoding determined by typeMarker: -/// 0x02 → Boolean (1 byte: 0/1) -/// 0x10 → GUID (16 bytes) -/// 0x18 → FILETIME UTC (Int64) -/// 0x31 → Int32 little-endian -/// 0x43 → UTF-16 string: UInt16 charCount + charCount × UInt16 chars -/// } -/// } -/// } -/// -/// -/// Compact ASCII string: 0x09 LEN 0x00 LEN×ASCII bytes (same encoding as -/// CTagMetadata strings). -/// -internal static class HistorianEventRowProtocol -{ - public const ushort EventRowProtocolVersion = 9; - public const uint RowMarker = 0x0000001Eu; - public const ushort RowFormatV9 = 7; - private const int HeaderSize = 6; - private const int RowFixedHeaderSize = 4 + 2 + 8 + 16; - - private const byte ValueTypeBool = 0x02; - private const byte ValueTypeGuid = 0x10; - private const byte ValueTypeFiletime = 0x18; - private const byte ValueTypeInt32 = 0x31; - private const byte ValueTypeUtf16String = 0x43; - - public static IReadOnlyList Parse(ReadOnlySpan buffer) - { - if (buffer.Length < HeaderSize) - { - return []; - } - - ushort version = BinaryPrimitives.ReadUInt16LittleEndian(buffer[..2]); - if (version != EventRowProtocolVersion) - { - return []; - } - - uint rowCount = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(2, 4)); - if (rowCount == 0) - { - return []; - } - - List events = new(checked((int)rowCount)); - int cursor = HeaderSize; - for (uint rowIndex = 0; rowIndex < rowCount; rowIndex++) - { - if (!TryReadRow(buffer, ref cursor, out HistorianEvent? row)) - { - break; - } - - events.Add(row); - } - - return events; - } - - private static bool TryReadRow(ReadOnlySpan buffer, ref int cursor, out HistorianEvent row) - { - row = null!; - if (cursor + RowFixedHeaderSize > buffer.Length) - { - return false; - } - - uint marker = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(cursor, 4)); - if (marker != RowMarker) - { - return false; - } - - ushort format = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(cursor + 4, 2)); - if (format != RowFormatV9) - { - return false; - } - - long filetime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(cursor + 6, 8)); - DateTime eventTimeUtc = DateTime.FromFileTimeUtc(filetime); - int afterFixedHeader = cursor + RowFixedHeaderSize; - - if (!TryReadCompactAsciiString(buffer, afterFixedHeader, out string eventType, out int afterType)) - { - return false; - } - - if (afterType + 2 > buffer.Length) - { - return false; - } - - ushort propertyCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(afterType, 2)); - int propertyCursor = afterType + 2; - - Dictionary properties = new(propertyCount, StringComparer.OrdinalIgnoreCase); - for (int p = 0; p < propertyCount; p++) - { - if (!TryReadCompactAsciiString(buffer, propertyCursor, out string name, out int afterName)) - { - return false; - } - - if (!TryReadValue(buffer, afterName, out object? value, out int afterValue)) - { - return false; - } - - properties[name] = value; - propertyCursor = afterValue; - } - - row = BuildEvent(eventTimeUtc, eventType, properties); - cursor = propertyCursor; - return true; - } - - private static HistorianEvent BuildEvent(DateTime eventTimeUtc, string eventType, Dictionary properties) - { - Guid id = TryGetGuid(properties, "alarm_id") ?? Guid.Empty; - DateTime receivedTime = TryGetFiletime(properties, "receivedtime") ?? eventTimeUtc; - string sourceName = TryGetString(properties, "source_processvariable") ?? TryGetString(properties, "source_object") ?? string.Empty; - string ns = TryGetString(properties, "namespace") ?? TryGetString(properties, "provider_system") ?? string.Empty; - ushort revisionVersion = TryGetInt32(properties, "revisionversion") is int rv && rv is >= 0 and <= ushort.MaxValue - ? (ushort)rv - : (ushort)0; - - return new HistorianEvent( - Id: id, - EventTimeUtc: eventTimeUtc, - ReceivedTimeUtc: receivedTime, - Type: eventType, - SourceName: sourceName, - Namespace: ns, - RevisionVersion: revisionVersion, - Properties: properties); - } - - private static Guid? TryGetGuid(Dictionary properties, string key) => - properties.TryGetValue(key, out object? value) && value is Guid g ? g : null; - - private static DateTime? TryGetFiletime(Dictionary properties, string key) => - properties.TryGetValue(key, out object? value) && value is DateTime dt ? dt : null; - - private static string? TryGetString(Dictionary properties, string key) => - properties.TryGetValue(key, out object? value) && value is string s ? s : null; - - private static int? TryGetInt32(Dictionary properties, string key) => - properties.TryGetValue(key, out object? value) && value is int i ? i : null; - - /// - /// Compact ASCII string encoding: 0x09 LEN 0x00 LEN×ASCII bytes. - /// - private static bool TryReadCompactAsciiString(ReadOnlySpan buffer, int offset, out string value, out int afterOffset) - { - value = string.Empty; - afterOffset = offset; - if (offset + 3 > buffer.Length || buffer[offset] != 0x09) - { - return false; - } - - byte length = buffer[offset + 1]; - int payloadStart = offset + 3; - if (payloadStart + length > buffer.Length) - { - return false; - } - - value = Encoding.ASCII.GetString(buffer.Slice(payloadStart, length)); - afterOffset = payloadStart + length; - return true; - } - - /// - /// Value encoding: typeMarker(1) + length(1) + status(1) + length×value bytes. - /// Decodes the value by typeMarker; unknown markers preserve the raw bytes as a - /// in the property bag. - /// - private static bool TryReadValue(ReadOnlySpan buffer, int offset, out object? value, out int afterOffset) - { - value = null; - afterOffset = offset; - if (offset + 3 > buffer.Length) - { - return false; - } - - byte typeMarker = buffer[offset]; - byte length = buffer[offset + 1]; - // buffer[offset + 2] is the status byte (observed 0x00 in successful captures). - int valueStart = offset + 3; - if (valueStart + length > buffer.Length) - { - return false; - } - - ReadOnlySpan valueBytes = buffer.Slice(valueStart, length); - value = typeMarker switch - { - ValueTypeBool when length >= 1 => valueBytes[0] != 0, - ValueTypeGuid when length == 16 => new Guid(valueBytes), - ValueTypeFiletime when length == 8 => DateTime.FromFileTimeUtc(BinaryPrimitives.ReadInt64LittleEndian(valueBytes)), - ValueTypeInt32 when length == 4 => BinaryPrimitives.ReadInt32LittleEndian(valueBytes), - ValueTypeUtf16String when length >= 2 => DecodeUtf16String(valueBytes), - _ => valueBytes.ToArray() - }; - - afterOffset = valueStart + length; - return true; - } - - private static string DecodeUtf16String(ReadOnlySpan valueBytes) - { - ushort charCount = BinaryPrimitives.ReadUInt16LittleEndian(valueBytes[..2]); - int byteCount = checked(charCount * 2); - if (byteCount > valueBytes.Length - 2) - { - byteCount = valueBytes.Length - 2; - } - - return Encoding.Unicode.GetString(valueBytes.Slice(2, byteCount)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianNativeHandshake.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianNativeHandshake.cs deleted file mode 100644 index 1518021..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianNativeHandshake.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Buffers.Binary; -using System.Diagnostics; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Transport-agnostic pieces of the native Historian connect handshake: building the -/// OpenConnection3 v6 request buffer, running the SSPI/NTLM token-exchange rounds, and -/// decoding the OpenConnection response. Shared by the WCF/MDAS path -/// () and the 2023 R2 gRPC path -/// (Grpc.HistorianGrpcReadOrchestrator). The byte payloads are identical across -/// transports — only the envelope (WCF operation vs gRPC method) differs. -/// -internal static class HistorianNativeHandshake -{ - private const int CredentialBlockSizeBytes = 1026; - private const int OpenConnectionMinResponseLength = 5; - private const int MaxTokenRounds = 8; - private const string ClientNodeNameFallback = "ZB.MOM.WW.SPHistorianClient"; - private const string ClientDataSourceId = "2020.406.2652.2"; - private const string ClientDllVersionString = "2020.406.2652.2"; - private const byte NativeClientType = 4; - private const byte NativeClientCommonInfoFormatVersion = 4; - private const ushort NativeHcalVersion = 17; - private const uint NativeClientVersionInt = 999_999; - private const ushort NativeOpen2ClientVersion = 9; - - /// Result of one transport-level credential-token exchange. - internal readonly record struct TokenExchangeResult(bool Success, byte[] ServerOutput, byte[] Error); - - /// - /// Performs a single credential-token round on the wire. is the - /// upper-case context-key GUID, is the AVEVA-wrapped SSPI - /// token (round byte + length + token). The WCF path maps this to - /// Hist.ValidateClientCredential; the gRPC path maps it to - /// HistoryService.ExchangeKey (the renamed handshake op). - /// - internal delegate TokenExchangeResult TokenExchange(string handle, byte[] wrappedToken, int round); - - /// - /// Drives the SSPI/NTLM negotiate loop against the supplied - /// delegate until the server signals terminal success. Mirrors the native two-round - /// (69→239, 93→1) sequence. - /// - public static void RunTokenRounds( - TokenExchange exchange, - Guid contextKey, - HistorianClientOptions options, - CancellationToken cancellationToken) - { - using HistorianSspiClient sspi = options.IntegratedSecurity - ? new HistorianSspiClient(options.TargetSpn) - : new HistorianSspiClient(options.TargetSpn, ParseDomain(options.UserName), ParseUserName(options.UserName), options.Password); - - string handle = contextKey.ToString("D").ToUpperInvariant(); - byte[] incoming = []; - - for (int round = 0; round < MaxTokenRounds; round++) - { - cancellationToken.ThrowIfCancellationRequested(); - - HistorianSspiStepResult step = sspi.Next(incoming); - byte[] outgoing = step.Token; - HistorianWcfAuthenticationProtocol.TryApplyNativeNtlmNegotiateVersionFlag(outgoing); - byte[] wrapped = HistorianWcfAuthenticationProtocol.WrapValidateClientCredentialToken(round == 0, outgoing); - - TokenExchangeResult result = exchange(handle, wrapped, round); - byte[] serverOutput = result.ServerOutput ?? []; - byte[] error = result.Error ?? []; - - if (!result.Success) - { - throw new InvalidOperationException($"Credential token round {round} rejected (errorLen={error.Length})."); - } - - ValidateClientCredentialResponse? response = HistorianWcfAuthenticationProtocol.TryReadValidateClientCredentialResponse(serverOutput); - if (response is null || !response.Continue) - { - return; - } - - incoming = response.Token; - if (step.IsCompleted && incoming.Length == 0) - { - return; - } - } - - throw new InvalidOperationException($"Credential token exchange exceeded {MaxTokenRounds} rounds without terminal success."); - } - - /// - /// Builds the native OpenConnection3 (Open2) version-6 request buffer. Identical bytes are - /// sent over WCF (Hist.OpenConnection2) and gRPC - /// (HistoryService.OpenConnection.btConnectionRequest). - /// - public static byte[] BuildOpenConnection3Request(string host, Guid contextKey, uint connectionMode) - { - Process current = Process.GetCurrentProcess(); - string machineName = Environment.MachineName; - string processName = string.IsNullOrEmpty(current.ProcessName) ? ClientNodeNameFallback : current.ProcessName; - _ = host; // host reserved for remote-orchestrator extension - - HistorianOpen2Request open2 = new( - HostName: machineName, - ProcessName: string.Empty, - ProcessId: checked((uint)current.Id), - UserName: string.Empty, - Password: [], - ClientType: NativeClientType, - ClientVersion: NativeOpen2ClientVersion, - ConnectionMode: connectionMode, - MetadataNamespace: HistorianMetadataNamespace.Empty); - - HistorianClientCommonInfo commonInfo = new( - FormatVersion: NativeClientCommonInfoFormatVersion, - ServerNodeName: machineName, - ClientNodeName: processName, - ProcessId: checked((uint)current.Id), - HcalVersion: NativeHcalVersion, - ProcessName: string.Empty, - Proxy: string.Empty, - DataSourceId: ClientDataSourceId, - ShardId: Guid.Empty, - ClientVersion: NativeClientVersionInt, - ClientTimestamp: (ulong)DateTime.UtcNow.ToFileTimeUtc(), - ClientDllVersion: ClientDllVersionString); - - return HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6( - open2, - commonInfo, - contextKey, - credentialBlock: new byte[CredentialBlockSizeBytes]); - } - - /// - /// Decodes the OpenConnection response blob: byte 0 = protocol version, bytes 1..4 = - /// transient /Retr client handle (UInt32 LE), bytes 5..20 = storage session GUID. - /// - public static (uint ClientHandle, Guid StorageSessionId) ParseOpenConnectionResponse(ReadOnlySpan response) - { - if (response.Length < OpenConnectionMinResponseLength) - { - throw new InvalidOperationException($"OpenConnection response too short (ResponseLen={response.Length})."); - } - - uint clientHandle = BinaryPrimitives.ReadUInt32LittleEndian(response.Slice(1, 4)); - Guid storageSessionId = response.Length >= 21 ? new Guid(response.Slice(5, 16)) : Guid.Empty; - return (clientHandle, storageSessionId); - } - - private static string ParseDomain(string userName) - { - if (string.IsNullOrEmpty(userName)) return string.Empty; - int slash = userName.IndexOf('\\'); - return slash > 0 ? userName[..slash] : string.Empty; - } - - private static string ParseUserName(string userName) - { - if (string.IsNullOrEmpty(userName)) return string.Empty; - int slash = userName.IndexOf('\\'); - return slash > 0 ? userName[(slash + 1)..] : userName; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianOpen2Protocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianOpen2Protocol.cs deleted file mode 100644 index c809772..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianOpen2Protocol.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Buffers.Binary; -using System.Text; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianOpen2Protocol -{ - public static byte[] SerializeLegacyVersion1(HistorianOpen2Request request) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write((ushort)1); - WriteHistorianString(writer, request.HostName); - WriteHistorianString(writer, request.ProcessName); - writer.Write(request.ProcessId); - WriteHistorianString(writer, request.UserName); - writer.Write((uint)request.Password.Length); - writer.Write(request.Password); - writer.Write(request.ClientType); - writer.Write(request.ClientVersion); - writer.Write(request.ConnectionMode); - WriteMetadataNamespace(writer, request.MetadataNamespace); - return stream.ToArray(); - } - - public static byte[] SerializeNativeVersion3(HistorianOpen2Request request, HistorianClientCommonInfo commonInfo) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write((byte)3); - WriteNativeOpenConnectionContent(writer, request, commonInfo); - return stream.ToArray(); - } - - public static byte[] SerializeNativeOpenConnection3Version6( - HistorianOpen2Request request, - HistorianClientCommonInfo commonInfo, - Guid clientKey, - byte[]? credentialBlock = null) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write((byte)6); - writer.Write(clientKey.ToByteArray()); - writer.Write((byte)0); - WriteNativeOpenConnectionContent(writer, request, commonInfo, credentialBlock, useCompactMetadataNamespace: true); - return stream.ToArray(); - } - - private static void WriteNativeOpenConnectionContent( - BinaryWriter writer, - HistorianOpen2Request request, - HistorianClientCommonInfo commonInfo, - byte[]? credentialBlock = null, - bool useCompactMetadataNamespace = false) - { - byte[] secretBytes = credentialBlock ?? request.Password; - WriteHistorianString(writer, request.HostName); - checked - { - writer.Write((ushort)secretBytes.Length); - } - - writer.Write(secretBytes); - writer.Write(request.ClientType); - writer.Write(request.ConnectionMode); - if (useCompactMetadataNamespace) - { - WriteCompactMetadataNamespace(writer, request.MetadataNamespace); - } - else - { - WriteMetadataNamespace(writer, request.MetadataNamespace); - } - - WriteHistorianString(writer, string.Empty); - WriteHistorianString(writer, string.Empty); - WriteClientCommonInfo(writer, commonInfo); - } - - public static HistorianNativeError? TryReadNativeError(ReadOnlySpan buffer) - { - if (buffer.Length < 5) - { - return null; - } - - byte type = buffer[0]; - uint code = BinaryPrimitives.ReadUInt32LittleEndian(buffer[1..5]); - return new HistorianNativeError(type, code, GetKnownErrorName(code)); - } - - public static HistorianLegacyOpen2Output? TryReadLegacyOpen2Output(ReadOnlySpan buffer) - { - if (buffer.Length != 32) - { - return null; - } - - uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer[..4]); - Guid storageSessionId = new(buffer.Slice(4, 16)); - long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(20, 8)); - uint serverStatus = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(28, 4)); - return new HistorianLegacyOpen2Output(handle, storageSessionId, connectTime, serverStatus); - } - - public static HistorianNativeOpen3Output? TryReadNativeOpen3Output(ReadOnlySpan buffer) - { - if (buffer.Length < 29) - { - return null; - } - - byte protocolVersion = buffer[0]; - if (protocolVersion is not (2 or 3)) - { - return null; - } - - int minimumLength = protocolVersion >= 3 ? 37 : 29; - if (buffer.Length < minimumLength) - { - return null; - } - - uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(1, 4)); - Guid storageSessionId = new(buffer.Slice(5, 16)); - long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(21, 8)); - long? serverTime = null; - if (protocolVersion >= 3) - { - serverTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(29, 8)); - } - - byte[] trailingBytes = buffer[minimumLength..].ToArray(); - return new HistorianNativeOpen3Output( - protocolVersion, - handle, - storageSessionId, - connectTime, - serverTime, - trailingBytes); - } - - public static byte[] EncodeWidePassword(string password) - { - return string.IsNullOrEmpty(password) ? [] : Encoding.Unicode.GetBytes(password); - } - - private static void WriteMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace) - { - writer.Write(metadataNamespace.HasValue ? (byte)1 : (byte)0); - WriteHistorianString(writer, metadataNamespace.Namespace); - WriteHistorianString(writer, metadataNamespace.TagPrefix); - WriteHistorianString(writer, metadataNamespace.PropertyPrefix); - } - - private static void WriteCompactMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace) - { - if (!metadataNamespace.HasValue - || metadataNamespace.Namespace.Length != 0 - || metadataNamespace.TagPrefix.Length != 0 - || metadataNamespace.PropertyPrefix.Length != 0) - { - throw new ProtocolEvidenceMissingException("OpenConnection3 non-empty metadata namespace"); - } - - writer.Write((byte)1); - WriteCompactEmptyString(writer); - WriteCompactEmptyString(writer); - WriteCompactEmptyString(writer); - } - - private static void WriteCompactEmptyString(BinaryWriter writer) - { - writer.Write((ushort)1); - writer.Write((byte)0); - } - - private static void WriteHistorianString(BinaryWriter writer, string value) - { - writer.Write((uint)value.Length); - if (value.Length > 0) - { - writer.Write(Encoding.Unicode.GetBytes(value)); - } - } - - private static void WriteClientCommonInfo(BinaryWriter writer, HistorianClientCommonInfo commonInfo) - { - writer.Write(commonInfo.FormatVersion); - WriteHistorianString(writer, commonInfo.ServerNodeName); - WriteHistorianString(writer, commonInfo.ClientNodeName); - writer.Write(commonInfo.ProcessId); - writer.Write(commonInfo.HcalVersion); - WriteHistorianString(writer, commonInfo.ProcessName); - WriteHistorianString(writer, commonInfo.Proxy); - WriteHistorianString(writer, commonInfo.DataSourceId); - writer.Write(commonInfo.ShardId.ToByteArray()); - writer.Write(commonInfo.ClientVersion); - if (commonInfo.FormatVersion >= 3) - { - writer.Write(commonInfo.ClientTimestamp); - } - - if (commonInfo.FormatVersion >= 4) - { - WriteHistorianString(writer, commonInfo.ClientDllVersion); - } - } - - private static string? GetKnownErrorName(uint code) - { - return code switch - { - 1 => "Failure", - 73 => "InvalidPacketVersion", - 171 => "AuthenticationFailed", - _ => null - }; - } -} - -internal sealed record HistorianOpen2Request( - string HostName, - string ProcessName, - uint ProcessId, - string UserName, - byte[] Password, - byte ClientType, - ushort ClientVersion, - uint ConnectionMode, - HistorianMetadataNamespace MetadataNamespace); - -internal sealed record HistorianMetadataNamespace( - bool HasValue, - string Namespace, - string TagPrefix, - string PropertyPrefix) -{ - public static HistorianMetadataNamespace Empty { get; } = new(true, string.Empty, string.Empty, string.Empty); -} - -internal sealed record HistorianNativeError(byte Type, uint Code, string? Name); - -internal sealed record HistorianLegacyOpen2Output( - uint Handle, - Guid StorageSessionId, - long ConnectTimeFileTimeUtc, - uint ServerStatus); - -internal sealed record HistorianNativeOpen3Output( - byte ProtocolVersion, - uint Handle, - Guid StorageSessionId, - long ConnectTimeFileTimeUtc, - long? ServerTimeFileTimeUtc, - byte[] TrailingBytes); - -internal sealed record HistorianClientCommonInfo( - byte FormatVersion, - string ServerNodeName, - string ClientNodeName, - uint ProcessId, - ushort HcalVersion, - string ProcessName, - string Proxy, - string DataSourceId, - Guid ShardId, - uint ClientVersion, - ulong ClientTimestamp, - string ClientDllVersion); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianSspiClient.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianSspiClient.cs deleted file mode 100644 index 2c6fba0..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianSspiClient.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Net; -using System.Net.Security; -using System.Security.Authentication.ExtendedProtection; -using System.Security.Principal; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Cross-platform Negotiate / NTLM token producer for the Historian's `Hist.ValCl` -/// authentication exchange. Uses under the hood -/// (Windows: SSPI; Linux/macOS: GSSAPI via libgssapi_krb5 / gss-ntlmssp). -/// -/// The native AVEVA wrapper passes specific request flags to -/// InitializeSecurityContextW: IDENTIFY | CONNECTION | CONFIDENTIALITY | -/// SEQUENCE_DETECT | REPLAY_DETECT on round 0 and the same minus IDENTIFY on -/// rounds 1+. The REPLAY_DETECT + SEQUENCE_DETECT pair drives NTLM MIC generation; -/// without it AcceptSecurityContext rejects the type-3 token with -/// SEC_E_INVALID_TOKEN. RequiredProtectionLevel.EncryptAndSign in -/// NegotiateAuthentication implicitly requests SEQUENCE + REPLAY + -/// CONFIDENTIALITY, and AllowedImpersonationLevel = Identification requests -/// IDENTIFY — together these produce a request flag set that AcceptSecurityContext -/// accepts on the server side. -/// -/// The constants and request-flag selection helpers below are preserved for the -/// existing unit tests in HistorianSspiClientTests — they document the -/// captured native flag values rather than driving the underlying API today. -/// -internal sealed class HistorianSspiClient : IDisposable -{ - public const int IscReqReplayDetect = 0x4; - public const int IscReqSequenceDetect = 0x8; - public const int IscReqConfidentiality = 0x10; - public const int IscReqConnection = 0x800; - public const int IscReqIdentify = 0x20000; - public const int IscReqAllocateMemory = 0x100; - - public const int NativeFlagsRound0 = IscReqIdentify | IscReqConnection | IscReqConfidentiality | IscReqSequenceDetect | IscReqReplayDetect; - public const int NativeFlagsRoundSubsequent = IscReqConnection | IscReqConfidentiality | IscReqSequenceDetect | IscReqReplayDetect; - - private readonly NegotiateAuthentication _auth; - private int _roundIndex; - private bool _disposed; - - public HistorianSspiClient(string targetName, string package = "Negotiate") - { - ArgumentException.ThrowIfNullOrWhiteSpace(targetName); - ArgumentException.ThrowIfNullOrWhiteSpace(package); - _auth = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions - { - Package = package, - TargetName = targetName, - RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, - AllowedImpersonationLevel = TokenImpersonationLevel.Identification, - RequireMutualAuthentication = false, - }); - } - - /// - /// Acquires Negotiate credentials for an explicit user/domain/password instead - /// of the calling thread's identity. On Linux this routes through GSSAPI's - /// credential acquisition; the supplied credential is wrapped in a - /// . - /// - public HistorianSspiClient(string targetName, string? domain, string userName, string? password, string package = "Negotiate") - { - ArgumentException.ThrowIfNullOrWhiteSpace(targetName); - ArgumentException.ThrowIfNullOrWhiteSpace(userName); - ArgumentException.ThrowIfNullOrWhiteSpace(package); - _auth = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions - { - Package = package, - TargetName = targetName, - Credential = new NetworkCredential(userName, password ?? string.Empty, domain ?? string.Empty), - RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, - AllowedImpersonationLevel = TokenImpersonationLevel.Identification, - RequireMutualAuthentication = false, - }); - } - - /// Internal accessor for tests; returns the request flag bitmask the next Next call corresponds to. - internal int NextRequestFlags => SelectRequestFlags(_roundIndex) | IscReqAllocateMemory; - - public static int SelectRequestFlags(int roundIndex) => roundIndex == 0 ? NativeFlagsRound0 : NativeFlagsRoundSubsequent; - - public HistorianSspiStepResult Next(byte[] incoming) - { - ArgumentNullException.ThrowIfNull(incoming); - ObjectDisposedException.ThrowIf(_disposed, this); - - byte[]? outgoing = _auth.GetOutgoingBlob(incoming.Length == 0 ? null : incoming, out NegotiateAuthenticationStatusCode status); - _roundIndex++; - - bool completed = status switch - { - NegotiateAuthenticationStatusCode.Completed => true, - NegotiateAuthenticationStatusCode.ContinueNeeded => false, - _ => throw new InvalidOperationException($"Negotiate handshake failed: {status}"), - }; - - return new HistorianSspiStepResult(outgoing ?? [], completed); - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - _auth.Dispose(); - } -} - -internal readonly record struct HistorianSspiStepResult(byte[] Token, bool IsCompleted); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianStatusProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianStatusProtocol.cs deleted file mode 100644 index eee2e77..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianStatusProtocol.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Buffers.Binary; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianStatusProtocol -{ - public const int SystemTimeByteCount = 16; - - public static DateTime? TryReadSystemTime(ReadOnlySpan buffer) - { - if (buffer.Length < SystemTimeByteCount) - { - return null; - } - - ushort year = BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]); - ushort month = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]); - ushort day = BinaryPrimitives.ReadUInt16LittleEndian(buffer[6..8]); - ushort hour = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]); - ushort minute = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]); - ushort second = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]); - ushort millisecond = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]); - - try - { - return new DateTime(year, month, day, hour, minute, second, millisecond, DateTimeKind.Unspecified); - } - catch (ArgumentOutOfRangeException) - { - return null; - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianTagQueryProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianTagQueryProtocol.cs deleted file mode 100644 index bec78d1..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianTagQueryProtocol.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianTagQueryProtocol -{ - public const ushort NativeStartTagQueryMarker = 26_449; - public const ushort NativeStartTagQueryVersion = 1; - - public static HistorianTagQueryAttempt CreateStartTagQueryAttempt(string tagFilter) - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write(NativeStartTagQueryMarker); - writer.Write(NativeStartTagQueryVersion); - WriteHistorianString(writer, tagFilter); - - byte[] request = stream.ToArray(); - return new HistorianTagQueryAttempt( - "native-start-tag-query-version1", - request, - Convert.ToHexString(SHA256.HashData(request)).ToLowerInvariant()); - } - - public static HistorianTagQueryAttempt CreateStartTagQueryHeaderOnlyAttempt() - { - using MemoryStream stream = new(); - using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); - - writer.Write(NativeStartTagQueryMarker); - writer.Write(NativeStartTagQueryVersion); - - byte[] request = stream.ToArray(); - return new HistorianTagQueryAttempt( - "native-start-tag-query-header-only", - request, - Convert.ToHexString(SHA256.HashData(request)).ToLowerInvariant()); - } - - public static HistorianTagQueryStartResponse ParseStartTagQueryResponse(ReadOnlySpan response) - { - if (response.Length != 8) - { - throw new InvalidDataException("StartTagQuery response must be exactly 8 bytes."); - } - - return new HistorianTagQueryStartResponse( - BitConverter.ToUInt32(response[..4]), - BitConverter.ToUInt32(response[4..8])); - } - - public static IReadOnlyList ParseGetTagInfoResponse(ReadOnlySpan response) - { - if (response.Length < 4) - { - throw new InvalidDataException("GetTagInfo response is missing the tag count."); - } - - int cursor = 0; - uint count = ReadUInt32(response, ref cursor); - List tags = new(checked((int)count)); - for (uint index = 0; index < count; index++) - { - tags.Add(ParseTagInfoRecord(response, ref cursor)); - } - - return tags; - } - - public static HistorianTagInfoResponse ParseGetTagInfoFromNameResponse(ReadOnlySpan response) - { - int cursor = 0; - return ParseTagInfoRecord(response, ref cursor); - } - - public static IReadOnlyList ParseGetLikeTagNamesResponse(ReadOnlySpan response) - { - if (response.Length < 4) - { - throw new InvalidDataException("GetLikeTagnames response is missing the tag count."); - } - - int cursor = 0; - uint count = ReadUInt32(response, ref cursor); - List tagNames = new(checked((int)count)); - for (uint index = 0; index < count; index++) - { - uint charLength = ReadUInt32(response, ref cursor); - int byteLength = checked((int)charLength * 2); - EnsureAvailable(response, cursor, byteLength); - tagNames.Add(Encoding.Unicode.GetString(response.Slice(cursor, byteLength))); - cursor += byteLength; - } - - if (cursor != response.Length) - { - throw new InvalidDataException("GetLikeTagnames response has trailing bytes."); - } - - return tagNames; - } - - private static void WriteHistorianString(BinaryWriter writer, string value) - { - writer.Write((uint)value.Length); - if (value.Length > 0) - { - writer.Write(Encoding.Unicode.GetBytes(value)); - } - } - - private static string ReadCompactAsciiString(ReadOnlySpan response, ref int cursor) - { - EnsureAvailable(response, cursor, 3); - byte marker = response[cursor++]; - if (marker != 0x09) - { - throw new InvalidDataException($"Expected compact string marker 0x09, found 0x{marker:X2}."); - } - - ushort byteLength = ReadUInt16(response, ref cursor); - EnsureAvailable(response, cursor, byteLength); - string value = Encoding.UTF8.GetString(response.Slice(cursor, byteLength)); - cursor += byteLength; - return value; - } - - private static HistorianTagInfoResponse ParseTagInfoRecord(ReadOnlySpan response, ref int cursor) - { - EnsureAvailable(response, cursor, 24); - byte[] nativeDataTypeDescriptor = response.Slice(cursor, 4).ToArray(); - cursor += 4; - Guid typeId = new(response.Slice(cursor, 16)); - cursor += 16; - uint tagKey = ReadUInt32(response, ref cursor); - - // The compact-ASCII string slot count varies by tag origin (decoded from - // GetTagInfoFromName captures across multiple tag types): - // 1 string : TagName only (degenerate / unknown shape) - // 2 strings : TagName + MetadataProvider (e.g., MDAS-routed external tags) - // 4 strings : TagName + Description + ItemName + CreatedBy (local Sys tags) - // Walk strings dynamically until the next byte isn't the 0x09 marker. - List strings = new(4); - while (cursor < response.Length && response[cursor] == 0x09) - { - strings.Add(ReadCompactAsciiString(response, ref cursor)); - } - - string tagName = strings.Count > 0 ? strings[0] : string.Empty; - // String at position 1 is Description for full-shape tags or MetadataProvider - // for MDAS-routed tags. Both are useful; expose under MetadataProvider for back-compat - // and Description for new semantics. - string metadataProvider = strings.Count > 1 ? strings[1] : string.Empty; - string? description = strings.Count >= 4 ? strings[1] : null; - - EnsureAvailable(response, cursor, 4); - byte nativeTagClass = response[cursor++]; - byte storageType = response[cursor++]; - byte deadbandType = response[cursor++]; - byte interpolationType = response[cursor++]; - - // Trailing region after the fixed 4-byte block holds: - // - some alignment / int32 fields (StorageRate, AcquisitionRate, TimeDeadband) - // - Int64 FILETIME (DateCreated) - // - For analog tags: pair of doubles (MinEU/MaxEU and/or MinRaw/MaxRaw) - // - Optional compact-ASCII EngineeringUnit string - // - Optional double RolloverValue - // - Trailer marker (often FE 00 or 00) - // The exact layout varies by tag type and storage mode; rather than commit fragile - // positional parsing, scan the trailing region for the first two consecutive - // 8-byte-aligned doubles and treat them as a (MinEU, MaxEU) pair. Both must be - // finite and the EU range must be sane (Min ≤ Max). - ReadOnlySpan trailing = response[cursor..]; - (double? min, double? max, string? engineeringUnit) = TryReadAnalogTrailing(trailing); - cursor = response.Length; - - return new HistorianTagInfoResponse( - tagName, - tagKey, - typeId, - nativeDataTypeDescriptor, - metadataProvider, - nativeTagClass, - storageType, - deadbandType, - interpolationType, - description, - min, - max, - engineeringUnit); - } - - private static (double? min, double? max, string? engineeringUnit) TryReadAnalogTrailing(ReadOnlySpan trailing) - { - double? foundMin = null; - double? foundMax = null; - string? foundEu = null; - - // Look for an EngineeringUnit compact-ASCII string anywhere in the trailing region. - for (int i = 0; i < trailing.Length - 3; i++) - { - if (trailing[i] != 0x09) continue; - ushort len = BitConverter.ToUInt16(trailing.Slice(i + 1, 2)); - // Accept 1-32 byte ASCII strings as plausible EUs. Range chosen to filter false - // positives (most engineering units are short — "kPa", "Seconds", "RPM", etc.). - if (len < 1 || len > 32) continue; - int payloadStart = i + 3; - if (payloadStart + len > trailing.Length) continue; - // All bytes must be printable ASCII. - ReadOnlySpan payload = trailing.Slice(payloadStart, len); - bool allAscii = true; - foreach (byte b in payload) - { - if (b < 0x20 || b > 0x7E) { allAscii = false; break; } - } - if (!allAscii) continue; - string candidate = Encoding.ASCII.GetString(payload); - // Skip implausible values (numerics, mostly-special-chars). - if (double.TryParse(candidate, out _)) continue; - foundEu = candidate; - break; - } - - // Look for two consecutive 8-byte-aligned doubles forming a sane EU range. - // Try each plausible alignment relative to the trailing-region start. - for (int alignOffset = 0; alignOffset < 8; alignOffset++) - { - for (int i = alignOffset; i + 16 <= trailing.Length; i += 8) - { - if (!TryReadDouble(trailing, i, out double a)) continue; - if (!TryReadDouble(trailing, i + 8, out double b)) continue; - // Both finite, both within sane EU range, a ≤ b. - if (!double.IsFinite(a) || !double.IsFinite(b)) continue; - if (Math.Abs(a) > 1e15 || Math.Abs(b) > 1e15) continue; - if (a > b) continue; - // Reject the all-zeros pair (uninformative). - if (a == 0 && b == 0) continue; - foundMin = a; - foundMax = b; - return (foundMin, foundMax, foundEu); - } - } - return (foundMin, foundMax, foundEu); - } - - private static bool TryReadDouble(ReadOnlySpan buffer, int offset, out double value) - { - if (offset + 8 > buffer.Length) { value = 0; return false; } - value = BitConverter.ToDouble(buffer.Slice(offset, 8)); - return true; - } - - private static ushort ReadUInt16(ReadOnlySpan response, ref int cursor) - { - EnsureAvailable(response, cursor, 2); - ushort value = BitConverter.ToUInt16(response.Slice(cursor, 2)); - cursor += 2; - return value; - } - - private static uint ReadUInt32(ReadOnlySpan response, ref int cursor) - { - EnsureAvailable(response, cursor, 4); - uint value = BitConverter.ToUInt32(response.Slice(cursor, 4)); - cursor += 4; - return value; - } - - private static void EnsureAvailable(ReadOnlySpan response, int cursor, int byteCount) - { - if (cursor < 0 || byteCount < 0 || cursor > response.Length - byteCount) - { - throw new InvalidDataException("GetTagInfo response ended unexpectedly."); - } - } -} - -internal sealed record HistorianTagQueryAttempt(string Name, byte[] RequestBuffer, string RequestSha256); - -internal sealed record HistorianTagQueryStartResponse(uint QueryHandle, uint TagCount); - -internal sealed record HistorianTagInfoResponse( - string TagName, - uint TagKey, - Guid TypeId, - byte[] NativeDataTypeDescriptor, - string MetadataProvider, - byte NativeTagClass, - byte StorageType, - byte DeadbandType, - byte InterpolationType, - string? Description = null, - double? MinEU = null, - double? MaxEU = null, - string? EngineeringUnit = null); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianTagWriteProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianTagWriteProtocol.cs deleted file mode 100644 index 71085f2..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianTagWriteProtocol.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System.Text; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Serializers for the EnsT2 (CTagMetadata) and DelT (tag-name list) write paths. -/// Decoded from native captures landed in -/// artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/bothmessage-write-with-delt-latest.ndjson -/// — see docs/plans/write-commands-reverse-engineering.md Phase 2 findings. -/// -/// Per the captured analog CTagMetadata, the layout is: -/// -/// 1-byte leading marker = 4E (purpose unclear; observed constant — possibly "CTagMetadata" type tag) -/// 10-byte fixed header = 67 03 00 01 00 00 00 04 C6 02 -/// 1-byte data-type code = 0x01 Float, 0x21 Double, 0x29 Int2, 0x31 Int4, 0x11 UInt4 -/// 16 zero bytes (placeholder GUID + 2 bytes; future server-assigned tag id) -/// compact ASCII tag name -/// 16 bytes of 0xFF (sentinel — likely common-event-type GUID equivalent unused for analog) -/// compact ASCII description -/// compact ASCII metadata provider ("MDAS") -/// 7-byte flag block = 02 01 01 00 00 00 01 -/// uint32 storage rate (ms) -/// int64 date-created FILETIME UTC -/// scaling block either compact `1A 03` (default 0/100/0/100) OR -/// `1F 00` + 4 doubles (MinEU, MaxEU, MinRaw, MaxRaw) -/// compact ASCII engineering unit -/// uint32 = 0x2710 (10000 — purpose unclear; observed constant) -/// 8-byte double = 1.0 (likely IntegralDivisor) -/// 2-byte trailer = `FE 00` for ApplyScaling=false; `FE 01` for ApplyScaling=true -/// -/// The trailer's second byte is the ApplyScaling flag — verified 2026-05-04 by -/// capturing native CTagMetadata bytes for both values with identical -/// MinEU/MaxEU/MinRaw/MaxRaw inputs and observing that the server persists distinct -/// MinRaw/MaxRaw (and sets AnalogTag.Scaling=1) only when this byte is 0x01. -/// -internal static class HistorianTagWriteProtocol -{ - private const byte CompactAsciiMarker = 0x09; - - /// - /// 11 bytes preceding the data-type discriminator. Byte 0 is the leading 0x4E - /// marker, bytes 1-9 are the fixed CTagMetadata signature, byte 10 is the - /// storage-type sub-marker (`0x02` for Cyclic, `0x06` for Delta — captured - /// 2026-05-04 by toggling --write-storage-type on the harness). - /// - private static readonly byte[] AnalogHeaderUpToTypeCodeCyclic = - [ - 0x4E, - 0x67, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0xC6, - 0x02, - ]; - - private static readonly byte[] AnalogHeaderUpToTypeCodeDelta = - [ - 0x4E, - 0x67, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0xC6, - 0x06, - ]; - - /// - /// Native CDataType wire codes per data type — captured 2026-05-04 by probing - /// every type via instrument-wcf-writemessage. Matches the codes already documented - /// in MapDataType for the read path. - /// - public static byte GetAnalogDataTypeCode(Models.HistorianDataType dataType) => dataType switch - { - Models.HistorianDataType.Float => 0x01, - Models.HistorianDataType.Double => 0x21, - Models.HistorianDataType.UInt2 => 0x09, - Models.HistorianDataType.UInt4 => 0x11, - Models.HistorianDataType.Int2 => 0x29, - Models.HistorianDataType.Int4 => 0x31, - _ => throw new ProtocolEvidenceMissingException( - $"EnsureTagAsync data type {dataType} has no captured CTagMetadata wire code; supported: Float, Double, UInt2, UInt4, Int2, Int4."), - }; - - private static readonly byte[] AnalogPadding16 = new byte[16]; - private static readonly byte[] AnalogPostNamePadding = new byte[16]; - - static HistorianTagWriteProtocol() - { - // 16 bytes of 0xFF observed between tag name and description. - for (int i = 0; i < AnalogPostNamePadding.Length; i++) - { - AnalogPostNamePadding[i] = 0xFF; - } - } - - // After MDAS, the captured layout is a 7-byte flag block followed by uint32 - // storage rate. The flag block's second byte is the StorageType (1 = Cyclic, - // 2 = Delta — captured 2026-05-04). When StorageType=Delta, an additional - // 4 zero bytes are inserted between the storage rate and the FILETIME (likely - // a placeholder for Delta-specific deadband / threshold config). - private static readonly byte[] AnalogFlagBlockCyclic = [0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01]; - private static readonly byte[] AnalogFlagBlockDelta = [0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x01]; - private static readonly byte[] AnalogDeltaPostStorageRatePadding = new byte[4]; - /// Compact "use defaults" scaling marker — emitted when MinEU/MaxEU/MinRaw/MaxRaw are 0/100/0/100. - private static readonly byte[] AnalogScalingDefaultsMarker = [0x1A, 0x03]; - /// Explicit-scaling marker (2 bytes) — followed by 4 doubles in order MinEU, MaxEU, MinRaw, MaxRaw. - private static readonly byte[] AnalogScalingExplicitMarker = [0x1F, 0x00]; - // 2-byte trailer: `FE` marker + ApplyScaling byte (0x00 = false, 0x01 = true). Verified - // against native captures by toggling ApplyScaling on the harness and confirming that - // the server persists distinct MinRaw/MaxRaw + sets AnalogTag.Scaling=1 only when the - // second byte is 0x01. The WCF binary encoder may split InBuff across two - // Bytes8Text chunks (e.g., `9E B7 ... 9F 01 00`) which can make the trailer look - // 1-byte from the wire, but the semantic CTagMetadata content is always 2 bytes. - private static readonly byte[] AnalogTrailerScalingDisabled = [0xFE, 0x00]; - private static readonly byte[] AnalogTrailerScalingEnabled = [0xFE, 0x01]; - - private const double DefaultMinEU = 0.0; - private const double DefaultMaxEU = 100.0; - private const double DefaultMinRaw = 0.0; - private const double DefaultMaxRaw = 100.0; - - private const string MetadataProvider = "MDAS"; - private const uint IntegralDivisorMagic = 0x2710u; - private const uint DefaultStorageRateMs = 1000u; - - /// - /// Serializes a CTagMetadata payload for an analog tag. Live-verified for Float, - /// Double, Int2, Int4, UInt4 — see for the - /// type-code mapping. Output matches the byte-for-byte capture for the same inputs. - /// When MinEU/MaxEU/MinRaw/MaxRaw are all defaults (0/100/0/100) emits the compact - /// `1A 03` scaling marker; otherwise emits `1F` + 4 doubles in order. - /// - /// Tag name (ASCII). - /// Tag description (ASCII; null/empty allowed). - /// EU label (ASCII; null/empty allowed). - /// Native data type — Float by default for backward compat. - /// DateCreated FILETIME (caller passes ). - /// Engineering-units lower bound. - /// Engineering-units upper bound. - /// Raw lower bound. - /// Raw upper bound. - /// StorageRate in milliseconds. - public static byte[] SerializeAnalogCTagMetadata( - string tagName, - string? description, - string? engineeringUnit, - DateTime dateCreatedUtc, - Models.HistorianDataType dataType = Models.HistorianDataType.Float, - double minEU = DefaultMinEU, - double maxEU = DefaultMaxEU, - double minRaw = DefaultMinRaw, - double maxRaw = DefaultMaxRaw, - uint storageRateMs = DefaultStorageRateMs, - bool applyScaling = false, - Models.HistorianStorageType storageType = Models.HistorianStorageType.Cyclic, - double integralDivisor = 1.0) - { - if (storageRateMs == 0) - { - throw new ArgumentOutOfRangeException(nameof(storageRateMs), "Storage rate must be > 0 ms."); - } - ArgumentException.ThrowIfNullOrWhiteSpace(tagName); - byte typeCode = GetAnalogDataTypeCode(dataType); - bool isDelta = storageType == Models.HistorianStorageType.Delta; - - using MemoryStream ms = new(); - using BinaryWriter w = new(ms); - - w.Write(isDelta ? AnalogHeaderUpToTypeCodeDelta : AnalogHeaderUpToTypeCodeCyclic); // 11 bytes - w.Write(typeCode); // 1 byte data-type discriminator - w.Write(AnalogPadding16); // 16 bytes (all zero — placeholder GUID + 2) - WriteCompactAscii(w, tagName); // var - w.Write(AnalogPostNamePadding); // 16 bytes of 0xFF - WriteCompactAscii(w, description ?? string.Empty); // var - WriteCompactAscii(w, MetadataProvider); // 7 bytes ("MDAS") - w.Write(isDelta ? AnalogFlagBlockDelta : AnalogFlagBlockCyclic); // 7 bytes - w.Write(storageRateMs); // uint32 - if (isDelta) - { - w.Write(AnalogDeltaPostStorageRatePadding); // 4 bytes (Delta-only) - } - w.Write(dateCreatedUtc.ToUniversalTime().ToFileTimeUtc()); // int64 - - if (minEU == DefaultMinEU && maxEU == DefaultMaxEU && minRaw == DefaultMinRaw && maxRaw == DefaultMaxRaw) - { - w.Write(AnalogScalingDefaultsMarker); // 2 bytes (1A 03) - } - else - { - w.Write(AnalogScalingExplicitMarker); // 2 bytes (1F 00) - w.Write(minEU); - w.Write(maxEU); - w.Write(minRaw); - w.Write(maxRaw); // 32 bytes total for the 4 doubles - } - - WriteCompactAscii(w, engineeringUnit ?? string.Empty); // var - w.Write(IntegralDivisorMagic); // uint32 (purpose unclear — captured constant) - w.Write(integralDivisor); // double IntegralDivisor (default 1.0) - w.Write(applyScaling ? AnalogTrailerScalingEnabled : AnalogTrailerScalingDisabled); - - return ms.ToArray(); - } - - /// - /// Serializes the tagNames byte buffer for the DelT (DeleteTags) WCF op. - /// Decoded layout from a captured DelT request: - /// - /// ushort header1 = 0x6751 - /// ushort header2 = 1 - /// uint32 tagCount - /// for each tag: uint32 charCount + charCount × UTF-16 LE chars - /// - /// - public static byte[] SerializeDeleteTagNames(IReadOnlyList tagNames) - { - ArgumentNullException.ThrowIfNull(tagNames); - if (tagNames.Count == 0) - { - throw new ArgumentException("DeleteTags requires at least one tag name.", nameof(tagNames)); - } - - using MemoryStream ms = new(); - using BinaryWriter w = new(ms); - w.Write((ushort)0x6751); - w.Write((ushort)1); - w.Write(checked((uint)tagNames.Count)); - foreach (string name in tagNames) - { - ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(tagNames)); - w.Write(checked((uint)name.Length)); - w.Write(Encoding.Unicode.GetBytes(name)); - } - return ms.ToArray(); - } - - /// Compact ASCII string: 0x09 + UInt16 byteLen + LEN ASCII bytes. - private static void WriteCompactAscii(BinaryWriter writer, string value) - { - byte[] ascii = Encoding.ASCII.GetBytes(value); - if (ascii.Length > ushort.MaxValue) - { - throw new ArgumentOutOfRangeException(nameof(value), "Compact ASCII strings cannot exceed UInt16 length."); - } - writer.Write(CompactAsciiMarker); - writer.Write((ushort)ascii.Length); - writer.Write(ascii); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfAuthChainHelper.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfAuthChainHelper.cs deleted file mode 100644 index 99ededf..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfAuthChainHelper.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfAuthChainHelper -{ - private const int OpenConnection3MinResponseLength = 5; - public const uint NativeIntegratedReadOnlyConnectionMode = 0x402; - public const uint NativeIntegratedEventConnectionMode = 0x501; - /// - /// Process + write-enabled + integrated security. Per native ilspy - /// (HistorianAccessUtil.SetConnectionMode): Process=1, OR 0x400 for integratedSecurity. - /// EnsT2 and DelT silently return false with err code 132 (OperationNotEnabled) when - /// Open2 is opened with 0x402 (read-only); 0x401 unlocks write capability. - /// - public const uint NativeIntegratedWriteEnabledConnectionMode = 0x401; - - /// - /// Runs Hist.GetV → Hist.ValCl × N → Hist.Open2 against the configured /Hist endpoint and - /// returns the transient /Retr client handle decoded from the OpenConnection3 response. - /// Caller is responsible for opening the matching /Retr channel. - /// - public static uint OpenAuthenticatedConnection( - HistorianClientOptions options, - Binding historyBinding, - EndpointAddress historyEndpoint, - Guid contextKey, - CancellationToken cancellationToken, - uint connectionMode = NativeIntegratedReadOnlyConnectionMode, - Action? additionalSetup = null) - { - ChannelFactory historyFactory = new(historyBinding, historyEndpoint); - HistorianWcfClientCredentialsHelper.Configure(historyFactory, options); - historyFactory.Endpoint.EndpointBehaviors.Add(new HistorianWcfHistAddressingBehavior()); - if (HistorianWcfMessageCaptureBehavior.IsEnabled) - { - historyFactory.Endpoint.EndpointBehaviors.Add(new HistorianWcfMessageCaptureBehavior()); - } - - try - { - IHistoryServiceContract2 historyChannel = historyFactory.CreateChannel(); - ICommunicationObject historyChannelCo = (ICommunicationObject)historyChannel; - try - { - historyChannel.GetInterfaceVersion(out _); - RunValClRounds(historyChannel, contextKey, options, cancellationToken); - - byte[] open2Request = HistorianNativeHandshake.BuildOpenConnection3Request(options.Host, contextKey, connectionMode); - bool open2Success = historyChannel.OpenConnection2(ref open2Request, out byte[] open2Response, out byte[] open2Error); - open2Response ??= []; - open2Error ??= []; - if (!open2Success || open2Response.Length < OpenConnection3MinResponseLength) - { - throw new InvalidOperationException( - $"Open2 failed (Success={open2Success}, ResponseLen={open2Response.Length}, ErrorLen={open2Error.Length})."); - } - - (uint clientHandle, Guid storageSessionId) = HistorianNativeHandshake.ParseOpenConnectionResponse(open2Response); - - if (additionalSetup is not null) - { - additionalSetup(historyChannel, new OpenConnectionContext(contextKey, clientHandle, storageSessionId)); - } - - return clientHandle; - } - finally - { - CloseChannelSafely(historyChannelCo); - } - } - finally - { - CloseFactorySafely(historyFactory); - } - } - - public readonly record struct OpenConnectionContext(Guid ContextKey, uint ClientHandle, Guid StorageSessionId); - - private static void RunValClRounds(IHistoryServiceContract2 channel, Guid contextKey, HistorianClientOptions options, CancellationToken cancellationToken) - { - HistorianNativeHandshake.RunTokenRounds( - (handle, wrapped, _) => - { - bool serverSuccess = channel.ValidateClientCredential(handle, wrapped, out byte[] serverOutput, out byte[] errorBuffer); - return new HistorianNativeHandshake.TokenExchangeResult(serverSuccess, serverOutput ?? [], errorBuffer ?? []); - }, - contextKey, - options, - cancellationToken); - } - - private static void CloseChannelSafely(ICommunicationObject channel) - { - try - { - if (channel.State == CommunicationState.Faulted) channel.Abort(); - else channel.Close(); - } - catch { try { channel.Abort(); } catch { } } - } - - private static void CloseFactorySafely(ChannelFactory factory) - { - try - { - if (factory.State == CommunicationState.Faulted) factory.Abort(); - else factory.Close(); - } - catch { try { factory.Abort(); } catch { } } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfAuthenticationProtocol.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfAuthenticationProtocol.cs deleted file mode 100644 index e096b44..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfAuthenticationProtocol.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Buffers.Binary; -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfAuthenticationProtocol -{ - private const uint NativeNtlmNegotiateVersionFlag = 0x0010_0000; - - public static byte[] WrapValidateClientCredentialToken(bool isFirstRound, ReadOnlySpan token) - { - byte[] buffer = new byte[checked(1 + sizeof(uint) + token.Length)]; - buffer[0] = isFirstRound ? (byte)1 : (byte)0; - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(1, sizeof(uint)), checked((uint)token.Length)); - token.CopyTo(buffer.AsSpan(1 + sizeof(uint))); - return buffer; - } - - public static bool TryApplyNativeNtlmNegotiateVersionFlag(Span token) - { - ReadOnlySpan ntlmSignature = "NTLMSSP\0"u8; - if (token.Length < 16 - || !token[..ntlmSignature.Length].SequenceEqual(ntlmSignature) - || BinaryPrimitives.ReadUInt32LittleEndian(token.Slice(8, sizeof(uint))) != 1) - { - return false; - } - - uint flags = BinaryPrimitives.ReadUInt32LittleEndian(token.Slice(12, sizeof(uint))); - BinaryPrimitives.WriteUInt32LittleEndian( - token.Slice(12, sizeof(uint)), - flags | NativeNtlmNegotiateVersionFlag); - return true; - } - - public static ValidateClientCredentialToken? TryReadWrappedValidateClientCredentialToken(ReadOnlySpan buffer) - { - if (buffer.Length < 1 + sizeof(uint)) - { - return null; - } - - uint tokenLength = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(1, sizeof(uint))); - if (tokenLength > int.MaxValue || buffer.Length != 1 + sizeof(uint) + (int)tokenLength) - { - return null; - } - - return new ValidateClientCredentialToken(buffer[0] != 0, buffer[(1 + sizeof(uint))..].ToArray()); - } - - public static ValidateClientCredentialResponse? TryReadValidateClientCredentialResponse(ReadOnlySpan buffer) - { - if (buffer.Length == 0) - { - return null; - } - - return new ValidateClientCredentialResponse(buffer[0] != 0, buffer[1..].ToArray()); - } -} - -internal sealed record ValidateClientCredentialToken(bool IsFirstRound, byte[] Token); - -internal sealed record ValidateClientCredentialResponse(bool Continue, byte[] Token); diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfBindingFactory.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfBindingFactory.cs deleted file mode 100644 index 90b0b70..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfBindingFactory.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.Net.Security; -using System.ServiceModel; -using System.ServiceModel.Channels; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfBindingFactory -{ - public const string Scheme = "net.tcp"; - public const int DefaultPort = 32568; - - public static Binding CreateMdasNetTcpBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024) - { - var encoding = new MdasMessageEncodingBindingElement( - new BinaryMessageEncodingBindingElement - { - MessageVersion = MessageVersion.Soap12WSAddressing10 - }); - - var transport = new TcpTransportBindingElement - { - MaxReceivedMessageSize = maxReceivedMessageSize, - TransferMode = TransferMode.Buffered - }; - - return new CustomBinding(encoding, transport) - { - CloseTimeout = timeout, - OpenTimeout = timeout, - ReceiveTimeout = timeout, - SendTimeout = timeout - }; - } - - public static Binding CreateMdasNetTcpWindowsBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024) - { - NetTcpBinding nativeShape = new(SecurityMode.Transport) - { - MaxReceivedMessageSize = maxReceivedMessageSize, - MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue)) - }; - nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize; - nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows; - nativeShape.Security.Transport.ProtectionLevel = ProtectionLevel.None; - - BindingElementCollection elements = nativeShape.CreateBindingElements(); - for (int i = 0; i < elements.Count; i++) - { - if (elements[i] is MessageEncodingBindingElement encoding) - { - elements[i] = new MdasMessageEncodingBindingElement(encoding); - break; - } - } - - return new CustomBinding(elements) - { - CloseTimeout = timeout, - OpenTimeout = timeout, - ReceiveTimeout = timeout, - SendTimeout = timeout - }; - } - - public static Binding CreateMdasNetTcpCertificateBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024) - { - NetTcpBinding nativeShape = new(SecurityMode.Transport) - { - MaxReceivedMessageSize = maxReceivedMessageSize, - MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue)) - }; - nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize; - nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.None; - - BindingElementCollection elements = nativeShape.CreateBindingElements(); - for (int i = 0; i < elements.Count; i++) - { - if (elements[i] is MessageEncodingBindingElement encoding) - { - elements[i] = new MdasMessageEncodingBindingElement(encoding); - break; - } - } - - return new CustomBinding(elements) - { - CloseTimeout = timeout, - OpenTimeout = timeout, - ReceiveTimeout = timeout, - SendTimeout = timeout - }; - } - - // 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() - { - MaxBufferSize = maxBufferSize, - MaxReceivedMessageSize = maxBufferSize - }; - nativeShape.Security.Mode = NetNamedPipeSecurityMode.None; - nativeShape.ReaderQuotas.MaxArrayLength = maxBufferSize; - - BindingElementCollection elements = nativeShape.CreateBindingElements(); - for (int i = 0; i < elements.Count; i++) - { - if (elements[i] is MessageEncodingBindingElement encoding) - { - elements[i] = new MdasMessageEncodingBindingElement(encoding); - break; - } - } - - return new CustomBinding(elements) - { - CloseTimeout = timeout, - OpenTimeout = timeout, - ReceiveTimeout = timeout, - SendTimeout = timeout - }; - } -#pragma warning restore CA1416 - - public static (Binding HistoryBinding, EndpointAddress HistoryEndpoint, Binding RetrievalBinding, EndpointAddress RetrievalEndpoint) CreateBindingPair( - HistorianClientOptions options) - { - TimeSpan timeout = options.RequestTimeout; - - return options.Transport switch - { - HistorianTransport.LocalPipe => ( - CreateMdasNetNamedPipeBinding(timeout), - CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.History), - CreateMdasNetNamedPipeBinding(timeout), - CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.Retrieval)), - HistorianTransport.RemoteTcpIntegrated => ( - CreateMdasNetTcpWindowsBinding(timeout), - CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryIntegrated), - CreateMdasNetTcpBinding(timeout), - CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)), - HistorianTransport.RemoteTcpCertificate => ( - CreateMdasNetTcpCertificateBinding(timeout), - CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate, options.ServerDnsIdentity), - CreateMdasNetTcpBinding(timeout), - CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)), - _ => throw new NotSupportedException($"Transport {options.Transport} is not supported.") - }; - } - - public static EndpointAddress CreateEndpointAddress(string host, int port, string serviceName, string? dnsIdentity = null) - { - ArgumentException.ThrowIfNullOrWhiteSpace(host); - ArgumentException.ThrowIfNullOrWhiteSpace(serviceName); - - Uri uri = new($"{Scheme}://{host}:{port}/{serviceName}"); - return string.IsNullOrWhiteSpace(dnsIdentity) - ? new EndpointAddress(uri) - : new EndpointAddress(uri, new DnsEndpointIdentity(dnsIdentity)); - } - - public static EndpointAddress CreatePipeEndpointAddress(string host, string serviceName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(host); - ArgumentException.ThrowIfNullOrWhiteSpace(serviceName); - - return new EndpointAddress($"net.pipe://{host}/{serviceName}"); - } - - /// - /// Returns the appropriate endpoint address for an auxiliary service (Stat, Trx, etc.) - /// based on the transport — net.pipe for LocalPipe, net.tcp for the remote variants. - /// Use this rather than directly when the calling - /// code may run under any transport. - /// - public static EndpointAddress CreateAuxiliaryEndpointAddress(HistorianClientOptions options, string serviceName) - { - ArgumentNullException.ThrowIfNull(options); - ArgumentException.ThrowIfNullOrWhiteSpace(serviceName); - - return options.Transport == HistorianTransport.LocalPipe - ? CreatePipeEndpointAddress(options.Host, serviceName) - : CreateEndpointAddress(options.Host, options.Port, serviceName); - } - - /// - /// Returns the appropriate binding for an auxiliary service (Stat, Trx, etc.) given the - /// transport. For LocalPipe, same NamedPipe binding as History. For remote TCP variants, - /// plain — auxiliaries don't repeat the Windows- - /// transport-security upgrade that the History service negotiates; the established session - /// authenticates the client already. - /// - // 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); - - TimeSpan timeout = options.RequestTimeout; - return options.Transport switch - { - HistorianTransport.LocalPipe => CreateMdasNetNamedPipeBinding(timeout), - HistorianTransport.RemoteTcpIntegrated => CreateMdasNetTcpBinding(timeout), - HistorianTransport.RemoteTcpCertificate => CreateMdasNetTcpBinding(timeout), - _ => throw new NotSupportedException($"Transport {options.Transport} is not supported.") - }; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfClientCredentialsHelper.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfClientCredentialsHelper.cs deleted file mode 100644 index 8534ea8..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfClientCredentialsHelper.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.IdentityModel.Selectors; -using System.IdentityModel.Tokens; -using System.Security.Cryptography.X509Certificates; -using System.ServiceModel; -using System.ServiceModel.Security; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Centralizes per-channel-factory credentials configuration that's not bound to a -/// single binding type. Today this covers ServerCertificateValidation for the -/// cert-transport binding when callers opt into . -/// Apply at every ChannelFactory<T> instantiation point in the WCF layer. -/// -internal static class HistorianWcfClientCredentialsHelper -{ - public static void Configure(ChannelFactory factory, HistorianClientOptions options) - { - ArgumentNullException.ThrowIfNull(factory); - ArgumentNullException.ThrowIfNull(options); - - if (options.AllowUntrustedServerCertificate) - { - factory.Credentials.ServiceCertificate.SslCertificateAuthentication = new X509ServiceCertificateAuthentication - { - CertificateValidationMode = X509CertificateValidationMode.Custom, - CustomCertificateValidator = AcceptAnyCertificateValidator.Instance, - RevocationMode = X509RevocationMode.NoCheck, - }; - } - } - - private sealed class AcceptAnyCertificateValidator : X509CertificateValidator - { - public static readonly AcceptAnyCertificateValidator Instance = new(); - public override void Validate(X509Certificate2 certificate) { } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfEventOrchestrator.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfEventOrchestrator.cs deleted file mode 100644 index 6915d7d..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfEventOrchestrator.cs +++ /dev/null @@ -1,449 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.CompilerServices; -using System.Runtime.Versioning; -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Mirrors HistorianWcfReadOrchestrator but targets IRetrievalServiceContract4 for the event flow. -/// Event row buffer layout is undecoded as of this pass — when StartEventQuery succeeds, this -/// orchestrator returns an empty enumeration but logs the row-buffer length via the -/// diagnostic so a follow-up capture can decode the wire shape. -/// -internal sealed class HistorianWcfEventOrchestrator -{ - private const int OpenConnection3MinResponseLength = 5; - private const int CredentialBlockSizeBytes = 1026; - private const int MaxValClRounds = 8; - private const string ClientNodeNameFallback = "ZB.MOM.WW.SPHistorianClient"; - private const string ClientDataSourceId = "2020.406.2652.2"; - private const string ClientDllVersionString = "2020.406.2652.2"; - private const byte NativeClientType = 4; - private const uint NativeIntegratedReadOnlyConnectionMode = 0x402; - private const byte NativeClientCommonInfoFormatVersion = 4; - private const ushort NativeHcalVersion = 17; - private const uint NativeClientVersionInt = 999_999; - private const ushort NativeOpen2ClientVersion = 9; - - /// - /// Documented native CM_EVENT default tag id used by aahClientManaged.dll - /// CreateDefaultEventTag → ConvertEventTagToTagMetadata. Registering this tag via - /// IHistoryServiceContract2.RegisterTags2 before StartEventQuery causes the server - /// to subscribe the session to CM_EVENT events; without it, - /// GetNextEventQueryResultBuffer returns native error type=4 code=85 (0x55). - /// - private static readonly Guid CmEventTagId = new("353b8145-5df0-4d46-a253-871aef49b321"); - - private readonly HistorianClientOptions _options; - - public HistorianWcfEventOrchestrator(HistorianClientOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// Diagnostic: length of the most recent event-row result buffer the server sent. - public int LastResultBufferLength { get; private set; } - - /// Diagnostic: type+code description of the most recent error/terminal buffer. - public string LastErrorBufferDescription { get; private set; } = string.Empty; - - /// Diagnostic: handle string passed to EnsT2. - public static string LastEnsT2Handle { get; private set; } = string.Empty; - - /// Diagnostic: SHA256 of the CTagMetadata payload sent to EnsT2. - public static string LastEnsT2PayloadSha256 { get; private set; } = string.Empty; - - /// Diagnostic: native return code from the prerequisite UpdC3 call. - public static uint LastUpdC3ReturnCode { get; private set; } - - /// Diagnostic: native return code from the prerequisite RTag2 call. - public static uint LastRTag2ReturnCode { get; private set; } - - public async IAsyncEnumerable ReadEventsAsync( - DateTime startUtc, - DateTime endUtc, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - if (!_options.IntegratedSecurity && string.IsNullOrEmpty(_options.UserName)) - { - throw new ProtocolEvidenceMissingException( - "Managed event flow currently requires IntegratedSecurity or an explicit UserName + Password."); - } - - cancellationToken.ThrowIfCancellationRequested(); - - IReadOnlyList events = await Task.Run( - () => RunEventChain(startUtc, endUtc, cancellationToken), - cancellationToken).ConfigureAwait(false); - - foreach (HistorianEvent evt in events) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return evt; - } - } - - private List RunEventChain(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, retrBinding, retrEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(_options); - Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options); - EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status); - EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction); - uint clientHandle = HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( - _options, histBinding, histEndpoint, contextKey, cancellationToken, - connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode, - additionalSetup: (historyChannel, context) => - AddCmEventTagViaAddT(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrBinding, retrEndpoint)); - return RunEventQuery(retrBinding, retrEndpoint, clientHandle, startUtc, endUtc, cancellationToken); - } - - private List RunEventQuery( - Binding binding, - EndpointAddress retrievalEndpoint, - uint clientHandle, - DateTime startUtc, - DateTime endUtc, - CancellationToken cancellationToken) - { - ChannelFactory factory = new(binding, retrievalEndpoint); - HistorianWcfClientCredentialsHelper.Configure(factory, _options); - - try - { - IRetrievalServiceContract4 channel = factory.CreateChannel(); - ICommunicationObject channelCo = (ICommunicationObject)channel; - try - { - channel.GetInterfaceVersion(out _); - - uint isAllowedReturn = channel.IsOriginalAllowed(clientHandle, out bool isAllowed); - if (isAllowedReturn != 0 || !isAllowed) - { - throw new InvalidOperationException( - $"Retr.IsOriginalAllowed denied the connection (return={isAllowedReturn}, isAllowed={isAllowed})."); - } - - IReadOnlyList attempts = HistorianEventQueryProtocol.CreateStartEventQueryAttempts( - startUtc.ToUniversalTime(), - endUtc.ToUniversalTime(), - eventCount: 5); - byte[] requestBuffer = attempts[0].RequestBuffer; - - uint queryHandle = 0; - bool startSuccess = channel.StartEventQuery( - clientHandle, - HistorianEventQueryProtocol.QueryRequestTypeEvent, - checked((uint)requestBuffer.Length), - requestBuffer, - out _, - out _, - ref queryHandle, - out _, - out byte[] startError); - startError ??= []; - if (!startSuccess) - { - throw new InvalidOperationException( - $"Retr.StartEventQuery failed (errorLen={startError.Length}, error5={DescribeNativeError(startError)})."); - } - - List events = []; - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - bool nextSuccess = channel.GetNextEventQueryResultBuffer( - clientHandle, - queryHandle, - out _, - out byte[] resultBuffer, - out _, - out byte[] errorBuffer); - resultBuffer ??= []; - errorBuffer ??= []; - - LastResultBufferLength = resultBuffer.Length; - LastErrorBufferDescription = DescribeNativeError(errorBuffer); - - // Any 5-byte type=4 error is treated as a soft terminal so the chain can - // surface evidence even when an unfamiliar code (e.g. 85 / 0x55 observed - // on first end-to-end runs without an event-tag registration step) blocks - // row enumeration. Code 30 (NoMoreData) is the canonical terminal; other - // codes mean "stop reading and let the caller see the diagnostic". When - // nextSuccess is false the server signaled hard failure; if there is also - // a 5-byte type=4 error buffer we still return the buffer length as - // evidence and surface via LastErrorBufferDescription rather than throw. - if (errorBuffer.Length == 5 && errorBuffer[0] == 4) - { - return events; - } - - if (!nextSuccess) - { - throw new InvalidOperationException( - $"Retr.GetNextEventQueryResultBuffer failed (errorLen={errorBuffer.Length}, error5={DescribeNativeError(errorBuffer)})."); - } - - if (resultBuffer.Length > 0) - { - events.AddRange(HistorianEventRowProtocol.Parse(resultBuffer)); - } - - if (resultBuffer.Length == 0 && errorBuffer.Length == 0) - { - return events; - } - } - } - finally - { - CloseChannelSafely(channelCo); - } - } - finally - { - CloseFactorySafely(factory); - } - } - - /// Diagnostic: native return code from the last AddT(CM_EVENT) call. - public static uint LastAddReturnCode { get; private set; } - - /// Diagnostic: byte length of the AddT response output buffer. - public static int LastAddOutputLength { get; private set; } - - /// - /// Calls IHistoryServiceContract.AddTags with the documented CM_EVENT CTagMetadata - /// payload. The chain now reaches the server's AddT handler (a real WCF response is - /// returned rather than the previous parameter-binding failure) but currently receives - /// native return code 76 against this Historian. Combined with code 85 from - /// GetNextEventQueryResultBuffer, two specific server rejections remain to decode - /// before live event reads return rows. The orchestrator continues regardless so the - /// caller can see the chain outcome via , - /// , and . - /// Next concrete step: instrument Wcf.AddT.Request on a successful native event - /// run and compare byte-for-byte against this serialiser's output. - /// - /// - /// Replays the native event-tag registration sequence captured via the - /// instrument-wcf-writemessage IL-rewrite tool: UpdC3 (UpdateClientStatus3) → RTag2 - /// (RegisterTags2 with the CM_EVENT tag id) → EnsT2 (EnsureTags2 with the full - /// CTagMetadata blob). The 81-byte UpdC3 status blob and 24-byte RTag2 buffer are - /// captured byte-for-byte from a successful native event read; the EnsT2 payload is - /// regenerated by . - /// The Stat-service queries the native client also issues (Stat/GetV, Stat/GETHI, - /// Stat/GetSystemParameter for AllowOriginals/HistorianPartner/HistorianVersion/ - /// MaxCyclicStorageTimeout/RealTimeWindow/FutureTimeThreshold/AllowRenameTags) appear - /// informational and are skipped here. - /// - private static void AddCmEventTagViaAddT( - IHistoryServiceContract2 historyChannel, - HistorianWcfAuthChainHelper.OpenConnectionContext context, - Binding statusBinding, - EndpointAddress statusEndpoint, - EndpointAddress transactionEndpoint, - Binding retrievalBinding, - EndpointAddress retrievalEndpoint) - { - string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); - LastEnsT2Handle = handle; - - ChannelFactory statusFactory = new(statusBinding, statusEndpoint); - IStatusServiceContract2 statusChannel = statusFactory.CreateChannel(); - ICommunicationObject statusCo = (ICommunicationObject)statusChannel; - - ChannelFactory transactionFactory = new(statusBinding, transactionEndpoint); - ITransactionServiceContract transactionChannel = transactionFactory.CreateChannel(); - ICommunicationObject transactionCo = (ICommunicationObject)transactionChannel; - - ChannelFactory retrievalFactory = new(retrievalBinding, retrievalEndpoint); - IRetrievalServiceContract4 retrievalChannel = retrievalFactory.CreateChannel(); - ICommunicationObject retrievalCo = (ICommunicationObject)retrievalChannel; - - try - { - // Replays the discovery dance the native event flow runs between Open2 and EnsT2, - // captured byte-for-byte via instrument-wcf-{write,read}message. Best-effort — - // individual calls may fail on this server; the chain continues regardless because - // the goal is to put the server-side session into the state EnsT2 expects. - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - - byte[] historianVersionRequest = BuildGetHistorianInfoRequest("HistorianVersion"); - TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); - TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); - - byte[] clientStatus = BuildUpdC3ClientStatusBlob(); - bool updSuccess = historyChannel.UpdateClientStatus3( - handle: handle, - clientStatusSize: (uint)clientStatus.Length, - clientStatus: ref clientStatus, - serverStatusSize: out _, - serverStatus: out _, - errorSize: out _, - errorBuffer: out _); - LastUpdC3ReturnCode = updSuccess ? 0u : 1u; - - // Records 11-16: 6 system-parameter queries before RTag2. - foreach (string parameterName in NativeStatusParametersBeforeRTag2) - { - TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, parameterName, out _, out _, out _)); - } - - byte[] registerBuffer = BuildRTag2CmEventInputBuffer(); - bool registerSuccess = historyChannel.RegisterTags2( - handle: handle, - elementCount: 1, - inputBuffer: registerBuffer, - outputBuffer: out _, - errorBuffer: out _); - LastRTag2ReturnCode = registerSuccess ? 0u : 1u; - - // Record 18: one more system-parameter query after RTag2 before EnsT2. - TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, "AllowRenameTags", out _, out _, out _)); - - // Records 19-21: cross-service version probes the native client makes between - // RTag2 and EnsT2. They likely register the client with each service's session - // table; without them EnsT2 may reject the session. - TryRun(() => transactionChannel.GetInterfaceVersion(out _)); - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - TryRun(() => retrievalChannel.GetInterfaceVersion(out _)); - - byte[] payload = HistorianAddTagsProtocol.SerializeCmEventCTagMetadata(DateTime.UtcNow); - using (var sha = System.Security.Cryptography.SHA256.Create()) - { - byte[] hash = sha.ComputeHash(payload); - LastEnsT2PayloadSha256 = BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); - } - - bool ensureSuccess = historyChannel.EnsureTags2( - handle: handle, - elementCount: 1, - inputBuffer: payload, - outputBuffer: out byte[] addOutput, - errorBuffer: out _); - - LastAddReturnCode = ensureSuccess ? 0u : 1u; - LastAddOutputLength = addOutput?.Length ?? 0; - } - catch (Exception ex) - { - LastAddReturnCode = 0xFFFFFFFFu; - LastAddOutputLength = 0; - _ = ex; - } - finally - { - CloseChannelSafely(retrievalCo); - CloseFactorySafely(retrievalFactory); - CloseChannelSafely(transactionCo); - CloseFactorySafely(transactionFactory); - CloseChannelSafely(statusCo); - CloseFactorySafely(statusFactory); - } - } - - private static readonly string[] NativeStatusParametersBeforeRTag2 = - [ - "AllowOriginals", - "HistorianPartner", - "HistorianVersion", - "MaxCyclicStorageTimeout", - "RealTimeWindow", - "FutureTimeThreshold", - ]; - - private static void TryRun(Action action) - { - try { action(); } - catch { } - } - - /// - /// Native GETHI pRequestBuff layout for a parameter-name query: 8-byte header - /// (UInt16 0x6753 + UInt16 0x0002 + UInt32 nameLength) + UTF-16 LE chars (no - /// trailing null byte — observed truncated by 1 byte vs full UTF-16 in the - /// captured native bytes). Layout taken from - /// writemessage-capture-event-latest.ndjson record 8. - /// - private static byte[] BuildGetHistorianInfoRequest(string parameterName) - { - byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(parameterName); - // Native truncates the trailing high byte of the last UTF-16 char. - int payloadLength = nameBytes.Length > 0 ? nameBytes.Length - 1 : 0; - byte[] buffer = new byte[8 + payloadLength]; - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 0x6753); - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 0x0002); - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), (uint)parameterName.Length); - Buffer.BlockCopy(nameBytes, 0, buffer, 8, payloadLength); - return buffer; - } - - /// - /// 81-byte UpdC3 clientStatus blob captured from a native event read (record 10 of - /// writemessage-capture-event-latest.ndjson). Layout: 0x02 0x01 + 76 zero bytes + - /// uint32(0x0000001E). The trailing 30 is likely an interval / timeout in seconds; all - /// other observed fields are zero for a fresh session. - /// - private static byte[] BuildUpdC3ClientStatusBlob() - { - byte[] blob = new byte[81]; - blob[0] = 0x02; - blob[1] = 0x01; - blob[77] = 0x1E; - return blob; - } - - /// - /// 24-byte RTag2 pInBuff captured from a native event read (record 17). Layout: - /// 8-byte header (0x50 0x67 0x02 0x00 + uint32 element count = 1) + 16-byte tag id GUID. - /// - private static byte[] BuildRTag2CmEventInputBuffer() - { - byte[] buffer = new byte[24]; - buffer[0] = 0x50; - buffer[1] = 0x67; - buffer[2] = 0x02; - buffer[3] = 0x00; - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), 1u); - CmEventTagId.ToByteArray().CopyTo(buffer.AsSpan(8, 16)); - return buffer; - } - - private static string DescribeNativeError(byte[] errorBuffer) - { - if (errorBuffer.Length < 5) - { - return ""; - } - - byte type = errorBuffer[0]; - uint code = BinaryPrimitives.ReadUInt32LittleEndian(errorBuffer.AsSpan(1, 4)); - return $"type={type} code={code} (0x{code:X})"; - } - - private static void CloseChannelSafely(ICommunicationObject channel) - { - try - { - if (channel.State == CommunicationState.Faulted) channel.Abort(); - else channel.Close(); - } - catch { try { channel.Abort(); } catch { } } - } - - private static void CloseFactorySafely(ChannelFactory factory) - { - try - { - if (factory.State == CommunicationState.Faulted) factory.Abort(); - else factory.Close(); - } - catch { try { factory.Abort(); } catch { } } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfHistAddressingBehavior.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfHistAddressingBehavior.cs deleted file mode 100644 index 5fe945e..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfHistAddressingBehavior.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Runtime.Versioning; -using System.ServiceModel; -using System.ServiceModel.Channels; -using System.ServiceModel.Description; -using System.ServiceModel.Dispatcher; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Forces an explicit wsa:To URI on every outgoing message. Native captures -/// of EnsT2 / DelT include net.pipe://localhost/Hist in the addressing header -/// block; without it the server appears to accept the body but not act on it -/// (silent fail observed for both write ops). WCF normally derives To from the -/// endpoint address, but the captured SDK bytes show it absent — re-asserting it -/// here closes the gap. -/// -internal sealed class HistorianWcfHistAddressingBehavior : IEndpointBehavior -{ - public void Validate(ServiceEndpoint endpoint) { } - public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } - public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } - - public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) - { - clientRuntime.ClientMessageInspectors.Add(new ToHeaderInspector(endpoint.Address.Uri)); - } - - private sealed class ToHeaderInspector(Uri toUri) : IClientMessageInspector - { - public object? BeforeSendRequest(ref Message request, IClientChannel channel) - { - request.Headers.To = toUri; - return null; - } - - public void AfterReceiveReply(ref Message reply, object? correlationState) { } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfMessageCaptureBehavior.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfMessageCaptureBehavior.cs deleted file mode 100644 index da9bfa7..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfMessageCaptureBehavior.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Runtime.Versioning; -using System.ServiceModel; -using System.ServiceModel.Channels; -using System.ServiceModel.Description; -using System.ServiceModel.Dispatcher; -using System.Text.Json; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Reverse-engineering aid: when the env var AVEVA_HISTORIAN_SDK_WIRE_CAPTURE is set, -/// every outgoing WCF message body and every incoming response body on this endpoint is -/// captured to that file as one ndjson record per call. Pair with the -/// instrument-wcf-{write,read}message native captures and diff offset-by-offset to -/// isolate SDK-vs-native differences. NEVER enable in production. -/// -internal sealed class HistorianWcfMessageCaptureBehavior : IEndpointBehavior -{ - public const string CapturePathEnvVar = "AVEVA_HISTORIAN_SDK_WIRE_CAPTURE"; - - public static bool IsEnabled => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(CapturePathEnvVar)); - - public void Validate(ServiceEndpoint endpoint) { } - public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } - public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } - - public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) - { - clientRuntime.ClientMessageInspectors.Add(new MessageCaptureInspector()); - } - - private sealed class MessageCaptureInspector : IClientMessageInspector - { - private static readonly object Lock = new(); - - public object? BeforeSendRequest(ref Message request, IClientChannel channel) - { - CaptureMessage("SDK.WriteMessage.Body", ref request); - return null; - } - - public void AfterReceiveReply(ref Message reply, object? correlationState) - { - CaptureMessage("SDK.ReadMessage.Body", ref reply); - } - - private static void CaptureMessage(string phase, ref Message message) - { - string? path = Environment.GetEnvironmentVariable(CapturePathEnvVar); - if (string.IsNullOrWhiteSpace(path) || message.IsEmpty) - { - return; - } - - try - { - // Buffer the message so we can both inspect and forward the bytes. - MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue); - Message copy = buffer.CreateMessage(); - using MemoryStream ms = new(); - BinaryMessageEncodingBindingElement binaryEncoder = new(); - MessageEncoderFactory factory = binaryEncoder.CreateMessageEncoderFactory(); - factory.Encoder.WriteMessage(copy, ms); - byte[] bytes = ms.ToArray(); - message = buffer.CreateMessage(); - - string action = message.Headers.Action ?? ""; - var record = new - { - TimestampUtc = DateTimeOffset.UtcNow.ToString("O"), - Phase = phase, - Action = action, - Length = bytes.Length, - Base64 = Convert.ToBase64String(bytes), - }; - - string? dir = Path.GetDirectoryName(Path.GetFullPath(path)); - if (!string.IsNullOrEmpty(dir)) - { - Directory.CreateDirectory(dir); - } - - lock (Lock) - { - File.AppendAllText(path, JsonSerializer.Serialize(record) + Environment.NewLine); - } - } - catch - { - // Capture is reverse-engineering aid — never let it break the live call. - } - } - } - -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfProbe.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfProbe.cs deleted file mode 100644 index 9b7cfc6..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfProbe.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.ServiceModel; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfProbe -{ - public static async Task ProbeAsync(HistorianClientOptions options, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(options); - cancellationToken.ThrowIfCancellationRequested(); - - TimeSpan timeout = options.ConnectTimeout > TimeSpan.Zero - ? options.ConnectTimeout - : TimeSpan.FromSeconds(5); - - return await Task.Run(() => - { - cancellationToken.ThrowIfCancellationRequested(); - - WcfServiceVersion history = ProbeService( - options, - HistorianWcfServiceNames.History, - static channel => - { - uint returnCode = channel.GetInterfaceVersion(out uint version); - return new WcfServiceVersion(returnCode, version); - }, - timeout); - - WcfServiceVersion retrieval = ProbeService( - options, - HistorianWcfServiceNames.Retrieval, - static channel => - { - uint returnCode = channel.GetInterfaceVersion(out uint version); - return new WcfServiceVersion(returnCode, version); - }, - timeout); - - WcfServiceVersion status = ProbeService( - options, - HistorianWcfServiceNames.Status, - static channel => - { - uint returnCode = channel.GetInterfaceVersion(out uint version); - return new WcfServiceVersion(returnCode, version); - }, - timeout); - - return history.ReturnCode == 0 - && history.InterfaceVersion > 0 - && retrieval.ReturnCode == 0 - && retrieval.InterfaceVersion > 0 - && status.ReturnCode == 0; - }, cancellationToken).ConfigureAwait(false); - } - - private static WcfServiceVersion ProbeService( - HistorianClientOptions options, - string serviceName, - Func call, - TimeSpan timeout) - where TContract : class - { - ChannelFactory? factory = null; - TContract? channel = null; - try - { - factory = new ChannelFactory( - HistorianWcfBindingFactory.CreateMdasNetTcpBinding(timeout), - HistorianWcfBindingFactory.CreateEndpointAddress(options.Host, options.Port, serviceName)); - factory.Open(); - - channel = factory.CreateChannel(); - if (channel is IClientChannel clientChannel) - { - clientChannel.Open(); - } - - return call(channel); - } - finally - { - AbortOrClose(channel); - AbortOrClose(factory); - } - } - - private static void AbortOrClose(object? communicationObject) - { - if (communicationObject is not ICommunicationObject clientChannel) - { - return; - } - - try - { - if (clientChannel.State == CommunicationState.Faulted) - { - clientChannel.Abort(); - } - else - { - clientChannel.Close(); - } - } - catch - { - clientChannel.Abort(); - } - } - - private readonly record struct WcfServiceVersion(uint ReturnCode, uint InterfaceVersion); -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfReadOrchestrator.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfReadOrchestrator.cs deleted file mode 100644 index dbd73d9..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfReadOrchestrator.cs +++ /dev/null @@ -1,474 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.Versioning; -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal sealed class HistorianWcfReadOrchestrator -{ - private const ushort StartQueryRequestType = HistorianDataQueryProtocol.QueryRequestTypeData; - private const int CredentialBlockSizeBytes = 1026; - private const int OpenConnection3MinResponseLength = 5; - private const string ClientNodeNameFallback = "ZB.MOM.WW.SPHistorianClient"; - private const string ClientDataSourceId = "2020.406.2652.2"; - private const string ClientDllVersionString = "2020.406.2652.2"; - private const byte NativeClientType = 4; - private const uint NativeIntegratedReadOnlyConnectionMode = 0x402; - private const byte NativeClientCommonInfoFormatVersion = 4; - private const ushort NativeHcalVersion = 17; - private const uint NativeClientVersionInt = 999_999; - private const ushort NativeOpen2ClientVersion = 9; - private const int MaxValClRounds = 8; - - private readonly HistorianClientOptions _options; - - public HistorianWcfReadOrchestrator(HistorianClientOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public async IAsyncEnumerable ReadRawAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - int maxValues, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - ValidateTransportAndAuth(); - cancellationToken.ThrowIfCancellationRequested(); - - IReadOnlyList rows = await Task.Run(() => RunRawChain(tag, startUtc, endUtc, maxValues, cancellationToken), cancellationToken).ConfigureAwait(false); - foreach (HistorianSample sample in rows) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return sample; - } - } - - public async IAsyncEnumerable ReadAggregateAsync( - string tag, - DateTime startUtc, - DateTime endUtc, - Models.RetrievalMode mode, - TimeSpan interval, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - ValidateTransportAndAuth(); - cancellationToken.ThrowIfCancellationRequested(); - - IReadOnlyList rows = await Task.Run( - () => RunAggregateChain(tag, startUtc, endUtc, mode, interval, cancellationToken), - cancellationToken).ConfigureAwait(false); - foreach (HistorianAggregateSample sample in rows) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return sample; - } - } - - public async Task> ReadAtTimeAsync( - string tag, - IReadOnlyList timestampsUtc, - CancellationToken cancellationToken) - { - ValidateTransportAndAuth(); - cancellationToken.ThrowIfCancellationRequested(); - - return await Task.Run(() => RunAtTimeChain(tag, timestampsUtc, cancellationToken), cancellationToken).ConfigureAwait(false); - } - - private void ValidateTransportAndAuth() - { - if (!_options.IntegratedSecurity && string.IsNullOrEmpty(_options.UserName)) - { - throw new ProtocolEvidenceMissingException( - "Managed read flow currently requires IntegratedSecurity or an explicit UserName + Password."); - } - } - - private List RunRawChain( - string tag, - DateTime startUtc, - DateTime endUtc, - int maxValues, - CancellationToken cancellationToken) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, retrBinding, retrEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(_options); - uint clientHandle = HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(_options, histBinding, histEndpoint, contextKey, cancellationToken); - return RunQuery(retrBinding, retrEndpoint, clientHandle, tag, startUtc, endUtc, maxValues, cancellationToken); - } - - private List RunAggregateChain( - string tag, - DateTime startUtc, - DateTime endUtc, - Models.RetrievalMode mode, - TimeSpan interval, - CancellationToken cancellationToken) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, retrBinding, retrEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(_options); - uint clientHandle = HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(_options, histBinding, histEndpoint, contextKey, cancellationToken); - return RunAggregateQuery(retrBinding, retrEndpoint, clientHandle, tag, startUtc, endUtc, mode, interval, cancellationToken); - } - - private List RunAtTimeChain( - string tag, - IReadOnlyList timestampsUtc, - CancellationToken cancellationToken) - { - if (timestampsUtc.Count == 0) - { - return []; - } - - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, retrBinding, retrEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(_options); - uint clientHandle = HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(_options, histBinding, histEndpoint, contextKey, cancellationToken); - - List results = new(timestampsUtc.Count); - foreach (DateTime ts in timestampsUtc) - { - cancellationToken.ThrowIfCancellationRequested(); - DateTime tsUtc = ts.ToUniversalTime(); - DateTime windowStart = tsUtc - TimeSpan.FromTicks(1); - DateTime windowEnd = tsUtc + TimeSpan.FromTicks(1); - List aggregates = RunAggregateQuery( - retrBinding, - retrEndpoint, - clientHandle, - tag, - windowStart, - windowEnd, - Models.RetrievalMode.Interpolated, - TimeSpan.FromTicks(2), - cancellationToken); - - if (aggregates.Count == 0) - { - continue; - } - - HistorianAggregateSample chosen = aggregates[0]; - results.Add(new HistorianSample( - TagName: chosen.TagName, - TimestampUtc: tsUtc, - NumericValue: chosen.Value, - StringValue: null, - Quality: chosen.Quality, - QualityDetail: chosen.QualityDetail, - OpcQuality: chosen.OpcQuality, - PercentGood: 100)); - } - - return results; - } - - private List RunQuery( - Binding binding, - EndpointAddress retrievalEndpoint, - uint clientHandle, - string tag, - DateTime startUtc, - DateTime endUtc, - int maxValues, - CancellationToken cancellationToken) - { - ChannelFactory retrievalFactory = new(binding, retrievalEndpoint); - HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, _options); - - try - { - IRetrievalServiceContract2 retrievalChannel = retrievalFactory.CreateChannel(); - ICommunicationObject retrievalChannelCo = (ICommunicationObject)retrievalChannel; - try - { - retrievalChannel.GetInterfaceVersion(out _); - - uint isAllowedReturn = retrievalChannel.IsOriginalAllowed(clientHandle, out bool isAllowed); - if (isAllowedReturn != 0 || !isAllowed) - { - throw new InvalidOperationException( - $"Retr.IsOriginalAllowed denied the connection (return={isAllowedReturn}, isAllowed={isAllowed})."); - } - - byte[] requestBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(BuildDataQueryRequest(tag, startUtc, endUtc, maxValues)); - - uint queryHandle = 0; - bool startSuccess = retrievalChannel.StartQuery2( - clientHandle, - StartQueryRequestType, - checked((uint)requestBuffer.Length), - requestBuffer, - out _, - out _, - ref queryHandle, - out _, - out byte[] startError); - startError ??= []; - if (!startSuccess) - { - throw new InvalidOperationException( - $"Retr.StartQuery2 failed (errorLen={startError.Length})."); - } - - List samples = []; - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - bool nextSuccess = retrievalChannel.GetNextQueryResultBuffer2( - clientHandle, - queryHandle, - out _, - out byte[] resultBuffer, - out _, - out byte[] errorBuffer); - resultBuffer ??= []; - errorBuffer ??= []; - - if (!nextSuccess) - { - throw new InvalidOperationException( - $"Retr.GetNextQueryResultBuffer2 failed (errorLen={errorBuffer.Length})."); - } - - if (!HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows(resultBuffer, errorBuffer, out IReadOnlyList rows, out bool hasMoreData)) - { - throw new InvalidOperationException( - $"Retr.GetNextQueryResultBuffer2 returned an unparsable result buffer (length={resultBuffer.Length})."); - } - - foreach (HistorianSample sample in rows) - { - samples.Add(sample); - if (samples.Count >= maxValues) - { - return samples; - } - } - - if (!hasMoreData) - { - return samples; - } - } - } - finally - { - CloseChannelSafely(retrievalChannelCo); - } - } - finally - { - CloseFactorySafely(retrievalFactory); - } - } - - private List RunAggregateQuery( - Binding binding, - EndpointAddress retrievalEndpoint, - uint clientHandle, - string tag, - DateTime startUtc, - DateTime endUtc, - Models.RetrievalMode mode, - TimeSpan interval, - CancellationToken cancellationToken) - { - ChannelFactory retrievalFactory = new(binding, retrievalEndpoint); - HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, _options); - - try - { - IRetrievalServiceContract2 retrievalChannel = retrievalFactory.CreateChannel(); - ICommunicationObject retrievalChannelCo = (ICommunicationObject)retrievalChannel; - try - { - retrievalChannel.GetInterfaceVersion(out _); - - uint isAllowedReturn = retrievalChannel.IsOriginalAllowed(clientHandle, out bool isAllowed); - if (isAllowedReturn != 0 || !isAllowed) - { - throw new InvalidOperationException( - $"Retr.IsOriginalAllowed denied the connection (return={isAllowedReturn}, isAllowed={isAllowed})."); - } - - HistorianDataQueryRequest request = BuildAggregateQueryRequest(tag, startUtc, endUtc, mode, interval); - byte[] requestBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(request); - - uint queryHandle = 0; - bool startSuccess = retrievalChannel.StartQuery2( - clientHandle, - StartQueryRequestType, - checked((uint)requestBuffer.Length), - requestBuffer, - out _, - out _, - ref queryHandle, - out _, - out byte[] startError); - startError ??= []; - if (!startSuccess) - { - throw new InvalidOperationException( - $"Retr.StartQuery2 (aggregate {mode}) failed (errorLen={startError.Length})."); - } - - List samples = []; - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - bool nextSuccess = retrievalChannel.GetNextQueryResultBuffer2( - clientHandle, - queryHandle, - out _, - out byte[] resultBuffer, - out _, - out byte[] errorBuffer); - resultBuffer ??= []; - errorBuffer ??= []; - - if (!nextSuccess) - { - throw new InvalidOperationException( - $"Retr.GetNextQueryResultBuffer2 (aggregate {mode}) failed (errorLen={errorBuffer.Length})."); - } - - if (!HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferAggregateRows( - resultBuffer, - errorBuffer, - mode, - interval, - out IReadOnlyList rows, - out bool hasMoreData)) - { - throw new InvalidOperationException( - $"Retr.GetNextQueryResultBuffer2 (aggregate {mode}) returned an unparsable buffer (length={resultBuffer.Length})."); - } - - samples.AddRange(rows); - - if (!hasMoreData) - { - return samples; - } - } - } - finally - { - CloseChannelSafely(retrievalChannelCo); - } - } - finally - { - CloseFactorySafely(retrievalFactory); - } - } - - internal static HistorianDataQueryRequest BuildDataQueryRequest(string tag, DateTime startUtc, DateTime endUtc, int maxValues) - { - return new HistorianDataQueryRequest( - TagNames: [tag], - StartUtc: startUtc.ToUniversalTime(), - EndUtc: endUtc.ToUniversalTime(), - MaxStates: checked((ushort)Math.Min(maxValues, ushort.MaxValue)), - BatchSize: 1, - Option: string.Empty); - } - - internal static HistorianDataQueryRequest BuildAggregateQueryRequest( - string tag, - DateTime startUtc, - DateTime endUtc, - Models.RetrievalMode mode, - TimeSpan interval) - { - uint queryType = MapRetrievalModeToQueryType(mode); - return new HistorianDataQueryRequest( - TagNames: [tag], - StartUtc: startUtc.ToUniversalTime(), - EndUtc: endUtc.ToUniversalTime(), - MaxStates: 0, - BatchSize: 1, - Option: string.Empty) - { - QueryType = queryType, - Resolution = interval, - AggregationType = MapRetrievalModeToAggregationType(mode) - }; - } - - /// - /// QueryType wire value matches the native ArchestrA.HistorianRetrievalMode enum - /// ordinal exactly — verified 2026-05-04 by probing every mode through the - /// instrument-wcf-writemessage capture pipeline and reading the QueryType uint32 - /// at offset 2 of pRequestBuff: - /// - /// Cyclic=0 Delta=1 Full=2 Interpolated=3 BestFit=4 TimeWeightedAverage=5 - /// MinimumWithTime=6 MaximumWithTime=7 Integral=8 Slope=9 Counter=10 - /// ValueState=11 RoundTrip=12 StartBound=13 EndBound=14 - /// - /// The public enum mirrors the native order, so the - /// mapping reduces to (uint)mode. Prior version mapped Cyclic to 4 - /// (BestFit's value) and threw for everything outside the four common modes. - /// - internal static uint MapRetrievalModeToQueryType(Models.RetrievalMode mode) - { - if (!Enum.IsDefined(mode)) - { - throw new ProtocolEvidenceMissingException($"Retrieval mode {mode} is not a defined RetrievalMode value."); - } - return (uint)mode; - } - - internal static uint MapRetrievalModeToAggregationType(Models.RetrievalMode mode) => mode switch - { - Models.RetrievalMode.TimeWeightedAverage => 0, - Models.RetrievalMode.Interpolated => 3, - _ => 3 - }; - - private static void CloseChannelSafely(ICommunicationObject channel) - { - try - { - if (channel.State == CommunicationState.Faulted) - { - channel.Abort(); - } - else - { - channel.Close(); - } - } - catch - { - try { channel.Abort(); } catch { /* swallow */ } - } - } - - private static void CloseFactorySafely(ChannelFactory factory) - { - try - { - if (factory.State == CommunicationState.Faulted) - { - factory.Abort(); - } - else - { - factory.Close(); - } - } - catch - { - try { factory.Abort(); } catch { /* swallow */ } - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfRevisionOrchestrator.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfRevisionOrchestrator.cs deleted file mode 100644 index 1503c37..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfRevisionOrchestrator.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Buffers.Binary; -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Drives the AddNonStreamValuesBegin / AddNonStreamValues / AddNonStreamValuesEnd -/// WCF op group on the /Trx service end-to-end. The native AVEVA wrapper's -/// equivalent surface (HistorianAccess.AddRevisionValues*) is gated by the -/// C++ HistorianClient's per-connection cache and rejects all writes from a -/// managed client with err 129 TagNotFoundInCache. This SDK orchestrator -/// bypasses the wrapper entirely — talks WCF directly — to test whether the SERVER -/// gates on the same condition. -/// -/// Live behavior is unverified. The first iteration is probe-only: open the auth -/// chain, drive the standard write priming, call AddNonStreamValuesBegin and -/// surface whatever the server returns. -/// -internal sealed class HistorianWcfRevisionOrchestrator -{ - private readonly HistorianClientOptions _options; - - public HistorianWcfRevisionOrchestrator(HistorianClientOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public Task ProbeBeginAsync(CancellationToken cancellationToken) - => Task.Run(() => ProbeBegin(cancellationToken), cancellationToken); - - private HistorianRevisionProbeResult ProbeBegin(CancellationToken cancellationToken) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options); - Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options); - EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction); - - HistorianRevisionProbeResult result = new(); - - HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( - _options, histBinding, histEndpoint, contextKey, cancellationToken, - connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode, - additionalSetup: (historyChannel, context) => - { - result.OpenSucceeded = true; - result.ClientHandle = context.ClientHandle; - result.StorageSessionId = context.StorageSessionId; - - // Run the same priming chain that EnsT2/DelT use — without it, the Trx - // service rejects calls with err 51 UnknownClient because the client - // hasn't registered itself across the auxiliary services. - EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status); - EndpointAddress retrievalEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Retrieval); - RunPrimingChain(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint); - - // Hypothesis: calling RTag2 (RegisterTags2) cascades client identity into - // the Trx service's session table. The event flow uses RTag2 with the - // CM_EVENT tag id and subsequent ops succeed. Try RTag2 with that same - // tag id here as a registration probe. - try - { - string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); - byte[] rtag2Buffer = BuildRTag2CmEventInputBuffer(); - bool rtag2Ok = historyChannel.RegisterTags2( - handle: handle, - elementCount: 1, - inputBuffer: rtag2Buffer, - outputBuffer: out byte[] rtag2Out, - errorBuffer: out byte[] rtag2Err); - result.RTag2Succeeded = rtag2Ok; - result.RTag2OutHex = rtag2Out is null || rtag2Out.Length == 0 ? null : Convert.ToHexString(rtag2Out); - result.RTag2ErrorHex = rtag2Err is null || rtag2Err.Length == 0 ? null : Convert.ToHexString(rtag2Err); - } - catch (Exception ex) - { - result.RTag2Exception = $"{ex.GetType().Name}: {ex.Message}"; - } - - ChannelFactory trxFactory = new(auxBinding, transactionEndpoint); - HistorianWcfClientCredentialsHelper.Configure(trxFactory, _options); - ITransactionServiceContract2 trxChannel = trxFactory.CreateChannel(); - ICommunicationObject trxCo = (ICommunicationObject)trxChannel; - try - { - // Get interface version first to register the client in the Trx service's - // session table (matches the cross-service GetV priming pattern used by - // RunWritePriming for EnsT2/DelT). - try - { - uint trxRc = trxChannel.GetInterfaceVersion(out uint trxVersion); - result.TrxInterfaceVersionReturnCode = trxRc; - result.TrxInterfaceVersion = trxVersion; - } - catch (Exception ex) - { - result.TrxInterfaceVersionException = $"{ex.GetType().Name}: {ex.Message}"; - } - - // Probe V2 AddNonStreamValuesBegin2. Try BOTH possible handle formats — - // the server returns 0433000000 (UnknownClient = 51) when the wrong one - // is sent. Capture which one (if any) is recognized. - foreach ((string label, string handle) in new[] - { - ("contextKey", contextKey.ToString("D").ToUpperInvariant()), - ("storageSessionId", context.StorageSessionId.ToString("D").ToUpperInvariant()), - ("contextKey-lower", contextKey.ToString("D")), - ("clientHandle-as-string", context.ClientHandle.ToString()), - }) - { - try - { - string? transactionId = null; - byte[]? errorBuffer = null; - bool ok = trxChannel.AddNonStreamValuesBegin2(handle, out transactionId, out errorBuffer); - result.BeginAttempts.Add(new HistorianRevisionBeginAttempt - { - HandleLabel = label, - HandleSent = handle, - Succeeded = ok, - TransactionId = transactionId, - ErrorHex = errorBuffer is null || errorBuffer.Length == 0 ? null : Convert.ToHexString(errorBuffer), - }); - if (ok && !string.IsNullOrEmpty(transactionId)) - { - result.BeginSucceeded = true; - result.BeginTransactionId = transactionId; - break; - } - } - catch (Exception ex) - { - result.BeginAttempts.Add(new HistorianRevisionBeginAttempt - { - HandleLabel = label, - HandleSent = handle, - Exception = $"{ex.GetType().Name}: {ex.Message}", - }); - } - } - } - finally - { - try { if (trxCo.State == CommunicationState.Faulted) trxCo.Abort(); else trxCo.Close(); } catch { try { trxCo.Abort(); } catch { } } - try { if (trxFactory.State == CommunicationState.Faulted) trxFactory.Abort(); else trxFactory.Close(); } catch { try { trxFactory.Abort(); } catch { } } - } - }); - - return result; - } - - /// - /// Mirrors HistorianWcfTagWriteOrchestrator.RunWritePriming. The cross-service GetV - /// calls + UpdC3 register the client in each aux service's session table so that - /// subsequent ops (like AddNonStreamValuesBegin2 on /Trx) recognize the handle. - /// - private static void RunPrimingChain( - IHistoryServiceContract2 historyChannel, - HistorianWcfAuthChainHelper.OpenConnectionContext context, - Binding auxBinding, - EndpointAddress statusEndpoint, - EndpointAddress transactionEndpoint, - EndpointAddress retrievalEndpoint) - { - string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); - - ChannelFactory statusFactory = new(auxBinding, statusEndpoint); - IStatusServiceContract2 statusChannel = statusFactory.CreateChannel(); - ChannelFactory transactionFactory = new(auxBinding, transactionEndpoint); - ITransactionServiceContract transactionChannel = transactionFactory.CreateChannel(); - ChannelFactory retrievalFactory = new(auxBinding, retrievalEndpoint); - IRetrievalServiceContract4 retrievalChannel = retrievalFactory.CreateChannel(); - - try - { - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - byte[] historianVersionRequest = BuildGetHistorianInfoRequest("HistorianVersion"); - TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); - TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); - - byte[] clientStatus = BuildUpdC3ClientStatusBlob(); - TryRun(() => historyChannel.UpdateClientStatus3(handle, (uint)clientStatus.Length, ref clientStatus, out _, out _, out _, out _)); - - foreach (string parameterName in new[] { "AllowOriginals", "HistorianPartner", "HistorianVersion", "MaxCyclicStorageTimeout", "RealTimeWindow", "FutureTimeThreshold", "AllowRenameTags" }) - { - TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, parameterName, out _, out _, out _)); - } - TryRun(() => transactionChannel.GetInterfaceVersion(out _)); - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - TryRun(() => retrievalChannel.GetInterfaceVersion(out _)); - } - finally - { - CloseSafely(retrievalChannel, retrievalFactory); - CloseSafely(transactionChannel, transactionFactory); - CloseSafely(statusChannel, statusFactory); - } - } - - /// Same 24-byte RTag2 buffer the event flow uses (CM_EVENT tag id). - private static byte[] BuildRTag2CmEventInputBuffer() - { - byte[] buffer = new byte[24]; - buffer[0] = 0x50; - buffer[1] = 0x67; - buffer[2] = 0x02; - buffer[3] = 0x00; - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), 1u); - // CM_EVENT tag id — duplicated here to avoid a cross-class dependency on the - // event orchestrator. Verify against HistorianWcfEventOrchestrator.CmEventTagId - // if the value ever needs updating. - new Guid("353b8145-5df0-4d46-a253-871aef49b321").ToByteArray().CopyTo(buffer.AsSpan(8, 16)); - return buffer; - } - - private static byte[] BuildUpdC3ClientStatusBlob() - { - byte[] blob = new byte[81]; - blob[0] = 0x02; - blob[1] = 0x01; - blob[77] = 0x1E; - return blob; - } - - private static byte[] BuildGetHistorianInfoRequest(string parameterName) - { - byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(parameterName); - int payloadLength = nameBytes.Length > 0 ? nameBytes.Length - 1 : 0; - byte[] buffer = new byte[8 + payloadLength]; - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 0x6753); - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 0x0002); - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), (uint)parameterName.Length); - Buffer.BlockCopy(nameBytes, 0, buffer, 8, payloadLength); - return buffer; - } - - private static void TryRun(Action a) { try { a(); } catch { } } - - private static void CloseSafely(object channel, ICommunicationObject factory) - { - try { if (channel is ICommunicationObject co) { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } } catch { } - try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { } - } -} - -internal sealed class HistorianRevisionProbeResult -{ - public bool OpenSucceeded { get; set; } - public uint ClientHandle { get; set; } - public Guid StorageSessionId { get; set; } - public uint? TrxInterfaceVersionReturnCode { get; set; } - public uint? TrxInterfaceVersion { get; set; } - public string? TrxInterfaceVersionException { get; set; } - public string? BeginTransactionId { get; set; } - public bool BeginSucceeded { get; set; } - public string? BeginErrorHex { get; set; } - public string? BeginException { get; set; } - public List BeginAttempts { get; } = new(); - public bool RTag2Succeeded { get; set; } - public string? RTag2OutHex { get; set; } - public string? RTag2ErrorHex { get; set; } - public string? RTag2Exception { get; set; } -} - -internal sealed class HistorianRevisionBeginAttempt -{ - public string HandleLabel { get; set; } = ""; - public string HandleSent { get; set; } = ""; - public bool Succeeded { get; set; } - public string? TransactionId { get; set; } - public string? ErrorHex { get; set; } - public string? Exception { get; set; } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfServiceNames.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfServiceNames.cs deleted file mode 100644 index d8ea55d..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfServiceNames.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfServiceNames -{ - public const string Namespace = "aa"; - - public const string History = "Hist"; - - public const string HistoryCertificate = "HistCert"; - - public const string HistoryIntegrated = "Hist-Integrated"; - - public const string Retrieval = "Retr"; - - public const string Storage = "Storage"; - - public const string Status = "Stat"; - - public const string Transaction = "Trx"; -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfStatusClient.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfStatusClient.cs deleted file mode 100644 index b0a8d1e..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfStatusClient.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Runtime.Versioning; -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfStatusClient -{ - public static Task GetSystemParameterAsync( - HistorianClientOptions options, - string parameterName, - CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrWhiteSpace(parameterName); - return Task.Run(() => GetSystemParameter(options, parameterName), cancellationToken); - } - - public static Task GetConnectionStatusAsync( - HistorianClientOptions options, - CancellationToken cancellationToken) - { - return Task.Run(() => SynthesizeConnectionStatus(options), cancellationToken); - } - - public static Task GetStoreForwardStatusAsync( - HistorianClientOptions options, - CancellationToken cancellationToken) - { - return Task.Run(() => SynthesizeStoreForwardStatus(options), cancellationToken); - } - - private static string? GetSystemParameter(HistorianClientOptions options, string parameterName) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options); - Binding statusBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(options); - EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, HistorianWcfServiceNames.Status); - - string? value = null; - HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( - options, histBinding, histEndpoint, contextKey, CancellationToken.None, - additionalSetup: (_, context) => value = QuerySystemParameter(statusBinding, statusEndpoint, context.ClientHandle, parameterName)); - return value; - } - - private static string? QuerySystemParameter(Binding statusBinding, EndpointAddress statusEndpoint, uint clientHandle, string parameterName) - { - ChannelFactory factory = new(statusBinding, statusEndpoint); - IStatusServiceContract2 channel = factory.CreateChannel(); - ICommunicationObject co = (ICommunicationObject)channel; - try - { - bool ok = channel.GetSystemParameter(clientHandle, parameterName, out string parameterValue, out _, out _); - return ok ? parameterValue : null; - } - finally - { - try { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } catch { try { co.Abort(); } catch { } } - try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { try { factory.Abort(); } catch { } } - } - } - - /// - /// AVEVA's native HistorianAccess.GetConnectionStatus reads local C++ - /// HistorianClient state (no WCF op exists for it). We synthesize an equivalent - /// by attempting an authenticated session open: a successful auth+open implies - /// ConnectedToServer = true. Store-forward and partner-connection state are not - /// observable from a single client probe and remain false. - /// - private static HistorianConnectionStatus SynthesizeConnectionStatus(HistorianClientOptions options) - { - bool connected; - string? error = null; - try - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options); - HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( - options, histBinding, histEndpoint, contextKey, CancellationToken.None); - connected = true; - } - catch (Exception ex) - { - connected = false; - error = $"{ex.GetType().Name}: {ex.Message}"; - } - - return new HistorianConnectionStatus( - ServerName: options.Host, - Pending: false, - ErrorOccurred: !connected, - Error: error, - ConnectedToServer: connected, - ConnectedToServerStorage: connected, - ConnectedToStoreForward: false, - ConnectionKind: HistorianConnectionKind.Process); - } - - /// - /// Native HistorianAccess.GetStoreForwardStatus is also client-side state. - /// Without a local store-forward sidecar to probe, we report defaults: not pending, - /// no error, no data stored, not actively storing. Connection kind is Process by - /// convention (event-only sessions are uncommon for this status helper). - /// - private static HistorianStoreForwardStatus SynthesizeStoreForwardStatus(HistorianClientOptions options) - { - return new HistorianStoreForwardStatus( - ServerName: options.Host, - Pending: false, - ErrorOccurred: false, - Error: null, - DataStored: false, - Storing: false, - ConnectionKind: HistorianConnectionKind.Process); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfTagClient.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfTagClient.cs deleted file mode 100644 index 4a89b51..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfTagClient.cs +++ /dev/null @@ -1,456 +0,0 @@ -using System.Net; -using System.Runtime.CompilerServices; -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal static class HistorianWcfTagClient -{ - public static async IAsyncEnumerable BrowseTagNamesAsync( - HistorianClientOptions options, - string filter, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - IReadOnlyList tagNames = await Task.Run( - () => BrowseTagNames(options, filter), - cancellationToken).ConfigureAwait(false); - - foreach (string tagName in tagNames) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return tagName; - } - } - - public static Task GetTagMetadataAsync( - HistorianClientOptions options, - string tag, - CancellationToken cancellationToken) - { - return Task.Run(() => GetTagMetadata(options, tag), cancellationToken); - } - - private static IReadOnlyList BrowseTagNames(HistorianClientOptions options, string filter) - { - using WcfRetrievalSession session = WcfRetrievalSession.Open(options); - uint startReturnCode = session.RetrievalChannel.StartLikeTagNameSearch( - session.Handle, - NormalizeLikeFilter(filter), - (uint)InsqlTagType.All, - isNotLike: false); - if (startReturnCode != 0) - { - throw new InvalidOperationException($"StartLikeTagNameSearch failed with return code {startReturnCode}."); - } - - List tagNames = []; - bool isMore; - do - { - uint getReturnCode = session.RetrievalChannel.GetLikeTagnames( - session.Handle, - out byte[] tagNameBuffer, - out uint tagNameBufferSize, - out isMore); - if (getReturnCode != 0) - { - throw new InvalidOperationException($"GetLikeTagnames failed with return code {getReturnCode}."); - } - - if (tagNameBuffer.Length != tagNameBufferSize) - { - throw new InvalidDataException("GetLikeTagnames returned a buffer size that does not match the byte array length."); - } - - tagNames.AddRange(HistorianTagQueryProtocol.ParseGetLikeTagNamesResponse(tagNameBuffer)); - } - while (isMore); - - return tagNames; - } - - private static HistorianTagMetadata? GetTagMetadata(HistorianClientOptions options, string tag) - { - using WcfRetrievalSession session = WcfRetrievalSession.Open(options); - uint returnCode = session.RetrievalChannel.GetTagInfoFromName( - session.Handle, - tag, - out _, - out byte[] tagMetadata); - if (returnCode != 0) - { - return null; - } - - if (tagMetadata.Length == 0) - { - return null; - } - - HistorianTagInfoResponse parsed = HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(tagMetadata); - return new HistorianTagMetadata( - Name: parsed.TagName, - Key: parsed.TagKey, - DataType: MapDataType(parsed.NativeDataTypeDescriptor), - Description: parsed.Description, - EngineeringUnit: parsed.EngineeringUnit, - MinRaw: parsed.MinEU, - MaxRaw: parsed.MaxEU); - } - - /// - /// Reverse-engineering helper: returns the parsed tag-info response (including the raw - /// 4-byte native data-type descriptor) without dispatching through . - /// Used by TagMetadataDescriptorProbeTests to discover descriptors for new tag - /// types so they can be added to the dispatch table. - /// - internal static HistorianTagInfoResponse GetTagInfoForDescriptorProbe(HistorianClientOptions options, string tag) - { - using WcfRetrievalSession session = WcfRetrievalSession.Open(options); - return GetTagInfoForDescriptorProbe(session, tag); - } - - /// Bulk variant: probes many tags and returns the raw response bytes alongside the parsed record (for byte-layout reverse engineering). - internal static IReadOnlyDictionary GetTagInfoRawBytesForProbe( - HistorianClientOptions options, - IEnumerable tags) - { - Dictionary results = new(StringComparer.Ordinal); - using WcfRetrievalSession session = WcfRetrievalSession.Open(options); - foreach (string tag in tags) - { - try - { - uint rc = session.RetrievalChannel.GetTagInfoFromName(session.Handle, tag, out _, out byte[] bytes); - results[tag] = (rc == 0 && bytes.Length > 0) ? bytes : null; - } - catch { results[tag] = null; } - } - return results; - } - - /// Bulk variant: probes many tags through a single session. - internal static IReadOnlyDictionary GetTagInfosForDescriptorProbe( - HistorianClientOptions options, - IEnumerable tags) - { - Dictionary results = new(StringComparer.Ordinal); - using WcfRetrievalSession session = WcfRetrievalSession.Open(options); - foreach (string tag in tags) - { - try { results[tag] = GetTagInfoForDescriptorProbe(session, tag); } - catch { results[tag] = null; } - } - return results; - } - - private static HistorianTagInfoResponse GetTagInfoForDescriptorProbe(WcfRetrievalSession session, string tag) - { - uint returnCode = session.RetrievalChannel.GetTagInfoFromName( - session.Handle, - tag, - out _, - out byte[] tagMetadata); - if (returnCode != 0 || tagMetadata.Length == 0) - { - throw new InvalidOperationException($"GetTagInfoFromName({tag}) returned code {returnCode}, {tagMetadata.Length} bytes."); - } - return HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(tagMetadata); - } - - internal static string NormalizeLikeFilter(string filter) - { - return filter == "*" ? "%" : filter.Replace('*', '%'); - } - - /// - /// Decodes the 4-byte native data-type descriptor returned by GetTagInfoFromName. - /// Layout determined by probing live tags + reading the CDataType predicate IL - /// (IsAnalog, IsDiscrete, IsString, IsWideString, - /// IsEvent, IsStruct, IsBoolean, IsConvertableToInt64, - /// IsConvertableToUInt64, IsConvertableToDouble) in - /// current/aahClientManaged.dll: - /// - /// byte 0 = 0x03 (descriptor format version) - /// byte 1 = tag-origin marker — observed 0xCF (system / built-in) and 0xC3 (user-created). - /// byte 2 = storage attribute byte — varies per tag (0x00 vs 0x04 observed for the same data type). - /// byte 3 = data-type code (the load-bearing field; matches the native CDataType byte 0). - /// - /// Bit pattern of byte 3 (deduced from the predicate IL): - /// - /// bit 0x80: extended/reserved marker — when set the type is treated specially (e.g., 0x81 = Boolean). - /// bit 0x40: wide-string variant (set for , clear for ). - /// bit 0x20: integer signed flag (UInt16=0x09 → Int16=0x29; UInt32=0x11 → Int32=0x31). - /// low 3 bits: type class — 1=numeric, 2=discrete/bool, 3=string, 4=event, 5=structure, 7=fixed-string. - /// - /// Type-code dispatch: - /// - /// 0x01 — probed: SysDataAcqOverallItemsPerSec → 03 CF 00 01 - /// 0x02 (Discrete/Bool) — probed: SysClassicDataRedirector → 03 CF 00 02 - /// 0x03 — IL inference (string class without bit 0x40) - /// 0x04 — IL inference (IsEvent low 3 bits == 4) - /// 0x05 — IL inference (IsStruct low 3 bits == 5) - /// 0x09 — probed: SysCritErrCnt → 03 CF 00 09, SysTimeSec → 03 CF 04 09 - /// 0x11 — probed: SysConfigStatus → 03 CF 04 11 - /// 0x21 — IL inference (IsConvertableToDouble matches 33) - /// 0x29 — IL inference (IsConvertableToInt64 matches 41 = signed UInt16 bit pattern) - /// 0x31 — probed: OtOpcUaParityTest_001.Counter → 03 C3 00 31 - /// 0x43 — probed: SysString → 03 CF 00 43 - /// - /// Extended dispatch (recovered from the same IL): - /// - /// 0x08 — 1-byte unsigned (in IsConvertableToUInt64 list) - /// 0x10 — 16-byte GUID (matches IsGuid) - /// 0x18 — Windows FILETIME (matches IsFileTime) - /// 0x19 — 8-byte signed (in IsConvertableToInt64 list, follows Int16=0x29 / Int32=0x31) - /// 0x39 — 8-byte unsigned (in IsConvertableToUInt64 list, follows UInt16=0x09 / UInt32=0x11 with signed-bit set) - /// 0x81 — Boolean extended form (matches IsBoolean's literal byte=129 check; same semantic as 0x02 Discrete) - /// - /// Code 0x38 also appears in CDataType.IsConvertableToUInt64's allow-list but is - /// NEVER produced by any tag-creation path (verified by reading the IL of - /// CDataType.InitializeAnalog/InitializeDiscrete/InitializeStruct/ - /// InitializeString, and by probing all 198 tags in a sample Runtime DB via the - /// EnumerateAllTagDescriptorsAcrossOneSession probe — 0x38 does not appear). - /// It is a value-side type used during data conversion / query result decoding, never a - /// tag descriptor; intentionally left unmapped so an unexpected 0x38 in a tag descriptor - /// throws rather than being silently - /// treated as . - /// - internal static HistorianDataType MapDataType(byte[] nativeDataTypeDescriptor) - { - // byte 1 origin marker: 0xCF = system / built-in tag, 0xC3 = MDAS-routed - // (e.g. OPC UA imported), 0xC7 = SDK-created via EnsT2 (live-verified by the - // EnsureTagAsync round-trip test). - if (nativeDataTypeDescriptor is not [0x03, 0xCF or 0xC3 or 0xC7, _, _]) - { - throw new ProtocolEvidenceMissingException( - $"GetTagInfoFromName data type descriptor {Convert.ToHexString(nativeDataTypeDescriptor)}"); - } - - return nativeDataTypeDescriptor[3] switch - { - 0x01 => HistorianDataType.Float, - 0x02 => HistorianDataType.Int1, - 0x03 => HistorianDataType.SingleByteString, - 0x04 => HistorianDataType.Event, - 0x05 => HistorianDataType.Structure, - 0x08 => HistorianDataType.UInt1, - 0x09 => HistorianDataType.UInt2, - 0x10 => HistorianDataType.Guid, - 0x11 => HistorianDataType.UInt4, - 0x18 => HistorianDataType.FileTime, - 0x19 => HistorianDataType.Int8, - 0x21 => HistorianDataType.Double, - 0x29 => HistorianDataType.Int2, - 0x31 => HistorianDataType.Int4, - 0x39 => HistorianDataType.UInt8, - 0x43 => HistorianDataType.DoubleByteString, - 0x81 => HistorianDataType.Int1, - _ => throw new ProtocolEvidenceMissingException( - $"GetTagInfoFromName data type descriptor {Convert.ToHexString(nativeDataTypeDescriptor)}") - }; - } - - private sealed class WcfRetrievalSession : IDisposable - { - private readonly ChannelFactory _historyFactory; - private readonly IHistoryServiceContract2 _historyChannel; - private readonly ChannelFactory _retrievalFactory; - - private WcfRetrievalSession( - ChannelFactory historyFactory, - IHistoryServiceContract2 historyChannel, - ChannelFactory retrievalFactory, - IRetrievalServiceContract2 retrievalChannel, - uint handle) - { - _historyFactory = historyFactory; - _historyChannel = historyChannel; - _retrievalFactory = retrievalFactory; - RetrievalChannel = retrievalChannel; - Handle = handle; - } - - public IRetrievalServiceContract2 RetrievalChannel { get; } - - public uint Handle { get; } - - public static WcfRetrievalSession Open(HistorianClientOptions options) - { - ValidateSupportedAuth(options); - - // The browse/metadata code uses the legacy Open2-V1 buffer, which carries - // its own auth blob. That buffer is only valid against the WCF transport that - // negotiates Windows security at the channel level (`/Hist-Integrated`) or - // against the cert binding (which trusts the channel-level cert identity). - // For LocalPipe and RemoteTcpIntegrated the original behaviour stays — - // hit the Integrated endpoint with the Windows transport binding. Only - // RemoteTcpCertificate gets the cert binding here, so browse/metadata - // works from a Linux client over the cert transport. - (Binding historyBinding, EndpointAddress historyEndpoint) = options.Transport switch - { - HistorianTransport.RemoteTcpCertificate => ( - HistorianWcfBindingFactory.CreateMdasNetTcpCertificateBinding(options.RequestTimeout), - HistorianWcfBindingFactory.CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate, options.ServerDnsIdentity)), - _ => ( - HistorianWcfBindingFactory.CreateMdasNetTcpWindowsBinding(options.RequestTimeout), - HistorianWcfBindingFactory.CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryIntegrated)), - }; - - ChannelFactory? historyFactory = null; - IHistoryServiceContract2? historyChannel = null; - ChannelFactory? retrievalFactory = null; - IRetrievalServiceContract2? retrievalChannel = null; - try - { - historyFactory = new ChannelFactory(historyBinding, historyEndpoint); - HistorianWcfClientCredentialsHelper.Configure(historyFactory, options); - if (options.Transport != HistorianTransport.RemoteTcpCertificate) - { - // Windows transport-security only applies to the integrated-auth binding. - historyFactory.Credentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; - ApplyWindowsCredential(historyFactory, options); - } - historyFactory.Open(); - - historyChannel = historyFactory.CreateChannel(); - ((IClientChannel)historyChannel).Open(); - - byte[] openBuffer = BuildOpen2Buffer(options); - bool openSuccess = historyChannel.OpenConnection2(ref openBuffer, out byte[] openOut, out byte[] openError); - HistorianLegacyOpen2Output? openOutput = HistorianOpen2Protocol.TryReadLegacyOpen2Output(openOut); - if (!openSuccess || openOutput is null) - { - HistorianNativeError? nativeError = HistorianOpen2Protocol.TryReadNativeError(openError); - string code = nativeError is null ? "unknown" : nativeError.Code.ToString(System.Globalization.CultureInfo.InvariantCulture); - throw new InvalidOperationException($"OpenConnection2 failed for tag browse; native error code {code}."); - } - - retrievalFactory = new ChannelFactory( - HistorianWcfBindingFactory.CreateMdasNetTcpBinding(options.RequestTimeout), - HistorianWcfBindingFactory.CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)); - HistorianWcfClientCredentialsHelper.Configure(retrievalFactory, options); - retrievalFactory.Open(); - - retrievalChannel = retrievalFactory.CreateChannel(); - ((IClientChannel)retrievalChannel).Open(); - - return new WcfRetrievalSession( - historyFactory, - historyChannel, - retrievalFactory, - retrievalChannel, - openOutput.Handle); - } - catch - { - AbortOrClose(retrievalChannel); - AbortOrClose(retrievalFactory); - AbortOrClose(historyChannel); - AbortOrClose(historyFactory); - throw; - } - } - - public void Dispose() - { - try - { - _historyChannel.CloseConnection(Handle); - } - catch - { - // Close best-effort; channel cleanup below still runs. - } - - AbortOrClose(RetrievalChannel); - AbortOrClose(_retrievalFactory); - AbortOrClose(_historyChannel); - AbortOrClose(_historyFactory); - } - - private static void ValidateSupportedAuth(HistorianClientOptions options) - { - // Three valid auth shapes: - // 1. IntegratedSecurity=true (current Windows identity, no UserName/Password) - // 2. IntegratedSecurity=false + UserName + Password (NTLM/Kerberos with explicit creds) - // 3. IntegratedSecurity=true + UserName + Password (impersonation/explicit override) - // The fourth combination — IntegratedSecurity=false with no UserName/Password — has - // no way to authenticate against the /Hist-Integrated endpoint and is rejected. - if (!options.IntegratedSecurity - && string.IsNullOrEmpty(options.UserName) - && string.IsNullOrEmpty(options.Password)) - { - throw new ProtocolEvidenceMissingException( - "Tag browse / metadata requires either IntegratedSecurity=true OR an explicit UserName + Password."); - } - } - - private static void ApplyWindowsCredential(ChannelFactory factory, HistorianClientOptions options) - { - if (string.IsNullOrWhiteSpace(options.UserName)) - { - return; - } - - NetworkCredential credential = new(); - int slash = options.UserName.IndexOf('\\'); - if (slash > 0 && slash < options.UserName.Length - 1) - { - credential.Domain = options.UserName[..slash]; - credential.UserName = options.UserName[(slash + 1)..]; - } - else - { - credential.UserName = options.UserName; - } - - credential.Password = options.Password; - factory.Credentials.Windows.ClientCredential = credential; - } - - private static byte[] BuildOpen2Buffer(HistorianClientOptions options) - { - string processName = Path.GetFileNameWithoutExtension(Environment.ProcessPath) ?? "ZB.MOM.WW.SPHistorianClient"; - HistorianOpen2Request request = new( - options.Host, - processName, - (uint)Environment.ProcessId, - string.Empty, - [], - 4, - 11, - 1026, - HistorianMetadataNamespace.Empty); - - return HistorianOpen2Protocol.SerializeLegacyVersion1(request); - } - - private static void AbortOrClose(object? communicationObject) - { - if (communicationObject is not ICommunicationObject channel) - { - return; - } - - try - { - if (channel.State == CommunicationState.Faulted) - { - channel.Abort(); - } - else - { - channel.Close(); - } - } - catch - { - channel.Abort(); - } - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfTagWriteOrchestrator.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfTagWriteOrchestrator.cs deleted file mode 100644 index 133aaf7..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/HistorianWcfTagWriteOrchestrator.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.Versioning; -using System.ServiceModel; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -/// -/// Drives the EnsT2 (EnsureTags2) and DelT (DeleteTags) WCF operations end-to-end. -/// Mirrors for the reads flow — opens an -/// authenticated session, runs the documented priming chain (UpdC3 + 7× -/// Stat.GetSystemParameter + Trx/Stat/Retr GetV) and then issues the write op. -/// -/// AddS2 is intentionally NOT here — it is blocked architecturally per -/// docs/plans/write-commands-reverse-engineering.md Phase 2 findings. -/// -internal sealed class HistorianWcfTagWriteOrchestrator -{ - private readonly HistorianClientOptions _options; - - public HistorianWcfTagWriteOrchestrator(HistorianClientOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public Task EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(definition); - ArgumentException.ThrowIfNullOrWhiteSpace(definition.TagName, nameof(definition)); - // GetAnalogDataTypeCode throws ProtocolEvidenceMissingException for unsupported - // types (String, Int1/Int8/UInt8, Guid, Event, Structure) — surface that early. - _ = HistorianTagWriteProtocol.GetAnalogDataTypeCode(definition.DataType); - return Task.Run(() => EnsureTag(definition), cancellationToken); - } - - public Task DeleteTagAsync(string tagName, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrWhiteSpace(tagName); - return Task.Run(() => DeleteTag(tagName), cancellationToken); - } - - private bool EnsureTag(HistorianTagDefinition definition) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options); - Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options); - EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status); - EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction); - EndpointAddress retrievalEndpoint = HistorianWcfBindingFactory.CreatePipeEndpointAddress(_options.Host, HistorianWcfServiceNames.Retrieval); - if (_options.Transport != HistorianTransport.LocalPipe) - { - retrievalEndpoint = HistorianWcfBindingFactory.CreateEndpointAddress(_options.Host, _options.Port, HistorianWcfServiceNames.Retrieval); - } - - bool result = false; - HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( - _options, histBinding, histEndpoint, contextKey, CancellationToken.None, - connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode, - additionalSetup: (historyChannel, context) => result = SendEnsureTags2( - historyChannel, context, definition, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint)); - return result; - } - - private bool DeleteTag(string tagName) - { - Guid contextKey = Guid.NewGuid(); - var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options); - Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options); - EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status); - EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction); - EndpointAddress retrievalEndpoint = _options.Transport == HistorianTransport.LocalPipe - ? HistorianWcfBindingFactory.CreatePipeEndpointAddress(_options.Host, HistorianWcfServiceNames.Retrieval) - : HistorianWcfBindingFactory.CreateEndpointAddress(_options.Host, _options.Port, HistorianWcfServiceNames.Retrieval); - - bool result = false; - HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( - _options, histBinding, histEndpoint, contextKey, CancellationToken.None, - connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode, - additionalSetup: (historyChannel, context) => - { - RunWritePriming(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint); - result = SendDeleteTags(historyChannel, context, tagName); - }); - return result; - } - - private static bool SendEnsureTags2( - IHistoryServiceContract2 historyChannel, - HistorianWcfAuthChainHelper.OpenConnectionContext context, - HistorianTagDefinition definition, - Binding auxBinding, - EndpointAddress statusEndpoint, - EndpointAddress transactionEndpoint, - EndpointAddress retrievalEndpoint) - { - RunWritePriming(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint); - - string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); - byte[] payload = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: definition.TagName, - description: definition.Description, - engineeringUnit: definition.EngineeringUnit, - dateCreatedUtc: DateTime.UtcNow, - dataType: definition.DataType, - minEU: definition.MinEU, - maxEU: definition.MaxEU, - minRaw: definition.MinRaw, - maxRaw: definition.MaxRaw, - storageRateMs: definition.StorageRateMs, - applyScaling: definition.ApplyScaling, - storageType: definition.StorageType, - integralDivisor: definition.IntegralDivisor); - - bool ok = historyChannel.EnsureTags2( - handle: handle, - elementCount: 1, - inputBuffer: payload, - outputBuffer: out byte[] outBuf, - errorBuffer: out byte[] errBuf); - WriteDiag("EnsT2", $"Returned={ok} OutLen={outBuf?.Length ?? -1} OutHex={(outBuf is null ? "" : Convert.ToHexString(outBuf))} ErrLen={errBuf?.Length ?? -1} ErrHex={(errBuf is null ? "" : Convert.ToHexString(errBuf))}"); - return ok; - } - - /// - /// Runs the priming chain captured between Open2 and the actual write op (EnsT2 / DelT). - /// Both paths share the same priming per the native flow capture: - /// Stat.GetV ×2 → Stat.GETHI(HistorianVersion) ×2 → UpdC3 → 6 GetSystemParameter → - /// GetSystemParameter("AllowRenameTags") → Trx.GetV → Stat.GetV → Retr.GetV. - /// - private static void RunWritePriming( - IHistoryServiceContract2 historyChannel, - HistorianWcfAuthChainHelper.OpenConnectionContext context, - Binding auxBinding, - EndpointAddress statusEndpoint, - EndpointAddress transactionEndpoint, - EndpointAddress retrievalEndpoint) - { - string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); - - ChannelFactory statusFactory = new(auxBinding, statusEndpoint); - IStatusServiceContract2 statusChannel = statusFactory.CreateChannel(); - ChannelFactory transactionFactory = new(auxBinding, transactionEndpoint); - ITransactionServiceContract transactionChannel = transactionFactory.CreateChannel(); - ChannelFactory retrievalFactory = new(auxBinding, retrievalEndpoint); - IRetrievalServiceContract4 retrievalChannel = retrievalFactory.CreateChannel(); - - try - { - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - byte[] historianVersionRequest = BuildGetHistorianInfoRequest("HistorianVersion"); - TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); - TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); - - byte[] clientStatus = BuildUpdC3ClientStatusBlob(); - historyChannel.UpdateClientStatus3(handle, (uint)clientStatus.Length, ref clientStatus, out _, out _, out _, out _); - - foreach (string parameterName in NativeStatusParametersBeforeAnalogEnsT2) - { - TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, parameterName, out _, out _, out _)); - } - TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, "AllowRenameTags", out _, out _, out _)); - TryRun(() => transactionChannel.GetInterfaceVersion(out _)); - TryRun(() => statusChannel.GetInterfaceVersion(out _)); - TryRun(() => retrievalChannel.GetInterfaceVersion(out _)); - } - finally - { - CloseSafely(retrievalChannel, retrievalFactory); - CloseSafely(transactionChannel, transactionFactory); - CloseSafely(statusChannel, statusFactory); - } - } - - private static bool SendDeleteTags( - IHistoryServiceContract2 historyChannel, - HistorianWcfAuthChainHelper.OpenConnectionContext context, - string tagName) - { - // DelT uses the uint clientHandle, NOT the GUID handle (decoded from wire capture). - // Native DelT request encodes statusSize as MS-NBFS marker 0x81 - // (ZeroTextWithEndElement = value 0) and status as xsi:nil. Earlier notes called - // 0x81 "OneText" — that was wrong; the WithEndElement-pair table is: - // 0x80/0x81 ZeroText, 0x82/0x83 OneText, 0x84/0x85 FalseText, - // 0x86/0x87 TrueText, 0x88/0x89 Int8Text. - // Sending statusSize=1 (which WCF encodes as 0x83 OneTextWithEndElement) made the - // server return DelTResult=false with err=04 84 00 00 00 (HistorianAccessError - // type 4 / code 132). statusSize=0 matches the native parity request. - byte[] tagNamesBytes = HistorianTagWriteProtocol.SerializeDeleteTagNames([tagName]); - uint statusSize = 0; - byte[] status = null!; - - bool ok = historyChannel.DeleteTags( - handle: context.ClientHandle, - tagNamesSize: checked((uint)tagNamesBytes.Length), - tagNames: tagNamesBytes, - statusSize: ref statusSize, - status: ref status, - errorSize: out uint errorSize, - errorBuffer: out byte[] errorBuffer); - - WriteDiag("DelT", $"Returned={ok} ClientHandle={context.ClientHandle} StatusSize={statusSize} StatusLen={status?.Length ?? -1} StatusHex={(status is null ? "" : Convert.ToHexString(status))} ErrorSize={errorSize} ErrorLen={errorBuffer?.Length ?? -1} ErrorHex={(errorBuffer is null ? "" : Convert.ToHexString(errorBuffer))}"); - return ok; - } - - private static void WriteDiag(string op, string line) - { - string? diagPath = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_DELT_DIAG"); - if (string.IsNullOrWhiteSpace(diagPath)) return; - try { File.AppendAllText(diagPath, $"{DateTimeOffset.UtcNow:O} {op} {line}{Environment.NewLine}"); } catch { } - } - - private static readonly string[] NativeStatusParametersBeforeAnalogEnsT2 = - [ - "AllowOriginals", - "HistorianPartner", - "HistorianVersion", - "MaxCyclicStorageTimeout", - "RealTimeWindow", - "FutureTimeThreshold", - ]; - - private static void TryRun(Action a) { try { a(); } catch { } } - - /// 81-byte UpdC3 status blob captured from native (same as event flow). - private static byte[] BuildUpdC3ClientStatusBlob() - { - byte[] blob = new byte[81]; - blob[0] = 0x02; - blob[1] = 0x01; - blob[77] = 0x1E; - return blob; - } - - /// GETHI request bytes for a parameter-name query (decoded from native). - private static byte[] BuildGetHistorianInfoRequest(string parameterName) - { - byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(parameterName); - int payloadLength = nameBytes.Length > 0 ? nameBytes.Length - 1 : 0; - byte[] buffer = new byte[8 + payloadLength]; - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 0x6753); - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 0x0002); - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), (uint)parameterName.Length); - Buffer.BlockCopy(nameBytes, 0, buffer, 8, payloadLength); - return buffer; - } - - private static void CloseSafely(object channel, ICommunicationObject factory) - { - try { if (channel is ICommunicationObject co) { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } } catch { } - try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncoder.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncoder.cs deleted file mode 100644 index 2d31e32..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncoder.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.ServiceModel.Channels; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal sealed class MdasMessageEncoder : MessageEncoder -{ - public const string MdasContentType = "application/x-mdas"; - - private readonly MessageEncoder inner; - - public MdasMessageEncoder(MessageEncoder inner) - { - this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); - } - - public override string ContentType => MdasContentType; - - public override string MediaType => MdasContentType; - - public override MessageVersion MessageVersion => inner.MessageVersion; - - public override bool IsContentTypeSupported(string contentType) - { - return contentType.StartsWith(MdasContentType, StringComparison.OrdinalIgnoreCase) - || inner.IsContentTypeSupported(contentType); - } - - public override Message ReadMessage(ArraySegment buffer, BufferManager bufferManager, string contentType) - { - return inner.ReadMessage(buffer, bufferManager, inner.ContentType); - } - - public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType) - { - return inner.ReadMessage(stream, maxSizeOfHeaders, inner.ContentType); - } - - public override void WriteMessage(Message message, Stream stream) - { - inner.WriteMessage(message, stream); - } - - public override ArraySegment WriteMessage( - Message message, - int maxMessageSize, - BufferManager bufferManager, - int messageOffset) - { - return inner.WriteMessage(message, maxMessageSize, bufferManager, messageOffset); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncoderFactory.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncoderFactory.cs deleted file mode 100644 index 4ea2583..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncoderFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ServiceModel.Channels; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal sealed class MdasMessageEncoderFactory : MessageEncoderFactory -{ - private readonly MessageEncoderFactory inner; - private readonly MessageEncoder encoder; - - public MdasMessageEncoderFactory(MessageEncoderFactory inner) - { - this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); - encoder = new MdasMessageEncoder(inner.Encoder); - } - - public override MessageEncoder Encoder => encoder; - - public override MessageVersion MessageVersion => inner.MessageVersion; -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncodingBindingElement.cs b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncodingBindingElement.cs deleted file mode 100644 index 6332fc3..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/Wcf/MdasMessageEncodingBindingElement.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ServiceModel.Channels; -using System.Xml; - -namespace ZB.MOM.WW.SPHistorianClient.Wcf; - -internal sealed class MdasMessageEncodingBindingElement : MessageEncodingBindingElement -{ - private readonly MessageEncodingBindingElement inner; - - public MdasMessageEncodingBindingElement(MessageEncodingBindingElement inner) - { - this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); - } - - private MdasMessageEncodingBindingElement(MdasMessageEncodingBindingElement source) - { - inner = (MessageEncodingBindingElement)source.inner.Clone(); - } - - public override MessageVersion MessageVersion - { - get => inner.MessageVersion; - set => inner.MessageVersion = value; - } - - public override MessageEncoderFactory CreateMessageEncoderFactory() - { - return new MdasMessageEncoderFactory(inner.CreateMessageEncoderFactory()); - } - - public override BindingElement Clone() - { - return new MdasMessageEncodingBindingElement(this); - } - - public override IChannelFactory BuildChannelFactory(BindingContext context) - { - ArgumentNullException.ThrowIfNull(context); - context.BindingParameters.Add(this); - return context.BuildInnerChannelFactory(); - } - - public override bool CanBuildChannelFactory(BindingContext context) - { - ArgumentNullException.ThrowIfNull(context); - context.BindingParameters.Add(this); - return context.CanBuildInnerChannelFactory(); - } - - public override T? GetProperty(BindingContext context) where T : class - { - ArgumentNullException.ThrowIfNull(context); - return inner.GetProperty(context) ?? context.GetInnerProperty(); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.csproj b/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.csproj deleted file mode 100644 index 1d72c4d..0000000 --- a/ZB.MOM.WW.SPHistorianClient/src/ZB.MOM.WW.SPHistorianClient/ZB.MOM.WW.SPHistorianClient.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - true - ZB.MOM.WW.SPHistorianClient - ZB.MOM.WW - Pure-managed .NET 10 client for AVEVA System Platform Historian (Wonderware) for the ZB.MOM.WW SCADA family. The wire protocol is reverse-engineered and re-implemented in C# — no native AVEVA runtime dependency. Surfaces history reads (raw / aggregate / at-time / event), tag browse + metadata, status, and tag create/delete over the WCF/MDAS transports (Windows) plus a cross-platform gRPC transport for 2023 R2. - aveva;wonderware;historian;system-platform;scada;timeseries;grpc;wcf;zb-mom-ww - https://gitea.dohertylan.com/dohertj2/zb-mom-ww-sphistorianclient - https://gitea.dohertylan.com/dohertj2/zb-mom-ww-sphistorianclient - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - <_Parameter1>ZB.MOM.WW.SPHistorianClient.Tests - - - - diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/BinaryPrimitiveTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/BinaryPrimitiveTests.cs deleted file mode 100644 index d53e057..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/BinaryPrimitiveTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text; -using ZB.MOM.WW.SPHistorianClient.Protocol; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class BinaryPrimitiveTests -{ - [Fact] - public void ToFileTimeUtc_TreatsUnspecifiedAsUtc() - { - DateTime value = new(2020, 4, 5, 10, 7, 42, DateTimeKind.Unspecified); - - long actual = HistorianBinaryPrimitives.ToFileTimeUtc(value); - - Assert.Equal(DateTime.SpecifyKind(value, DateTimeKind.Utc).ToFileTimeUtc(), actual); - } - - [Fact] - public void WriteUtf16NullTerminated_WritesUnicodeWithTerminator() - { - using MemoryStream stream = new(); - - HistorianBinaryPrimitives.WriteUtf16NullTerminated(stream, "UTC"); - - Assert.Equal(Encoding.Unicode.GetBytes("UTC\0"), stream.ToArray()); - } - - [Fact] - public void WriteFileTimeUtc_WritesLittleEndianUInt64() - { - DateTime value = new(2020, 4, 5, 10, 7, 42, DateTimeKind.Utc); - using MemoryStream stream = new(); - - HistorianBinaryPrimitives.WriteFileTimeUtc(stream, value); - - Assert.Equal(BitConverter.GetBytes(value.ToFileTimeUtc()), stream.ToArray()); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs deleted file mode 100644 index 1b36707..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/DependencyInjectionTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using ZB.MOM.WW.SPHistorianClient; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public class DependencyInjectionTests -{ - [Fact] - public async Task AddZbSpHistorianClient_resolves_client_and_options() - { - var services = new ServiceCollection(); - var options = new HistorianClientOptions { Host = "localhost" }; - - services.AddZbSpHistorianClient(options); - - // HistorianClient is IAsyncDisposable-only, so the container must be disposed - // asynchronously (a synchronous `using` throws InvalidOperationException). - await using var sp = services.BuildServiceProvider(); - Assert.Same(options, sp.GetRequiredService()); - Assert.NotNull(sp.GetRequiredService()); - } - - [Fact] - public void AddZbSpHistorianClient_throws_when_host_missing() - { - var services = new ServiceCollection(); - var options = new HistorianClientOptions { Host = "" }; - - Assert.Throws(() => services.AddZbSpHistorianClient(options)); - } - - [Fact] - public void AddZbSpHistorianClient_throws_on_null_options() - { - var services = new ServiceCollection(); - Assert.Throws(() => services.AddZbSpHistorianClient(null!)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/EnumCompatibilityTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/EnumCompatibilityTests.cs deleted file mode 100644 index 3e9f593..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/EnumCompatibilityTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Models; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class EnumCompatibilityTests -{ - [Fact] - public void RetrievalMode_ValuesMatchManagedWrapper() - { - Assert.Equal(0, (int)RetrievalMode.Cyclic); - Assert.Equal(1, (int)RetrievalMode.Delta); - Assert.Equal(2, (int)RetrievalMode.Full); - Assert.Equal(3, (int)RetrievalMode.Interpolated); - Assert.Equal(11, (int)RetrievalMode.ValueState); - Assert.Equal(14, (int)RetrievalMode.EndBound); - } - - [Fact] - public void ConnectionKind_ValuesMatchManagedWrapper() - { - Assert.Equal(1, (int)HistorianConnectionKind.Process); - Assert.Equal(2, (int)HistorianConnectionKind.Event); - } - - [Fact] - public void InterpolationType_ValuesMatchManagedWrapper() - { - Assert.Equal(0, (int)InterpolationType.StairStep); - Assert.Equal(1, (int)InterpolationType.Linear); - Assert.Equal(254, (int)InterpolationType.SystemDefault); - Assert.Equal(255, (int)InterpolationType.None); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/EventChainDiagnosticTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/EventChainDiagnosticTests.cs deleted file mode 100644 index 328faa6..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/EventChainDiagnosticTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Runtime.Versioning; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using Xunit.Abstractions; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -[SupportedOSPlatform("windows")] -public sealed class EventChainDiagnosticTests -{ - private readonly ITestOutputHelper _output; - - public EventChainDiagnosticTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public async Task EventOrchestrator_DiagnosticDump_AgainstLocalHistorian() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClientOptions options = new() - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }; - - HistorianWcfEventOrchestrator orchestrator = new(options); - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromDays(7); - - int observed = 0; - ZB.MOM.WW.SPHistorianClient.Models.HistorianEvent? firstEvent = null; - await foreach (var evt in orchestrator.ReadEventsAsync(startUtc, endUtc, CancellationToken.None)) - { - observed++; - firstEvent ??= evt; - } - - _output.WriteLine($"Events observed: {observed}"); - if (firstEvent is not null) - { - _output.WriteLine($" EventTimeUtc: {firstEvent.EventTimeUtc:O}"); - _output.WriteLine($" ReceivedTimeUtc: {firstEvent.ReceivedTimeUtc:O}"); - _output.WriteLine($" Type: {firstEvent.Type}"); - _output.WriteLine($" Properties.Count:{firstEvent.Properties.Count}"); - _output.WriteLine($" Has alarm_id: {firstEvent.Id != Guid.Empty}"); - } - _output.WriteLine($"LastEnsT2Handle: {HistorianWcfEventOrchestrator.LastEnsT2Handle}"); - _output.WriteLine($"LastEnsT2PayloadSha256: {HistorianWcfEventOrchestrator.LastEnsT2PayloadSha256}"); - _output.WriteLine($"LastUpdC3ReturnCode: {HistorianWcfEventOrchestrator.LastUpdC3ReturnCode}"); - _output.WriteLine($"LastRTag2ReturnCode: {HistorianWcfEventOrchestrator.LastRTag2ReturnCode}"); - _output.WriteLine($"LastAddReturnCode (EnsT2): {HistorianWcfEventOrchestrator.LastAddReturnCode}"); - _output.WriteLine($"LastAddOutputLength: {HistorianWcfEventOrchestrator.LastAddOutputLength}"); - _output.WriteLine($"LastResultBufferLength: {orchestrator.LastResultBufferLength}"); - _output.WriteLine($"LastErrorBufferDescription: {orchestrator.LastErrorBufferDescription}"); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/FrameTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/FrameTests.cs deleted file mode 100644 index 813ad71..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/FrameTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Protocol; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class FrameTests -{ - [Fact] - public async Task FrameWriterAndReader_RoundTrip() - { - HistorianFrame frame = new((HistorianMessageType)42, 123u, new byte[] { 1, 2, 3, 4 }); - byte[] bytes = HistorianFrameWriter.ToArray(frame); - - HistorianFrame actual = await HistorianFrameReader.ReadAsync(new MemoryStream(bytes), CancellationToken.None); - - Assert.Equal(frame.MessageType, actual.MessageType); - Assert.Equal(frame.CorrelationId, actual.CorrelationId); - Assert.True(frame.Payload.Span.SequenceEqual(actual.Payload.Span)); - } - - [Fact] - public async Task FrameReader_RejectsInvalidLength() - { - byte[] bytes = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - await Assert.ThrowsAsync(async () => - await HistorianFrameReader.ReadAsync(new MemoryStream(bytes), CancellationToken.None)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianClientIntegrationTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianClientIntegrationTests.cs deleted file mode 100644 index 38e44cf..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianClientIntegrationTests.cs +++ /dev/null @@ -1,737 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class HistorianClientIntegrationTests -{ - [Fact] - public async Task ProbeAsync_ReturnsTrueForConfiguredHistorian() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host)) - { - return; - } - - int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort) - ? parsedPort - : HistorianClientOptions.DefaultPort; - HistorianClient client = new(new HistorianClientOptions { Host = host, Port = port }); - - Assert.True(await client.ProbeAsync(CancellationToken.None)); - } - - [Fact] - public async Task BrowseTagNamesAsync_ReturnsConfiguredTestTag() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - string? filter = Environment.GetEnvironmentVariable("HISTORIAN_TAG_FILTER") ?? testTag; - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || string.IsNullOrWhiteSpace(filter)) - { - return; - } - - int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort) - ? parsedPort - : HistorianClientOptions.DefaultPort; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - Port = port, - IntegratedSecurity = true, - UserName = Environment.GetEnvironmentVariable("HISTORIAN_USER") ?? string.Empty, - Password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD") ?? string.Empty - }); - - List tagNames = []; - await foreach (string tagName in client.BrowseTagNamesAsync(filter, CancellationToken.None)) - { - tagNames.Add(tagName); - } - - Assert.Contains(testTag, tagNames); - } - - [Fact] - public async Task ReadRawAsync_AgainstLocalHistorian_ReturnsAtLeastOneRow() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) - { - return; - } - - if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)) - { - // The managed read flow currently only supports the LocalPipe transport. - return; - } - - if (!OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromDays(7); - - List samples = []; - await foreach (ZB.MOM.WW.SPHistorianClient.Models.HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None)) - { - samples.Add(sample); - } - - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - } - - [Fact] - public async Task ReadAggregateAsync_AgainstLocalHistorian_ReturnsTimeWeightedAverageRows() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) - { - return; - } - - if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromMinutes(10); - - List samples = []; - await foreach (ZB.MOM.WW.SPHistorianClient.Models.HistorianAggregateSample sample in client.ReadAggregateAsync( - testTag, startUtc, endUtc, - ZB.MOM.WW.SPHistorianClient.Models.RetrievalMode.TimeWeightedAverage, - TimeSpan.FromMinutes(1), - CancellationToken.None)) - { - samples.Add(sample); - } - - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - Assert.All(samples, s => Assert.Equal(ZB.MOM.WW.SPHistorianClient.Models.RetrievalMode.TimeWeightedAverage, s.RetrievalMode)); - } - - // Verifies a previously-unmapped RetrievalMode (one of the 11 modes that prior to - // 2026-05-04 threw ProtocolEvidenceMissingException). MinimumWithTime → QueryType=6 - // exercises the "QueryType is the native enum ordinal" mapping against the live server. - [Theory] - [InlineData(ZB.MOM.WW.SPHistorianClient.Models.RetrievalMode.MinimumWithTime)] - [InlineData(ZB.MOM.WW.SPHistorianClient.Models.RetrievalMode.MaximumWithTime)] - [InlineData(ZB.MOM.WW.SPHistorianClient.Models.RetrievalMode.BestFit)] - public async Task ReadAggregateAsync_AgainstLocalHistorian_AcceptsPreviouslyUnmappedRetrievalMode( - ZB.MOM.WW.SPHistorianClient.Models.RetrievalMode mode) - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) - || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) - || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromMinutes(10); - - List samples = []; - await foreach (ZB.MOM.WW.SPHistorianClient.Models.HistorianAggregateSample s in client.ReadAggregateAsync( - testTag, startUtc, endUtc, mode, TimeSpan.FromMinutes(2), CancellationToken.None)) - { - samples.Add(s); - } - - // Server should accept the request without error. Even if no rows come back - // (unlikely for a 10-minute window on a steadily-counting tag), the absence of an - // exception proves the QueryType byte was accepted. - Assert.All(samples, s => Assert.Equal(mode, s.RetrievalMode)); - } - - [Fact] - public async Task ReadAtTimeAsync_AgainstLocalHistorian_ReturnsRequestedTimestamps() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) - { - return; - } - - if (!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - DateTime nowUtc = DateTime.UtcNow; - DateTime[] timestamps = - [ - nowUtc - TimeSpan.FromMinutes(5), - nowUtc - TimeSpan.FromMinutes(2), - nowUtc - TimeSpan.FromMinutes(1) - ]; - - IReadOnlyList samples = await client.ReadAtTimeAsync(testTag, timestamps, CancellationToken.None); - - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - } - - [Fact] - public async Task ReadEventsAsync_AgainstLocalHistorian_DoesNotThrow() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromDays(7); - - // The event-row WCF wire format is not yet decoded; this test verifies the chain - // (ValCl + Open2 + Retr.IsOriginalAllowed + Retr.StartEventQuery) reaches the server - // without throwing. An empty event list is acceptable until row parsing is wired. - List events = []; - await foreach (ZB.MOM.WW.SPHistorianClient.Models.HistorianEvent evt in client.ReadEventsAsync(startUtc, endUtc, CancellationToken.None)) - { - events.Add(evt); - } - - Assert.NotNull(events); - } - - [Fact] - public async Task GetSystemParameterAsync_AgainstLocalHistorian_ReturnsHistorianVersion() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - string? value = await client.GetSystemParameterAsync("HistorianVersion", CancellationToken.None); - - // The server returns a non-empty version string for the documented HistorianVersion parameter. - Assert.False(string.IsNullOrWhiteSpace(value)); - } - - [Fact] - public async Task GetConnectionStatusAsync_AgainstLocalHistorian_ReportsConnectedToServer() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianConnectionStatus status = - await client.GetConnectionStatusAsync(CancellationToken.None); - - Assert.True(status.ConnectedToServer); - Assert.False(status.ErrorOccurred); - Assert.False(status.Pending); - Assert.Equal(host, status.ServerName); - } - - [Fact] - public async Task GetStoreForwardStatusAsync_AgainstLocalHistorian_ReturnsDefaults() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianStoreForwardStatus status = - await client.GetStoreForwardStatusAsync(CancellationToken.None); - - // The synthesized status returns defaults — no store-forward sidecar to probe in this build. - Assert.False(status.ErrorOccurred); - Assert.False(status.Pending); - Assert.Equal(host, status.ServerName); - } - - // The validator inside HistorianWcfTagClient now allows IntegratedSecurity=false WHEN - // explicit UserName + Password are provided (NTLM/Kerberos with non-current-user creds). - // It still rejects the no-credentials-at-all case since there's no way to authenticate - // against /Hist-Integrated. - [Fact] - public async Task GetTagMetadataAsync_NoAuthAndNoCredentials_Throws() - { - HistorianClient client = new(new HistorianClientOptions - { - Host = "localhost", - IntegratedSecurity = false, - UserName = string.Empty, - Password = string.Empty, - }); - await Assert.ThrowsAsync( - () => client.GetTagMetadataAsync("anytag", CancellationToken.None)); - } - - [Fact] - public async Task GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian() - { - // Live verification of the explicit-creds tag-metadata path. Gated on - // HISTORIAN_USER + HISTORIAN_PASSWORD being set; skips cleanly otherwise. The path - // routes through WCF Windows transport security with Credentials.Windows.ClientCredential. - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); - string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) - || string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password) - || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = false, - UserName = user, - Password = password, - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianTagMetadata? metadata = - await client.GetTagMetadataAsync(testTag, CancellationToken.None); - Assert.NotNull(metadata); - Assert.Equal(testTag, metadata.Name); - } - - [Fact] - public async Task GetTagMetadataAsync_ReturnsConfiguredTestTagMetadata() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) - { - return; - } - - int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_PORT"), out int parsedPort) - ? parsedPort - : HistorianClientOptions.DefaultPort; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - Port = port, - IntegratedSecurity = true, - UserName = Environment.GetEnvironmentVariable("HISTORIAN_USER") ?? string.Empty, - Password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD") ?? string.Empty - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianTagMetadata? metadata = - await client.GetTagMetadataAsync(testTag, CancellationToken.None); - - Assert.NotNull(metadata); - Assert.Equal(testTag, metadata.Name); - Assert.NotNull(metadata.Key); - } - - [Fact] - public async Task EnsureTagAsync_AndDeleteTagAsync_RoundTrip_AgainstLocalHistorian() - { - // Per docs/plans/write-commands-reverse-engineering.md safety rules: localhost only, - // sandbox tag name must start with "RetestSdkWrite", tag is created if missing and - // always deleted at the end so the test leaves zero residue. - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string? sandboxTag = Environment.GetEnvironmentVariable("HISTORIAN_WRITE_SANDBOX_TAG"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - if (string.IsNullOrWhiteSpace(sandboxTag) || !sandboxTag.StartsWith("RetestSdkWrite", StringComparison.Ordinal)) - { - return; // safety gate per the plan - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition definition = new() - { - TagName = sandboxTag, - Description = "SDK live integration test sandbox", - EngineeringUnit = "test", - DataType = ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - MinEU = 0.0, - MaxEU = 100.0, - }; - - // Both EnsureTagAsync and DeleteTagAsync now work end-to-end against the live - // Historian. Open2 must use write-enabled connectionMode 0x401 (not the default - // 0x402 read-only); the EnsT2 InBuff layout is corrected to native parity (144 - // bytes incl 0x4E leading marker, no trailing 01 01 01 closing markers). - bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None); - Assert.True(ensured, "EnsureTagAsync returned false against the live Historian."); - - bool deleted = await client.DeleteTagAsync(sandboxTag, CancellationToken.None); - Assert.True(deleted, "DeleteTagAsync returned false against the live Historian."); - } - - // Round-trip every live-verified analog data type + the non-default-range case. The - // sandbox tag name is suffixed per case so the runs don't collide. Always cleans up. - [Theory] - [InlineData("RetestSdkWriteFloatRT", ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, 0.0, 100.0, 0.0, 100.0)] - [InlineData("RetestSdkWriteDoubleRT", ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Double, 0.0, 100.0, 0.0, 100.0)] - [InlineData("RetestSdkWriteInt2RT", ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Int2, 0.0, 100.0, 0.0, 100.0)] - [InlineData("RetestSdkWriteInt4RT", ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Int4, 0.0, 100.0, 0.0, 100.0)] - [InlineData("RetestSdkWriteUInt4RT", ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.UInt4, 0.0, 100.0, 0.0, 100.0)] - [InlineData("RetestSdkWriteFloatRangesRT", ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, -50.0, 200.0, 10.0, 4095.0)] - public async Task EnsureTagAsync_AndDeleteTagAsync_RoundTrip_PerDataTypeAndRange( - string sandboxTag, - ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType dataType, - double minEU, double maxEU, double minRaw, double maxRaw) - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition definition = new() - { - TagName = sandboxTag, - Description = $"SDK round-trip {dataType}", - EngineeringUnit = "test", - DataType = dataType, - MinEU = minEU, - MaxEU = maxEU, - MinRaw = minRaw, - MaxRaw = maxRaw, - }; - - try - { - bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None); - Assert.True(ensured, $"EnsureTagAsync({dataType}) returned false against the live Historian."); - } - finally - { - // Always clean up — DeleteTagAsync returns true on a freshly-created tag. - await client.DeleteTagAsync(sandboxTag, CancellationToken.None); - } - } - - [Fact] - public async Task EnsureTagAsync_StorageTypeDelta_PersistsToTagTableAsTwo() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - const string sandboxTag = "RetestSdkWriteStorageTypeDeltaRT"; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe, - }); - - try - { - bool ok = await client.EnsureTagAsync(new ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition - { - TagName = sandboxTag, - Description = "SDK Delta round-trip", - EngineeringUnit = "test", - DataType = ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - StorageType = ZB.MOM.WW.SPHistorianClient.Models.HistorianStorageType.Delta, - }, CancellationToken.None); - Assert.True(ok, "EnsureTagAsync(Delta) returned false"); - - using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True"); - sql.Open(); - using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand(); - cmd.CommandText = "SELECT StorageType FROM Tag WHERE TagName = @t"; - cmd.Parameters.AddWithValue("@t", sandboxTag); - object? st = cmd.ExecuteScalar(); - Assert.NotNull(st); - Assert.Equal((int)ZB.MOM.WW.SPHistorianClient.Models.HistorianStorageType.Delta, Convert.ToInt32(st)); - } - finally - { - await client.DeleteTagAsync(sandboxTag, CancellationToken.None); - } - } - - [Fact] - public async Task EnsureTagAsync_NonDefaultStorageRate_PersistsToTagTable() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - const string sandboxTag = "RetestSdkWriteStorageRateRT"; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe, - }); - - try - { - bool ok = await client.EnsureTagAsync(new ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition - { - TagName = sandboxTag, - Description = "SDK StorageRate round-trip", - EngineeringUnit = "test", - DataType = ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - // Server only accepts quantized rates — 1000, 5000, 10000, 60000, 300000 ms. - StorageRateMs = 5000u, - }, CancellationToken.None); - Assert.True(ok, "EnsureTagAsync returned false"); - - using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True"); - sql.Open(); - using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand(); - cmd.CommandText = "SELECT StorageRate FROM Tag WHERE TagName = @t"; - cmd.Parameters.AddWithValue("@t", sandboxTag); - object? rate = cmd.ExecuteScalar(); - Assert.NotNull(rate); - Assert.Equal(5000, Convert.ToInt32(rate)); - } - finally - { - await client.DeleteTagAsync(sandboxTag, CancellationToken.None); - } - } - - [Fact] - public async Task EnsureTagAsync_CalledTwiceOnSameTag_UpdatesFieldsInPlace() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - const string sandboxTag = "RetestSdkWriteIdempotencyRT"; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe, - }); - - try - { - bool firstOk = await client.EnsureTagAsync(new ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition - { - TagName = sandboxTag, - Description = "First version", - EngineeringUnit = "test", - DataType = ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - MinEU = 0.0, MaxEU = 100.0, MinRaw = 0.0, MaxRaw = 100.0, - ApplyScaling = false, - }, CancellationToken.None); - Assert.True(firstOk, "First EnsureTagAsync returned false"); - (string desc1, double minEU1, double maxEU1, double minRaw1, double maxRaw1, int scaling1) = ReadTagState(sandboxTag); - Assert.Equal("First version", desc1); - Assert.Equal(0.0, minEU1); - Assert.Equal(0, scaling1); - - bool secondOk = await client.EnsureTagAsync(new ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition - { - TagName = sandboxTag, - Description = "Second version", - EngineeringUnit = "kPa", - DataType = ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - MinEU = -50.0, MaxEU = 200.0, MinRaw = 10.0, MaxRaw = 4095.0, - ApplyScaling = true, - }, CancellationToken.None); - Assert.True(secondOk, "Second EnsureTagAsync returned false"); - (string desc2, double minEU2, double maxEU2, double minRaw2, double maxRaw2, int scaling2) = ReadTagState(sandboxTag); - - // EnsureTagAsync upserts: second call updates the existing row in place. - Assert.Equal("Second version", desc2); - Assert.Equal(-50.0, minEU2); - Assert.Equal(200.0, maxEU2); - Assert.Equal(10.0, minRaw2); - Assert.Equal(4095.0, maxRaw2); - Assert.Equal(1, scaling2); - } - finally - { - await client.DeleteTagAsync(sandboxTag, CancellationToken.None); - } - - static (string desc, double minEU, double maxEU, double minRaw, double maxRaw, int scaling) ReadTagState(string tagName) - { - using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True"); - sql.Open(); - using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand(); - cmd.CommandText = "SELECT t.[Description], a.MinEU, a.MaxEU, a.MinRaw, a.MaxRaw, a.Scaling FROM Tag t JOIN AnalogTag a ON a.TagName=t.TagName WHERE t.TagName=@t"; - cmd.Parameters.AddWithValue("@t", tagName); - using Microsoft.Data.SqlClient.SqlDataReader r = cmd.ExecuteReader(); - Assert.True(r.Read(), $"Tag {tagName} not found"); - return (r.GetString(0), r.GetDouble(1), r.GetDouble(2), r.GetDouble(3), r.GetDouble(4), Convert.ToInt32(r.GetValue(5))); - } - } - - [Fact] - public async Task EnsureTagAsync_ApplyScalingTrue_PersistsDistinctMinRawAndMaxRaw() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - const string sandboxTag = "RetestSdkWriteApplyScalingRT"; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe, - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianTagDefinition definition = new() - { - TagName = sandboxTag, - Description = "SDK ApplyScaling round-trip", - EngineeringUnit = "test", - DataType = ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - MinEU = -50.0, - MaxEU = 200.0, - MinRaw = 10.0, - MaxRaw = 4095.0, - ApplyScaling = true, - }; - - try - { - bool ensured = await client.EnsureTagAsync(definition, CancellationToken.None); - Assert.True(ensured, "EnsureTagAsync(ApplyScaling=true) returned false against the live Historian."); - - // Verify directly against the AnalogTag table — the read-path GetTagMetadataAsync - // surfaces only one of (MinRaw, MinEU); SQL is the unambiguous source of truth. - using Microsoft.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;Encrypt=False;TrustServerCertificate=True"); - sql.Open(); - using Microsoft.Data.SqlClient.SqlCommand cmd = sql.CreateCommand(); - cmd.CommandText = "SELECT MinEU, MaxEU, MinRaw, MaxRaw, Scaling FROM AnalogTag WHERE TagName = @t"; - cmd.Parameters.AddWithValue("@t", sandboxTag); - using Microsoft.Data.SqlClient.SqlDataReader r = cmd.ExecuteReader(); - Assert.True(r.Read(), $"AnalogTag row for {sandboxTag} not found after EnsureTag."); - Assert.Equal(-50.0, r.GetDouble(0)); - Assert.Equal(200.0, r.GetDouble(1)); - Assert.Equal(10.0, r.GetDouble(2)); - Assert.Equal(4095.0, r.GetDouble(3)); - Assert.Equal(1, Convert.ToInt32(r.GetValue(4))); - } - finally - { - await client.DeleteTagAsync(sandboxTag, CancellationToken.None); - } - } - - [Fact] - public async Task GetTagMetadataAsync_PopulatesDescriptionAndEuRangeForAnalogTag() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - // SysTimeSec is a built-in analog UInt16 tag with non-empty Description, MaxEU, - // and an EngineeringUnit. Verifies the parser populates those new fields end-to-end. - const string analogTag = "SysTimeSec"; - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe - }); - - ZB.MOM.WW.SPHistorianClient.Models.HistorianTagMetadata? metadata = - await client.GetTagMetadataAsync(analogTag, CancellationToken.None); - - Assert.NotNull(metadata); - Assert.Equal(analogTag, metadata.Name); - Assert.False(string.IsNullOrWhiteSpace(metadata.Description)); - Assert.NotNull(metadata.MaxRaw); - Assert.True(metadata.MaxRaw is > 0 and <= 1e15); - Assert.False(string.IsNullOrWhiteSpace(metadata.EngineeringUnit)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianEventRowProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianEventRowProtocolTests.cs deleted file mode 100644 index 716a0eb..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianEventRowProtocolTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System.Buffers.Binary; -using System.Text; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class HistorianEventRowProtocolTests -{ - private static readonly Guid PlaceholderAlarmId = new("00000000-0000-0000-0000-000000000001"); - - [Fact] - public void Parse_EmptyBuffer_ReturnsEmpty() - { - IReadOnlyList events = HistorianEventRowProtocol.Parse([]); - Assert.Empty(events); - } - - [Fact] - public void Parse_HeaderWithZeroRowCount_ReturnsEmpty() - { - byte[] buffer = BuildHeader(rowCount: 0); - IReadOnlyList events = HistorianEventRowProtocol.Parse(buffer); - Assert.Empty(events); - } - - [Fact] - public void Parse_WrongVersion_ReturnsEmpty() - { - byte[] buffer = new byte[6]; - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 8); // not 9 - BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(2, 4), 5u); - IReadOnlyList events = HistorianEventRowProtocol.Parse(buffer); - Assert.Empty(events); - } - - [Fact] - public void Parse_TwoSyntheticRows_ReturnsTimestampsAndEventTypes() - { - DateTime t1 = new(2026, 1, 2, 3, 4, 5, DateTimeKind.Utc); - DateTime t2 = t1.AddSeconds(10); - - byte[] buffer = Concat( - BuildHeader(rowCount: 2), - BuildRow(t1, "Alarm.Set", []), - BuildRow(t2, "Alarm.Clear", [])); - - IReadOnlyList events = HistorianEventRowProtocol.Parse(buffer); - - Assert.Equal(2, events.Count); - Assert.Equal(t1, events[0].EventTimeUtc); - Assert.Equal("Alarm.Set", events[0].Type); - Assert.Equal(t2, events[1].EventTimeUtc); - Assert.Equal("Alarm.Clear", events[1].Type); - } - - [Fact] - public void Parse_RowWithKnownProperties_PopulatesEventFields() - { - DateTime eventTime = new(2026, 1, 2, 3, 4, 5, DateTimeKind.Utc); - DateTime receivedTime = eventTime.AddMilliseconds(250); - - var properties = new (string Name, byte[] Value)[] - { - ("alarm_inalarm", BuildBool(true)), - ("alarm_id", BuildGuid(PlaceholderAlarmId)), - ("severity", BuildInt32(2)), - ("priority", BuildInt32(500)), - ("alarm_class", BuildUtf16String("DSC")), - ("source_processvariable", BuildUtf16String("Sample.Tag")), - ("provider_system", BuildUtf16String("Application Server")), - ("receivedtime", BuildFiletime(receivedTime)), - ("revisionversion", BuildInt32(7)), - }; - - byte[] buffer = Concat(BuildHeader(rowCount: 1), BuildRow(eventTime, "Alarm.Set", properties)); - IReadOnlyList events = HistorianEventRowProtocol.Parse(buffer); - - HistorianEvent evt = Assert.Single(events); - Assert.Equal(PlaceholderAlarmId, evt.Id); - Assert.Equal(eventTime, evt.EventTimeUtc); - Assert.Equal(receivedTime, evt.ReceivedTimeUtc); - Assert.Equal("Alarm.Set", evt.Type); - Assert.Equal("Sample.Tag", evt.SourceName); - Assert.Equal("Application Server", evt.Namespace); - Assert.Equal(7, evt.RevisionVersion); - Assert.Equal(true, evt.Properties["alarm_inalarm"]); - Assert.Equal("DSC", evt.Properties["alarm_class"]); - Assert.Equal(2, evt.Properties["severity"]); - Assert.Equal(500, evt.Properties["priority"]); - } - - [Fact] - public void Parse_UnknownTypeMarker_KeepsRawBytesInPropertyBag() - { - DateTime eventTime = new(2026, 1, 2, 3, 4, 5, DateTimeKind.Utc); - // Custom type 0xAA with 3-byte value. - byte[] customValue = [0xAA, 0x03, 0x00, 0xDE, 0xAD, 0xBE]; - byte[] buffer = Concat( - BuildHeader(rowCount: 1), - BuildRowWithRawValue(eventTime, "Alarm.Set", "custom_field", customValue)); - - IReadOnlyList events = HistorianEventRowProtocol.Parse(buffer); - HistorianEvent evt = Assert.Single(events); - Assert.IsType(evt.Properties["custom_field"]); - Assert.Equal([0xDE, 0xAD, 0xBE], (byte[])evt.Properties["custom_field"]!); - } - - [Fact] - public void Parse_RowWithMissingMarker_StopsAtBadRow() - { - DateTime t1 = new(2026, 1, 2, 3, 4, 5, DateTimeKind.Utc); - byte[] goodRow = BuildRow(t1, "Alarm.Set", []); - byte[] badRow = new byte[goodRow.Length]; - byte[] buffer = Concat(BuildHeader(rowCount: 2), goodRow, badRow); - - IReadOnlyList events = HistorianEventRowProtocol.Parse(buffer); - - Assert.Single(events); - Assert.Equal("Alarm.Set", events[0].Type); - } - - private static byte[] BuildHeader(uint rowCount) - { - byte[] header = new byte[6]; - BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(0, 2), HistorianEventRowProtocol.EventRowProtocolVersion); - BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(2, 4), rowCount); - return header; - } - - private static byte[] BuildRow(DateTime eventTimeUtc, string eventType, (string Name, byte[] Value)[] properties) - { - byte[] eventTypeBytes = BuildCompactAscii(eventType); - ushort propertyCount = (ushort)properties.Length; - int propertyBlockSize = 0; - byte[][] propertyBlocks = new byte[properties.Length][]; - for (int i = 0; i < properties.Length; i++) - { - byte[] nameBlock = BuildCompactAscii(properties[i].Name); - propertyBlocks[i] = Concat(nameBlock, properties[i].Value); - propertyBlockSize += propertyBlocks[i].Length; - } - - byte[] row = new byte[4 + 2 + 8 + 16 + eventTypeBytes.Length + 2 + propertyBlockSize]; - Span span = row; - BinaryPrimitives.WriteUInt32LittleEndian(span[..4], HistorianEventRowProtocol.RowMarker); - BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(4, 2), HistorianEventRowProtocol.RowFormatV9); - BinaryPrimitives.WriteInt64LittleEndian(span.Slice(6, 8), eventTimeUtc.ToFileTimeUtc()); - // 16 bytes of zeroed slot ushorts left as-is. - int eventTypeOffset = 4 + 2 + 8 + 16; - eventTypeBytes.CopyTo(span[eventTypeOffset..]); - BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(eventTypeOffset + eventTypeBytes.Length, 2), propertyCount); - int cursor = eventTypeOffset + eventTypeBytes.Length + 2; - foreach (byte[] block in propertyBlocks) - { - block.CopyTo(span[cursor..]); - cursor += block.Length; - } - return row; - } - - private static byte[] BuildRowWithRawValue(DateTime eventTimeUtc, string eventType, string propertyName, byte[] rawValueBytes) - { - return BuildRow(eventTimeUtc, eventType, [(propertyName, rawValueBytes)]); - } - - private static byte[] BuildCompactAscii(string s) - { - byte[] ascii = Encoding.ASCII.GetBytes(s); - byte[] result = new byte[3 + ascii.Length]; - result[0] = 0x09; - result[1] = (byte)ascii.Length; - result[2] = 0x00; - ascii.CopyTo(result, 3); - return result; - } - - private static byte[] BuildBool(bool value) => [0x02, 0x01, 0x00, value ? (byte)1 : (byte)0]; - - private static byte[] BuildInt32(int value) - { - byte[] result = [0x31, 0x04, 0x00, 0, 0, 0, 0]; - BinaryPrimitives.WriteInt32LittleEndian(result.AsSpan(3, 4), value); - return result; - } - - private static byte[] BuildGuid(Guid value) - { - byte[] result = new byte[19]; - result[0] = 0x10; - result[1] = 0x10; - result[2] = 0x00; - value.ToByteArray().CopyTo(result, 3); - return result; - } - - private static byte[] BuildFiletime(DateTime value) - { - byte[] result = [0x18, 0x08, 0x00, 0, 0, 0, 0, 0, 0, 0, 0]; - BinaryPrimitives.WriteInt64LittleEndian(result.AsSpan(3, 8), value.ToFileTimeUtc()); - return result; - } - - private static byte[] BuildUtf16String(string value) - { - byte[] chars = Encoding.Unicode.GetBytes(value); - ushort innerLength = (ushort)(2 + chars.Length); // UInt16 charCount + chars - byte[] result = new byte[3 + innerLength]; - result[0] = 0x43; - result[1] = (byte)innerLength; - result[2] = 0x00; - BinaryPrimitives.WriteUInt16LittleEndian(result.AsSpan(3, 2), (ushort)value.Length); - chars.CopyTo(result, 5); - return result; - } - - private static byte[] Concat(params byte[][] arrays) - { - int total = 0; - foreach (byte[] a in arrays) total += a.Length; - byte[] result = new byte[total]; - int offset = 0; - foreach (byte[] a in arrays) - { - Buffer.BlockCopy(a, 0, result, offset, a.Length); - offset += a.Length; - } - return result; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianGrpcIntegrationTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianGrpcIntegrationTests.cs deleted file mode 100644 index e9f7b20..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianGrpcIntegrationTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Models; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -/// -/// Live integration tests for the 2023 R2 RemoteGrpc transport. Gated on a dedicated -/// HISTORIAN_GRPC_HOST env var (plus HISTORIAN_TEST_TAG) so they skip cleanly until -/// a 2023 R2 Historian is available. Optional: -/// HISTORIAN_GRPC_PORT (default 32565), HISTORIAN_GRPC_TLS (true/false), -/// HISTORIAN_USER / HISTORIAN_PASSWORD (explicit creds; otherwise IntegratedSecurity), -/// HISTORIAN_GRPC_DNSID (server certificate name when connecting by IP over TLS). -/// -public sealed class HistorianGrpcIntegrationTests -{ - [Fact] - public async Task ReadRawAsync_OverGrpc_ReturnsAtLeastOneRow() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_TEST_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag)) - { - return; - } - - HistorianClient client = new(BuildOptions(host)); - - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromDays(7); - - List samples = []; - await foreach (HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None)) - { - samples.Add(sample); - } - - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - } - - private static HistorianClientOptions BuildOptions(string host) - { - string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); - string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); - bool explicitCreds = !string.IsNullOrEmpty(user); - int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_PORT"), out int parsed) - ? parsed - : HistorianClientOptions.DefaultGrpcPort; - bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase); - - return new HistorianClientOptions - { - Host = host, - Port = port, - Transport = HistorianTransport.RemoteGrpc, - GrpcUseTls = tls, - AllowUntrustedServerCertificate = tls, - ServerDnsIdentity = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_DNSID"), - IntegratedSecurity = !explicitCreds, - UserName = user ?? string.Empty, - Password = password ?? string.Empty - }; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianGrpcTransportTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianGrpcTransportTests.cs deleted file mode 100644 index 0fbf9cb..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianGrpcTransportTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Grpc; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using Google.Protobuf; -using ArchestrA.Grpc.Contract.Retrieval; -using GrpcHistory = ArchestrA.Grpc.Contract.History; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -/// -/// Unit coverage for the 2023 R2 RemoteGrpc transport — the parts that do not require a live -/// server: channel address/port resolution, metadata, transport routing, and the invariant that -/// gRPC request messages carry the same native byte buffers the WCF path uses. -/// -public sealed class HistorianGrpcTransportTests -{ - private static HistorianClientOptions Options( - string host = "histserver", - int port = HistorianClientOptions.DefaultPort, - bool tls = false, - string? dnsIdentity = null, - bool compression = false) => new() - { - Host = host, - Port = port, - Transport = HistorianTransport.RemoteGrpc, - GrpcUseTls = tls, - ServerDnsIdentity = dnsIdentity, - Compression = compression, - IntegratedSecurity = true - }; - - [Fact] - public void ResolvePort_DefaultWcfPort_SubstitutesGrpcDefault() - { - Assert.Equal(HistorianClientOptions.DefaultGrpcPort, HistorianGrpcChannelFactory.ResolvePort(Options(port: HistorianClientOptions.DefaultPort))); - } - - [Fact] - public void ResolvePort_ExplicitPort_IsHonoured() - { - Assert.Equal(443, HistorianGrpcChannelFactory.ResolvePort(Options(port: 443))); - } - - [Fact] - public void ResolveAddress_Plaintext_UsesHttpAndHost() - { - Assert.Equal("http://histserver:32565", HistorianGrpcChannelFactory.ResolveAddress(Options())); - } - - [Fact] - public void ResolveAddress_Tls_UsesHttpsAndHostWhenNoDnsIdentity() - { - Assert.Equal("https://histserver:32565", HistorianGrpcChannelFactory.ResolveAddress(Options(tls: true))); - } - - [Fact] - public void ResolveAddress_Tls_PrefersDnsIdentityForCertMatch() - { - string address = HistorianGrpcChannelFactory.ResolveAddress(Options(host: "10.0.0.5", tls: true, dnsIdentity: "localhost")); - Assert.Equal("https://localhost:32565", address); - } - - [Fact] - public void Create_CompressionDisabled_EmitsNoEncodingHeader() - { - using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(Options(compression: false)); - Assert.DoesNotContain(connection.Metadata, e => e.Key == "grpc-internal-encoding-request"); - } - - [Fact] - public void Create_CompressionEnabled_AdvertisesGzipRequestEncoding() - { - using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(Options(compression: true)); - global::Grpc.Core.Metadata.Entry entry = Assert.Single(connection.Metadata, e => e.Key == "grpc-internal-encoding-request"); - Assert.Equal("gzip", entry.Value); - } - - [Fact] - public void StartQueryRequest_CarriesNativeDataQueryBufferUnchanged() - { - // The gRPC envelope must wrap the exact bytes the WCF StartQuery2 path sends, so the - // already-reverse-engineered DataQueryRequest serializer is reused verbatim. - HistorianDataQueryRequest request = HistorianWcfReadOrchestrator.BuildDataQueryRequest( - "Tag.Counter", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc), 100); - byte[] nativeBuffer = HistorianDataQueryProtocol.SerializeFullHistoryRequest(request); - - var message = new StartQueryRequest - { - UiHandle = 7, - UiQueryRequestType = HistorianDataQueryProtocol.QueryRequestTypeData, - BtRequestBuffer = ByteString.CopyFrom(nativeBuffer) - }; - - // Round-trip through protobuf and confirm the native buffer survives byte-for-byte. - byte[] wire = message.ToByteArray(); - var decoded = StartQueryRequest.Parser.ParseFrom(wire); - Assert.Equal(nativeBuffer, decoded.BtRequestBuffer.ToByteArray()); - Assert.Equal(7u, decoded.UiHandle); - Assert.Equal((uint)HistorianDataQueryProtocol.QueryRequestTypeData, decoded.UiQueryRequestType); - } - - [Fact] - public void OpenConnectionRequest_CarriesNativeOpen2BufferUnchanged() - { - byte[] open2 = HistorianNativeHandshake.BuildOpenConnection3Request( - "histserver", Guid.NewGuid(), HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode); - - var message = new GrpcHistory.OpenConnectionRequest { BtConnectionRequest = ByteString.CopyFrom(open2) }; - var decoded = GrpcHistory.OpenConnectionRequest.Parser.ParseFrom(message.ToByteArray()); - - Assert.Equal(open2, decoded.BtConnectionRequest.ToByteArray()); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianRetrievalModeMappingTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianRetrievalModeMappingTests.cs deleted file mode 100644 index 8bc70e6..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianRetrievalModeMappingTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Runtime.Versioning; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -[SupportedOSPlatform("windows")] -public sealed class HistorianRetrievalModeMappingTests -{ - // Probed 2026-05-04 via instrument-wcf-writemessage against every - // ArchestrA.HistorianRetrievalMode value — see HistorianWcfReadOrchestrator - // MapRetrievalModeToQueryType doc comment for capture details. - [Theory] - [InlineData(RetrievalMode.Cyclic, 0u)] - [InlineData(RetrievalMode.Delta, 1u)] - [InlineData(RetrievalMode.Full, 2u)] - [InlineData(RetrievalMode.Interpolated, 3u)] - [InlineData(RetrievalMode.BestFit, 4u)] - [InlineData(RetrievalMode.TimeWeightedAverage, 5u)] - [InlineData(RetrievalMode.MinimumWithTime, 6u)] - [InlineData(RetrievalMode.MaximumWithTime, 7u)] - [InlineData(RetrievalMode.Integral, 8u)] - [InlineData(RetrievalMode.Slope, 9u)] - [InlineData(RetrievalMode.Counter, 10u)] - [InlineData(RetrievalMode.ValueState, 11u)] - [InlineData(RetrievalMode.RoundTrip, 12u)] - [InlineData(RetrievalMode.StartBound, 13u)] - [InlineData(RetrievalMode.EndBound, 14u)] - public void MapRetrievalModeToQueryType_MatchesNativeEnumOrdinal(RetrievalMode mode, uint expectedQueryType) - { - Assert.Equal(expectedQueryType, HistorianWcfReadOrchestrator.MapRetrievalModeToQueryType(mode)); - } - - [Fact] - public void MapRetrievalModeToQueryType_UndefinedValue_Throws() - { - Assert.Throws( - () => HistorianWcfReadOrchestrator.MapRetrievalModeToQueryType((RetrievalMode)999)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianSspiClientTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianSspiClientTests.cs deleted file mode 100644 index 9d66ea0..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianSspiClientTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Runtime.Versioning; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -[SupportedOSPlatform("windows")] -public sealed class HistorianSspiClientTests -{ - [Fact] - public void NativeFlagsRound0_MatchesDocumentedNativeWrapperValue() - { - Assert.Equal(0x2081C, HistorianSspiClient.NativeFlagsRound0); - } - - [Fact] - public void NativeFlagsRoundSubsequent_MatchesDocumentedNativeWrapperValue() - { - Assert.Equal(0x81C, HistorianSspiClient.NativeFlagsRoundSubsequent); - } - - [Fact] - public void Round0FlagsIncludeIdentify_LaterRoundsDoNot() - { - Assert.Equal(HistorianSspiClient.IscReqIdentify, HistorianSspiClient.NativeFlagsRound0 & HistorianSspiClient.IscReqIdentify); - Assert.Equal(0, HistorianSspiClient.NativeFlagsRoundSubsequent & HistorianSspiClient.IscReqIdentify); - } - - [Fact] - public void AllRoundsRequestReplayAndSequenceDetection() - { - const int both = HistorianSspiClient.IscReqReplayDetect | HistorianSspiClient.IscReqSequenceDetect; - Assert.Equal(both, HistorianSspiClient.NativeFlagsRound0 & both); - Assert.Equal(both, HistorianSspiClient.NativeFlagsRoundSubsequent & both); - } - - [Fact] - public void SelectRequestFlags_DispatchesByRoundIndex() - { - Assert.Equal(HistorianSspiClient.NativeFlagsRound0, HistorianSspiClient.SelectRequestFlags(0)); - Assert.Equal(HistorianSspiClient.NativeFlagsRoundSubsequent, HistorianSspiClient.SelectRequestFlags(1)); - Assert.Equal(HistorianSspiClient.NativeFlagsRoundSubsequent, HistorianSspiClient.SelectRequestFlags(7)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianTagWriteProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianTagWriteProtocolTests.cs deleted file mode 100644 index 15f74c8..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianTagWriteProtocolTests.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System.Text; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class HistorianTagWriteProtocolTests -{ - [Fact] - public void SerializeAnalogCTagMetadata_MatchesCapturedNativeBytesByteForByte() - { - // Reproduces the captured native EnsT2(Float) CTagMetadata bytes for the sandbox - // tag with default ranges and ApplyScaling=false. 2-byte trailer = `FE 00` where - // the second byte is the ApplyScaling flag (0x00 = false; 0x01 = true). - const string ExpectedHex = - "4E6703000100000004C6020100000000000000000000000000000000" - + "09150052657465737453646B577269746553616E64626F78" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E803000049D087CDBFDBDC011A030904007465737410270000000000000000F03FFE00"; - - byte[] expected = Convert.FromHexString(ExpectedHex); - byte[] actual = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteSandbox", - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01DCDBBFCD87D049L)); - - Assert.Equal(144, expected.Length); - Assert.Equal(144, actual.Length); - Assert.Equal(Convert.ToHexString(expected), Convert.ToHexString(actual)); - } - - // Per-data-type captures from instrument-wcf-writemessage 2026-05-04 — the only - // diff vs the Float baseline is byte 11 (the data-type discriminator) plus tag-name - // length. All other inputs (description, EU, default ranges, storage rate) match - // the captured baseline so the byte-for-byte assertion exercises the dispatch. - [Theory] - [InlineData( - ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Double, - "RetestSdkWriteDouble", 0x01dcdbed24988f3aL, - "4E6703000100000004C6022100000000000000000000000000000000" - + "09140052657465737453646B5772697465446F75626C65" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300003A8F9824EDDBDC011A030904007465737410270000000000000000F03FFE00")] - [InlineData( - ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Int4, - "RetestSdkWriteInt4", 0x01dcdbed292e1cecL, - "4E6703000100000004C6023100000000000000000000000000000000" - + "09120052657465737453646B5772697465496E7434" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E8030000EC1C2E29EDDBDC011A030904007465737410270000000000000000F03FFE00")] - [InlineData( - ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.UInt4, - "RetestSdkWriteUInt4", 0x01dcdbed2d33b02cL, - "4E6703000100000004C6021100000000000000000000000000000000" - + "09130052657465737453646B577269746555496E7434" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E80300002CB0332DEDDBDC011A030904007465737410270000000000000000F03FFE00")] - [InlineData( - ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Int2, - "RetestSdkWriteInt2", 0x01dcdbed360e9b54L, - "4E6703000100000004C6022900000000000000000000000000000000" - + "09120052657465737453646B5772697465496E7432" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - + "09180053444B2077726974652D52452073616E64626F78207461670904004D44415302010100000001E8030000549B0E36EDDBDC011A030904007465737410270000000000000000F03FFE00")] - public void SerializeAnalogCTagMetadata_PerDataType_MatchesCapturedNativeBytes( - ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType dataType, - string tagName, - long fileTimeUtc, - string expectedHex) - { - byte[] expected = Convert.FromHexString(expectedHex); - byte[] actual = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: tagName, - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(fileTimeUtc), - dataType: dataType); - - Assert.Equal(expected.Length, actual.Length); - Assert.Equal(Convert.ToHexString(expected), Convert.ToHexString(actual)); - } - - // Captured 2026-05-04 with MinEU=-50, MaxEU=200, MinRaw=10, MaxRaw=4095. Verifies - // the explicit-scaling marker `1F` + 4 doubles in order (MinEU, MaxEU, MinRaw, MaxRaw). - [Fact] - public void SerializeAnalogCTagMetadata_NonDefaultRanges_EmitsExplicitMarkerAndFourDoubles() - { - const string ExpectedHex = - "4E6703000100000004C6020100000000000000000000000000000000" - + "09190052657465737453646B5772697465466C6F617452616E676573" - + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09180053444B207772697465" - + "2D52452073616E64626F78207461670904004D444153020101000000" - + "01E8030000BE294B47EDDBDC011F0000000000000049C00000000000" - + "00694000000000000024400000000000FEAF40090400746573741027" - + "0000000000000000F03FFE00"; - - byte[] expected = Convert.FromHexString(ExpectedHex); - byte[] actual = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteFloatRanges", - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdbed474b29beL), - dataType: ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - minEU: -50.0, - maxEU: 200.0, - minRaw: 10.0, - maxRaw: 4095.0); - - Assert.Equal(180, expected.Length); - Assert.Equal(180, actual.Length); - Assert.Equal(Convert.ToHexString(expected), Convert.ToHexString(actual)); - } - - [Fact] - public void SerializeAnalogCTagMetadata_NonDefaultStorageRate_EncodesUInt32LittleEndianAtKnownOffset() - { - byte[] defaultRate = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteRate", - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdbed474b29beL)); - byte[] customRate = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteRate", - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdbed474b29beL), - storageRateMs: 2500u); - - Assert.Equal(defaultRate.Length, customRate.Length); - // Storage-rate uint32 is at the byte position immediately after the - // "MDAS" + flag-block sequence; the only diff between the two payloads - // is those 4 bytes. - int firstDiff = 0; - while (firstDiff < defaultRate.Length && defaultRate[firstDiff] == customRate[firstDiff]) firstDiff++; - Assert.Equal(0xE8, defaultRate[firstDiff]); // 1000 = 0x000003E8 LE → 0xE8 0x03 0x00 0x00 - Assert.Equal(0x03, defaultRate[firstDiff + 1]); - Assert.Equal(0xC4, customRate[firstDiff]); // 2500 = 0x000009C4 LE → 0xC4 0x09 0x00 0x00 - Assert.Equal(0x09, customRate[firstDiff + 1]); - // Beyond the 4-byte rate field, the rest is identical. - Assert.Equal( - Convert.ToHexString(defaultRate.AsSpan(firstDiff + 4)), - Convert.ToHexString(customRate.AsSpan(firstDiff + 4))); - } - - [Fact] - public void SerializeAnalogCTagMetadata_ZeroStorageRate_Throws() - { - Assert.Throws(() => HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteRate", - description: "x", - engineeringUnit: "test", - dateCreatedUtc: DateTime.UtcNow, - storageRateMs: 0u)); - } - - [Fact] - public void SerializeAnalogCTagMetadata_StorageTypeDelta_FlipsHeaderByte10AndFlagBlockByte1AndAddsFourBytePadding() - { - // Captured 2026-05-04 by toggling --write-storage-type on the native harness: - // Delta differs from Cyclic in three places — header byte 10 (0x02 -> 0x06), - // flag-block byte 1 (0x01 -> 0x02), and 4 zero bytes inserted after StorageRate - // before the FILETIME. Net length difference is +4 bytes for Delta. - byte[] cyclic = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteStorageTypeRT", - description: "x", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL), - storageType: ZB.MOM.WW.SPHistorianClient.Models.HistorianStorageType.Cyclic); - byte[] delta = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteStorageTypeRT", - description: "x", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL), - storageType: ZB.MOM.WW.SPHistorianClient.Models.HistorianStorageType.Delta); - - Assert.Equal(cyclic.Length + 4, delta.Length); - // Header byte 10 (storage-type sub-marker before the data-type code). - Assert.Equal(0x02, cyclic[10]); - Assert.Equal(0x06, delta[10]); - // The data-type code at byte 11 is unchanged. - Assert.Equal(cyclic[11], delta[11]); - } - - [Fact] - public void SerializeAnalogCTagMetadata_NonDefaultIntegralDivisor_FlipsEightBytesBeforeTrailer() - { - byte[] @default = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteIntDiv", - description: "x", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL)); - byte[] custom = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteIntDiv", - description: "x", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdc34_5a1dff6dL), - integralDivisor: 2.5); - - Assert.Equal(@default.Length, custom.Length); - // The 8 bytes immediately before the 2-byte trailer are the IntegralDivisor double. - ReadOnlySpan defaultDivisor = @default.AsSpan(@default.Length - 10, 8); - ReadOnlySpan customDivisor = custom.AsSpan(custom.Length - 10, 8); - Assert.Equal(1.0, BitConverter.ToDouble(defaultDivisor)); - Assert.Equal(2.5, BitConverter.ToDouble(customDivisor)); - // Bytes preceding the divisor are identical. - Assert.Equal( - Convert.ToHexString(@default.AsSpan(0, @default.Length - 10)), - Convert.ToHexString(custom.AsSpan(0, custom.Length - 10))); - } - - [Fact] - public void SerializeAnalogCTagMetadata_ApplyScalingTrue_FlipsTrailerSecondByte() - { - // Captured 2026-05-04 by toggling --write-apply-scaling on the native harness: - // ApplyScaling=true sets the trailer's second byte to 0x01 (vs 0x00 for false). - // Live-verified: with 0x01 the server persists distinct MinRaw/MaxRaw and sets - // AnalogTag.Scaling=1; with 0x00 it mirrors MinRaw to MinEU and sets Scaling=0. - byte[] withFlag = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteFloatRanges", - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdbed474b29beL), - dataType: ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - minEU: -50.0, maxEU: 200.0, minRaw: 10.0, maxRaw: 4095.0, - applyScaling: true); - byte[] withoutFlag = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata( - tagName: "RetestSdkWriteFloatRanges", - description: "SDK write-RE sandbox tag", - engineeringUnit: "test", - dateCreatedUtc: DateTime.FromFileTimeUtc(0x01dcdbed474b29beL), - dataType: ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Float, - minEU: -50.0, maxEU: 200.0, minRaw: 10.0, maxRaw: 4095.0, - applyScaling: false); - - Assert.Equal(withoutFlag.Length, withFlag.Length); - Assert.Equal(0xFE, withFlag[^2]); - Assert.Equal(0x01, withFlag[^1]); - Assert.Equal(0xFE, withoutFlag[^2]); - Assert.Equal(0x00, withoutFlag[^1]); - Assert.Equal( - Convert.ToHexString(withoutFlag.AsSpan(0, withoutFlag.Length - 1)), - Convert.ToHexString(withFlag.AsSpan(0, withFlag.Length - 1))); - } - - [Fact] - public void GetAnalogDataTypeCode_UnsupportedType_Throws() - { - Assert.Throws( - () => HistorianTagWriteProtocol.GetAnalogDataTypeCode(ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.SingleByteString)); - Assert.Throws( - () => HistorianTagWriteProtocol.GetAnalogDataTypeCode(ZB.MOM.WW.SPHistorianClient.Models.HistorianDataType.Int1)); - } - - [Fact] - public void SerializeAnalogCTagMetadata_DifferentInputsProducesDifferentBytesInExpectedSlots() - { - DateTime t = new(2026, 5, 4, 12, 0, 0, DateTimeKind.Utc); - byte[] a = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata("Tag1", "DescA", "uA", t); - byte[] b = HistorianTagWriteProtocol.SerializeAnalogCTagMetadata("Tag2", "DescB", "uB", t); - Assert.NotEqual(Convert.ToHexString(a), Convert.ToHexString(b)); - // First difference must be inside the tagName region (offset 27+ after the 9-byte - // header + 16-byte zero block + 2-byte compact-ASCII len-prefix). - int firstDiff = 0; - while (firstDiff < a.Length && a[firstDiff] == b[firstDiff]) firstDiff++; - Assert.InRange(firstDiff, 25, a.Length); - } - - [Fact] - public void SerializeDeleteTagNames_SingleTagMatchesCapturedShape() - { - // Captured DelT.tagNames bytes for ['RetestSdkWriteSandbox']: - // ushort 0x6751 + ushort 1 + uint32 1 + uint32 21 + UTF-16 "RetestSdkWriteSandbox" - // = 12-byte header + 42-byte UTF-16 string = 54 bytes total. - byte[] expected = Concat( - [0x51, 0x67, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00], - Encoding.Unicode.GetBytes("RetestSdkWriteSandbox")); - - byte[] actual = HistorianTagWriteProtocol.SerializeDeleteTagNames(["RetestSdkWriteSandbox"]); - - Assert.Equal(54, actual.Length); - Assert.Equal(Convert.ToHexString(expected), Convert.ToHexString(actual)); - } - - [Fact] - public void SerializeDeleteTagNames_MultipleTagsAppendsEach() - { - byte[] result = HistorianTagWriteProtocol.SerializeDeleteTagNames(["A", "BB", "CCC"]); - // 8-byte header (ushort 0x6751 + ushort 1 + uint32 tagCount) - // + 3 × (uint32 charCount + UTF-16 chars) - // = 8 + (4 + 2) + (4 + 4) + (4 + 6) = 32 bytes - Assert.Equal(32, result.Length); - // Header: 0x6751 + 0x0001 + count=3 - Assert.Equal(0x51, result[0]); Assert.Equal(0x67, result[1]); - Assert.Equal(0x01, result[2]); Assert.Equal(0x00, result[3]); - Assert.Equal(3, BitConverter.ToInt32(result, 4)); - } - - [Fact] - public void SerializeDeleteTagNames_EmptyListThrows() - { - Assert.Throws(() => HistorianTagWriteProtocol.SerializeDeleteTagNames([])); - } - - private static byte[] Concat(params byte[][] arrays) - { - int total = 0; foreach (byte[] a in arrays) total += a.Length; - byte[] result = new byte[total]; int off = 0; - foreach (byte[] a in arrays) { Buffer.BlockCopy(a, 0, result, off, a.Length); off += a.Length; } - return result; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianWcfCertOptionTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianWcfCertOptionTests.cs deleted file mode 100644 index 1b6ce44..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianWcfCertOptionTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.IdentityModel.Selectors; -using System.Security.Cryptography.X509Certificates; -using System.ServiceModel; -using System.ServiceModel.Channels; -using System.ServiceModel.Security; -using ZB.MOM.WW.SPHistorianClient; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class HistorianWcfCertOptionTests -{ - private static HistorianClientOptions BaseOptions(bool allowUntrusted = false, string? dnsIdentity = null) => - new() - { - Host = "10.0.0.1", - Port = HistorianClientOptions.DefaultPort, - Transport = HistorianTransport.RemoteTcpCertificate, - IntegratedSecurity = false, - UserName = "user", - Password = "pass", - AllowUntrustedServerCertificate = allowUntrusted, - ServerDnsIdentity = dnsIdentity, - }; - - [Fact] - public void ClientCredentialsHelper_Disabled_LeavesValidationModeAtDefault() - { - Binding binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5)); - ChannelFactory factory = new(binding, new EndpointAddress("net.tcp://10.0.0.1:32568/Hist")); - try - { - HistorianWcfClientCredentialsHelper.Configure(factory, BaseOptions(allowUntrusted: false)); - - X509ServiceCertificateAuthentication auth = factory.Credentials.ServiceCertificate.SslCertificateAuthentication - ?? factory.Credentials.ServiceCertificate.Authentication; - // Default validation mode is ChainTrust — explicitly NOT None / Custom. - Assert.NotEqual(X509CertificateValidationMode.None, auth.CertificateValidationMode); - Assert.Null(auth.CustomCertificateValidator); - } - finally - { - factory.Abort(); - } - } - - [Fact] - public void ClientCredentialsHelper_Enabled_InstallsAcceptAnyValidator() - { - Binding binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5)); - ChannelFactory factory = new(binding, new EndpointAddress("net.tcp://10.0.0.1:32568/Hist")); - try - { - HistorianWcfClientCredentialsHelper.Configure(factory, BaseOptions(allowUntrusted: true)); - - X509ServiceCertificateAuthentication auth = factory.Credentials.ServiceCertificate.SslCertificateAuthentication; - Assert.NotNull(auth); - Assert.Equal(X509CertificateValidationMode.Custom, auth.CertificateValidationMode); - Assert.Equal(X509RevocationMode.NoCheck, auth.RevocationMode); - Assert.NotNull(auth.CustomCertificateValidator); - Assert.IsAssignableFrom(auth.CustomCertificateValidator); - } - finally - { - factory.Abort(); - } - } - - [Fact] - public void CreateEndpointAddress_WithoutDnsIdentity_HasNullIdentity() - { - EndpointAddress address = HistorianWcfBindingFactory.CreateEndpointAddress("10.0.0.1", 32568, "Hist"); - Assert.Null(address.Identity); - } - - [Fact] - public void CreateEndpointAddress_WithDnsIdentity_AttachesDnsEndpointIdentity() - { - EndpointAddress address = HistorianWcfBindingFactory.CreateEndpointAddress("10.0.0.1", 32568, "HistCert", "localhost"); - Assert.NotNull(address.Identity); - DnsEndpointIdentity dns = Assert.IsType(address.Identity); - Assert.Equal("localhost", dns.IdentityClaim.Resource); - } - - [Fact] - public void CreateBindingPair_RemoteTcpCertificate_PropagatesServerDnsIdentity() - { - HistorianClientOptions options = BaseOptions(dnsIdentity: "localhost"); - var (_, historyEndpoint, _, retrievalEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(options); - - DnsEndpointIdentity historyIdentity = Assert.IsType(historyEndpoint.Identity); - Assert.Equal("localhost", historyIdentity.IdentityClaim.Resource); - // The Retrieval endpoint uses plain MdasNetTcp without TLS — no DNS identity needed. - Assert.Null(retrievalEndpoint.Identity); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianWcfRevisionProbeTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianWcfRevisionProbeTests.cs deleted file mode 100644 index 2b6a3c0..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/HistorianWcfRevisionProbeTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Runtime.Versioning; -using ZB.MOM.WW.SPHistorianClient; -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using Xunit.Abstractions; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -/// -/// Probes the SDK-direct WCF revision-write path (D2 new path). Calls -/// AddNonStreamValuesBegin through -/// against the live local Historian and surfaces what the server returns. The -/// underlying native wrapper is gated client-side by err 129 TagNotFoundInCache; -/// this test bypasses the wrapper entirely and asks the SERVER directly. Gated on -/// HISTORIAN_HOST=localhost; skips otherwise. -/// -[SupportedOSPlatform("windows")] -public sealed class HistorianWcfRevisionProbeTests -{ - private readonly ITestOutputHelper _output; - - public HistorianWcfRevisionProbeTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public async Task AddNonStreamValuesBegin_ProbeReturnsServerResult() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClientOptions options = new() - { - Host = host, - IntegratedSecurity = true, - Transport = HistorianTransport.LocalPipe, - }; - - HistorianWcfRevisionOrchestrator orchestrator = new(options); - HistorianRevisionProbeResult result = await orchestrator.ProbeBeginAsync(CancellationToken.None); - - _output.WriteLine($"OpenSucceeded: {result.OpenSucceeded}"); - _output.WriteLine($"ClientHandle: {result.ClientHandle}"); - _output.WriteLine($"StorageSessionId: {result.StorageSessionId}"); - _output.WriteLine($"TrxInterfaceVersion: {result.TrxInterfaceVersion} (rc={result.TrxInterfaceVersionReturnCode}) ex={result.TrxInterfaceVersionException}"); - _output.WriteLine($"RTag2Succeeded: {result.RTag2Succeeded} OutHex={result.RTag2OutHex} ErrHex={result.RTag2ErrorHex} Ex={result.RTag2Exception}"); - _output.WriteLine($"BeginSucceeded: {result.BeginSucceeded}"); - _output.WriteLine($"BeginTransactionId: {result.BeginTransactionId}"); - foreach (HistorianRevisionBeginAttempt attempt in result.BeginAttempts) - { - _output.WriteLine($" attempt[{attempt.HandleLabel}] handle={attempt.HandleSent} ok={attempt.Succeeded} tx={attempt.TransactionId} err={attempt.ErrorHex} ex={attempt.Exception}"); - } - - Assert.True(result.OpenSucceeded, "Auth chain failed; revision probe never reached the Trx endpoint."); - // Don't assert BeginSucceeded — we're surfacing whatever the server says, not requiring success. - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ProtocolGuardrailTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ProtocolGuardrailTests.cs deleted file mode 100644 index 9abb9bf..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ProtocolGuardrailTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class ProtocolGuardrailTests -{ - [Fact] - public async Task ReadAtTime_RequiresAuthCredentials() - { - HistorianClient client = new(new HistorianClientOptions { Host = "localhost", IntegratedSecurity = false }); - - ProtocolEvidenceMissingException ex = await Assert.ThrowsAsync(() => - client.ReadAtTimeAsync("SysTimeSec", [DateTime.UtcNow], CancellationToken.None)); - - Assert.Contains("IntegratedSecurity", ex.Operation); - } - -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/RemoteTcpIntegrationTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/RemoteTcpIntegrationTests.cs deleted file mode 100644 index 48a4cf1..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/RemoteTcpIntegrationTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System.Runtime.Versioning; -using ZB.MOM.WW.SPHistorianClient.Models; -using Xunit.Abstractions; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -/// -/// Live verification of the RemoteTcpIntegrated and RemoteTcpCertificate transports -/// per docs/plans/tcp-connection-validation.md. Gated by env vars: -/// -/// HISTORIAN_REMOTE_TCP_HOST — hostname or IP of a reachable remote Historian. -/// HISTORIAN_REMOTE_TCP_TAG — tag with non-zero history rows. -/// HISTORIAN_REMOTE_TCP_SPN — optional Kerberos SPN override (default per HistorianClientOptions.TargetSpn). -/// HISTORIAN_REMOTE_TCPCERT_HOST + HISTORIAN_REMOTE_TCPCERT_DNS — for the certificate transport variant. -/// -/// All tests skip cleanly if the gating env var isn't set. -/// -[SupportedOSPlatform("windows")] -public sealed class RemoteTcpIntegrationTests -{ - private readonly ITestOutputHelper _output; - - public RemoteTcpIntegrationTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public async Task ProbeAsync_RemoteTcpIntegrated_ReturnsTrue() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - bool reachable = await client.ProbeAsync(CancellationToken.None); - Assert.True(reachable, "ProbeAsync against remote-TCP host returned false"); - } - - [Fact] - public async Task ReadRawAsync_RemoteTcpIntegrated_ReturnsAtLeastOneRow() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromDays(7); - - List samples = []; - await foreach (HistorianSample sample in client.ReadRawAsync(testTag, startUtc, endUtc, maxValues: 8, CancellationToken.None)) - { - samples.Add(sample); - } - - _output.WriteLine($"Returned {samples.Count} samples for {testTag}"); - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - } - - [Fact] - public async Task GetTagMetadataAsync_RemoteTcpIntegrated_PopulatesFields() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - HistorianTagMetadata? metadata = await client.GetTagMetadataAsync(testTag, CancellationToken.None); - Assert.NotNull(metadata); - Assert.Equal(testTag, metadata.Name); - } - - [Fact] - public async Task GetSystemParameterAsync_RemoteTcpIntegrated_ReturnsHistorianVersion() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - string? value = await client.GetSystemParameterAsync("HistorianVersion", CancellationToken.None); - _output.WriteLine($"HistorianVersion: {value}"); - Assert.False(string.IsNullOrWhiteSpace(value)); - } - - [Fact] - public async Task ReadAggregateAsync_RemoteTcpIntegrated_ReturnsTimeWeightedRows() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromMinutes(10); - - List samples = []; - await foreach (HistorianAggregateSample sample in client.ReadAggregateAsync( - testTag, startUtc, endUtc, RetrievalMode.TimeWeightedAverage, TimeSpan.FromMinutes(1), CancellationToken.None)) - { - samples.Add(sample); - } - - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - } - - [Fact] - public async Task ReadAtTimeAsync_RemoteTcpIntegrated_ReturnsTimestamps() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - DateTime now = DateTime.UtcNow; - DateTime[] timestamps = [now - TimeSpan.FromMinutes(5), now - TimeSpan.FromMinutes(2), now - TimeSpan.FromMinutes(1)]; - IReadOnlyList samples = await client.ReadAtTimeAsync(testTag, timestamps, CancellationToken.None); - Assert.NotEmpty(samples); - Assert.All(samples, s => Assert.Equal(testTag, s.TagName)); - } - - [Fact] - public async Task BrowseTagNamesAsync_RemoteTcpIntegrated_FindsTestTag() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - string? testTag = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_TAG"); - if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(testTag) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - List names = []; - await foreach (string name in client.BrowseTagNamesAsync(testTag, CancellationToken.None)) - { - names.Add(name); - } - Assert.Contains(testTag, names); - } - - [Fact] - public async Task ReadEventsAsync_RemoteTcpIntegrated_DoesNotThrow() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - DateTime endUtc = DateTime.UtcNow; - DateTime startUtc = endUtc - TimeSpan.FromDays(1); - - // Empty result is acceptable — we're just verifying the chain doesn't throw over TCP. - List events = []; - await foreach (HistorianEvent evt in client.ReadEventsAsync(startUtc, endUtc, CancellationToken.None)) - { - events.Add(evt); - } - Assert.NotNull(events); - } - - [Fact] - public async Task GetConnectionStatusAsync_RemoteTcpIntegrated_ReportsConnectedToServer() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(BuildIntegratedOptions(host)); - HistorianConnectionStatus status = await client.GetConnectionStatusAsync(CancellationToken.None); - Assert.True(status.ConnectedToServer); - Assert.False(status.ErrorOccurred); - Assert.Equal(host, status.ServerName); - } - - [Fact] - public async Task ProbeAsync_RemoteTcpCertificate_ReturnsTrue() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCPCERT_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClient client = new(new HistorianClientOptions - { - Host = host, - Port = HistorianClientOptions.DefaultPort, - IntegratedSecurity = false, - Transport = HistorianTransport.RemoteTcpCertificate, - }); - - bool reachable = await client.ProbeAsync(CancellationToken.None); - Assert.True(reachable, "ProbeAsync over RemoteTcpCertificate returned false"); - } - - private static HistorianClientOptions BuildIntegratedOptions(string host) - { - string? spn = Environment.GetEnvironmentVariable("HISTORIAN_REMOTE_TCP_SPN"); - return new HistorianClientOptions - { - Host = host, - Port = HistorianClientOptions.DefaultPort, - IntegratedSecurity = true, - Transport = HistorianTransport.RemoteTcpIntegrated, - // SPN default in HistorianClientOptions is "NT SERVICE\aahClientAccessPoint" which is the - // LocalPipe service identity; for remote TCP, override via env var if needed. - TargetSpn = string.IsNullOrWhiteSpace(spn) ? "NT SERVICE\\aahClientAccessPoint" : spn, - }; - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/TagMetadataDescriptorProbeTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/TagMetadataDescriptorProbeTests.cs deleted file mode 100644 index a436d73..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/TagMetadataDescriptorProbeTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Runtime.Versioning; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using Xunit.Abstractions; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -[SupportedOSPlatform("windows")] -public sealed class TagMetadataDescriptorProbeTests -{ - private readonly ITestOutputHelper _output; - - public TagMetadataDescriptorProbeTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void ProbeDescriptorsForKnownSampleTags() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - string[] sampleTags = (Environment.GetEnvironmentVariable("HISTORIAN_DESCRIPTOR_PROBE_TAGS") - ?? string.Empty) - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (sampleTags.Length == 0) - { - return; - } - - HistorianClientOptions options = new() - { - Host = host, - IntegratedSecurity = true, - }; - - foreach (string tagName in sampleTags) - { - try - { - HistorianTagInfoResponse parsed = HistorianWcfTagClient.GetTagInfoForDescriptorProbe(options, tagName); - _output.WriteLine($" {tagName,-50} descriptor=0x{Convert.ToHexString(parsed.NativeDataTypeDescriptor)}"); - } - catch (Exception ex) - { - _output.WriteLine($" {tagName,-50} ERROR: {ex.GetType().Name}: {ex.Message}"); - } - } - } - - [Fact] - public void DumpRawTagInfoBytesForLayoutDecoding() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - string[] sampleTags = (Environment.GetEnvironmentVariable("HISTORIAN_RAW_TAGINFO_TAGS") ?? string.Empty) - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (string.IsNullOrWhiteSpace(host) || sampleTags.Length == 0 || !OperatingSystem.IsWindows()) - { - return; - } - - HistorianClientOptions options = new() { Host = host, IntegratedSecurity = true }; - var results = HistorianWcfTagClient.GetTagInfoRawBytesForProbe(options, sampleTags); - foreach (var (tag, bytes) in results) - { - if (bytes is null) { _output.WriteLine($" {tag}: "); continue; } - _output.WriteLine($" {tag} ({bytes.Length} bytes): {Convert.ToHexString(bytes)}"); - } - } - - [Fact] - public void EnumerateAllTagDescriptorsAcrossOneSession() - { - string? host = Environment.GetEnvironmentVariable("HISTORIAN_HOST"); - if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows()) - { - return; - } - - string[] sampleTags = (Environment.GetEnvironmentVariable("HISTORIAN_DESCRIPTOR_PROBE_TAGS") ?? string.Empty) - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (sampleTags.Length == 0) - { - return; - } - - HistorianClientOptions options = new() - { - Host = host, - IntegratedSecurity = true, - }; - - IReadOnlyDictionary results = - HistorianWcfTagClient.GetTagInfosForDescriptorProbe(options, sampleTags); - - // Group by descriptor (hex string) and report counts only — no tag names in output to - // avoid leaking customer-tag identifiers. - var grouped = results - .Where(static kv => kv.Value is not null) - .GroupBy(static kv => Convert.ToHexString(kv.Value!.NativeDataTypeDescriptor)) - .OrderBy(static g => g.Key); - _output.WriteLine($"Probed {results.Count} tags ({results.Count(static kv => kv.Value is null)} errors)."); - foreach (var grp in grouped) - { - _output.WriteLine($" 0x{grp.Key} count={grp.Count()}"); - } - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfAuthenticationProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfAuthenticationProtocolTests.cs deleted file mode 100644 index b18e338..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfAuthenticationProtocolTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfAuthenticationProtocolTests -{ - [Fact] - public void WrapValidateClientCredentialToken_UsesNativeRoundAndLengthEnvelope() - { - byte[] actual = HistorianWcfAuthenticationProtocol.WrapValidateClientCredentialToken( - isFirstRound: true, - [0x4E, 0x54, 0x4C, 0x4D]); - - Assert.Equal([0x01, 0x04, 0x00, 0x00, 0x00, 0x4E, 0x54, 0x4C, 0x4D], actual); - } - - [Fact] - public void TryReadWrappedValidateClientCredentialToken_ReadsNativeEnvelope() - { - ValidateClientCredentialToken? actual = - HistorianWcfAuthenticationProtocol.TryReadWrappedValidateClientCredentialToken( - [0x00, 0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC]); - - Assert.NotNull(actual); - Assert.False(actual.IsFirstRound); - Assert.Equal([0xAA, 0xBB, 0xCC], actual.Token); - } - - [Fact] - public void TryReadWrappedValidateClientCredentialToken_RejectsLengthMismatch() - { - Assert.Null(HistorianWcfAuthenticationProtocol.TryReadWrappedValidateClientCredentialToken( - [0x01, 0x04, 0x00, 0x00, 0x00, 0xAA])); - } - - [Fact] - public void TryReadValidateClientCredentialResponse_ReadsContinueFlagAndServerToken() - { - ValidateClientCredentialResponse? actual = - HistorianWcfAuthenticationProtocol.TryReadValidateClientCredentialResponse( - [0x01, 0x11, 0x22, 0x33]); - - Assert.NotNull(actual); - Assert.True(actual.Continue); - Assert.Equal([0x11, 0x22, 0x33], actual.Token); - } - - [Fact] - public void TryReadValidateClientCredentialResponse_ReadsTerminalOneByteResponse() - { - ValidateClientCredentialResponse? actual = - HistorianWcfAuthenticationProtocol.TryReadValidateClientCredentialResponse([0x00]); - - Assert.NotNull(actual); - Assert.False(actual.Continue); - Assert.Empty(actual.Token); - } - - [Fact] - public void TryReadValidateClientCredentialResponse_RejectsEmptyResponse() - { - Assert.Null(HistorianWcfAuthenticationProtocol.TryReadValidateClientCredentialResponse([])); - } - - [Fact] - public void TryApplyNativeNtlmNegotiateVersionFlag_MatchesObservedNativeFirstTokenFlag() - { - byte[] token = - [ - 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, - 0x01, 0x00, 0x00, 0x00, - 0xB7, 0xB2, 0x08, 0xE2 - ]; - - bool changed = HistorianWcfAuthenticationProtocol.TryApplyNativeNtlmNegotiateVersionFlag(token); - - Assert.True(changed); - Assert.Equal( - [ - 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, - 0x01, 0x00, 0x00, 0x00, - 0xB7, 0xB2, 0x18, 0xE2 - ], - token); - } - - [Fact] - public void TryApplyNativeNtlmNegotiateVersionFlag_IgnoresNonNtlmNegotiateTokens() - { - byte[] token = [0x4B, 0x52, 0x42, 0x35]; - - bool changed = HistorianWcfAuthenticationProtocol.TryApplyNativeNtlmNegotiateVersionFlag(token); - - Assert.False(changed); - Assert.Equal([0x4B, 0x52, 0x42, 0x35], token); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfBindingFactoryTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfBindingFactoryTests.cs deleted file mode 100644 index d41f7fb..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfBindingFactoryTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Runtime.Versioning; -using System.ServiceModel.Channels; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -[SupportedOSPlatform("windows")] -public sealed class WcfBindingFactoryTests -{ - [Fact] - public void CreateMdasNetNamedPipeBinding_WrapsTheInnerEncoderInMdas() - { - if (!OperatingSystem.IsWindows()) - { - return; - } - - Binding binding = HistorianWcfBindingFactory.CreateMdasNetNamedPipeBinding(TimeSpan.FromSeconds(5)); - - BindingElementCollection elements = binding.CreateBindingElements(); - Assert.Contains(elements, e => e is MdasMessageEncodingBindingElement); - } - - [Fact] - public void CreateMdasNetNamedPipeBinding_AppliesProvidedTimeout() - { - if (!OperatingSystem.IsWindows()) - { - return; - } - - TimeSpan timeout = TimeSpan.FromSeconds(7); - - Binding binding = HistorianWcfBindingFactory.CreateMdasNetNamedPipeBinding(timeout); - - Assert.Equal(timeout, binding.OpenTimeout); - Assert.Equal(timeout, binding.CloseTimeout); - Assert.Equal(timeout, binding.SendTimeout); - Assert.Equal(timeout, binding.ReceiveTimeout); - } - - [Fact] - public void CreatePipeEndpointAddress_BuildsNetPipeUri() - { - var address = HistorianWcfBindingFactory.CreatePipeEndpointAddress("localhost", "Hist"); - - Assert.Equal(new Uri("net.pipe://localhost/Hist"), address.Uri); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfDataQueryProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfDataQueryProtocolTests.cs deleted file mode 100644 index 4df32b3..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfDataQueryProtocolTests.cs +++ /dev/null @@ -1,156 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfDataQueryProtocolTests -{ - [Fact] - public void SerializerMatchesInstrumentedNativeFullHistoryRequest() - { - byte[] actual = HistorianDataQueryProtocol.SerializeFullHistoryRequest(new HistorianDataQueryRequest( - ["OtOpcUaParityTest_001.Counter"], - new DateTime(2026, 5, 1, 14, 17, 5, 659, DateTimeKind.Utc).AddTicks(3154), - new DateTime(2026, 5, 2, 14, 17, 5, 659, DateTimeKind.Utc).AddTicks(3154), - MaxStates: 100, - BatchSize: 1, - Option: string.Empty)); - - byte[] expected = Convert.FromBase64String( - "CQACAAAAAAAAAAAAAAAC4ScwddncAQKhkVo+2twBAAAAAAAAAAAAAAAAAAAAAAMAAABVAFQAQwABAAAAAAABAP8BAAAAAAgAAABOAG8ARgBpAGwAdABlAHIAAQADAAEA/4IHAIKBAAABAAAAHQAAAE8AdABPAHAAYwBVAGEAUABhAHIAaQB0AHkAVABlAHMAdABfADAAMAAxAC4AQwBvAHUAbgB0AGUAcgBkAAEBAAABAAABAAAJAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - - Assert.Equal(expected, actual); - } - - [Fact] - public void SerializerMatchesInstrumentedNativeTimeWeightedAverageRequest() - { - byte[] actual = HistorianDataQueryProtocol.SerializeFullHistoryRequest(new HistorianDataQueryRequest( - ["OtOpcUaParityTest_001.Counter"], - new DateTime(2026, 5, 1, 14, 29, 2, 223, DateTimeKind.Utc).AddTicks(2955), - new DateTime(2026, 5, 2, 14, 29, 2, 223, DateTimeKind.Utc).AddTicks(2955), - MaxStates: 100, - BatchSize: 3, - Option: string.Empty) - { - QueryType = 5, - Resolution = TimeSpan.FromMinutes(1) - }); - - byte[] expected = Convert.FromBase64String( - "CQAFAAAAAAAAAAAAAAB73ULbdtncAXudrAVA2twBAAAAAKPhwUEAAAAAAAAAAAMAAABVAFQAQwABAAAAAAABAP8BAAAAAAgAAABOAG8ARgBpAGwAdABlAHIAAQADAAEA/4IHAIKBAAABAAAAHQAAAE8AdABPAHAAYwBVAGEAUABhAHIAaQB0AHkAVABlAHMAdABfADAAMAAxAC4AQwBvAHUAbgB0AGUAcgBkAAEBAAABAAABAAAJAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAABg3vt0BQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - - Assert.Equal(expected, actual); - } - - [Fact] - public void SerializerMatchesInstrumentedNativeInterpolatedRequest() - { - byte[] actual = HistorianDataQueryProtocol.SerializeFullHistoryRequest(new HistorianDataQueryRequest( - ["OtOpcUaParityTest_001.Counter"], - new DateTime(2026, 5, 1, 14, 32, 12, 72, DateTimeKind.Utc).AddTicks(8924), - new DateTime(2026, 5, 2, 14, 32, 12, 72, DateTimeKind.Utc).AddTicks(8924), - MaxStates: 100, - BatchSize: 3, - Option: string.Empty) - { - QueryType = 3, - Resolution = TimeSpan.FromMinutes(1) - }); - - byte[] expected = Convert.FromBase64String( - "CQADAAAAAAAAAAAAAABcnWtMd9ncAVxd1XZA2twBAAAAAKPhwUEAAAAAAAAAAAMAAABVAFQAQwABAAAAAAABAP8BAAAAAAgAAABOAG8ARgBpAGwAdABlAHIAAQADAAEA/4IHAIKBAAABAAAAHQAAAE8AdABPAHAAYwBVAGEAUABhAHIAaQB0AHkAVABlAHMAdABfADAAMAAxAC4AQwBvAHUAbgB0AGUAcgBkAAEBAAABAAABAAAJAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAABg3vt0BQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - - Assert.Equal(expected, actual); - } - - [Fact] - public void SerializerUsesDecompiledEmptyMetadataAndAutoSummaryLayout() - { - byte[] actual = HistorianDataQueryProtocol.SerializeFullHistoryRequest(new HistorianDataQueryRequest( - ["T"], - new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), - new DateTime(2026, 1, 1, 0, 1, 0, DateTimeKind.Utc), - MaxStates: 100, - BatchSize: 1, - Option: string.Empty)); - - byte[] expectedMiddle = - [ - 0x64, 0x00, - 0x01, - 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, - 0x09, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00 - ]; - - AssertContains(expectedMiddle, actual); - AssertEndsWith(ExpectedEmptyEndpointAndAutoSummarySuffix(), actual); - } - - [Fact] - public void SerializerWritesPackedCqtiFlagsSeparatelyFromColumnSelectorFlags() - { - byte[] actual = HistorianDataQueryProtocol.SerializeFullHistoryRequest(new HistorianDataQueryRequest( - ["T"], - new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), - new DateTime(2026, 1, 1, 0, 1, 0, DateTimeKind.Utc), - MaxStates: 100, - BatchSize: 1, - Option: "NoOption") - { - InterpolationType = 255, - TimestampRule = 1, - QualityRule = 0, - ColumnSelectorFlags = 0x0000_0000_0003_FFFF - }); - - int resultBufferOffset = 2 + 4 + 4 + 4 + 8 + 8 + 8 + 4 + 4 + 10 + 4; - Assert.Equal([0x00, 0x00, 0x01, 0x00, 0xFF, 0x01], actual[resultBufferOffset..(resultBufferOffset + 6)]); - AssertContains([0x01, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], actual); - } - - private static byte[] ExpectedEmptyEndpointAndAutoSummarySuffix() - { - List expected = []; - AppendEmptyEndpoint(expected); - AppendEmptyEndpoint(expected); - expected.AddRange(new byte[8]); - expected.AddRange([0x00, 0x00, 0x00, 0x00]); - expected.AddRange([0x00, 0x00, 0x00, 0x00]); - expected.AddRange([0x01, 0x00]); - expected.AddRange(new byte[16]); - expected.AddRange(new byte[5]); - expected.AddRange([0x00, 0x00, 0x00, 0x00]); - return expected.ToArray(); - } - - private static void AppendEmptyEndpoint(List bytes) - { - bytes.AddRange([0x01, 0x00]); - bytes.AddRange([0x00, 0x00, 0x00, 0x00]); - bytes.AddRange([0x00, 0x00]); - } - - private static void AssertContains(byte[] expected, byte[] actual) - { - for (int index = 0; index <= actual.Length - expected.Length; index++) - { - if (actual.AsSpan(index, expected.Length).SequenceEqual(expected)) - { - return; - } - } - - Assert.Fail($"Expected byte sequence {Convert.ToHexString(expected)} was not found."); - } - - private static void AssertEndsWith(byte[] expectedSuffix, byte[] actual) - { - Assert.True(actual.Length >= expectedSuffix.Length); - Assert.Equal(expectedSuffix, actual[^expectedSuffix.Length..]); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfDataQueryResultBufferTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfDataQueryResultBufferTests.cs deleted file mode 100644 index 8a969f2..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfDataQueryResultBufferTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Models; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfDataQueryResultBufferTests -{ - // Captured from artifacts/reverse-engineering/instrumented-openconnection3-correlation/capture.ndjson - // Wcf.GetNextQueryResultBuffer2.ResultBytes for a 4-row OtOpcUaParityTest_001.Counter Full read. - private static readonly byte[] CapturedResultBytes = Convert.FromBase64String( - "CQAEAAAA7gAAAB0AAABPAHQATwBwAGMAVQBhAFAAYQByAGkAdAB5AFQAZQBzAHQAXwAwADAAMQAu" + - "AEMAbwB1AG4AdABlAHIAAQAAAGvPzFvD2dwBhQAAAPgAAADAAAAAAAAAAAAAAAAAAAAAAABZQAAA" + - "AWvPzFvD2dwBAAAAAAAAAAClBtClfAAAAAAAAAAAAAAA7gAAAB0AAABPAHQATwBwAGMAVQBhAFAA" + - "YQByAGkAdAB5AFQAZQBzAHQAXwAwADAAMQAuAEMAbwB1AG4AdABlAHIAAQAAABDWnAFA2twBAQAA" + - "ABgAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAARDWnAFA2twBAAAAAAAAAAAwZOgAAAAAAAEAAAAA" + - "AAAA7gAAAB0AAABPAHQATwBwAGMAVQBhAFAAYQByAGkAdAB5AFQAZQBzAHQAXwAwADAAMQAuAEMA" + - "bwB1AG4AdABlAHIAAQAAAEA6hQJA2twBAQAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUA6" + - "hQJA2twBAAAAAAAAAABQwwAAAAAAAAEAAAAAAAAA7gAAAB0AAABPAHQATwBwAGMAVQBhAFAAYQBy" + - "AGkAdAB5AFQAZQBzAHQAXwAwADAAMQAuAEMAbwB1AG4AdABlAHIAAQAAAJD9hQJA2twBAAAAAPgA" + - "AADAAAAAAAAAAAAAAAAAAAAAAABZQAAAAZD9hQJA2twBAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAA"); - - private static readonly byte[] TerminalNoMoreData = Convert.FromBase64String("BB4AAAA="); - - [Fact] - public void TryParseGetNextQueryResultBufferRows_ParsesFourCanonicalFixtureRows() - { - bool ok = HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows( - CapturedResultBytes, - TerminalNoMoreData, - out IReadOnlyList rows, - out bool hasMoreData); - - Assert.True(ok); - Assert.False(hasMoreData); - Assert.Equal(4, rows.Count); - - Assert.All(rows, r => Assert.Equal("OtOpcUaParityTest_001.Counter", r.TagName)); - - HistorianSample row0 = rows[0]; - Assert.Equal(133, row0.Quality); - Assert.Equal(248u, row0.QualityDetail); - Assert.Equal(192, row0.OpcQuality); - Assert.Equal(0, row0.NumericValue); - Assert.Equal(100.0, row0.PercentGood); - Assert.Equal(DateTime.FromFileTimeUtc(0x01DCD9C35BCCCF6B), row0.TimestampUtc); - - HistorianSample row3 = rows[3]; - Assert.Equal(0, row3.Quality); - Assert.Equal(248u, row3.QualityDetail); - Assert.Equal(192, row3.OpcQuality); - Assert.Equal(100.0, row3.PercentGood); - } - - [Fact] - public void TryParseGetNextQueryResultBufferRows_FlagsContinuationWhenErrorTerminalIsEmpty() - { - bool ok = HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows( - CapturedResultBytes, - errorTerminal: [], - out _, - out bool hasMoreData); - - Assert.True(ok); - Assert.True(hasMoreData); - } - - [Fact] - public void TryParseGetNextQueryResultBufferRows_FlagsContinuationWhenErrorIsNotNoMoreData() - { - bool ok = HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows( - CapturedResultBytes, - errorTerminal: [0x04, 0x01, 0x00, 0x00, 0x00], - out _, - out bool hasMoreData); - - Assert.True(ok); - Assert.True(hasMoreData); - } - - [Fact] - public void TryParseGetNextQueryResultBufferRows_RejectsBufferWithUnsupportedVersion() - { - byte[] mangled = (byte[])CapturedResultBytes.Clone(); - mangled[0] = 0x07; - - bool ok = HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows( - mangled, - TerminalNoMoreData, - out IReadOnlyList rows, - out _); - - Assert.False(ok); - Assert.Empty(rows); - } - - [Fact] - public void TryParseGetNextQueryResultBufferRows_HandlesEmptyResultBuffer() - { - bool ok = HistorianDataQueryProtocol.TryParseGetNextQueryResultBufferRows( - result: [], - TerminalNoMoreData, - out IReadOnlyList rows, - out bool hasMoreData); - - Assert.True(ok); - Assert.False(hasMoreData); - Assert.Empty(rows); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfEventQueryProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfEventQueryProtocolTests.cs deleted file mode 100644 index ee1c1b4..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfEventQueryProtocolTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfEventQueryProtocolTests -{ - [Fact] - public void SerializerMatchesInstrumentedNativeEventRequest() - { - HistorianEventQueryAttempt attempt = Assert.Single(HistorianEventQueryProtocol.CreateStartEventQueryAttempts( - new DateTime(2026, 4, 25, 14, 39, 36, 800, DateTimeKind.Utc).AddTicks(1646), - new DateTime(2026, 5, 2, 14, 39, 36, 800, DateTimeKind.Utc).AddTicks(1646), - 3)); - - byte[] expected = Convert.FromBase64String( - "BQBuHAVXwdTcAW5c6X9B2twBAwAAAAAAAAAAAAEAAAAAAAAAAAAAAQADAAAAVQBUAEMAAQEAAAEAAAEAAAAAAAA="); - - Assert.Equal(expected, attempt.RequestBuffer); - Assert.Equal("6b955b02087047a3199a8c74f3eee85c3b49aaa29b05de12eff2dd536f2da0d5", attempt.RequestSha256); - } - - [Fact] - public void NativeEmptyFilterAttemptMatchesDecompiledSaveOrder() - { - HistorianEventQueryAttempt attempt = Assert.Single(HistorianEventQueryProtocol.CreateStartEventQueryAttempts( - new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), - new DateTime(2026, 1, 1, 0, 1, 0, DateTimeKind.Utc), - 3)); - - byte[] actual = attempt.RequestBuffer; - - Assert.Equal("native-empty-filter-version5", attempt.Name); - Assert.Equal(3, HistorianEventQueryProtocol.QueryRequestTypeEvent); - Assert.Equal(65, actual.Length); - Assert.Equal([0x05, 0x00], actual[..2]); - Assert.Equal(3u, BitConverter.ToUInt32(actual, 18)); - Assert.Equal(0u, BitConverter.ToUInt32(actual, 22)); - Assert.Equal(0, BitConverter.ToUInt16(actual, 26)); - Assert.Equal(1, BitConverter.ToUInt16(actual, 28)); - Assert.Equal([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], actual[30..37]); - Assert.Equal(65_536u, BitConverter.ToUInt32(actual, 37)); - Assert.Equal([0x03, 0x00, 0x00, 0x00, 0x55, 0x00, 0x54, 0x00, 0x43, 0x00], actual[41..51]); - Assert.Equal([0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00], actual[51..61]); - Assert.Equal([0x00, 0x00, 0x00, 0x00], actual[^4..]); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfEvidenceTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfEvidenceTests.cs deleted file mode 100644 index dcfb16b..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfEvidenceTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Reflection; -using System.ServiceModel; -using ZB.MOM.WW.SPHistorianClient.Wcf; -using ZB.MOM.WW.SPHistorianClient.Wcf.Contracts; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfEvidenceTests -{ - [Fact] - public void ServiceContractsUseDecompiledNamesAndNamespace() - { - AssertServiceContract("Hist"); - AssertServiceContract("Hist"); - AssertServiceContract("Retr"); - AssertServiceContract("Retr"); - AssertServiceContract("Stat"); - AssertServiceContract("Stat"); - AssertServiceContract("Storage"); - AssertServiceContract("Trx"); - } - - [Fact] - public void RelayEvidenceIdentifiesHistorySecurityEndpointNames() - { - Assert.Equal("HistCert", HistorianWcfServiceNames.HistoryCertificate); - Assert.Equal("Hist-Integrated", HistorianWcfServiceNames.HistoryIntegrated); - } - - [Fact] - public void KnownOperationAliasesMatchManagedWrapperEvidence() - { - AssertOperation(nameof(IHistoryServiceContract.GetInterfaceVersion), "GetV"); - AssertOperation(nameof(IHistoryServiceContract.OpenConnection), "Open"); - AssertOperation(nameof(IHistoryServiceContract.ValidateClient), "VldC"); - AssertOperation(nameof(IHistoryServiceContract.UpdateClientStatus), "UpdC"); - AssertOperation(nameof(IHistoryServiceContract2.OpenConnection2), "Open2"); - AssertOperation(nameof(IHistoryServiceContract2.ExchangeKey), "ExKey"); - AssertOperation(nameof(IRetrievalServiceContract2.GetTagInfosFromId), "GetTg"); - AssertOperation(nameof(IRetrievalServiceContract3.StartTagQuery), "QTB"); - AssertOperation(nameof(IRetrievalServiceContract4.GetTagExtendedPropertiesFromName), "GetTepByNm"); - AssertOperation(nameof(IStorageServiceContract.OpenStorageConnection), "Open"); - AssertOperation(nameof(IStorageServiceContract.LoadBlocks), "LoadB"); - AssertOperation(nameof(ITransactionServiceContract.GetInterfaceVersion), "GetV"); - AssertDefaultOperation(nameof(IRetrievalServiceContract.StartQuery)); - AssertDefaultOperation(nameof(IRetrievalServiceContract4.StartEventQuery)); - AssertDefaultOperation(nameof(IStatusServiceContract.GetServerTime)); - AssertDefaultOperation(nameof(IStatusServiceContract2.GetSystemParameter)); - AssertOperation(nameof(IStatusServiceContract2.GetHistorianInfo), "GETHI"); - AssertOperation(nameof(IStatusServiceContract2.PingServer), "PNGS"); - AssertOperation(nameof(IStatusServiceContract2.PingPipe), "PNGP"); - } - - [Fact] - public void MdasBindingUsesNetTcpAndCustomContentType() - { - var binding = HistorianWcfBindingFactory.CreateMdasNetTcpBinding(TimeSpan.FromSeconds(5)); - var encoder = binding.CreateBindingElements().Find(); - var endpoint = HistorianWcfBindingFactory.CreateEndpointAddress("localhost", HistorianWcfBindingFactory.DefaultPort, HistorianWcfServiceNames.History); - - Assert.NotNull(encoder); - Assert.Equal("net.tcp://localhost:32568/Hist", endpoint.Uri.AbsoluteUri); - Assert.Equal(MdasMessageEncoder.MdasContentType, encoder.CreateMessageEncoderFactory().Encoder.ContentType); - } - - [Fact] - public void CertificateBindingUsesMdasEncodingOverTransportSecurity() - { - var binding = HistorianWcfBindingFactory.CreateMdasNetTcpCertificateBinding(TimeSpan.FromSeconds(5)); - var elements = binding.CreateBindingElements(); - var encoder = elements.Find(); - var security = elements.Find(); - - Assert.NotNull(encoder); - Assert.NotNull(security); - Assert.Equal(MdasMessageEncoder.MdasContentType, encoder.CreateMessageEncoderFactory().Encoder.ContentType); - } - - private static void AssertServiceContract(string name) - { - var attribute = typeof(TContract).GetCustomAttribute(); - - Assert.NotNull(attribute); - Assert.Equal(name, attribute.Name); - Assert.Equal("aa", attribute.Namespace); - } - - private static void AssertOperation(string methodName, string operationName) - { - var method = typeof(TContract).GetMethod(methodName); - var attribute = method?.GetCustomAttribute(); - - Assert.NotNull(attribute); - Assert.Equal(operationName, attribute.Name); - } - - private static void AssertDefaultOperation(string methodName) - { - var method = typeof(TContract).GetMethod(methodName); - var attribute = method?.GetCustomAttribute(); - - Assert.NotNull(attribute); - Assert.Null(attribute.Name); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfOpen2ProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfOpen2ProtocolTests.cs deleted file mode 100644 index c3d6ffd..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfOpen2ProtocolTests.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System.Text; -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfOpen2ProtocolTests -{ - [Fact] - public void LegacyVersion1SerializerMatchesDecompiledSaveOpenConnectionParamsLayout() - { - byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request( - HostName: "H", - ProcessName: "P", - ProcessId: 0x01020304, - UserName: "U", - Password: Encoding.Unicode.GetBytes("pw"), - ClientType: 4, - ClientVersion: 11, - ConnectionMode: 2, - MetadataNamespace: HistorianMetadataNamespace.Empty)); - - byte[] expected = - [ - 0x01, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, - 0x04, 0x03, 0x02, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x55, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x70, 0x00, 0x77, 0x00, - 0x04, - 0x0B, 0x00, - 0x02, 0x00, 0x00, 0x00, - 0x01, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - ]; - - Assert.Equal(expected, actual); - } - - [Fact] - public void LegacyVersion1SerializerUsesUtf16CodeUnitStringLengths() - { - byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request( - HostName: "A\ud83d\ude00", - ProcessName: string.Empty, - ProcessId: 0, - UserName: string.Empty, - Password: [], - ClientType: 4, - ClientVersion: 0, - ConnectionMode: 2, - MetadataNamespace: HistorianMetadataNamespace.Empty)); - - Assert.Equal([0x03, 0x00, 0x00, 0x00], actual[2..6]); - Assert.Equal(Encoding.Unicode.GetBytes("A\ud83d\ude00"), actual[6..12]); - } - - [Fact] - public void NativeErrorParserReadsObservedFiveByteBuffers() - { - HistorianNativeError? error = HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00, 0x00]); - - Assert.NotNull(error); - Assert.Equal(4, error.Type); - Assert.Equal(171, error.Code); - Assert.Equal("AuthenticationFailed", error.Name); - } - - [Fact] - public void NativeErrorParserRejectsShortBuffers() - { - Assert.Null(HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00])); - } - - [Fact] - public void LegacyOpen2OutputParserReadsObservedWcfLayout() - { - byte[] buffer = - [ - 0x78, 0x56, 0x34, 0x12, - 0x33, 0x22, 0x11, 0x00, - 0x55, 0x44, - 0x77, 0x66, - 0x88, 0x99, 0xAA, 0xBB, - 0xCC, 0xDD, 0xEE, 0xFF, - 0x08, 0x07, 0x06, 0x05, - 0x04, 0x03, 0x02, 0x01, - 0x44, 0x33, 0x22, 0x11 - ]; - - HistorianLegacyOpen2Output? output = HistorianOpen2Protocol.TryReadLegacyOpen2Output(buffer); - - Assert.NotNull(output); - Assert.Equal(0x12345678, output.Handle); - Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId); - Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc); - Assert.Equal(0x11223344, output.ServerStatus); - } - - [Fact] - public void LegacyOpen2OutputParserRejectsNonLegacyLength() - { - Assert.Null(HistorianOpen2Protocol.TryReadLegacyOpen2Output([0x00])); - } - - [Fact] - public void NativeOpen3OutputParserReadsObservedDeserializerLayout() - { - byte[] buffer = - [ - 0x03, - 0x78, 0x56, 0x34, 0x12, - 0x33, 0x22, 0x11, 0x00, - 0x55, 0x44, - 0x77, 0x66, - 0x88, 0x99, 0xAA, 0xBB, - 0xCC, 0xDD, 0xEE, 0xFF, - 0x08, 0x07, 0x06, 0x05, - 0x04, 0x03, 0x02, 0x01, - 0x18, 0x17, 0x16, 0x15, - 0x14, 0x13, 0x12, 0x11, - 0x44, 0x33, 0x22, 0x11, - 0x00 - ]; - - HistorianNativeOpen3Output? output = HistorianOpen2Protocol.TryReadNativeOpen3Output(buffer); - - Assert.NotNull(output); - Assert.Equal(3, output.ProtocolVersion); - Assert.Equal(0x12345678, output.Handle); - Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId); - Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc); - Assert.Equal(0x1112131415161718, output.ServerTimeFileTimeUtc); - Assert.Equal([0x44, 0x33, 0x22, 0x11, 0x00], output.TrailingBytes); - } - - [Fact] - public void NativeOpen3OutputParserRejectsUnsupportedVersion() - { - Assert.Null(HistorianOpen2Protocol.TryReadNativeOpen3Output([0x01, 0x00, 0x00, 0x00])); - } - - [Fact] - public void NativeVersion3SerializerMatchesDecompiledFieldOrder() - { - byte[] actual = HistorianOpen2Protocol.SerializeNativeVersion3( - new HistorianOpen2Request( - HostName: "H", - ProcessName: "P", - ProcessId: 0x01020304, - UserName: string.Empty, - Password: [0xAA, 0xBB], - ClientType: 4, - ClientVersion: 11, - ConnectionMode: 1026, - MetadataNamespace: HistorianMetadataNamespace.Empty), - new HistorianClientCommonInfo( - FormatVersion: 3, - ServerNodeName: "S", - ClientNodeName: "C", - ProcessId: 0x11223344, - HcalVersion: 17, - ProcessName: "Proc", - Proxy: string.Empty, - DataSourceId: string.Empty, - ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"), - ClientVersion: 0x55667788, - ClientTimestamp: 0x0102030405060708, - ClientDllVersion: string.Empty)); - - byte[] expectedPrefix = - [ - 0x03, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, - 0x02, 0x00, 0xAA, 0xBB, - 0x04, - 0x02, 0x04, 0x00, 0x00 - ]; - Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]); - - Assert.Contains(0x03, actual); - byte[] expectedSuffix = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; - Assert.Equal(expectedSuffix, actual[^expectedSuffix.Length..]); - } - - [Fact] - public void NativeOpenConnection3Version6SerializerAddsObservedPrefixBeforeContent() - { - HistorianOpen2Request request = new( - HostName: "H", - ProcessName: "P", - ProcessId: 0x01020304, - UserName: string.Empty, - Password: [0xAA, 0xBB], - ClientType: 4, - ClientVersion: 11, - ConnectionMode: 1026, - MetadataNamespace: HistorianMetadataNamespace.Empty); - HistorianClientCommonInfo commonInfo = new( - FormatVersion: 3, - ServerNodeName: "S", - ClientNodeName: "C", - ProcessId: 0x11223344, - HcalVersion: 17, - ProcessName: "Proc", - Proxy: string.Empty, - DataSourceId: string.Empty, - ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"), - ClientVersion: 0x55667788, - ClientTimestamp: 0x0102030405060708, - ClientDllVersion: string.Empty); - - byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6( - request, - commonInfo, - new Guid("00112233-4455-6677-8899-aabbccddeeff")); - - byte[] expectedPrefix = - [ - 0x06, - 0x33, 0x22, 0x11, 0x00, - 0x55, 0x44, - 0x77, 0x66, - 0x88, 0x99, 0xAA, 0xBB, - 0xCC, 0xDD, 0xEE, 0xFF, - 0x00 - ]; - Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]); - byte[] expectedContentPrefix = - [ - 0x01, 0x00, 0x00, 0x00, 0x48, 0x00, - 0x02, 0x00, 0xAA, 0xBB, - 0x04, - 0x02, 0x04, 0x00, 0x00, - 0x01, - 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00 - ]; - Assert.Equal(expectedContentPrefix, actual[expectedPrefix.Length..(expectedPrefix.Length + expectedContentPrefix.Length)]); - } - - [Fact] - public void NativeOpenConnection3Version6SerializerCanUseSeparateCredentialBlock() - { - HistorianOpen2Request request = new( - HostName: "H", - ProcessName: "P", - ProcessId: 0x01020304, - UserName: string.Empty, - Password: [0xAA, 0xBB], - ClientType: 4, - ClientVersion: 11, - ConnectionMode: 1026, - MetadataNamespace: HistorianMetadataNamespace.Empty); - HistorianClientCommonInfo commonInfo = new( - FormatVersion: 2, - ServerNodeName: string.Empty, - ClientNodeName: string.Empty, - ProcessId: 0, - HcalVersion: 17, - ProcessName: string.Empty, - Proxy: string.Empty, - DataSourceId: string.Empty, - ShardId: Guid.Empty, - ClientVersion: 0, - ClientTimestamp: 0, - ClientDllVersion: string.Empty); - - byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6( - request, - commonInfo, - Guid.Empty, - [0x00, 0x00, 0x00, 0x00]); - - int hostLengthOffset = 18; - int credentialLengthOffset = hostLengthOffset + 4 + Encoding.Unicode.GetByteCount("H"); - Assert.Equal([0x04, 0x00], actual[credentialLengthOffset..(credentialLengthOffset + 2)]); - Assert.Equal([0x00, 0x00, 0x00, 0x00], actual[(credentialLengthOffset + 2)..(credentialLengthOffset + 6)]); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfStatusProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfStatusProtocolTests.cs deleted file mode 100644 index bcc509d..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfStatusProtocolTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfStatusProtocolTests -{ - [Fact] - public void SystemTimeParserReadsWindowsSystemTimeLayout() - { - byte[] buffer = - [ - 0xEA, 0x07, - 0x04, 0x00, - 0x04, 0x00, - 0x1E, 0x00, - 0x0D, 0x00, - 0x2A, 0x00, - 0x07, 0x00, - 0x7B, 0x00 - ]; - - DateTime? parsed = HistorianStatusProtocol.TryReadSystemTime(buffer); - - Assert.Equal(new DateTime(2026, 4, 30, 13, 42, 7, 123), parsed); - } - - [Fact] - public void SystemTimeParserRejectsShortAndInvalidBuffers() - { - Assert.Null(HistorianStatusProtocol.TryReadSystemTime([0xEA, 0x07])); - - byte[] invalidMonth = - [ - 0xEA, 0x07, - 0x00, 0x00, - 0x04, 0x00, - 0x1E, 0x00, - 0x0D, 0x00, - 0x2A, 0x00, - 0x07, 0x00, - 0x7B, 0x00 - ]; - Assert.Null(HistorianStatusProtocol.TryReadSystemTime(invalidMonth)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfTagQueryProtocolTests.cs b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfTagQueryProtocolTests.cs deleted file mode 100644 index e9198fa..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/WcfTagQueryProtocolTests.cs +++ /dev/null @@ -1,276 +0,0 @@ -using ZB.MOM.WW.SPHistorianClient.Wcf; - -namespace ZB.MOM.WW.SPHistorianClient.Tests; - -public sealed class WcfTagQueryProtocolTests -{ - [Fact] - public void SerializerMatchesInstrumentedNativeTagQueryRequest() - { - HistorianTagQueryAttempt attempt = HistorianTagQueryProtocol.CreateStartTagQueryAttempt( - "TagName eq 'OtOpcUaParityTest_001.Counter'"); - - byte[] expected = Convert.FromBase64String( - "UWcBACoAAABUAGEAZwBOAGEAbQBlACAAZQBxACAAJwBPAHQATwBwAGMAVQBhAFAAYQByAGkAdAB5AFQAZQBzAHQAXwAwADAAMQAuAEMAbwB1AG4AdABlAHIAJwA="); - - Assert.Equal(expected, attempt.RequestBuffer); - Assert.Equal("af1dbcdd3eb0ad91a18882c22252aa74aff82998e96a39b63415ab4792a962ac", attempt.RequestSha256); - } - - [Fact] - public void SerializerUsesDecompiledMarkerVersionAndUtf16Filter() - { - HistorianTagQueryAttempt attempt = HistorianTagQueryProtocol.CreateStartTagQueryAttempt("T*"); - byte[] actual = attempt.RequestBuffer; - - Assert.Equal("native-start-tag-query-version1", attempt.Name); - Assert.Equal(HistorianTagQueryProtocol.NativeStartTagQueryMarker, BitConverter.ToUInt16(actual, 0)); - Assert.Equal(HistorianTagQueryProtocol.NativeStartTagQueryVersion, BitConverter.ToUInt16(actual, 2)); - Assert.Equal(2u, BitConverter.ToUInt32(actual, 4)); - Assert.Equal("T*", System.Text.Encoding.Unicode.GetString(actual, 8, actual.Length - 8)); - } - - [Fact] - public void SerializerMatchesInstrumentedNativeHeaderOnlyTagQueryRequest() - { - HistorianTagQueryAttempt attempt = HistorianTagQueryProtocol.CreateStartTagQueryHeaderOnlyAttempt(); - - Assert.Equal("native-start-tag-query-header-only", attempt.Name); - Assert.Equal(Convert.FromBase64String("UWcBAA=="), attempt.RequestBuffer); - Assert.Equal("17956e4fbe53d5edc0f9170203b013432e4afcc0591c795a10522a98d9fce926", attempt.RequestSha256); - } - - [Fact] - public void ParsesInstrumentedNativeStartTagQueryResponse() - { - byte[] response = Convert.FromBase64String("CAAAAAEAAAA="); - - HistorianTagQueryStartResponse parsed = HistorianTagQueryProtocol.ParseStartTagQueryResponse(response); - - Assert.Equal(8u, parsed.QueryHandle); - Assert.Equal(1u, parsed.TagCount); - } - - [Fact] - public void ParsesInstrumentedNativeGetTagInfoResponse() - { - byte[] response = Convert.FromBase64String( - "AQAAAAPDADGEIoxAWOGHSphLPb7L4KpC7gAAAAkdAE90T3BjVWFQYXJpdHlUZXN0XzAwMS5Db3VudGVyCQQATURBUwIDAQIAAADQV/SUZdjcAQoAAAAAAAAAJEAAAAAAAAAkQP4AAAAAAA=="); - - IReadOnlyList tags = HistorianTagQueryProtocol.ParseGetTagInfoResponse(response); - - HistorianTagInfoResponse tag = Assert.Single(tags); - Assert.Equal("OtOpcUaParityTest_001.Counter", tag.TagName); - Assert.Equal(238u, tag.TagKey); - Assert.Equal(new Guid("408c2284-e158-4a87-984b-3dbecbe0aa42"), tag.TypeId); - Assert.Equal([0x03, 0xC3, 0x00, 0x31], tag.NativeDataTypeDescriptor); - Assert.Equal("MDAS", tag.MetadataProvider); - Assert.Equal(2, tag.NativeTagClass); - Assert.Equal(3, tag.StorageType); - Assert.Equal(1, tag.DeadbandType); - Assert.Equal(2, tag.InterpolationType); - } - - [Fact] - public void ParsesDirectWcfGetTagInfoFromNameResponse() - { - byte[] response = Convert.FromBase64String( - "A8MAMYQijEBY4YdKmEs9vsvgqkLuAAAACR0AT3RPcGNVYVBhcml0eVRlc3RfMDAxLkNvdW50ZXIJBABNREFTAgMBAgAAANBX9JRl2NwBCgAAAAAAAAAkQAAAAAAAACRA/gA="); - - HistorianTagInfoResponse tag = HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(response); - - Assert.Equal("OtOpcUaParityTest_001.Counter", tag.TagName); - Assert.Equal(238u, tag.TagKey); - Assert.Equal([0x03, 0xC3, 0x00, 0x31], tag.NativeDataTypeDescriptor); - Assert.Equal(Models.HistorianDataType.Int4, HistorianWcfTagClient.MapDataType(tag.NativeDataTypeDescriptor)); - } - - [Fact] - public void MapDataType_UInt2Descriptor_ReturnsUInt2() - { - // Built-in SysTimeSec exposes this descriptor (Runtime AnalogTag.IntegerSize=16, - // SignedInteger=0 → UInt16). - Assert.Equal(Models.HistorianDataType.UInt2, HistorianWcfTagClient.MapDataType([0x03, 0xCF, 0x04, 0x09])); - } - - [Theory] - // Captured descriptors from TagMetadataDescriptorProbeTests against a live local Historian. - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x01 }, Models.HistorianDataType.Float)] // SysDataAcqOverallItemsPerSec (RawType=2, IntegerSize=0) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x02 }, Models.HistorianDataType.Int1)] // SysClassicDataRedirector (DiscreteTag) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x09 }, Models.HistorianDataType.UInt2)] // SysCritErrCnt (UInt16, StorageType=Delta) - [InlineData(new byte[] { 0x03, 0xCF, 0x04, 0x09 }, Models.HistorianDataType.UInt2)] // SysTimeSec (UInt16, StorageType=Cyclic) - [InlineData(new byte[] { 0x03, 0xCF, 0x04, 0x11 }, Models.HistorianDataType.UInt4)] // SysConfigStatus (UInt32) - [InlineData(new byte[] { 0x03, 0xC3, 0x00, 0x31 }, Models.HistorianDataType.Int4)] // OtOpcUaParityTest_001.Counter (Int32) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x43 }, Models.HistorianDataType.DoubleByteString)] // SysString - // Inferred from CDataType predicate IL (IsConvertableToDouble/Int64/UInt64, IsEvent, IsStruct, IsString): - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x03 }, Models.HistorianDataType.SingleByteString)] // string class without bit 0x40 (wide flag) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x04 }, Models.HistorianDataType.Event)] // IsEvent: low 3 bits == 4 - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x05 }, Models.HistorianDataType.Structure)] // IsStruct: low 3 bits == 5 - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x21 }, Models.HistorianDataType.Double)] // IsConvertableToDouble matches 33 - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x29 }, Models.HistorianDataType.Int2)] // IsConvertableToInt64 matches 41 (= UInt16=0x09 + signed bit 0x20) - // Newly extended HistorianDataType enum entries (codes recovered from same predicate IL): - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x08 }, Models.HistorianDataType.UInt1)] // 1-byte unsigned (IsConvertableToUInt64 matches 8) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x10 }, Models.HistorianDataType.Guid)] // IsGuid: byte == 16 - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x18 }, Models.HistorianDataType.FileTime)] // IsFileTime: byte == 24 - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x19 }, Models.HistorianDataType.Int8)] // 8-byte signed (IsConvertableToInt64 matches 25) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x39 }, Models.HistorianDataType.UInt8)] // 8-byte unsigned (IsConvertableToUInt64 matches 57) - [InlineData(new byte[] { 0x03, 0xCF, 0x00, 0x81 }, Models.HistorianDataType.Int1)] // Boolean extended (IsBoolean: byte == 129) - public void MapDataType_KnownDescriptors_ReturnsExpectedType(byte[] descriptor, Models.HistorianDataType expected) - { - Assert.Equal(expected, HistorianWcfTagClient.MapDataType(descriptor)); - } - - [Theory] - // Storage-attribute byte (byte 2) variants should still map to the same data type because - // the dispatch is on byte 3. - [InlineData(new byte[] { 0x03, 0xCF, 0x07, 0x09 }, Models.HistorianDataType.UInt2)] - [InlineData(new byte[] { 0x03, 0xCF, 0xFF, 0x11 }, Models.HistorianDataType.UInt4)] - public void MapDataType_StorageAttributeVariants_DispatchesByDataTypeCode(byte[] descriptor, Models.HistorianDataType expected) - { - Assert.Equal(expected, HistorianWcfTagClient.MapDataType(descriptor)); - } - - [Fact] - public void MapDataType_UnknownDataTypeCode_ThrowsEvidenceMissing() - { - // Byte 3 = 0xFF is not yet observed; should throw rather than guess. - Assert.Throws(() => - HistorianWcfTagClient.MapDataType([0x03, 0xCF, 0x00, 0xFF])); - } - - [Fact] - public void MapDataType_WrongFormatVersion_ThrowsEvidenceMissing() - { - // Byte 0 must be 0x03; anything else throws. - Assert.Throws(() => - HistorianWcfTagClient.MapDataType([0x04, 0xCF, 0x00, 0x09])); - } - - [Fact] - public void Parse_FullShape4Strings_PopulatesDescription() - { - byte[] response = BuildSyntheticTagInfo( - descriptor: [0x03, 0xCF, 0x00, 0x09], - tagKey: 42, - strings: ["TAG", "Tag description here", "TAG", "DOMAIN\\user"], - fixedBlock: [0x03, 0x02, 0x01, 0x00], - trailingDoubles: null, - trailingEu: null); - HistorianTagInfoResponse parsed = HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(response); - Assert.Equal("TAG", parsed.TagName); - Assert.Equal(42u, parsed.TagKey); - Assert.Equal("Tag description here", parsed.Description); - // 4-string shape uses position 1 as Description AND MetadataProvider for back-compat. - Assert.Equal("Tag description here", parsed.MetadataProvider); - } - - [Fact] - public void Parse_TwoStringShape_DoesNotMisinterpretMetadataProviderAsDescription() - { - byte[] response = BuildSyntheticTagInfo( - descriptor: [0x03, 0xC3, 0x00, 0x31], - tagKey: 99, - strings: ["EXT.TAG.NAME", "MDAS"], - fixedBlock: [0x02, 0x03, 0x01, 0x02], - trailingDoubles: null, - trailingEu: null); - HistorianTagInfoResponse parsed = HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(response); - Assert.Equal("EXT.TAG.NAME", parsed.TagName); - Assert.Equal("MDAS", parsed.MetadataProvider); - // 2-string shape: don't conflate MetadataProvider with Description. - Assert.Null(parsed.Description); - } - - [Fact] - public void Parse_TrailingDoublesAndEu_PopulatesMinMaxAndUnit() - { - byte[] response = BuildSyntheticTagInfo( - descriptor: [0x03, 0xCF, 0x00, 0x09], - tagKey: 12, - strings: ["TAG", "desc", "TAG", "DOMAIN\\u"], - fixedBlock: [0x03, 0x02, 0x01, 0x00], - trailingDoubles: (0.0, 59.0), - trailingEu: "Seconds"); - HistorianTagInfoResponse parsed = HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(response); - Assert.Equal(0.0, parsed.MinEU); - Assert.Equal(59.0, parsed.MaxEU); - Assert.Equal("Seconds", parsed.EngineeringUnit); - } - - [Fact] - public void Parse_TrailingNoDoubles_LeavesMinMaxNull() - { - byte[] response = BuildSyntheticTagInfo( - descriptor: [0x03, 0xCF, 0x00, 0x02], - tagKey: 97, - strings: ["DiscreteTag", "Description", "DiscreteTag", "DOMAIN\\u"], - fixedBlock: [0x03, 0x02, 0x01, 0x00], - trailingDoubles: null, - trailingEu: null); - HistorianTagInfoResponse parsed = HistorianTagQueryProtocol.ParseGetTagInfoFromNameResponse(response); - Assert.Null(parsed.MinEU); - Assert.Null(parsed.MaxEU); - Assert.Null(parsed.EngineeringUnit); - } - - private static byte[] BuildSyntheticTagInfo( - byte[] descriptor, - uint tagKey, - string[] strings, - byte[] fixedBlock, - (double Min, double Max)? trailingDoubles, - string? trailingEu) - { - using MemoryStream ms = new(); - using BinaryWriter w = new(ms); - w.Write(descriptor); // 4 bytes - w.Write(System.Guid.NewGuid().ToByteArray()); // 16 bytes - w.Write(tagKey); // 4 bytes - foreach (string s in strings) - { - byte[] ascii = System.Text.Encoding.ASCII.GetBytes(s); - w.Write((byte)0x09); - w.Write((ushort)ascii.Length); - w.Write(ascii); - } - w.Write(fixedBlock); // 4 bytes - // Trailing region: padding + (optional doubles aligned to 8) + (optional EU compact ASCII). - // To keep doubles 8-byte aligned within the trailing region, pad to next 8-byte boundary. - long trailingStart = ms.Length; - // Plain alignment: add zero padding so doubles start at a stable 8-byte aligned offset - // within the trailing region — the parser scans alignments 0..7 so any padding works. - if (trailingDoubles is { } d) - { - w.Write(d.Min); - w.Write(d.Max); - } - if (trailingEu is not null) - { - byte[] euAscii = System.Text.Encoding.ASCII.GetBytes(trailingEu); - w.Write((byte)0x09); - w.Write((ushort)euAscii.Length); - w.Write(euAscii); - } - return ms.ToArray(); - } - - [Fact] - public void ParsesManagedWcfLikeTagNamesResponse() - { - byte[] response = Convert.FromBase64String( - "AQAAAB0AAABPAHQATwBwAGMAVQBhAFAAYQByAGkAdAB5AFQAZQBzAHQAXwAwADAAMQAuAEMAbwB1AG4AdABlAHIA"); - - IReadOnlyList tagNames = HistorianTagQueryProtocol.ParseGetLikeTagNamesResponse(response); - - Assert.Equal(["OtOpcUaParityTest_001.Counter"], tagNames); - } - - [Theory] - [InlineData("*", "%")] - [InlineData("Sys*", "Sys%")] - [InlineData("OtOpcUaParityTest%", "OtOpcUaParityTest%")] - public void NormalizesPublicWildcardToHistorianLikeWildcard(string filter, string expected) - { - Assert.Equal(expected, HistorianWcfTagClient.NormalizeLikeFilter(filter)); - } -} diff --git a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj b/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj deleted file mode 100644 index 011e716..0000000 --- a/ZB.MOM.WW.SPHistorianClient/tests/ZB.MOM.WW.SPHistorianClient.Tests/ZB.MOM.WW.SPHistorianClient.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - false - - - - - - - - - - - - - - - - - - - -