using System.Diagnostics; using System.Text; using Google.Protobuf; using AVEVA.Historian.Client.Wcf; using GrpcStorage = ArchestrA.Grpc.Contract.Storage; namespace AVEVA.Historian.Client.Grpc; /// /// Live probe for the M3 follow-up step that the R3.1 decode pinned as the missing precondition: /// StorageService.OpenStorageConnection. The R3.1 finding (see /// docs/plans/revision-write-path.md §R3.1) was that AddNonStreamValues reaches the /// server-side CHistStorageConnection::StoreNonStreamValues, which routes to the /// \\.\pipe\aahStorageEngine\console,sid(...) named pipe and fails for lack of a console /// session. OpenStorageConnection is the op that creates exactly that console sid /// session (returning its own uint handle + a NEW storage-session GUID, distinct from the /// Open2 session). /// /// Unlike AddNonStreamValues, this op has NO opaque btInput buffer — all 12 request /// fields are typed protobuf fields (see StorageService.proto). So there are no wire bytes to /// guess; the only unknowns are the VALUES for a handful of inferable fields (ConnectionMode, the /// in/out StorageSessionId, FreeDiskSpace, credential framing). This probe sweeps a small matrix of /// those and reports the server's response for each, so one live run reveals which combination the /// storage engine accepts. It writes NO historical data — on success it immediately calls /// CloseStorageConnection to release the console session it opened. /// internal sealed class HistorianGrpcStorageConnectionProbe { // Native client identity constants, mirrored from HistorianNativeHandshake so the storage // engine sees the same client fingerprint the Open2 handshake presented. private const uint NativeClientType = 4; private const uint NativeClientVersionInt = 999_999; private const string EngineConsolePath = @"\\.\pipe\aahStorageEngine\console"; private readonly HistorianClientOptions _options; public HistorianGrpcStorageConnectionProbe(HistorianClientOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); } public Task ProbeAsync(CancellationToken cancellationToken) => Task.Run(() => Probe(cancellationToken), cancellationToken); private HistorianGrpcOpenStorageConnectionResult Probe(CancellationToken cancellationToken) { var result = new HistorianGrpcOpenStorageConnectionResult(); using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(_options); HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession( connection, _options, cancellationToken, connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode); result.OpenSucceeded = true; result.ClientHandle = session.ClientHandle; result.StorageSessionId = session.StorageSessionId; var storageClient = new GrpcStorage.StorageService.StorageServiceClient(connection.Channel); DateTime Deadline() => DateTime.UtcNow.Add(_options.RequestTimeout); // Prime the Storage service's interface-version / session table (matches the cross-service // GetV priming the other write paths use). try { GrpcStorage.GetInterfaceVersionResponse version = storageClient.GetInterfaceVersion( new GrpcStorage.GetInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken); result.StorageInterfaceVersion = version.UiVersion; result.StorageInterfaceVersionError = version.UiError; } catch (Exception ex) { result.StorageInterfaceVersionException = $"{ex.GetType().Name}: {ex.Message}"; } Process current = Process.GetCurrentProcess(); string machineName = Environment.MachineName; string processName = string.IsNullOrEmpty(current.ProcessName) ? "AVEVA.Historian.Client" : current.ProcessName; uint processId = checked((uint)current.Id); string upperGuid = session.StringHandle; // Password framing: the gRPC session is already NTLM-authenticated (ValidateClientCredential), // so attempt 1 sends no credential (rely on the authenticated channel). If the storage engine // demands its own credential we'll see an auth-shaped error and add a credential-bearing // attempt next iteration. For explicit creds we still try UTF-16LE password bytes as a probe. byte[] emptyPwd = []; // Sweep the genuinely-uncertain fields. Order = most-likely-correct first; stop at first // success. ConnectionMode 0x401 = write-enabled (Process|Write|IntegratedSecurity), the same // mode Open2 used for the write session. StorageSessionId-in: the native client threads the // Open2 storage GUID through here (in/out); empty-string is the "create fresh" fallback. var attempts = new List<(string Label, uint ConnectionMode, string SessionIdIn, uint FreeDiskSpace, byte[] Password)> { ("mode=0x401, sid=open2-upper", 0x401, upperGuid, 0u, emptyPwd), ("mode=0x401, sid=empty", 0x401, string.Empty, 0u, emptyPwd), ("mode=0x402, sid=open2-upper", 0x402, upperGuid, 0u, emptyPwd), ("mode=0x1, sid=open2-upper", 0x1, upperGuid, 0u, emptyPwd), ("mode=0x401, sid=open2, disk=big", 0x401, upperGuid, 0xFFFFFFFFu, emptyPwd), }; foreach ((string label, uint mode, string sidIn, uint freeDisk, byte[] pwd) in attempts) { var attempt = new HistorianGrpcOpenStorageConnectionAttempt { Label = label, ConnectionMode = mode, SessionIdIn = sidIn, }; try { var request = new GrpcStorage.OpenStorageConnectionRequest { HostName = machineName, EnginePath = EngineConsolePath, FreeDiskSpace = freeDisk, ProcessName = processName, ProcessId = processId, UserName = _options.IntegratedSecurity ? string.Empty : _options.UserName, Password = ByteString.CopyFrom(pwd), PwdLength = (uint)pwd.Length, ClientType = NativeClientType, ClientVersion = NativeClientVersionInt, ConnectionMode = mode, ConnectionTimeout = (uint)Math.Max(1, _options.RequestTimeout.TotalMilliseconds), StorageSessionId = sidIn, }; GrpcStorage.OpenStorageConnectionResponse response = storageClient.OpenStorageConnection( request, connection.Metadata, Deadline(), cancellationToken); attempt.Succeeded = response.Status?.BSuccess ?? false; attempt.NewHandle = response.Handle; attempt.NewStorageSessionId = response.StorageSessionId; attempt.ServerStatus = response.ServerStatus; attempt.ConnectionTime = response.ConnectionTime; byte[] error = response.Status?.BtError?.ToByteArray() ?? []; attempt.ErrorHex = error.Length == 0 ? null : Convert.ToHexString(error); attempt.ErrorPreview = DescribeError(error); result.Attempts.Add(attempt); if (attempt.Succeeded) { result.OpenStorageSucceeded = true; result.AcceptedAttempt = label; result.NewStorageHandle = response.Handle; result.NewStorageSessionId = response.StorageSessionId; // Release the console session immediately — this probe persists nothing. try { GrpcStorage.CloseStorageConnectionResponse close = storageClient.CloseStorageConnection( new GrpcStorage.CloseStorageConnectionRequest { Handle = response.Handle }, connection.Metadata, Deadline(), cancellationToken); result.CloseSucceeded = close.Status?.BSuccess ?? false; } catch (Exception ex) { result.CloseException = $"{ex.GetType().Name}: {ex.Message}"; } break; } } catch (Exception ex) { attempt.Exception = $"{ex.GetType().Name}: {ex.Message}"; result.Attempts.Add(attempt); } } return result; } /// Short printable preview of a server error buffer (status codes/messages, no secrets). private static string? DescribeError(byte[] error) { if (error.Length == 0) { return null; } ReadOnlySpan preview = error.AsSpan(0, Math.Min(error.Length, 96)); var sb = new StringBuilder(preview.Length); foreach (byte b in preview) { sb.Append(b is >= 0x20 and < 0x7F ? (char)b : '.'); } return sb.ToString(); } } internal sealed class HistorianGrpcOpenStorageConnectionResult { public bool OpenSucceeded { get; set; } public uint ClientHandle { get; set; } public Guid StorageSessionId { get; set; } public uint? StorageInterfaceVersion { get; set; } public uint? StorageInterfaceVersionError { get; set; } public string? StorageInterfaceVersionException { get; set; } public bool OpenStorageSucceeded { get; set; } public string? AcceptedAttempt { get; set; } public uint NewStorageHandle { get; set; } public string? NewStorageSessionId { get; set; } public bool CloseSucceeded { get; set; } public string? CloseException { get; set; } public List Attempts { get; } = new(); } internal sealed class HistorianGrpcOpenStorageConnectionAttempt { public string Label { get; set; } = ""; public uint ConnectionMode { get; set; } public string SessionIdIn { get; set; } = ""; public bool Succeeded { get; set; } public uint NewHandle { get; set; } public string? NewStorageSessionId { get; set; } public uint ServerStatus { get; set; } public ulong ConnectionTime { get; set; } public string? ErrorHex { get; set; } public string? ErrorPreview { get; set; } public string? Exception { get; set; } }