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; }
}