feat(grpc): tool the WCF-only config ops over the gRPC transport
Wire the config operations that previously only worked over WCF onto RemoteGrpc, reusing the proven 2020 byte serializers verbatim inside the protobuf bytes fields (keyed by the Open2 session handle). Live-verified against a real 2023 R2 server where noted. Read ops (live-verified): - GetRuntimeParameterAsync -> StatusService.GetRuntimeParameter (GETRP serializer) - GetTagExtendedPropertiesAsync -> RetrievalService.GetTagExtendedPropertiesFromName (GetTepByNm serializer + sequence paging; page-0 FillBufferFromVector is the benign no-data terminator, matched to the WCF break-and-return-empty semantics) Server-walled (bounded with captured evidence): - ExecuteSqlCommandAsync -> RetrievalService.ExecuteSqlCommand. The request rides the RPC but the server-side CSrvDbConnection.ExecuteSqlCommand faults (IndexOutOfRange / native err 38) on a DB-connection precondition the pure managed gRPC session doesn't establish (same class as OpenStorageConnection). Surfaced as ProtocolEvidenceMissingException. Write ops (tooled + routed, sandbox-gated — not run destructively live): - EnsureTagAsync / DeleteTagAsync / RenameTagsAsync / AddTagExtendedPropertiesAsync via HistoryService.EnsureTags / DeleteTags / StartJob / AddTagExtendedProperties on a write-enabled (0x401) session, reusing the WCF golden serializers. The WCF priming discovery-dance is omitted (the M3 gRPC write probe worked without it); add it first if a live sandbox run is rejected. Routed in Historian2020ProtocolDialect / HistorianClient on the RemoteGrpc branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
using Google.Protobuf;
|
||||||
|
using AVEVA.Historian.Client.Models;
|
||||||
|
using AVEVA.Historian.Client.Protocol;
|
||||||
|
using AVEVA.Historian.Client.Wcf;
|
||||||
|
using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval;
|
||||||
|
|
||||||
|
namespace AVEVA.Historian.Client.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes SQL commands over the 2023 R2 gRPC transport (HCAL R1.1), mirroring
|
||||||
|
/// <see cref="HistorianWcfSqlClient"/>'s two-op <c>ExeC</c>/<c>GetR</c> flow. The 2020 WCF path uses a
|
||||||
|
/// dedicated <c>GetRecordSetByteStream</c> op; the gRPC front door has no such RPC, so the NRBF
|
||||||
|
/// recordset stream would be fetched through the generic <c>RetrievalService.GetNextQueryResultBuffer</c>
|
||||||
|
/// keyed by the query handle <c>ExecuteSqlCommand</c> returns. <c>ExecuteSqlCommand</c> takes the
|
||||||
|
/// uppercase string session handle; the result-buffer fetch takes the transient <c>uint</c> client
|
||||||
|
/// handle (both come from the one Open2 session).
|
||||||
|
/// <para>
|
||||||
|
/// <b>SERVER-WALLED (captured 2026-06-22).</b> The 2023 R2 front-door
|
||||||
|
/// <c>RetrievalService.ExecuteSqlCommand</c> faults server-side before returning a query handle:
|
||||||
|
/// the response carries native error 38 wrapping a managed
|
||||||
|
/// <c>System.IndexOutOfRangeException ... at aahClientAccessPoint.CSrvDbConnection.ExecuteSqlCommand</c>.
|
||||||
|
/// This is a server-side <c>CSrvDbConnection</c> (SQL DB-connection) precondition that the pure
|
||||||
|
/// managed gRPC session does not establish — the same class of wall as
|
||||||
|
/// <c>StorageService.OpenStorageConnection</c> (whose real precondition is the front-door
|
||||||
|
/// <c>HistoryService.RegisterTags</c> family). Priming <c>Retr.GetV</c> does not clear it. The request
|
||||||
|
/// framing here is the captured/expected shape; the op stays bounded behind
|
||||||
|
/// <see cref="ProtocolEvidenceMissingException"/> until the DB-connection registration is reproduced.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
internal static class HistorianGrpcSqlClient
|
||||||
|
{
|
||||||
|
// GetNextQueryResultBuffer is byte-stream-paged; a small record set returns in one page. Runaway guard.
|
||||||
|
private const int MaxPages = 4096;
|
||||||
|
|
||||||
|
public static Task<HistorianSqlResult> ExecuteSqlCommandAsync(
|
||||||
|
HistorianClientOptions options,
|
||||||
|
string command,
|
||||||
|
HistorianSqlExecuteOption option,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(command);
|
||||||
|
return Task.Run(() => ExecuteSqlCommand(options, command, option, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HistorianSqlResult ExecuteSqlCommand(
|
||||||
|
HistorianClientOptions options,
|
||||||
|
string command,
|
||||||
|
HistorianSqlExecuteOption option,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
|
||||||
|
var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel);
|
||||||
|
DateTime Deadline() => DateTime.UtcNow.Add(options.RequestTimeout);
|
||||||
|
|
||||||
|
// Prime the Retrieval service version handshake (Retr.GetV) before the string-handle SQL op, as
|
||||||
|
// the native WCF SQL path does — the server-side ExecuteSqlCommand otherwise faults.
|
||||||
|
retrievalClient.GetRetrievalInterfaceVersion(
|
||||||
|
new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken);
|
||||||
|
|
||||||
|
GrpcRetrieval.ExecuteSqlCommandResponse exec = retrievalClient.ExecuteSqlCommand(
|
||||||
|
new GrpcRetrieval.ExecuteSqlCommandRequest
|
||||||
|
{
|
||||||
|
StrHandle = session.StringHandle,
|
||||||
|
StrCommand = command,
|
||||||
|
UiOption = (uint)option,
|
||||||
|
UiQueryHandle = 0
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
Deadline(),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (!(exec.Status?.BSuccess ?? false))
|
||||||
|
{
|
||||||
|
// Captured 2026-06-22: the server-side CSrvDbConnection.ExecuteSqlCommand throws
|
||||||
|
// IndexOutOfRange (native error 38) — a DB-connection precondition the pure managed gRPC
|
||||||
|
// session doesn't establish. Surface the SDK's evidence-missing signal rather than a raw
|
||||||
|
// server fault. See the class remarks.
|
||||||
|
throw new ProtocolEvidenceMissingException(
|
||||||
|
"ExecuteSqlCommand over gRPC: server-side CSrvDbConnection.ExecuteSqlCommand faults " +
|
||||||
|
"(IndexOutOfRange / native error 38) — an unmet DB-connection precondition (gRPC transport). Use WCF.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int returnValue = exec.IRetValue;
|
||||||
|
uint queryHandle = exec.UiQueryHandle;
|
||||||
|
|
||||||
|
using MemoryStream accumulated = new();
|
||||||
|
for (int page = 0; page < MaxPages; page++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
GrpcRetrieval.GetNextQueryResultBufferResponse buffer = retrievalClient.GetNextQueryResultBuffer(
|
||||||
|
new GrpcRetrieval.GetNextQueryResultBufferRequest { UiHandle = session.ClientHandle, UiQueryHandle = queryHandle },
|
||||||
|
connection.Metadata,
|
||||||
|
Deadline(),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
byte[] resultBuffer = buffer.BtQueryResult?.ToByteArray() ?? [];
|
||||||
|
|
||||||
|
// GetR is false-even-on-success: the final page returns false with the data still in the
|
||||||
|
// buffer, so always consume the buffer first, then stop on a false status or an empty page.
|
||||||
|
if (resultBuffer.Length > 0)
|
||||||
|
{
|
||||||
|
accumulated.Write(resultBuffer, 0, resultBuffer.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(buffer.Status?.BSuccess ?? false) || resultBuffer.Length == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HistorianSqlResultProtocol.Parse(accumulated.ToArray(), returnValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using Google.Protobuf;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using AVEVA.Historian.Client.Models;
|
using AVEVA.Historian.Client.Models;
|
||||||
|
using AVEVA.Historian.Client.Wcf;
|
||||||
using GrpcStatus = ArchestrA.Grpc.Contract.Status;
|
using GrpcStatus = ArchestrA.Grpc.Contract.Status;
|
||||||
|
|
||||||
namespace AVEVA.Historian.Client.Grpc;
|
namespace AVEVA.Historian.Client.Grpc;
|
||||||
@@ -142,4 +144,46 @@ internal static class HistorianGrpcStatusClient
|
|||||||
string? value = response.StrSystemTimeZoneName;
|
string? value = response.StrSystemTimeZoneName;
|
||||||
return string.IsNullOrEmpty(value) ? null : value;
|
return string.IsNullOrEmpty(value) ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a Historian runtime parameter over gRPC (<c>StatusService.GetRuntimeParameter</c>).
|
||||||
|
/// The request/response byte buffers are the proven 2020 <c>GETRP</c> wire format
|
||||||
|
/// (<see cref="HistorianRuntimeParameterProtocol"/>) carried unchanged inside the protobuf
|
||||||
|
/// <c>btRequest</c>/<c>btResponse</c> fields; the op keys on the uppercase string session handle.
|
||||||
|
/// </summary>
|
||||||
|
public static Task<string?> GetRuntimeParameterAsync(
|
||||||
|
HistorianClientOptions options,
|
||||||
|
string parameterName,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(parameterName);
|
||||||
|
return Task.Run(() => GetRuntimeParameter(options, parameterName, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetRuntimeParameter(HistorianClientOptions options, string parameterName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
|
||||||
|
|
||||||
|
byte[] request = HistorianRuntimeParameterProtocol.SerializeRequest(parameterName);
|
||||||
|
|
||||||
|
var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel);
|
||||||
|
GrpcStatus.GetRuntimeParameterResponse response = statusClient.GetRuntimeParameter(
|
||||||
|
new GrpcStatus.GetRuntimeParameterRequest
|
||||||
|
{
|
||||||
|
StrHandle = session.StringHandle,
|
||||||
|
BtRequest = ByteString.CopyFrom(request)
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
DateTime.UtcNow.Add(options.RequestTimeout),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (!(response.Status?.BSuccess ?? false))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] responseBuffer = response.BtResponse?.ToByteArray() ?? [];
|
||||||
|
return HistorianRuntimeParameterProtocol.ParseSingleStringResult(responseBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,78 @@ internal static class HistorianGrpcTagClient
|
|||||||
return response.BtTagInfos?.ToByteArray() ?? [];
|
return response.BtTagInfos?.ToByteArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTagExtendedPropertiesFromName is sequence-paged; a single tag returns everything on page 0
|
||||||
|
// and an empty/false buffer next. The cap is a runaway guard (mirrors the WCF path).
|
||||||
|
private const int MaxExtendedPropertyPages = 64;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a tag's extended (user-defined) properties over gRPC
|
||||||
|
/// (<c>RetrievalService.GetTagExtendedPropertiesFromName</c>, a string-handle op). The request
|
||||||
|
/// <c>btTagNames</c> and response <c>btTeps</c> buffers are the proven 2020 <c>GetTepByNm</c> wire
|
||||||
|
/// format (<see cref="HistorianTagExtendedPropertyProtocol"/>) carried unchanged; paging follows
|
||||||
|
/// the same sequence loop as the WCF path.
|
||||||
|
/// </summary>
|
||||||
|
public static Task<IReadOnlyList<HistorianTagExtendedProperty>> GetTagExtendedPropertiesAsync(
|
||||||
|
HistorianClientOptions options,
|
||||||
|
string tag,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||||
|
return Task.Run(() => GetTagExtendedProperties(options, tag, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<HistorianTagExtendedProperty> GetTagExtendedProperties(
|
||||||
|
HistorianClientOptions options, string tag, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
|
||||||
|
var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel);
|
||||||
|
|
||||||
|
byte[] tagNames = HistorianTagExtendedPropertyProtocol.SerializeRequest(tag);
|
||||||
|
List<HistorianTagExtendedProperty> properties = [];
|
||||||
|
uint sequence = 0;
|
||||||
|
|
||||||
|
for (int page = 0; page < MaxExtendedPropertyPages; page++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
GrpcRetrieval.GetTagExtendedPropertiesFromNameResponse response = retrievalClient.GetTagExtendedPropertiesFromName(
|
||||||
|
new GrpcRetrieval.GetTagExtendedPropertiesFromNameRequest
|
||||||
|
{
|
||||||
|
StrHandle = session.StringHandle,
|
||||||
|
BtTagNames = ByteString.CopyFrom(tagNames),
|
||||||
|
UiSequence = sequence
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
DateTime.UtcNow.Add(options.RequestTimeout),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (!(response.Status?.BSuccess ?? false))
|
||||||
|
{
|
||||||
|
// A non-success terminates paging. The server signals "no more rows" with a
|
||||||
|
// CClientUtil::FillBufferFromVector marker (live-confirmed) — including on page 0 when
|
||||||
|
// the tag has no user-defined properties, which is a legitimate empty result, not an
|
||||||
|
// error. This mirrors the WCF path, which also breaks (returns empty) rather than throws.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<HistorianTagExtendedPropertyRow> rows =
|
||||||
|
HistorianTagExtendedPropertyProtocol.ParseResponse(response.BtTeps?.ToByteArray() ?? []);
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (HistorianTagExtendedPropertyRow row in rows)
|
||||||
|
{
|
||||||
|
properties.Add(new HistorianTagExtendedProperty(row.PropertyName, row.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence = response.UiSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
// QueryTag (browse paging) request framing, recovered from the .rdata packet-descriptor table
|
// QueryTag (browse paging) request framing, recovered from the .rdata packet-descriptor table
|
||||||
// in aahClientManaged.dll (entries {0x6751,1}=StartTagQuery, {0x6752,1}=QueryTag) and confirmed
|
// in aahClientManaged.dll (entries {0x6751,1}=StartTagQuery, {0x6752,1}=QueryTag) and confirmed
|
||||||
// live: btRequest = u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 count.
|
// live: btRequest = u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 count.
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
using Google.Protobuf;
|
||||||
|
using AVEVA.Historian.Client.Models;
|
||||||
|
using AVEVA.Historian.Client.Wcf;
|
||||||
|
using GrpcHistory = ArchestrA.Grpc.Contract.History;
|
||||||
|
|
||||||
|
namespace AVEVA.Historian.Client.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tag-configuration write ops over the 2023 R2 gRPC transport, mirroring
|
||||||
|
/// <see cref="HistorianWcfTagWriteOrchestrator"/>. Each op opens a <b>write-enabled</b> Open2 session
|
||||||
|
/// (<c>0x401</c>) and reuses the proven 2020 byte serializers verbatim inside the protobuf
|
||||||
|
/// <c>bytes</c> fields:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><see cref="EnsureTagAsync"/> → <c>HistoryService.EnsureTags</c> (string handle,
|
||||||
|
/// <c>btTagInfos</c> = <see cref="HistorianTagWriteProtocol.SerializeAnalogCTagMetadata"/>)</item>
|
||||||
|
/// <item><see cref="DeleteTagAsync"/> → <c>HistoryService.DeleteTags</c> (uint handle,
|
||||||
|
/// <c>btTagnames</c> = <see cref="HistorianTagWriteProtocol.SerializeDeleteTagNames"/>)</item>
|
||||||
|
/// <item><see cref="RenameTagsAsync"/> → <c>HistoryService.StartJob</c> (string handle,
|
||||||
|
/// <c>btInput</c> = <see cref="HistorianTagRenameProtocol.SerializeRenameJob"/>)</item>
|
||||||
|
/// <item><see cref="AddTagExtendedPropertiesAsync"/> → <c>HistoryService.AddTagExtendedProperties</c>
|
||||||
|
/// (string handle, <c>btTeps</c> = <see cref="HistorianTagExtendedPropertyProtocol.SerializeAddRequest"/>)</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Tooled but not yet live-verified.</b> The request framing reuses the WCF serializers proven on
|
||||||
|
/// the 2020 transport, and the read-side config ops confirm WCF config buffers ride the gRPC RPC
|
||||||
|
/// unchanged — but these mutate server state (create/delete/rename tags, write properties), so they
|
||||||
|
/// are gated behind a sandbox-tag in the integration tests and have not been run destructively against
|
||||||
|
/// a shared live server. The WCF path additionally runs a priming "discovery dance" (UpdC3 + system
|
||||||
|
/// parameters + cross-service GetV) before the write; the gRPC front door established the equivalent
|
||||||
|
/// session state in the M3 non-streamed-write probe without it, so it is omitted here pending live
|
||||||
|
/// confirmation. If a live run is rejected, that priming is the first thing to add.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class HistorianGrpcTagWriteOrchestrator
|
||||||
|
{
|
||||||
|
private const uint WriteEnabledConnectionMode = HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode;
|
||||||
|
|
||||||
|
private readonly HistorianClientOptions _options;
|
||||||
|
|
||||||
|
public HistorianGrpcTagWriteOrchestrator(HistorianClientOptions options)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(definition.TagName, nameof(definition));
|
||||||
|
// Surface unsupported (non-analog) types early, exactly as the WCF path does.
|
||||||
|
_ = HistorianTagWriteProtocol.GetAnalogDataTypeCode(definition.DataType);
|
||||||
|
return Task.Run(() => EnsureTag(definition, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureTag(HistorianTagDefinition definition, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
|
||||||
|
GrpcHistory.EnsureTagsResponse response = historyClient.EnsureTags(
|
||||||
|
new GrpcHistory.EnsureTagsRequest
|
||||||
|
{
|
||||||
|
StrHandle = session.StringHandle,
|
||||||
|
BtTagInfos = ByteString.CopyFrom(payload),
|
||||||
|
ElementCount = 1
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
DateTime.UtcNow.Add(_options.RequestTimeout),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return response.Status?.BSuccess ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> DeleteTagAsync(string tagName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
|
||||||
|
return Task.Run(() => DeleteTag(tagName, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DeleteTag(string tagName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode);
|
||||||
|
|
||||||
|
// DeleteTags takes the transient uint client handle (not the string handle), per the WCF wire capture.
|
||||||
|
byte[] tagNames = HistorianTagWriteProtocol.SerializeDeleteTagNames([tagName]);
|
||||||
|
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
|
||||||
|
GrpcHistory.DeleteTagsResponse response = historyClient.DeleteTags(
|
||||||
|
new GrpcHistory.DeleteTagsRequest
|
||||||
|
{
|
||||||
|
UiHandle = session.ClientHandle,
|
||||||
|
BtTagnames = ByteString.CopyFrom(tagNames)
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
DateTime.UtcNow.Add(_options.RequestTimeout),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return response.Status?.BSuccess ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> AddTagExtendedPropertiesAsync(
|
||||||
|
string tagName, IReadOnlyList<HistorianTagExtendedProperty> properties, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
|
||||||
|
ArgumentNullException.ThrowIfNull(properties);
|
||||||
|
if (properties.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one extended property is required.", nameof(properties));
|
||||||
|
}
|
||||||
|
return Task.Run(() => AddTagExtendedProperties(tagName, properties, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AddTagExtendedProperties(
|
||||||
|
string tagName, IReadOnlyList<HistorianTagExtendedProperty> properties, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode);
|
||||||
|
|
||||||
|
byte[] inBuff = HistorianTagExtendedPropertyProtocol.SerializeAddRequest(tagName, properties);
|
||||||
|
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
|
||||||
|
GrpcHistory.AddTagExtendedPropertiesResponse response = historyClient.AddTagExtendedProperties(
|
||||||
|
new GrpcHistory.AddTagExtendedPropertiesRequest
|
||||||
|
{
|
||||||
|
StrHandle = session.StringHandle,
|
||||||
|
BtTeps = ByteString.CopyFrom(inBuff)
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
DateTime.UtcNow.Add(_options.RequestTimeout),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return response.Status?.BSuccess ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HistorianTagRenameResult> RenameTagsAsync(
|
||||||
|
IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(pairs);
|
||||||
|
if (pairs.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one (old,new) name pair is required.", nameof(pairs));
|
||||||
|
}
|
||||||
|
foreach ((string oldName, string newName) in pairs)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(oldName, nameof(pairs));
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(newName, nameof(pairs));
|
||||||
|
}
|
||||||
|
return Task.Run(() => RenameTags(pairs, cancellationToken), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HistorianTagRenameResult RenameTags(IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options);
|
||||||
|
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, _options, cancellationToken, WriteEnabledConnectionMode);
|
||||||
|
|
||||||
|
byte[] jobBuffer = HistorianTagRenameProtocol.SerializeRenameJob(pairs);
|
||||||
|
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
|
||||||
|
GrpcHistory.StartJobResponse response = historyClient.StartJob(
|
||||||
|
new GrpcHistory.StartJobRequest
|
||||||
|
{
|
||||||
|
StrHandle = session.StringHandle,
|
||||||
|
BtInput = ByteString.CopyFrom(jobBuffer)
|
||||||
|
},
|
||||||
|
connection.Metadata,
|
||||||
|
DateTime.UtcNow.Add(_options.RequestTimeout),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
bool ok = response.Status?.BSuccess ?? false;
|
||||||
|
Guid parsedJobId = Guid.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(response.StrJobid))
|
||||||
|
{
|
||||||
|
Guid.TryParse(response.StrJobid.Trim().Trim('$', '{', '}'), out parsedJobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HistorianTagRenameResult
|
||||||
|
{
|
||||||
|
Accepted = ok,
|
||||||
|
JobId = parsedJobId,
|
||||||
|
PairCount = pairs.Count,
|
||||||
|
Error = ok ? null : "Server rejected the rename job (StartJob returned false). Check that the 'AllowRenameTags' system parameter is enabled.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -237,7 +237,9 @@ public sealed class HistorianClient : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||||
ArgumentNullException.ThrowIfNull(properties);
|
ArgumentNullException.ThrowIfNull(properties);
|
||||||
return new HistorianWcfTagWriteOrchestrator(_options).AddTagExtendedPropertiesAsync(tag, properties, cancellationToken);
|
return _options.Transport == HistorianTransport.RemoteGrpc
|
||||||
|
? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).AddTagExtendedPropertiesAsync(tag, properties, cancellationToken)
|
||||||
|
: new HistorianWcfTagWriteOrchestrator(_options).AddTagExtendedPropertiesAsync(tag, properties, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Convenience overload of <see cref="AddTagExtendedPropertiesAsync"/> for a single
|
/// <summary>Convenience overload of <see cref="AddTagExtendedPropertiesAsync"/> for a single
|
||||||
@@ -285,7 +287,9 @@ public sealed class HistorianClient : IAsyncDisposable
|
|||||||
public Task<bool> EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken = default)
|
public Task<bool> EnsureTagAsync(HistorianTagDefinition definition, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(definition);
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
return new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken);
|
return _options.Transport == HistorianTransport.RemoteGrpc
|
||||||
|
? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken)
|
||||||
|
: new HistorianWcfTagWriteOrchestrator(_options).EnsureTagAsync(definition, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -299,7 +303,9 @@ public sealed class HistorianClient : IAsyncDisposable
|
|||||||
public Task<bool> DeleteTagAsync(string tagName, CancellationToken cancellationToken = default)
|
public Task<bool> DeleteTagAsync(string tagName, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
|
ArgumentException.ThrowIfNullOrWhiteSpace(tagName);
|
||||||
return new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken);
|
return _options.Transport == HistorianTransport.RemoteGrpc
|
||||||
|
? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken)
|
||||||
|
: new HistorianWcfTagWriteOrchestrator(_options).DeleteTagAsync(tagName, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -325,7 +331,9 @@ public sealed class HistorianClient : IAsyncDisposable
|
|||||||
public Task<HistorianTagRenameResult> RenameTagsAsync(IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken = default)
|
public Task<HistorianTagRenameResult> RenameTagsAsync(IReadOnlyList<(string OldName, string NewName)> pairs, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(pairs);
|
ArgumentNullException.ThrowIfNull(pairs);
|
||||||
return new HistorianWcfTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken);
|
return _options.Transport == HistorianTransport.RemoteGrpc
|
||||||
|
? new Grpc.HistorianGrpcTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken)
|
||||||
|
: new HistorianWcfTagWriteOrchestrator(_options).RenameTagsAsync(pairs, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
|
|||||||
@@ -95,21 +95,27 @@ internal sealed class Historian2020ProtocolDialect
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
return Wcf.HistorianWcfStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken);
|
return UseGrpc
|
||||||
|
? HistorianGrpcStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken)
|
||||||
|
: Wcf.HistorianWcfStatusClient.GetRuntimeParameterAsync(_options, name, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IReadOnlyList<Models.HistorianTagExtendedProperty>> GetTagExtendedPropertiesAsync(string tag, CancellationToken cancellationToken)
|
public Task<IReadOnlyList<Models.HistorianTagExtendedProperty>> GetTagExtendedPropertiesAsync(string tag, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||||
return Wcf.HistorianWcfTagExtendedPropertyClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken);
|
return UseGrpc
|
||||||
|
? Grpc.HistorianGrpcTagClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken)
|
||||||
|
: Wcf.HistorianWcfTagExtendedPropertyClient.GetTagExtendedPropertiesAsync(_options, tag, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<HistorianSqlResult> ExecuteSqlCommandAsync(string command, HistorianSqlExecuteOption option, CancellationToken cancellationToken)
|
public Task<HistorianSqlResult> ExecuteSqlCommandAsync(string command, HistorianSqlExecuteOption option, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(command);
|
ArgumentException.ThrowIfNullOrWhiteSpace(command);
|
||||||
return Wcf.HistorianWcfSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken);
|
return UseGrpc
|
||||||
|
? Grpc.HistorianGrpcSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken)
|
||||||
|
: Wcf.HistorianWcfSqlClient.ExecuteSqlCommandAsync(_options, command, option, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async IAsyncEnumerable<T> Missing<T>(
|
private static async IAsyncEnumerable<T> Missing<T>(
|
||||||
|
|||||||
Reference in New Issue
Block a user