Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled

Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 06:21:00 -04:00
parent 43733699b0
commit fe2a6db786
3849 changed files with 352975 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Reference Include="ASBIDataV2Adapter">
<HintPath>C:\Windows\Microsoft.NET\assembly\GAC_MSIL\ASBIDataV2Adapter\v4.0_1.0.0.0__23106a86e706d0ae\ASBIDataV2Adapter.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesCommon">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesCommon.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesCommonDataContracts">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesCommonDataContracts.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesContractIAuthenticateASB">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesContractIAuthenticateASB.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesContractIData">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesContractIData.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesContractIDataV2">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesContractIDataV2.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesProxyIData">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesProxyIData.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="aaServicesProxyIDataV2">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Services\Authenticator\aaServicesProxyIDataV2.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceModel.Discovery" />
</ItemGroup>
</Project>
+305
View File
@@ -0,0 +1,305 @@
using System;
using System.Text;
using System.Linq;
using ArchestrAServices.Contract;
using ArchestrAServices.Proxy;
using ArchestrAServices.ASBIDataV2Contract;
using V2ItemIdentity = ArchestrAServices.ASBIDataV2Contract.ItemIdentity;
using V2ItemRegistration = ArchestrAServices.ASBIDataV2Contract.ItemRegistration;
using V2ItemStatus = ArchestrAServices.ASBIDataV2Contract.ItemStatus;
using V2RuntimeValue = ArchestrAServices.ASBIDataV2Contract.RuntimeValue;
using V2Variant = ArchestrAServices.ASBIDataContract.V2.Variant;
using V2VariantFactory = ArchestrAServices.ASBIDataContract.V2.VariantFactory;
using V2WriteValue = ArchestrAServices.ASBIDataV2Contract.WriteValue;
namespace AsbProxyProbe;
internal static class Program
{
private static int Main(string[] args)
{
string[] accessNames = GetStrings(args, "--access");
if (accessNames.Length == 0)
{
accessNames =
[
"ZB",
"ZB2",
"Default_ZB_MxDataProvider",
"Default_ZB2_MxDataProvider",
"Galaxy",
"localhost",
];
}
bool connect = HasFlag(args, "--connect");
bool register = HasFlag(args, "--register");
bool unregister = HasFlag(args, "--unregister");
bool read = HasFlag(args, "--read");
int? writeInt = GetInt(args, "--write-int");
string tag = GetString(args, "--tag") ?? "TestChildObject.TestInt";
Console.WriteLine($"process=x64:{Environment.Is64BitProcess}");
Console.WriteLine($"tag={tag}");
if (HasFlag(args, "--dump-register-payload"))
{
DumpRegisterPayload(tag);
return 0;
}
if (writeInt.HasValue)
{
Console.WriteLine($"write_int={writeInt.Value}");
}
foreach (string accessName in accessNames)
{
ProbeAccessName(accessName, connect, register, unregister, read, tag, writeInt);
}
return 0;
}
private static void ProbeAccessName(string accessName, bool connect, bool register, bool unregister, bool read, string tag, int? writeInt)
{
Console.WriteLine($"access={accessName}");
try
{
var response = ASBDataV2Proxy.FindIDataEndpoint(accessName, DiscoveryScope.Global);
int count = response?.Endpoints?.Count ?? 0;
Console.WriteLine($"idata_v2_endpoints={count}");
if (response?.Endpoints is not null)
{
for (int i = 0; i < response.Endpoints.Count; i++)
{
var endpoint = response.Endpoints[i];
Console.WriteLine($"idata_v2_endpoint[{i}].address={endpoint.Address?.Uri}");
Console.WriteLine($"idata_v2_endpoint[{i}].listen={string.Join(",", endpoint.ListenUris.Select(uri => uri.ToString()))}");
Console.WriteLine($"idata_v2_endpoint[{i}].contracts={string.Join(",", endpoint.ContractTypeNames.Select(name => name.ToString()))}");
Console.WriteLine($"idata_v2_endpoint[{i}].scopes={string.Join(",", endpoint.Scopes.Select(uri => uri.ToString()))}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"idata_v2_discovery_error={ex.GetType().FullName}: {ex.Message}");
}
try
{
object? proxy = IDataProxySelector.SelectProxyForLatestEndpoint(accessName, new AsbMxDataSettings(), out string selectorError);
Console.WriteLine($"selector_proxy={proxy?.GetType().FullName ?? "<null>"}");
Console.WriteLine($"selector_error={selectorError.Replace(Environment.NewLine, " ")}");
if (connect && proxy is ASBDataV2Proxy v2)
{
bool connected = v2.Connect(out string connectError);
Console.WriteLine($"connect_v2={connected}");
Console.WriteLine($"connect_v2_error={connectError.Replace(Environment.NewLine, " ")}");
Console.WriteLine($"connect_v2_state={v2.ChannelState}");
if (connected)
{
var result = v2.PublishWriteComplete(out var completeWrites);
Console.WriteLine($"publish_write_complete_error=0x{result.ErrorCode:X8}");
Console.WriteLine($"publish_write_complete_count={completeWrites?.Length ?? 0}");
V2ItemIdentity? registeredItem = null;
if (register)
{
registeredItem = ProbeRegister(v2, tag);
}
if (read)
{
ProbeRead(v2, tag);
}
if (writeInt.HasValue)
{
ProbeWriteInt(v2, tag, writeInt.Value);
ProbeRead(v2, tag);
result = v2.PublishWriteComplete(out completeWrites);
Console.WriteLine($"publish_write_complete_after_write_error=0x{result.ErrorCode:X8}");
Console.WriteLine($"publish_write_complete_after_write_count={completeWrites?.Length ?? 0}");
if (completeWrites is not null)
{
for (int i = 0; i < completeWrites.Length; i++)
{
Console.WriteLine($"publish_write_complete_after_write[{i}]=handle:{completeWrites[i].WriteHandle} handle_specified:{completeWrites[i].WriteHandleSpecified} status_items:{completeWrites[i].Status?.Length ?? 0}");
PrintStatuses($"publish_write_complete_after_write[{i}].status", completeWrites[i].Status);
}
}
}
if (unregister)
{
ProbeUnregister(v2, tag, registeredItem);
}
}
}
else if (connect && proxy is ASBDataProxy v1)
{
bool connected = v1.Connect(out string connectError);
Console.WriteLine($"connect_v1={connected}");
Console.WriteLine($"connect_v1_error={connectError.Replace(Environment.NewLine, " ")}");
}
}
catch (Exception ex)
{
Console.WriteLine($"selector_error_exception={ex.GetType().FullName}: {ex.Message}");
}
}
private static V2ItemIdentity? ProbeRegister(ASBDataV2Proxy proxy, string tag)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
var result = proxy.RegisterItems(out V2ItemStatus[] statuses, out V2ItemRegistration[] capabilities, RequireId: true, RegisterOnly: true, items);
Console.WriteLine($"register_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8}");
PrintStatuses("register_status", statuses);
if (capabilities is not null)
{
for (int i = 0; i < capabilities.Length; i++)
{
Console.WriteLine($"register_capability[{i}]=id:{capabilities[i].Id} id_specified:{capabilities[i].IdSpecified} write_capability:{capabilities[i].WriteCapability} write_specified:{capabilities[i].WriteCapabilitySpecified}");
}
}
return statuses is { Length: > 0 } ? statuses[0].Item : null;
}
private static void ProbeUnregister(ASBDataV2Proxy proxy, string tag, V2ItemIdentity? registeredItem)
{
V2ItemIdentity[] items = [registeredItem ?? CreateAbsoluteItem(tag)];
var result = proxy.UnregisterItems(out V2ItemStatus[] statuses, items);
Console.WriteLine($"unregister_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8}");
PrintStatuses("unregister_status", statuses);
}
private static void ProbeRead(ASBDataV2Proxy proxy, string tag)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
var result = proxy.Read(out V2ItemStatus[] statuses, out V2RuntimeValue[] values, items);
Console.WriteLine($"read_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8}");
PrintStatuses("read_status", statuses);
if (values is not null)
{
for (int i = 0; i < values.Length; i++)
{
V2RuntimeValue value = values[i];
PrintVariant($"read_value[{i}]", value.Value);
Console.WriteLine($"read_value[{i}].timestamp={value.Timestamp:o} timestamp_specified={value.TimestampSpecified}");
Console.WriteLine($"read_value[{i}].status_count={value.Status.Count} status_payload_len={value.Status.Payload?.Length ?? 0}");
}
}
}
private static void ProbeWriteInt(ASBDataV2Proxy proxy, string tag, int value)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
V2WriteValue[] values =
[
new V2WriteValue
{
Value = V2VariantFactory.MakeVariantValue(value),
Comment = "AsbProxyProbe write-int",
},
];
const uint writeHandle = 0xA5B20001;
var result = proxy.Write(out V2ItemStatus[] statuses, items, values, writeHandle);
Console.WriteLine($"write_error=0x{result.ErrorCode:X8} status=0x{result.Status:X8} specific=0x{result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("write_status", statuses);
}
private static V2ItemIdentity CreateAbsoluteItem(string tag)
{
return new V2ItemIdentity
{
Type = (ushort)ArchestrAServices.ASBIDataV2Contract.ItemIdentityType.Name,
ReferenceType = (ushort)ArchestrAServices.ASBIDataV2Contract.ItemReferenceType.Absolute,
Name = tag,
ContextName = string.Empty,
};
}
private static void DumpRegisterPayload(string tag)
{
V2ItemIdentity[] items = [CreateAbsoluteItem(tag)];
using var stream = new System.IO.MemoryStream();
var writer = new System.IO.BinaryWriter(stream);
items[0].WriteArrayToStream(items, ref writer);
byte[] payload = stream.ToArray();
Console.WriteLine($"register_payload_len={payload.Length}");
Console.WriteLine($"register_payload_b64={Convert.ToBase64String(payload)}");
}
private static void PrintStatuses(string prefix, V2ItemStatus[]? statuses)
{
if (statuses is null)
{
return;
}
for (int i = 0; i < statuses.Length; i++)
{
V2ItemStatus status = statuses[i];
Console.WriteLine($"{prefix}[{i}]=item:{status.Item.Name} id:{status.Item.Id} id_specified:{status.Item.IdSpecified} error:0x{status.ErrorCode:X8} error_specified:{status.ErrorCodeSpecified} status_count:{status.Status.Count} status_payload_len:{status.Status.Payload?.Length ?? 0}");
}
}
private static void PrintVariant(string prefix, V2Variant variant)
{
string preview = string.Empty;
if (variant.Payload is { Length: > 0 })
{
preview = variant.Type switch
{
0 when variant.Payload.Length >= 1 => variant.Payload[0].ToString(System.Globalization.CultureInfo.InvariantCulture),
1 when variant.Payload.Length >= 1 => ((char)variant.Payload[0]).ToString(),
2 when variant.Payload.Length >= 2 => BitConverter.ToInt16(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
3 when variant.Payload.Length >= 2 => BitConverter.ToUInt16(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
4 when variant.Payload.Length >= 4 => BitConverter.ToInt32(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
5 when variant.Payload.Length >= 4 => BitConverter.ToUInt32(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
6 when variant.Payload.Length >= 8 => BitConverter.ToInt64(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
7 when variant.Payload.Length >= 8 => BitConverter.ToUInt64(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
8 when variant.Payload.Length >= 4 => BitConverter.ToSingle(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
9 when variant.Payload.Length >= 8 => BitConverter.ToDouble(variant.Payload, 0).ToString(System.Globalization.CultureInfo.InvariantCulture),
10 => Encoding.Unicode.GetString(variant.Payload),
11 when variant.Payload.Length >= 8 => DateTime.FromFileTimeUtc(BitConverter.ToInt64(variant.Payload, 0)).ToString("o"),
17 when variant.Payload.Length >= 1 => BitConverter.ToBoolean(variant.Payload, 0).ToString(),
18 when variant.Payload.Length >= 1 => ((sbyte)variant.Payload[0]).ToString(System.Globalization.CultureInfo.InvariantCulture),
_ => BitConverter.ToString(variant.Payload).Replace("-", string.Empty),
};
}
Console.WriteLine($"{prefix}=type:{variant.Type} length:{variant.Length} payload_len:{variant.Payload?.Length ?? 0} preview:{preview}");
}
private static string[] GetStrings(string[] args, string name)
{
string prefix = name + "=";
return args
.Where(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(arg => arg.Substring(prefix.Length))
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
}
private static string? GetString(string[] args, string name)
{
string prefix = name + "=";
return args.FirstOrDefault(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))?.Substring(prefix.Length);
}
private static int? GetInt(string[] args, string name)
{
string? value = GetString(args, name);
return int.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int result)
? result
: null;
}
private static bool HasFlag(string[] args, string name)
{
return args.Any(arg => arg.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MxAsbClient\MxAsbClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</Project>
+830
View File
@@ -0,0 +1,830 @@
using MxAsbClient;
using System.Globalization;
string endpoint = GetArg(args, "--endpoint")
?? "net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2";
string[] tags = GetArgs(args, "--tag");
if (tags.Length == 0)
{
tags = ["TestChildObject.TestInt"];
}
string tag = tags[0];
string? solution = GetArg(args, "--solution");
bool dumpMessages = HasArg(args, "--dump-messages");
bool subscribe = HasArg(args, "--subscribe");
bool subscribeBuffered = HasArg(args, "--subscribe-buffered");
bool compatibilitySubscribe = HasArg(args, "--compat-subscribe");
bool probeConnectFailure = HasArg(args, "--probe-connect-failure");
bool probeReconnect = HasArg(args, "--probe-reconnect");
bool probeCanceledCleanup = HasArg(args, "--probe-canceled-cleanup");
bool probeOperationCompleteCandidates = HasArg(args, "--probe-operation-complete-candidates");
int publishCount = TryGetInt(args, "--publish-count") ?? 3;
int subscribeSampleMs = TryGetInt(args, "--subscribe-sample-ms") ?? 1000;
int publishDelayMs = TryGetInt(args, "--publish-delay-ms") ?? 500;
int reconnectAttempts = TryGetInt(args, "--reconnect-attempts") ?? 2;
int reconnectDelayMs = TryGetInt(args, "--reconnect-delay-ms") ?? 250;
int cleanupDisconnectTimeoutMs = TryGetInt(args, "--cleanup-disconnect-timeout-ms") ?? 30000;
int cleanupCloseTimeoutMs = TryGetInt(args, "--cleanup-close-timeout-ms") ?? 30000;
bool waitWriteComplete = HasArg(args, "--wait-write-complete");
int writeCompleteTimeoutMs = TryGetInt(args, "--write-complete-timeout-ms") ?? 5000;
int writeCompletePollMs = TryGetInt(args, "--write-complete-poll-ms") ?? 250;
int writeReadbackDelayMs = TryGetInt(args, "--write-readback-delay-ms") ?? 0;
Variant? writeVariant = GetWriteVariant(args);
bool probeErrorCases = HasArg(args, "--probe-error-cases");
bool probeInvalidTargets = probeErrorCases || HasArg(args, "--probe-invalid-targets");
bool probeWrongTypeWrite = probeErrorCases || HasArg(args, "--probe-wrong-type-write");
bool probeInvalidCleanup = probeErrorCases || HasArg(args, "--probe-invalid-cleanup");
bool probeEmptyPublish = probeErrorCases || HasArg(args, "--probe-empty-publish");
string invalidTag = GetArg(args, "--invalid-tag") ?? "DefinitelyMissingObject.DefinitelyMissingAttribute";
string wrongTypeWriteTag = GetArg(args, "--wrong-type-write-tag") ?? tag;
int errorPublishCount = TryGetInt(args, "--error-publish-count") ?? 2;
int errorWriteCompleteTimeoutMs = TryGetInt(args, "--error-write-complete-timeout-ms") ?? 2000;
int errorWriteCompletePollMs = TryGetInt(args, "--error-write-complete-poll-ms") ?? 250;
Console.WriteLine($"process=x64:{Environment.Is64BitProcess}");
Console.WriteLine($"endpoint={endpoint}");
Console.WriteLine($"tags={string.Join(",", tags)}");
if (args.Any(arg => arg.Equals("--dump-register-payload", StringComparison.OrdinalIgnoreCase)))
{
byte[] payload = AsbPayloadDebug.SerializeItemsForDebug(tag);
Console.WriteLine($"register_payload_len={payload.Length}");
Console.WriteLine($"register_payload_b64={Convert.ToBase64String(payload)}");
return;
}
if (probeConnectFailure)
{
try
{
using MxAsbDataClient connectFailureClient = MxAsbDataClient.Connect(endpoint, solution, Console.WriteLine, dumpMessages);
Console.WriteLine("connect_failure_observed=False");
}
catch (Exception ex)
{
Console.WriteLine("connect_failure_observed=True");
Console.WriteLine($"connect_failure_exception={FormatException(ex)}");
if (ex.InnerException is not null)
{
Console.WriteLine($"connect_failure_inner_exception={FormatException(ex.InnerException)}");
}
}
return;
}
if (compatibilitySubscribe)
{
RunCompatibilitySubscribe(endpoint, solution, tags, dumpMessages, publishCount, subscribeSampleMs, publishDelayMs);
return;
}
using MxAsbDataClient client = MxAsbDataClient.Connect(endpoint, solution, Console.WriteLine, dumpMessages);
int publishedEventCount = 0;
client.PublishedValueReceived += (_, value) =>
{
publishedEventCount++;
Console.WriteLine($"published_event[{publishedEventCount - 1}]=item:{value.ItemName ?? string.Empty} id:{value.ItemId} type:{value.VariantType} quality:{FormatNullableHex(value.Quality)} timestamp:{value.TimestampUtc:O} preview:{value.Preview}");
};
Console.WriteLine("connect=True");
RegisterItemsResponse register = client.RegisterMany(tags);
Console.WriteLine($"register_error=0x{register.Result.ErrorCode:X8} status=0x{register.Result.Status:X8} specific=0x{register.Result.SpecificErrorCode:X8}");
PrintStatuses("register_status", register.Status);
ItemIdentity[] registeredItems = register.Status?.Select(status => status.Item).ToArray() ?? [];
IReadOnlyDictionary<ulong, string> itemNamesById = AsbPublishMapper.CreateItemNameMap(register.Status);
ReadResponse read = client.ReadMany(tags);
Console.WriteLine($"read_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("read_status", read.Status);
PrintValues("read_value", read.Values);
if (probeReconnect)
{
AsbReconnectResult reconnect = client.Reconnect(new AsbReconnectOptions
{
MaxAttempts = reconnectAttempts,
Delay = TimeSpan.FromMilliseconds(reconnectDelayMs),
CleanupOptions = new AsbClientCleanupOptions
{
DisconnectTimeout = TimeSpan.FromMilliseconds(cleanupDisconnectTimeoutMs),
CloseTimeout = TimeSpan.FromMilliseconds(cleanupCloseTimeoutMs),
},
});
Console.WriteLine($"reconnect_succeeded={reconnect.Succeeded} attempts={reconnect.Attempts.Count}");
PrintCleanup("reconnect_cleanup", reconnect.CleanupResult);
for (int i = 0; i < reconnect.Attempts.Count; i++)
{
AsbReconnectAttempt attempt = reconnect.Attempts[i];
Console.WriteLine($"reconnect_attempt[{i}]=attempt:{attempt.Attempt} succeeded:{attempt.Succeeded} exception:{attempt.Exception?.GetType().Name ?? string.Empty}");
}
if (reconnect.Client is not null)
{
using MxAsbDataClient reconnectedClient = reconnect.Client;
RegisterItemsResponse reconnectRegister = reconnectedClient.RegisterMany(tags);
Console.WriteLine($"reconnect_register_error=0x{reconnectRegister.Result.ErrorCode:X8} status=0x{reconnectRegister.Result.Status:X8} specific=0x{reconnectRegister.Result.SpecificErrorCode:X8}");
PrintStatuses("reconnect_register_status", reconnectRegister.Status);
ReadResponse reconnectRead = reconnectedClient.ReadMany(tags);
Console.WriteLine($"reconnect_read_error=0x{reconnectRead.Result.ErrorCode:X8} status=0x{reconnectRead.Result.Status:X8} specific=0x{reconnectRead.Result.SpecificErrorCode:X8}");
PrintStatuses("reconnect_read_status", reconnectRead.Status);
PrintValues("reconnect_read_value", reconnectRead.Values);
}
return;
}
if (probeCanceledCleanup)
{
AsbClientCleanupResult cleanup = client.Cleanup(new AsbClientCleanupOptions
{
DisconnectTimeout = TimeSpan.FromMilliseconds(cleanupDisconnectTimeoutMs),
CloseTimeout = TimeSpan.FromMilliseconds(cleanupCloseTimeoutMs),
CancellationToken = new CancellationToken(canceled: true),
});
PrintCleanup("canceled_cleanup", cleanup);
return;
}
if (probeInvalidTargets)
{
RunInvalidTargetProbe(client, invalidTag, errorWriteCompleteTimeoutMs, errorWriteCompletePollMs);
}
if (probeWrongTypeWrite)
{
RunWrongTypeWriteProbe(client, wrongTypeWriteTag, errorWriteCompleteTimeoutMs, errorWriteCompletePollMs);
}
if (probeInvalidCleanup)
{
RunInvalidCleanupProbe(client, subscribeSampleMs);
}
if (probeEmptyPublish)
{
RunEmptyPublishProbe(client, errorPublishCount, subscribeSampleMs, publishDelayMs);
}
if (probeOperationCompleteCandidates)
{
RunOperationCompleteCandidateProbe(client, tags, subscribeSampleMs, publishDelayMs);
return;
}
if (subscribe)
{
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
AddMonitoredItemsResponse add = client.AddMonitoredItems(subscriptionId, tags, (ulong)subscribeSampleMs, buffered: subscribeBuffered);
Console.WriteLine($"add_monitored_error=0x{add.Result.ErrorCode:X8} status=0x{add.Result.Status:X8} specific=0x{add.Result.SpecificErrorCode:X8}");
PrintStatuses("add_monitored_status", add.Status);
itemNamesById = AsbPublishMapper.CreateItemNameMap(register.Status, add.Status);
ItemIdentity[] monitoredItems = add.Status?.Select(status => status.Item).ToArray() ?? [];
for (int i = 0; i < publishCount; i++)
{
if (i > 0 && publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
AsbPublishResult mapped = client.PublishValues(subscriptionId);
PublishResponse publish = mapped.Response;
Console.WriteLine($"publish[{i}]_error=0x{publish.Result.ErrorCode:X8} status=0x{publish.Result.Status:X8} specific=0x{publish.Result.SpecificErrorCode:X8}");
PrintStatuses($"publish[{i}]_status", publish.Status);
PrintMonitoredValues($"publish[{i}]_value", publish.Values);
PrintPublishedValues($"publish[{i}]_mapped", mapped.Values);
}
if (monitoredItems.Length > 0)
{
DeleteMonitoredItemsResponse deleteItems = client.DeleteMonitoredItems(subscriptionId, monitoredItems);
Console.WriteLine($"delete_monitored_error=0x{deleteItems.Result.ErrorCode:X8} status=0x{deleteItems.Result.Status:X8} specific=0x{deleteItems.Result.SpecificErrorCode:X8}");
PrintStatuses("delete_monitored_status", deleteItems.Status);
}
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"delete_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
}
}
}
if (writeVariant.HasValue)
{
const uint writeHandle = 0xA5B21001;
WriteResponse write = client.Write(tag, writeVariant.Value, writeHandle, "MxAsbClient probe write");
Console.WriteLine($"write_error=0x{write.Result.ErrorCode:X8} status=0x{write.Result.Status:X8} specific=0x{write.Result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("write_status", write.Status);
if (waitWriteComplete)
{
AsbWriteCompletionOptions options = new()
{
Timeout = TimeSpan.FromMilliseconds(writeCompleteTimeoutMs),
PollInterval = TimeSpan.FromMilliseconds(writeCompletePollMs),
ReadbackDelay = TimeSpan.FromMilliseconds(writeReadbackDelayMs),
};
AsbWriteCompletionReadbackResult completionAndReadback = client.WaitForWriteCompleteAndRead(tag, writeHandle, options);
AsbWriteCompletionResult completion = completionAndReadback.Completion;
Console.WriteLine($"write_completion handle=0x{completion.WriteHandle:X8} completed={completion.Completed} timed_out={completion.TimedOut} elapsed_ms={(long)completion.Elapsed.TotalMilliseconds} polls={completion.PollCount} raw_count={completion.CompleteWrites.Count}");
for (int i = 0; i < completion.Responses.Count; i++)
{
PublishWriteCompleteResponse response = completion.Responses[i];
Console.WriteLine($"write_completion_poll[{i}]_error=0x{response.Result.ErrorCode:X8} status=0x{response.Result.Status:X8} specific=0x{response.Result.SpecificErrorCode:X8} count={response.CompleteWrites?.Length ?? 0}");
}
PrintWriteCompletes("write_completion_raw", completion.CompleteWrites);
if (completion.MatchingComplete.HasValue)
{
PrintWriteCompletes("write_completion_match", [completion.MatchingComplete.Value]);
}
if (writeReadbackDelayMs > 0 && completion.Completed)
{
Console.WriteLine($"read_after_write_delay_ms={writeReadbackDelayMs}");
}
if (completionAndReadback.Readback is not null)
{
read = completionAndReadback.Readback;
Console.WriteLine($"read_after_write_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("read_after_write_status", read.Status);
PrintValues("read_after_write_value", read.Values);
}
}
else
{
PublishWriteCompleteResponse complete = client.PublishWriteComplete();
Console.WriteLine($"publish_write_complete_error=0x{complete.Result.ErrorCode:X8} status=0x{complete.Result.Status:X8} specific=0x{complete.Result.SpecificErrorCode:X8}");
Console.WriteLine($"publish_write_complete_count={complete.CompleteWrites?.Length ?? 0}");
PrintWriteCompletes("publish_write_complete", complete.CompleteWrites ?? []);
if (writeReadbackDelayMs > 0)
{
Console.WriteLine($"read_after_write_delay_ms={writeReadbackDelayMs}");
Thread.Sleep(TimeSpan.FromMilliseconds(writeReadbackDelayMs));
}
read = client.Read(tag);
Console.WriteLine($"read_after_write_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("read_after_write_status", read.Status);
PrintValues("read_after_write_value", read.Values);
}
}
UnregisterItemsResponse unregister = registeredItems.Length > 0
? client.UnregisterMany(registeredItems)
: client.Unregister(tag);
Console.WriteLine($"unregister_error=0x{unregister.Result.ErrorCode:X8} status=0x{unregister.Result.Status:X8} specific=0x{unregister.Result.SpecificErrorCode:X8}");
PrintStatuses("unregister_status", unregister.Status);
static void PrintStatuses(string prefix, ItemStatus[]? statuses)
{
if (statuses is null)
{
return;
}
for (int i = 0; i < statuses.Length; i++)
{
ItemStatus status = statuses[i];
AsbItemStatusSummary summary = AsbResultMapper.ToItemSummary(status);
Console.WriteLine($"{prefix}[{i}]=item:{status.Item.Name} id:{status.Item.Id} id_specified:{status.Item.IdSpecified} error:0x{status.ErrorCode:X8} error_name:{summary.Error} error_specified:{status.ErrorCodeSpecified} status_count:{status.Status.Count} status_payload_len:{status.Status.Payload?.Length ?? 0} status:{FormatStatusElements(summary.Status)}");
}
}
static void PrintValues(string prefix, RuntimeValue[]? values)
{
if (values is null)
{
return;
}
for (int i = 0; i < values.Length; i++)
{
RuntimeValue value = values[i];
Console.WriteLine($"{prefix}[{i}]=type:{value.Value.Type} length:{value.Value.Length} payload_len:{value.Value.Payload?.Length ?? 0} preview:{MxAsbDataClient.FormatVariant(value.Value)}");
Console.WriteLine($"{prefix}[{i}].timestamp={value.Timestamp:o} timestamp_specified={value.TimestampSpecified}");
Console.WriteLine($"{prefix}[{i}].status_count={value.Status.Count} status_payload_len={value.Status.Payload?.Length ?? 0} status:{FormatStatusElements(AsbPublishMapper.DecodeStatus(value.Status))}");
}
}
static void PrintMonitoredValues(string prefix, MonitoredItemValue[]? values)
{
if (values is null)
{
return;
}
for (int i = 0; i < values.Length; i++)
{
MonitoredItemValue item = values[i];
RuntimeValue value = item.Value;
Console.WriteLine($"{prefix}[{i}]=item:{item.Item.Name} id:{item.Item.Id} id_specified:{item.Item.IdSpecified} type:{value.Value.Type} length:{value.Value.Length} payload_len:{value.Value.Payload?.Length ?? 0} preview:{MxAsbDataClient.FormatVariant(value.Value)}");
Console.WriteLine($"{prefix}[{i}].timestamp={value.Timestamp:o} timestamp_specified={value.TimestampSpecified}");
Console.WriteLine($"{prefix}[{i}].status_count={value.Status.Count} status_payload_len={value.Status.Payload?.Length ?? 0}");
Console.WriteLine($"{prefix}[{i}].userdata_type={item.UserData.Type} userdata_length={item.UserData.Length} userdata_payload_len={item.UserData.Payload?.Length ?? 0}");
}
}
static void PrintPublishedValues(string prefix, IReadOnlyList<AsbPublishedValue> values)
{
for (int i = 0; i < values.Count; i++)
{
AsbPublishedValue value = values[i];
Console.WriteLine($"{prefix}[{i}]=item:{value.ItemName ?? string.Empty} id:{value.ItemId} type:{value.VariantType} quality:{FormatNullableHex(value.Quality)} timestamp:{value.TimestampUtc:O} preview:{value.Preview}");
Console.WriteLine($"{prefix}[{i}].status={FormatStatusElements(value.Status)} raw_count:{value.RawStatus.Count} raw_payload_len:{value.RawStatus.Payload?.Length ?? 0}");
}
}
static void PrintWriteCompletes(string prefix, IReadOnlyList<ItemWriteComplete> writes)
{
for (int i = 0; i < writes.Count; i++)
{
ItemWriteComplete item = writes[i];
Console.WriteLine($"{prefix}[{i}]=handle:{item.WriteHandle} handle_hex:0x{item.WriteHandle:X8} handle_specified:{item.WriteHandleSpecified} status_items:{item.Status?.Length ?? 0}");
PrintStatuses($"{prefix}[{i}].status", item.Status);
}
}
static void PrintPublishWriteComplete(string prefix, PublishWriteCompleteResponse response)
{
Console.WriteLine($"{prefix}_write_complete_error=0x{response.Result.ErrorCode:X8} status=0x{response.Result.Status:X8} specific=0x{response.Result.SpecificErrorCode:X8} count={response.CompleteWrites?.Length ?? 0}");
PrintWriteCompletes($"{prefix}_write_complete", response.CompleteWrites ?? []);
}
static string FormatStatusElements(IReadOnlyList<AsbStatusElement> status)
{
return status.Count == 0
? string.Empty
: string.Join("|", status.Select(item => $"{item.Type}:{item.Value}"));
}
static string FormatNullableHex(ushort? value)
{
return value.HasValue ? $"0x{value.Value:X4}" : string.Empty;
}
static void RunInvalidTargetProbe(MxAsbDataClient client, string invalidTag, int writeCompleteTimeoutMs, int writeCompletePollMs)
{
Console.WriteLine($"probe_invalid_targets tag={invalidTag}");
RegisterItemsResponse register = client.RegisterMany([invalidTag]);
Console.WriteLine($"invalid_register_error=0x{register.Result.ErrorCode:X8} status=0x{register.Result.Status:X8} specific=0x{register.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_register_status", register.Status);
ReadResponse read = client.ReadMany([invalidTag]);
Console.WriteLine($"invalid_read_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_read_status", read.Status);
PrintValues("invalid_read_value", read.Values);
const uint writeHandle = 0xA5B2E001;
WriteResponse write = client.Write(invalidTag, AsbVariantFactory.FromInt32(1), writeHandle, "MxAsbClient probe invalid-target write");
Console.WriteLine($"invalid_write_error=0x{write.Result.ErrorCode:X8} status=0x{write.Result.Status:X8} specific=0x{write.Result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("invalid_write_status", write.Status);
PrintWriteCompletionProbe(client, "invalid_write_completion", writeHandle, writeCompleteTimeoutMs, writeCompletePollMs);
ItemIdentity[] registeredItems = register.Status?.Select(status => status.Item).ToArray() ?? [];
UnregisterItemsResponse unregister = registeredItems.Length > 0
? client.UnregisterMany(registeredItems)
: client.Unregister(invalidTag);
Console.WriteLine($"invalid_unregister_error=0x{unregister.Result.ErrorCode:X8} status=0x{unregister.Result.Status:X8} specific=0x{unregister.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_unregister_status", unregister.Status);
}
static void RunWrongTypeWriteProbe(MxAsbDataClient client, string tag, int writeCompleteTimeoutMs, int writeCompletePollMs)
{
Console.WriteLine($"probe_wrong_type_write tag={tag}");
const uint writeHandle = 0xA5B2E002;
WriteResponse write = client.Write(tag, AsbVariantFactory.FromString("wrong-type-write-probe"), writeHandle, "MxAsbClient probe wrong-type write");
Console.WriteLine($"wrong_type_write_error=0x{write.Result.ErrorCode:X8} status=0x{write.Result.Status:X8} specific=0x{write.Result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("wrong_type_write_status", write.Status);
PrintWriteCompletionProbe(client, "wrong_type_write_completion", writeHandle, writeCompleteTimeoutMs, writeCompletePollMs);
ReadResponse read = client.Read(tag);
Console.WriteLine($"wrong_type_read_after_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("wrong_type_read_after_status", read.Status);
PrintValues("wrong_type_read_after_value", read.Values);
}
static void PrintWriteCompletionProbe(
MxAsbDataClient client,
string prefix,
uint writeHandle,
int writeCompleteTimeoutMs,
int writeCompletePollMs)
{
AsbWriteCompletionOptions options = new()
{
Timeout = TimeSpan.FromMilliseconds(writeCompleteTimeoutMs),
PollInterval = TimeSpan.FromMilliseconds(writeCompletePollMs),
};
AsbWriteCompletionResult completion = client.WaitForWriteComplete(writeHandle, options);
Console.WriteLine($"{prefix} handle=0x{completion.WriteHandle:X8} completed={completion.Completed} timed_out={completion.TimedOut} elapsed_ms={(long)completion.Elapsed.TotalMilliseconds} polls={completion.PollCount} raw_count={completion.CompleteWrites.Count}");
for (int i = 0; i < completion.Responses.Count; i++)
{
PublishWriteCompleteResponse response = completion.Responses[i];
Console.WriteLine($"{prefix}_poll[{i}]_error=0x{response.Result.ErrorCode:X8} status=0x{response.Result.Status:X8} specific=0x{response.Result.SpecificErrorCode:X8} count={response.CompleteWrites?.Length ?? 0}");
}
PrintWriteCompletes($"{prefix}_raw", completion.CompleteWrites);
if (completion.MatchingComplete.HasValue)
{
PrintWriteCompletes($"{prefix}_match", [completion.MatchingComplete.Value]);
}
}
static void RunInvalidCleanupProbe(MxAsbDataClient client, int subscribeSampleMs)
{
Console.WriteLine("probe_invalid_cleanup=True");
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"invalid_cleanup_create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
ItemIdentity invalidItem = CreateInvalidItemIdentity();
DeleteMonitoredItemsResponse deleteItems = client.DeleteMonitoredItems(subscriptionId, [invalidItem]);
Console.WriteLine($"invalid_cleanup_delete_monitored_error=0x{deleteItems.Result.ErrorCode:X8} status=0x{deleteItems.Result.Status:X8} specific=0x{deleteItems.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_cleanup_delete_monitored_status", deleteItems.Status);
long invalidSubscriptionId = subscriptionId == long.MaxValue ? subscriptionId - 1 : subscriptionId + 987654321;
DeleteSubscriptionResponse deleteInvalid = client.DeleteSubscription(invalidSubscriptionId);
Console.WriteLine($"invalid_cleanup_delete_subscription_error=0x{deleteInvalid.Result.ErrorCode:X8} status=0x{deleteInvalid.Result.Status:X8} specific=0x{deleteInvalid.Result.SpecificErrorCode:X8} subscription_id={invalidSubscriptionId}");
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"invalid_cleanup_delete_valid_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
}
}
}
static void RunEmptyPublishProbe(MxAsbDataClient client, int publishCount, int subscribeSampleMs, int publishDelayMs)
{
Console.WriteLine("probe_empty_publish=True");
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"empty_publish_create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
for (int i = 0; i < publishCount; i++)
{
if (i > 0 && publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
AsbPublishResult mapped = client.PublishValues(subscriptionId);
PublishResponse publish = mapped.Response;
Console.WriteLine($"empty_publish[{i}]_error=0x{publish.Result.ErrorCode:X8} status=0x{publish.Result.Status:X8} specific=0x{publish.Result.SpecificErrorCode:X8}");
PrintStatuses($"empty_publish[{i}]_status", publish.Status);
PrintMonitoredValues($"empty_publish[{i}]_value", publish.Values);
PrintPublishedValues($"empty_publish[{i}]_mapped", mapped.Values);
}
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"empty_publish_delete_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
}
}
}
static void RunOperationCompleteCandidateProbe(MxAsbDataClient client, string[] tags, int subscribeSampleMs, int publishDelayMs)
{
Console.WriteLine("probe_operation_complete_candidates=True");
PrintPublishWriteComplete("operation_candidate_initial", client.PublishWriteComplete());
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"operation_candidate_create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
PrintPublishWriteComplete("operation_candidate_after_create_subscription", client.PublishWriteComplete());
AddMonitoredItemsResponse add = client.AddMonitoredItems(subscriptionId, tags, (ulong)subscribeSampleMs);
Console.WriteLine($"operation_candidate_add_monitored_error=0x{add.Result.ErrorCode:X8} status=0x{add.Result.Status:X8} specific=0x{add.Result.SpecificErrorCode:X8}");
PrintStatuses("operation_candidate_add_monitored_status", add.Status);
PrintPublishWriteComplete("operation_candidate_after_add_monitored", client.PublishWriteComplete());
if (publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
PublishResponse publish = client.Publish(subscriptionId);
Console.WriteLine($"operation_candidate_publish_error=0x{publish.Result.ErrorCode:X8} status=0x{publish.Result.Status:X8} specific=0x{publish.Result.SpecificErrorCode:X8}");
PrintStatuses("operation_candidate_publish_status", publish.Status);
PrintMonitoredValues("operation_candidate_publish_value", publish.Values);
PrintPublishWriteComplete("operation_candidate_after_publish", client.PublishWriteComplete());
ItemIdentity[] monitoredItems = add.Status?.Select(status => status.Item).ToArray() ?? [];
if (monitoredItems.Length > 0)
{
DeleteMonitoredItemsResponse deleteItems = client.DeleteMonitoredItems(subscriptionId, monitoredItems);
Console.WriteLine($"operation_candidate_delete_monitored_error=0x{deleteItems.Result.ErrorCode:X8} status=0x{deleteItems.Result.Status:X8} specific=0x{deleteItems.Result.SpecificErrorCode:X8}");
PrintStatuses("operation_candidate_delete_monitored_status", deleteItems.Status);
PrintPublishWriteComplete("operation_candidate_after_delete_monitored", client.PublishWriteComplete());
}
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"operation_candidate_delete_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
PrintPublishWriteComplete("operation_candidate_after_delete_subscription", client.PublishWriteComplete());
}
}
}
static ItemIdentity CreateInvalidItemIdentity()
{
return new ItemIdentity
{
Type = (ushort)ItemIdentityType.Name,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Name = "DefinitelyMissingObject.DefinitelyMissingAttribute",
ContextName = string.Empty,
Id = ulong.MaxValue,
IdSpecified = true,
};
}
static void RunCompatibilitySubscribe(
string endpoint,
string? solution,
string[] tags,
bool dumpMessages,
int publishCount,
int subscribeSampleMs,
int publishDelayMs)
{
using MxAsbCompatibilityServer server = new();
int dataChangeCount = 0;
server.DataChanged += (_, evt) =>
{
dataChangeCount++;
Console.WriteLine($"compat_data_change[{dataChangeCount - 1}]=server:{evt.ServerHandle} item:{evt.ItemHandle} quality:0x{evt.Quality:X4} timestamp:{evt.TimestampUtc:O} value:{FormatObject(evt.Value)} status:{FormatStatusElements(evt.Status)}");
};
int serverHandle = server.Register(endpoint, solution, Console.WriteLine, dumpMessages);
Console.WriteLine($"compat_register_server={serverHandle}");
AsbSubscriptionOptions subscriptionOptions = new()
{
MaxQueueSize = 128,
SampleInterval = (ulong)subscribeSampleMs,
};
AsbMonitoredItemOptions monitoredItemOptions = new()
{
SampleInterval = (ulong)subscribeSampleMs,
};
int[] itemHandles = tags.Select(tag =>
{
int itemHandle = server.AddItem(serverHandle, tag);
Console.WriteLine($"compat_add_item tag:{tag} item:{itemHandle}");
server.Advise(serverHandle, itemHandle, subscriptionOptions, monitoredItemOptions);
Console.WriteLine($"compat_advise item:{itemHandle}");
return itemHandle;
}).ToArray();
for (int i = 0; i < publishCount; i++)
{
if (i > 0 && publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
AsbPublishResult result = server.Poll(serverHandle);
Console.WriteLine($"compat_poll[{i}] error=0x{result.Response.Result.ErrorCode:X8} values:{result.Values.Count}");
}
foreach (int itemHandle in itemHandles)
{
server.RemoveItem(serverHandle, itemHandle);
Console.WriteLine($"compat_remove_item item:{itemHandle}");
}
server.Unregister(serverHandle);
Console.WriteLine($"compat_unregister_server={serverHandle} data_changes={dataChangeCount}");
}
static string FormatObject(object? value)
{
return value switch
{
null => string.Empty,
Array array => string.Join(",", array.Cast<object?>()),
DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture),
TimeSpan timeSpan => timeSpan.ToString("c", CultureInfo.InvariantCulture),
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? string.Empty,
};
}
static void PrintCleanup(string prefix, AsbClientCleanupResult? cleanup)
{
if (cleanup is null)
{
return;
}
Console.WriteLine($"{prefix}=completed:{cleanup.Completed} succeeded:{cleanup.Succeeded} abort:{cleanup.UsedAbortFallback} requires_new:{cleanup.RequiresNewConnection} disconnect_attempted:{cleanup.DisconnectAttempted} disconnect_sent:{cleanup.DisconnectSent} disconnect_failure:{cleanup.DisconnectFailure ?? string.Empty}");
PrintCommunicationCleanup($"{prefix}.channel", cleanup.Channel);
PrintCommunicationCleanup($"{prefix}.factory", cleanup.Factory);
}
static void PrintCommunicationCleanup(string prefix, CommunicationObjectCleanupResult cleanup)
{
Console.WriteLine($"{prefix}=initial:{cleanup.InitialState} final:{cleanup.FinalState} close_attempted:{cleanup.CloseAttempted} closed:{cleanup.Closed} abort_attempted:{cleanup.AbortAttempted} aborted:{cleanup.Aborted} close_failure:{cleanup.CloseFailure ?? string.Empty} abort_failure:{cleanup.AbortFailure ?? string.Empty}");
}
static string? GetArg(string[] args, string name)
{
string prefix = name + "=";
return args.FirstOrDefault(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))?.Substring(prefix.Length);
}
static string[] GetArgs(string[] args, string name)
{
string prefix = name + "=";
return args
.Where(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(arg => arg.Substring(prefix.Length))
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
}
static int? TryGetInt(string[] args, string name)
{
return int.TryParse(GetArg(args, name), out int result) ? result : null;
}
static Variant? GetWriteVariant(string[] args)
{
int? writeInt = TryGetInt(args, "--write-int");
if (writeInt.HasValue)
{
return AsbVariantFactory.FromInt32(writeInt.Value);
}
string? writeBool = GetArg(args, "--write-bool");
if (bool.TryParse(writeBool, out bool boolResult))
{
return AsbVariantFactory.FromBoolean(boolResult);
}
string? writeFloat = GetArg(args, "--write-float");
if (float.TryParse(writeFloat, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatResult))
{
return AsbVariantFactory.FromSingle(floatResult);
}
string? writeDouble = GetArg(args, "--write-double");
if (double.TryParse(writeDouble, NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleResult))
{
return AsbVariantFactory.FromDouble(doubleResult);
}
string? writeString = GetArg(args, "--write-string");
if (writeString is not null)
{
return AsbVariantFactory.FromString(writeString);
}
string? writeDateTime = GetArg(args, "--write-datetime");
if (DateTime.TryParse(
writeDateTime,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out DateTime dateTimeResult))
{
return AsbVariantFactory.FromDateTime(dateTimeResult);
}
if (TryParseArray(args, "--write-int-array", int.TryParse, out int[] intArray))
{
return AsbVariantFactory.FromInt32Array(intArray);
}
if (TryParseArray(args, "--write-bool-array", bool.TryParse, out bool[] boolArray))
{
return AsbVariantFactory.FromBooleanArray(boolArray);
}
if (TryParseFloatArray(args, "--write-float-array", out float[] floatArray))
{
return AsbVariantFactory.FromSingleArray(floatArray);
}
if (TryParseDoubleArray(args, "--write-double-array", out double[] doubleArray))
{
return AsbVariantFactory.FromDoubleArray(doubleArray);
}
string? writeStringArray = GetArg(args, "--write-string-array");
if (writeStringArray is not null)
{
return AsbVariantFactory.FromStringArray(writeStringArray.Split('|'));
}
if (TryParseDateTimeArray(args, "--write-datetime-array", out DateTime[] dateTimeArray))
{
return AsbVariantFactory.FromDateTimeArray(dateTimeArray);
}
return null;
}
static bool TryParseArray<T>(string[] args, string name, TryParse<T> parser, out T[] values)
{
string? raw = GetArg(args, name);
if (raw is null)
{
values = [];
return false;
}
string[] parts = raw.Split(',', StringSplitOptions.TrimEntries);
values = new T[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!parser(parts[i], out values[i]))
{
values = [];
return false;
}
}
return true;
}
static bool TryParseFloatArray(string[] args, string name, out float[] values)
{
return TryParseArray(args, name, TryParseFloat, out values);
}
static bool TryParseDoubleArray(string[] args, string name, out double[] values)
{
return TryParseArray(args, name, TryParseDouble, out values);
}
static bool TryParseDateTimeArray(string[] args, string name, out DateTime[] values)
{
return TryParseArray(args, name, TryParseDateTime, out values);
}
static bool TryParseFloat(string value, out float result)
{
return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result);
}
static bool TryParseDouble(string value, out double result)
{
return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result);
}
static bool TryParseDateTime(string value, out DateTime result)
{
return DateTime.TryParse(
value,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out result);
}
static bool HasArg(string[] args, string name)
{
return args.Any(arg => arg.Equals(name, StringComparison.OrdinalIgnoreCase));
}
static string FormatException(Exception ex)
{
return $"{ex.GetType().Name}:0x{ex.HResult:X8}:{ex.Message}";
}
delegate bool TryParse<T>(string value, out T result);
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MxAsbClient\MxAsbClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</Project>
+779
View File
@@ -0,0 +1,779 @@
using MxAsbClient;
using System.ServiceModel;
RunVariantFactoryTests();
RunMonitoredItemValueTests();
RunPublishMapperTests();
RunCompatibilitySurfaceTests();
RunConnectionOptionsTests();
RunWriteOptionsTests();
RunCollectionArgumentGuardTests();
RunResultMapperTests();
RunWriteCompletionOptionsTests();
RunSubscriptionOptionsTests();
RunCleanupOptionsTests();
RunCommunicationCleanupTests();
RunReconnectSurfaceTests();
Console.WriteLine("MxAsbClient ASB contract tests passed.");
static void RunVariantFactoryTests()
{
AssertVariant("bool", AsbDataType.TypeBool, 1, AsbVariantFactory.FromBoolean(true), true);
AssertVariant("int", AsbDataType.TypeInt32, 4, AsbVariantFactory.FromInt32(123), 123);
AssertVariant("float", AsbDataType.TypeFloat, 4, AsbVariantFactory.FromSingle(1.25f), 1.25f);
AssertVariant("double", AsbDataType.TypeDouble, 8, AsbVariantFactory.FromDouble(1.125d), 1.125d);
AssertVariant("string", AsbDataType.TypeString, 10, AsbVariantFactory.FromString("Alpha"), "Alpha");
DateTime timestamp = new(2026, 4, 26, 12, 34, 56, DateTimeKind.Utc);
AssertVariant("datetime", AsbDataType.TypeDateTime, 8, AsbVariantFactory.FromDateTime(timestamp), timestamp);
TimeSpan duration = TimeSpan.FromSeconds(12.5);
AssertVariant("duration", AsbDataType.TypeDuration, 8, AsbVariantFactory.FromDuration(duration), duration);
AssertVariant("int[]", AsbDataType.TypeInt32Array, 12, AsbVariantFactory.FromInt32Array([1, 2, 3]), new[] { 1, 2, 3 });
AssertVariant("bool[]", AsbDataType.TypeBoolArray, 3, AsbVariantFactory.FromBooleanArray([true, false, true]), new[] { true, false, true });
AssertVariant("float[]", AsbDataType.TypeFloatArray, 8, AsbVariantFactory.FromSingleArray([1.25f, 2.5f]), new[] { 1.25f, 2.5f });
AssertVariant("double[]", AsbDataType.TypeDoubleArray, 16, AsbVariantFactory.FromDoubleArray([1.125d, 2.25d]), new[] { 1.125d, 2.25d });
AssertVariant("string[]", AsbDataType.TypeStringArray, 18, AsbVariantFactory.FromStringArray(["A", "", "BC"]), new[] { "A", "", "BC" });
AssertVariant("datetime[]", AsbDataType.TypeDateTimeArray, 16, AsbVariantFactory.FromDateTimeArray([timestamp, timestamp.AddMinutes(1)]), new[] { timestamp, timestamp.AddMinutes(1) });
AssertVariant("duration[]", AsbDataType.TypeDurationArray, 16, AsbVariantFactory.FromDurationArray([duration, duration.Add(TimeSpan.FromSeconds(1))]), new[] { duration, duration.Add(TimeSpan.FromSeconds(1)) });
}
static void AssertVariant<T>(string name, AsbDataType type, int length, Variant variant, T expected)
{
AssertEqual(name + " type", (ushort)type, variant.Type);
AssertEqual(name + " length", length, variant.Length);
AssertEqual(name + " payload length", length, variant.Payload?.Length ?? 0);
object? decoded = MxAsbDataClient.DecodeVariant(variant);
AssertValue(name + " decoded", expected, decoded);
}
static void AssertValue<T>(string name, T expected, object? actual)
{
if (expected is Array expectedArray)
{
if (actual is not Array actualArray || expectedArray.Length != actualArray.Length)
{
throw new InvalidOperationException($"{name}: array mismatch.");
}
for (int i = 0; i < expectedArray.Length; i++)
{
object? expectedItem = expectedArray.GetValue(i);
object? actualItem = actualArray.GetValue(i);
if (!Equals(expectedItem, actualItem))
{
throw new InvalidOperationException($"{name}[{i}]: expected {expectedItem}, got {actualItem}.");
}
}
return;
}
if (!Equals(expected, actual))
{
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
}
}
static void AssertEqual<T>(string name, T expected, T actual)
{
if (!Equals(expected, actual))
{
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
}
}
static void AssertNotNull(string name, object? value)
{
if (value is null)
{
throw new InvalidOperationException($"{name}: expected non-null.");
}
}
static void AssertThrows<TException>(string name, Action action)
where TException : Exception
{
try
{
action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException($"{name}: expected {typeof(TException).Name}.");
}
static void RunMonitoredItemValueTests()
{
MonitoredItemValue original = new()
{
Item = new ItemIdentity
{
Type = (ushort)ItemIdentityType.Name,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Name = "TestChildObject.TestInt",
ContextName = string.Empty,
Id = 123,
IdSpecified = true,
},
Value = new RuntimeValue
{
Timestamp = new DateTime(2026, 4, 26, 16, 0, 0, DateTimeKind.Utc),
TimestampSpecified = true,
Value = AsbVariantFactory.FromInt32(412),
Status = new AsbStatus { Count = 0, Payload = [] },
},
UserData = AsbVariantFactory.Empty,
};
using MemoryStream stream = new();
using BinaryWriter writer = new(stream, System.Text.Encoding.UTF8, leaveOpen: true);
original.WriteToStream(writer);
writer.Flush();
stream.Position = 0;
using BinaryReader reader = new(stream, System.Text.Encoding.UTF8, leaveOpen: true);
MonitoredItemValue decoded = new();
decoded.InitializeFromStream(reader);
AssertEqual("monitored item name", original.Item.Name, decoded.Item.Name);
AssertEqual("monitored item id", original.Item.Id, decoded.Item.Id);
AssertEqual("monitored value timestamp", original.Value.Timestamp, decoded.Value.Timestamp);
AssertValue("monitored value decoded", 412, MxAsbDataClient.DecodeVariant(decoded.Value.Value));
}
static void RunPublishMapperTests()
{
AsbStatus status = new()
{
Count = 7,
Payload = [0x85, 0x06, 0x10, 0x00, 0x07, 0xC0, 0x00],
};
IReadOnlyList<AsbStatusElement> elements = AsbPublishMapper.DecodeStatus(status);
AssertEqual("status count", 3, elements.Count);
AssertEqual("status category type", AsbStatusElementType.MxStatusCategory, elements[0].Type);
AssertEqual("status category value", (ushort)0, elements[0].Value);
AssertEqual("status detail type", AsbStatusElementType.MxStatusDetail, elements[1].Type);
AssertEqual("status detail value", (ushort)16, elements[1].Value);
AssertEqual("status quality type", AsbStatusElementType.MxQuality, elements[2].Type);
AssertEqual("status quality value", (ushort)0x00C0, elements[2].Value);
MonitoredItemValue item = new()
{
Item = new ItemIdentity
{
Type = (ushort)ItemIdentityType.Id,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Id = 456,
IdSpecified = true,
},
Value = new RuntimeValue
{
Timestamp = new DateTime(2026, 4, 26, 16, 5, 0, DateTimeKind.Utc),
TimestampSpecified = true,
Value = AsbVariantFactory.FromString("mapped"),
Status = status,
},
UserData = AsbVariantFactory.Empty,
};
AsbPublishedValue mapped = AsbPublishMapper.ToPublishedValue(item, new Dictionary<ulong, string> { [456] = "Test.Tag" });
AssertEqual("published item name", "Test.Tag", mapped.ItemName);
AssertEqual("published quality", (ushort?)0x00C0, mapped.Quality);
AssertEqual("published preview", "mapped", mapped.Preview);
AssertEqual("published value", "mapped", mapped.Value);
AssertEqual("published status summary quality", AsbStatusQuality.Good, mapped.StatusSummary.Quality);
AssertEqual("published status summary detail", AsbMxStatusDetail.RequestTimedOut, mapped.StatusSummary.Detail);
AsbPublishResult result = new(
new PublishResponse { Result = new ArchestrAResult { ErrorCode = 32, Success = false } },
[mapped]);
AssertEqual("publish result summary", AsbErrorCode.PublishComplete, result.Result.Error);
AssertEqual("publish result success-like", true, result.Result.IsSuccessLike);
AssertEqual("publish result has values", true, result.HasValues);
AsbPublishResult emptyResult = new(
new PublishResponse { Result = new ArchestrAResult { ErrorCode = 0, Success = true } },
[]);
AssertEqual("empty publish result summary", AsbErrorCode.Success, emptyResult.Result.Error);
AssertEqual("empty publish result success-like", true, emptyResult.Result.IsSuccessLike);
AssertEqual("empty publish result has values", false, emptyResult.HasValues);
}
static void RunCompatibilitySurfaceTests()
{
Type serverType = typeof(MxAsbCompatibilityServer);
AssertNotNull("asb compat data-change event", serverType.GetEvent(nameof(MxAsbCompatibilityServer.DataChanged)));
AssertNotNull("asb compat register", serverType.GetMethod(
nameof(MxAsbCompatibilityServer.Register),
[typeof(string), typeof(string), typeof(Action<string>), typeof(bool)]));
AssertNotNull("asb compat register options", serverType.GetMethod(nameof(MxAsbCompatibilityServer.Register), [typeof(AsbConnectionOptions)]));
AssertNotNull("asb compat unregister", serverType.GetMethod(nameof(MxAsbCompatibilityServer.Unregister)));
AssertNotNull("asb compat add item", serverType.GetMethod(nameof(MxAsbCompatibilityServer.AddItem)));
AssertNotNull("asb compat remove item", serverType.GetMethod(nameof(MxAsbCompatibilityServer.RemoveItem)));
AssertNotNull("asb compat advise", serverType.GetMethod(
nameof(MxAsbCompatibilityServer.Advise),
[typeof(int), typeof(int), typeof(ulong), typeof(long)]));
AssertNotNull("asb compat advise options", serverType.GetMethod(
nameof(MxAsbCompatibilityServer.Advise),
[typeof(int), typeof(int), typeof(AsbSubscriptionOptions), typeof(AsbMonitoredItemOptions)]));
AssertNotNull("asb compat poll", serverType.GetMethod(nameof(MxAsbCompatibilityServer.Poll)));
AssertNotNull("asb buffered add monitored", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.AddMonitoredItems),
[typeof(long), typeof(IEnumerable<string>), typeof(ulong), typeof(bool), typeof(bool)]));
AssertNotNull("asb options add monitored", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.AddMonitoredItems),
[typeof(long), typeof(IEnumerable<string>), typeof(AsbMonitoredItemOptions)]));
MxAsbDataChangeEvent dataChange = new(
ServerHandle: 1,
ItemHandle: 2,
Value: 412,
Quality: 0x00C0,
TimestampUtc: new DateTime(2026, 4, 26, 17, 0, 0, DateTimeKind.Utc),
Status:
[
new AsbStatusElement(AsbStatusElementType.MxStatusCategory, 0),
new AsbStatusElement(AsbStatusElementType.MxStatusDetail, 0),
new AsbStatusElement(AsbStatusElementType.MxQuality, 0x00C0),
]);
AssertEqual("compat status summary quality", AsbStatusQuality.Good, dataChange.StatusSummary.Quality);
AssertEqual("compat status summary error", AsbErrorCode.Success, dataChange.StatusSummary.StatusError);
using MxAsbCompatibilityServer server = new();
AssertThrows<ArgumentException>("unknown asb server handle", () => server.AddItem(404, "TestChildObject.TestInt"));
}
static void RunConnectionOptionsTests()
{
AssertNotNull("asb connect options overload", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.Connect),
[typeof(AsbConnectionOptions)]));
AsbConnectionOptions defaultOptions = new();
AssertEqual("default endpoint", string.Empty, defaultOptions.Endpoint);
AssertEqual("default solution", null, defaultOptions.SolutionName);
AssertEqual("default trace", null, defaultOptions.Trace);
AssertEqual("default dump messages", false, defaultOptions.DumpMessages);
AsbConnectionOptions options = new()
{
Endpoint = "net.tcp://example/ASB/IDataV2",
SolutionName = "Galaxy",
Trace = _ => { },
DumpMessages = true,
};
options.Validate();
AssertEqual("connection endpoint", "net.tcp://example/ASB/IDataV2", options.Endpoint);
AssertEqual("connection solution", "Galaxy", options.SolutionName);
AssertNotNull("connection trace", options.Trace);
AssertEqual("connection dump messages", true, options.DumpMessages);
AssertThrows<ArgumentException>(
"empty endpoint options",
() => new AsbConnectionOptions { Endpoint = " " }.Validate());
AssertThrows<ArgumentNullException>(
"null options connect",
() => MxAsbDataClient.Connect((AsbConnectionOptions)null!));
AssertThrows<ArgumentException>(
"empty endpoint connect",
() => MxAsbDataClient.Connect(" "));
AssertEqual("payload debug non-public", false, typeof(AsbPayloadDebug).IsPublic);
}
static void RunWriteOptionsTests()
{
AssertNotNull("asb write positional overload", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.Write),
[typeof(string), typeof(Variant), typeof(uint), typeof(string)]));
AssertNotNull("asb write options overload", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.Write),
[typeof(string), typeof(Variant), typeof(AsbWriteOptions)]));
AsbWriteOptions defaultOptions = new();
AssertEqual("write options default handle", 0u, defaultOptions.WriteHandle);
AssertEqual("write options default comment", null, defaultOptions.Comment);
AsbWriteOptions options = new()
{
WriteHandle = 0xA5B21001,
Comment = "contract write",
};
AssertEqual("write options handle", (uint)0xA5B21001, options.WriteHandle);
AssertEqual("write options comment", "contract write", options.Comment);
MxAsbDataClient uninitializedClient =
(MxAsbDataClient)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(MxAsbDataClient));
AssertThrows<ArgumentNullException>(
"write null options",
() => uninitializedClient.Write("Test.Tag", AsbVariantFactory.Empty, (AsbWriteOptions)null!));
}
static void RunCollectionArgumentGuardTests()
{
MxAsbDataClient uninitializedClient =
(MxAsbDataClient)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(MxAsbDataClient));
AssertThrows<ArgumentNullException>(
"register many null tags",
() => uninitializedClient.RegisterMany(null!));
AssertThrows<ArgumentNullException>(
"unregister many null items",
() => uninitializedClient.UnregisterMany(null!));
AssertThrows<ArgumentNullException>(
"read many null tags",
() => uninitializedClient.ReadMany(null!));
AssertThrows<ArgumentNullException>(
"add monitored null tags",
() => uninitializedClient.AddMonitoredItems(123, null!, AsbMonitoredItemOptions.Default));
AssertThrows<ArgumentNullException>(
"delete monitored null items",
() => uninitializedClient.DeleteMonitoredItems(123, null!));
}
static void RunResultMapperTests()
{
AsbResultSummary success = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 0, Success = true });
AssertEqual("success error", AsbErrorCode.Success, success.Error);
AssertEqual("success-like success", true, success.IsSuccessLike);
AsbResultSummary publishComplete = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 32, Success = false });
AssertEqual("publish complete error", AsbErrorCode.PublishComplete, publishComplete.Error);
AssertEqual("publish complete success", false, publishComplete.IsSuccess);
AssertEqual("publish complete success-like", true, publishComplete.IsSuccessLike);
AsbResultSummary unknown = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 12345, Success = false });
AssertEqual("unknown error", AsbErrorCode.Unknown, unknown.Error);
AssertEqual("unknown raw", 12345, unknown.RawErrorCode);
AsbResultSummary outOfRange = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 65536, Success = false });
AssertEqual("out-of-range error", AsbErrorCode.Unknown, outOfRange.Error);
AssertEqual("out-of-range raw", 65536, outOfRange.RawErrorCode);
AsbItemStatusSummary item = AsbResultMapper.ToItemSummary(new ItemStatus
{
Item = new ItemIdentity { Name = "Bad.Tag", Id = 42, IdSpecified = true },
ErrorCode = 10,
Status = new AsbStatus { Count = 1, Payload = [0x85] },
});
AssertEqual("item status error", AsbErrorCode.InvalidMonitoredItems, item.Error);
AssertEqual("item status success", false, item.IsSuccess);
AssertEqual("item status element", AsbStatusElementType.MxStatusCategory, item.Status[0].Type);
AssertEqual("item status summary category", AsbMxStatusCategory.Ok, item.StatusSummary.Category);
IReadOnlyList<AsbItemStatusSummary> nullItems = AsbResultMapper.ToItemSummaries(null);
AssertEqual("item summaries null", 0, nullItems.Count);
IReadOnlyList<AsbItemStatusSummary> emptyItems = AsbResultMapper.ToItemSummaries([]);
AssertEqual("item summaries empty", 0, emptyItems.Count);
IReadOnlyList<AsbItemStatusSummary> oneItem = AsbResultMapper.ToItemSummaries(
[
new ItemStatus
{
Item = new ItemIdentity { Name = "Good.Tag", Id = 43, IdSpecified = true },
ErrorCode = 0,
Status = StatusBytes(0x05, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0xC0, 0x00),
},
]);
AssertEqual("item summaries one count", 1, oneItem.Count);
AssertEqual("item summaries one item", "Good.Tag", oneItem[0].ItemName);
AssertEqual("item summaries one error", AsbErrorCode.Success, oneItem[0].Error);
AssertEqual("item summaries one status", AsbStatusQuality.Good, oneItem[0].StatusSummary.Quality);
AsbStatusSummary good = AsbResultMapper.ToStatusSummary(StatusBytes(
0x05, 0x00, 0x00,
0x06, 0x00, 0x00,
0x07, 0xC0, 0x00));
AssertEqual("good status category", AsbMxStatusCategory.Ok, good.Category);
AssertEqual("good status detail", AsbMxStatusDetail.None, good.Detail);
AssertEqual("good status quality", AsbStatusQuality.Good, good.Quality);
AssertEqual("good status error", AsbErrorCode.Success, good.StatusError);
AssertEqual("good status success-like", true, good.IsSuccessLike);
AsbStatusSummary badQuality = AsbResultMapper.ToStatusSummary(StatusBytes(
0x05, 0x00, 0x00,
0x06, 0x00, 0x00,
0x07, 0x00, 0x00));
AssertEqual("bad quality summary", AsbStatusQuality.Bad, badQuality.Quality);
AssertEqual("bad quality raw", (ushort?)0x0000, badQuality.RawQuality);
AssertEqual("bad quality success-like", false, badQuality.IsSuccessLike);
AsbStatusSummary timedOut = AsbResultMapper.ToStatusSummary(StatusBytes(
0x05, 0x03, 0x00,
0x06, 0x10, 0x00,
0x07, 0x00, 0x00));
AssertEqual("timed out category", AsbMxStatusCategory.CommunicationError, timedOut.Category);
AssertEqual("timed out detail", AsbMxStatusDetail.RequestTimedOut, timedOut.Detail);
AssertEqual("timed out error", AsbErrorCode.RequestTimedOut, timedOut.StatusError);
AssertEqual("timed out quality", AsbStatusQuality.Bad, timedOut.Quality);
AsbStatusSummary noCommunication = AsbResultMapper.ToStatusSummary(StatusBytes(
0x05, 0x03, 0x00,
0x06, 0x11, 0x00,
0x07, 0x00, 0x00));
AssertEqual("no communication category", AsbMxStatusCategory.CommunicationError, noCommunication.Category);
AssertEqual("no communication detail", AsbMxStatusDetail.PlatformCommunicationError, noCommunication.Detail);
AssertEqual("no communication error", AsbErrorCode.BadNoCommunication, noCommunication.StatusError);
AsbStatusSummary accessDenied = AsbResultMapper.ToStatusSummary(StatusBytes(
0x05, 0x06, 0x00,
0x06, 0x21, 0x00,
0x07, 0x00, 0x00));
AssertEqual("access denied category", AsbMxStatusCategory.SecurityError, accessDenied.Category);
AssertEqual("access denied detail", AsbMxStatusDetail.WriteAccessDenied, accessDenied.Detail);
AssertEqual("access denied error", AsbErrorCode.WriteFailedAccessDenied, accessDenied.StatusError);
AssertEqual("access denied success-like", false, accessDenied.IsSuccessLike);
AsbStatusSummary unknownStatus = AsbResultMapper.ToStatusSummary(StatusBytes(
0x63, 0x77, 0x77,
0x05, 0xEE, 0x02,
0x06, 0xE7, 0x03,
0x07, 0x80, 0x00));
AssertEqual("unknown status category", AsbMxStatusCategory.Unknown, unknownStatus.Category);
AssertEqual("unknown status raw category", (ushort?)750, unknownStatus.RawCategory);
AssertEqual("unknown status detail", AsbMxStatusDetail.Unknown, unknownStatus.Detail);
AssertEqual("unknown status raw detail", (ushort?)999, unknownStatus.RawDetail);
AssertEqual("unknown status quality", AsbStatusQuality.Unknown, unknownStatus.Quality);
AssertEqual("unknown status raw quality", (ushort?)0x0080, unknownStatus.RawQuality);
AssertEqual("unknown status raw type", (AsbStatusElementType)99, unknownStatus.Elements[0].Type);
AssertEqual("unknown status raw value", (ushort)0x7777, unknownStatus.Elements[0].Value);
}
static void RunWriteCompletionOptionsTests()
{
AssertNotNull("asb wait write completion overload", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.WaitForWriteComplete),
[typeof(uint), typeof(AsbWriteCompletionOptions)]));
AssertNotNull("asb wait write completion readback overload", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.WaitForWriteCompleteAndRead),
[typeof(string), typeof(uint), typeof(AsbWriteCompletionOptions)]));
AssertEqual("write completion default timeout", TimeSpan.FromSeconds(5), AsbWriteCompletionOptions.Default.Timeout);
AssertEqual("write completion default poll", TimeSpan.FromMilliseconds(250), AsbWriteCompletionOptions.Default.PollInterval);
AssertEqual("write completion default readback", TimeSpan.Zero, AsbWriteCompletionOptions.Default.ReadbackDelay);
AssertEqual("write completion default cancellation", false, AsbWriteCompletionOptions.Default.CancellationToken.IsCancellationRequested);
AsbWriteCompletionOptions.Default.ValidateReadback();
AssertNotNull("write completion cancellation option", typeof(AsbWriteCompletionOptions).GetProperty(nameof(AsbWriteCompletionOptions.CancellationToken)));
AssertEqual(
"write completion canceled token",
true,
new AsbWriteCompletionOptions { CancellationToken = new CancellationToken(canceled: true) }.CancellationToken.IsCancellationRequested);
new AsbWriteCompletionOptions { Timeout = TimeSpan.Zero, PollInterval = TimeSpan.FromMilliseconds(1) }.ValidatePolling();
AssertThrows<ArgumentOutOfRangeException>(
"write completion timeout",
() => new AsbWriteCompletionOptions { Timeout = TimeSpan.FromMilliseconds(-1) }.ValidatePolling());
AssertThrows<ArgumentOutOfRangeException>(
"write completion poll interval",
() => new AsbWriteCompletionOptions { PollInterval = TimeSpan.Zero }.ValidatePolling());
AssertThrows<ArgumentOutOfRangeException>(
"write completion readback delay",
() => new AsbWriteCompletionOptions { ReadbackDelay = TimeSpan.FromMilliseconds(-1) }.ValidateReadback());
ItemWriteComplete matchingComplete = new()
{
WriteHandle = 0xA5B21001,
Status =
[
new ItemStatus
{
Item = new ItemIdentity { Name = "Test.Tag", Id = 12, IdSpecified = true },
ErrorCode = 0,
Status = new AsbStatus { Count = 0, Payload = [] },
},
],
};
PublishWriteCompleteResponse response = new()
{
Result = new ArchestrAResult { ErrorCode = 32 },
CompleteWrites = [matchingComplete],
};
AsbWriteCompletionResult completion = new(
0xA5B21001,
Completed: true,
TimedOut: false,
TimeSpan.FromMilliseconds(20),
PollCount: 2,
[response],
[matchingComplete],
matchingComplete);
AssertEqual("write completion result handle", (uint)0xA5B21001, completion.WriteHandle);
AssertEqual("write completion result completed", true, completion.Completed);
AssertEqual("write completion result timed out", false, completion.TimedOut);
AssertEqual("write completion result polls", 2, completion.PollCount);
AssertEqual("write completion result responses", 1, completion.Responses.Count);
AssertEqual("write completion result raw", 1, completion.CompleteWrites.Count);
AssertEqual("write completion result match", (uint)0xA5B21001, completion.MatchingComplete?.WriteHandle);
AsbWriteCompletionReadbackResult readback = new(completion, Readback: null);
AssertEqual("write completion readback completion", completion, readback.Completion);
AssertEqual("write completion readback null", null, readback.Readback);
}
static void RunSubscriptionOptionsTests()
{
AssertNotNull("asb create subscription options overload", typeof(MxAsbDataClient).GetMethod(
nameof(MxAsbDataClient.CreateSubscription),
[typeof(AsbSubscriptionOptions)]));
AssertEqual("subscription default queue", 128L, AsbSubscriptionOptions.Default.MaxQueueSize);
AssertEqual("subscription default sample", (ulong)1000, AsbSubscriptionOptions.Default.SampleInterval);
AsbSubscriptionOptions.Default.Validate();
AsbSubscriptionOptions subscription = new() { MaxQueueSize = 128, SampleInterval = 1000 };
subscription.Validate();
AssertEqual("subscription queue", 128L, subscription.MaxQueueSize);
AssertEqual("subscription sample", (ulong)1000, subscription.SampleInterval);
AssertThrows<ArgumentOutOfRangeException>(
"subscription max queue size",
() => new AsbSubscriptionOptions { MaxQueueSize = 0 }.Validate());
AssertEqual("monitored item default sample", (ulong)1000, AsbMonitoredItemOptions.Default.SampleInterval);
AssertEqual("monitored item default active", true, AsbMonitoredItemOptions.Default.Active);
AssertEqual("monitored item default buffered", false, AsbMonitoredItemOptions.Default.Buffered);
AsbMonitoredItemOptions monitored = new()
{
SampleInterval = 250,
Active = false,
Buffered = true,
};
AssertEqual("monitored item sample", (ulong)250, monitored.SampleInterval);
AssertEqual("monitored item active", false, monitored.Active);
AssertEqual("monitored item buffered", true, monitored.Buffered);
}
static AsbStatus StatusBytes(params byte[] payload)
{
return new AsbStatus { Count = checked((sbyte)payload.Length), Payload = payload };
}
static void RunReconnectSurfaceTests()
{
AssertNotNull("asb reconnect method", typeof(MxAsbDataClient).GetMethod(nameof(MxAsbDataClient.Reconnect)));
AssertNotNull("asb channel state property", typeof(MxAsbDataClient).GetProperty(nameof(MxAsbDataClient.ChannelState)));
AssertNotNull("asb disposed property", typeof(MxAsbDataClient).GetProperty(nameof(MxAsbDataClient.IsDisposed)));
AsbReconnectOptions.Default.Validate();
AssertThrows<ArgumentOutOfRangeException>(
"reconnect attempts",
() => new AsbReconnectOptions { MaxAttempts = 0 }.Validate());
AssertThrows<ArgumentOutOfRangeException>(
"reconnect delay",
() => new AsbReconnectOptions { Delay = TimeSpan.FromMilliseconds(-1) }.Validate());
AssertThrows<ArgumentNullException>(
"reconnect cleanup options",
() => new AsbReconnectOptions { CleanupOptions = null! }.Validate());
AssertThrows<ArgumentOutOfRangeException>(
"reconnect cleanup disconnect timeout",
() => new AsbReconnectOptions
{
CleanupOptions = new AsbClientCleanupOptions { DisconnectTimeout = TimeSpan.FromMilliseconds(-1) },
}.Validate());
InvalidOperationException failure = new("failed");
AsbReconnectResult result = new(
Succeeded: false,
Client: null,
CleanupResult: null,
[new AsbReconnectAttempt(1, Succeeded: false, failure)]);
AssertEqual("reconnect last exception", failure, result.LastException);
}
static void RunCleanupOptionsTests()
{
AssertNotNull("asb cleanup overload", typeof(MxAsbDataClient).GetMethod(nameof(MxAsbDataClient.Cleanup), [typeof(AsbClientCleanupOptions)]));
AsbClientCleanupOptions.Default.Validate();
AssertThrows<ArgumentOutOfRangeException>(
"cleanup disconnect timeout",
() => new AsbClientCleanupOptions { DisconnectTimeout = TimeSpan.FromMilliseconds(-1) }.Validate());
AssertThrows<ArgumentOutOfRangeException>(
"cleanup close timeout",
() => new AsbClientCleanupOptions { CloseTimeout = TimeSpan.FromMilliseconds(-1) }.Validate());
AssertEqual(
"cleanup cancellation token",
true,
new AsbClientCleanupOptions { CancellationToken = new CancellationToken(canceled: true) }.CancellationToken.IsCancellationRequested);
}
static void RunCommunicationCleanupTests()
{
FakeCommunicationObject closed = new(CommunicationState.Closed);
CommunicationObjectCleanupResult closedResult = AsbCommunicationCleanup.CloseOrAbort(
closed,
"closed",
TimeSpan.FromSeconds(1),
trace: null);
AssertEqual("closed cleanup close attempted", false, closedResult.CloseAttempted);
AssertEqual("closed cleanup closed", true, closedResult.Closed);
AssertEqual("closed cleanup abort attempted", false, closedResult.AbortAttempted);
AssertEqual("closed cleanup succeeded", true, closedResult.Succeeded);
FakeCommunicationObject faulted = new(CommunicationState.Faulted);
CommunicationObjectCleanupResult faultedResult = AsbCommunicationCleanup.CloseOrAbort(
faulted,
"faulted",
TimeSpan.FromSeconds(1),
trace: null);
AssertEqual("faulted cleanup close attempted", false, faultedResult.CloseAttempted);
AssertEqual("faulted cleanup abort attempted", true, faultedResult.AbortAttempted);
AssertEqual("faulted cleanup aborted", true, faultedResult.Aborted);
AssertEqual("faulted cleanup succeeded", true, faultedResult.Succeeded);
FakeCommunicationObject closeFailure = new(CommunicationState.Opened)
{
ThrowOnClose = true,
};
CommunicationObjectCleanupResult closeFailureResult = AsbCommunicationCleanup.CloseOrAbort(
closeFailure,
"closeFailure",
TimeSpan.FromMilliseconds(25),
trace: null);
AssertEqual("close failure timeout", TimeSpan.FromMilliseconds(25), closeFailure.CloseTimeout);
AssertEqual("close failure close attempted", true, closeFailureResult.CloseAttempted);
AssertNotNull("close failure captured", closeFailureResult.CloseFailure);
AssertEqual("close failure abort attempted", true, closeFailureResult.AbortAttempted);
AssertEqual("close failure aborted", true, closeFailureResult.Aborted);
AssertEqual("close failure requires fallback", true, closeFailureResult.Succeeded is false);
FakeCommunicationObject abortFailure = new(CommunicationState.Faulted)
{
ThrowOnAbort = true,
};
CommunicationObjectCleanupResult abortFailureResult = AsbCommunicationCleanup.CloseOrAbort(
abortFailure,
"abortFailure",
TimeSpan.FromSeconds(1),
trace: null);
AssertEqual("abort failure abort attempted", true, abortFailureResult.AbortAttempted);
AssertEqual("abort failure aborted", false, abortFailureResult.Aborted);
AssertNotNull("abort failure captured", abortFailureResult.AbortFailure);
AssertEqual("abort failure succeeded", false, abortFailureResult.Succeeded);
FakeCommunicationObject canceledOpen = new(CommunicationState.Opened);
CommunicationObjectCleanupResult canceledOpenResult = AsbCommunicationCleanup.AbortOnly(
canceledOpen,
"canceledOpen",
trace: null);
AssertEqual("canceled open close attempted", false, canceledOpenResult.CloseAttempted);
AssertEqual("canceled open abort attempted", true, canceledOpenResult.AbortAttempted);
AssertEqual("canceled open aborted", true, canceledOpenResult.Aborted);
AssertEqual("canceled open closed", true, canceledOpenResult.Closed);
FakeCommunicationObject canceledClosed = new(CommunicationState.Closed);
CommunicationObjectCleanupResult canceledClosedResult = AsbCommunicationCleanup.AbortOnly(
canceledClosed,
"canceledClosed",
trace: null);
AssertEqual("canceled closed close attempted", false, canceledClosedResult.CloseAttempted);
AssertEqual("canceled closed abort attempted", false, canceledClosedResult.AbortAttempted);
AssertEqual("canceled closed closed", true, canceledClosedResult.Closed);
}
sealed class FakeCommunicationObject(CommunicationState initialState) : ICommunicationObject
{
public bool ThrowOnClose { get; init; }
public bool ThrowOnAbort { get; init; }
public TimeSpan? CloseTimeout { get; private set; }
public CommunicationState State { get; private set; } = initialState;
public event EventHandler? Closed;
public event EventHandler? Closing;
public event EventHandler? Faulted;
public event EventHandler? Opened;
public event EventHandler? Opening;
public void Abort()
{
if (ThrowOnAbort)
{
throw new InvalidOperationException("abort failed");
}
State = CommunicationState.Closed;
Closed?.Invoke(this, EventArgs.Empty);
}
public IAsyncResult BeginClose(AsyncCallback? callback, object? state)
{
Close();
Task task = Task.CompletedTask;
callback?.Invoke(task);
return task;
}
public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback? callback, object? state)
{
Close(timeout);
Task task = Task.CompletedTask;
callback?.Invoke(task);
return task;
}
public IAsyncResult BeginOpen(AsyncCallback? callback, object? state)
{
Open();
Task task = Task.CompletedTask;
callback?.Invoke(task);
return task;
}
public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback? callback, object? state)
{
Open(timeout);
Task task = Task.CompletedTask;
callback?.Invoke(task);
return task;
}
public void Close()
{
Close(TimeSpan.Zero);
}
public void Close(TimeSpan timeout)
{
CloseTimeout = timeout;
if (ThrowOnClose)
{
throw new InvalidOperationException("close failed");
}
Closing?.Invoke(this, EventArgs.Empty);
State = CommunicationState.Closed;
Closed?.Invoke(this, EventArgs.Empty);
}
public void EndClose(IAsyncResult result)
{
}
public void EndOpen(IAsyncResult result)
{
}
public void Open()
{
Open(TimeSpan.Zero);
}
public void Open(TimeSpan timeout)
{
Opening?.Invoke(this, EventArgs.Empty);
State = CommunicationState.Opened;
Opened?.Invoke(this, EventArgs.Empty);
}
public void RaiseFaulted()
{
State = CommunicationState.Faulted;
Faulted?.Invoke(this, EventArgs.Empty);
}
}
+54
View File
@@ -0,0 +1,54 @@
namespace MxAsbClient;
public sealed record AsbClientCleanupOptions
{
public static AsbClientCleanupOptions Default { get; } = new();
public TimeSpan DisconnectTimeout { get; init; } = TimeSpan.FromSeconds(30);
public TimeSpan CloseTimeout { get; init; } = TimeSpan.FromSeconds(30);
public CancellationToken CancellationToken { get; init; } = CancellationToken.None;
public void Validate()
{
if (DisconnectTimeout < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(DisconnectTimeout), "Disconnect timeout cannot be negative.");
}
if (CloseTimeout < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(CloseTimeout), "Close timeout cannot be negative.");
}
}
}
public sealed record AsbClientCleanupResult(
bool DisconnectAttempted,
bool DisconnectSent,
string? DisconnectFailure,
CommunicationObjectCleanupResult Channel,
CommunicationObjectCleanupResult Factory)
{
public bool Completed => Channel.Completed && Factory.Completed;
public bool Succeeded => DisconnectFailure is null && Channel.Succeeded && Factory.Succeeded;
public bool UsedAbortFallback => Channel.AbortAttempted || Factory.AbortAttempted;
public bool RequiresNewConnection => UsedAbortFallback || DisconnectFailure is not null;
}
public sealed record CommunicationObjectCleanupResult(
string Name,
string InitialState,
string FinalState,
bool CloseAttempted,
bool Closed,
string? CloseFailure,
bool AbortAttempted,
bool Aborted,
string? AbortFailure)
{
public bool Completed => Closed || Aborted;
public bool Succeeded => CloseFailure is null && AbortFailure is null && Completed;
public bool ClosedGracefully => Closed && CloseFailure is null;
}
+116
View File
@@ -0,0 +1,116 @@
using System.ServiceModel;
namespace MxAsbClient;
internal static class AsbCommunicationCleanup
{
public static CommunicationObjectCleanupResult AbortOnly(
ICommunicationObject communicationObject,
string name,
Action<string>? trace)
{
CommunicationState initialState = communicationObject.State;
bool abortAttempted = false;
bool aborted = false;
string? abortFailure = null;
if (initialState is not CommunicationState.Closed)
{
(abortAttempted, aborted, abortFailure) = AbortBestEffort(communicationObject, name, trace);
}
return new CommunicationObjectCleanupResult(
name,
initialState.ToString(),
communicationObject.State.ToString(),
CloseAttempted: false,
Closed: communicationObject.State is CommunicationState.Closed,
CloseFailure: null,
AbortAttempted: abortAttempted,
Aborted: aborted,
AbortFailure: abortFailure);
}
public static CommunicationObjectCleanupResult CloseOrAbort(
ICommunicationObject communicationObject,
string name,
TimeSpan closeTimeout,
Action<string>? trace)
{
CommunicationState initialState = communicationObject.State;
bool closeAttempted = false;
bool closed = false;
string? closeFailure = null;
bool abortAttempted = false;
bool aborted = false;
string? abortFailure = null;
if (initialState is CommunicationState.Closed)
{
return new CommunicationObjectCleanupResult(
name,
initialState.ToString(),
communicationObject.State.ToString(),
CloseAttempted: false,
Closed: true,
CloseFailure: null,
AbortAttempted: false,
Aborted: false,
AbortFailure: null);
}
if (initialState is CommunicationState.Faulted)
{
(abortAttempted, aborted, abortFailure) = AbortBestEffort(communicationObject, name, trace);
}
else
{
closeAttempted = true;
try
{
communicationObject.Close(closeTimeout);
closed = true;
}
catch (Exception ex)
{
closeFailure = FormatCleanupFailure(ex);
trace?.Invoke($"asb.cleanup.{name}.close.failed={closeFailure}");
(abortAttempted, aborted, abortFailure) = AbortBestEffort(communicationObject, name, trace);
}
}
return new CommunicationObjectCleanupResult(
name,
initialState.ToString(),
communicationObject.State.ToString(),
closeAttempted,
closed,
closeFailure,
abortAttempted,
aborted,
abortFailure);
}
private static (bool Attempted, bool Aborted, string? Failure) AbortBestEffort(
ICommunicationObject communicationObject,
string name,
Action<string>? trace)
{
try
{
communicationObject.Abort();
return (Attempted: true, Aborted: true, Failure: null);
}
catch (Exception ex)
{
string failure = FormatCleanupFailure(ex);
trace?.Invoke($"asb.cleanup.{name}.abort.failed={failure}");
return (Attempted: true, Aborted: false, Failure: failure);
}
}
internal static string FormatCleanupFailure(Exception ex)
{
return $"{ex.GetType().Name}: {ex.Message}";
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace MxAsbClient;
public sealed record AsbConnectionOptions
{
public string Endpoint { get; init; } = string.Empty;
public string? SolutionName { get; init; }
public Action<string>? Trace { get; init; }
public bool DumpMessages { get; init; }
public void Validate()
{
if (string.IsNullOrWhiteSpace(Endpoint))
{
throw new ArgumentException("ASB endpoint is required.", nameof(Endpoint));
}
}
}
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace MxAsbClient;
internal sealed class AsbMessageDumpBehavior : IEndpointBehavior
{
private readonly Action<string> trace;
public AsbMessageDumpBehavior(Action<string> trace)
{
this.trace = trace;
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.ClientMessageInspectors.Add(new Inspector(trace));
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
public void Validate(ServiceEndpoint endpoint)
{
}
private sealed class Inspector : IClientMessageInspector
{
private readonly Action<string> trace;
public Inspector(Action<string> trace)
{
this.trace = trace;
}
public object? BeforeSendRequest(ref Message request, IClientChannel channel)
{
MessageBuffer buffer = request.CreateBufferedCopy(int.MaxValue);
Message copy = buffer.CreateMessage();
request = buffer.CreateMessage();
string action = copy.Headers.Action ?? string.Empty;
if (action.Contains("registerItems", StringComparison.OrdinalIgnoreCase)
|| action.Contains("writeIn", StringComparison.OrdinalIgnoreCase)
|| action.Contains("readIn", StringComparison.OrdinalIgnoreCase)
|| action.Contains("publishWriteComplete", StringComparison.OrdinalIgnoreCase))
{
trace("asb.request=" + copy);
}
return null;
}
public void AfterReceiveReply(ref Message reply, object? correlationState)
{
MessageBuffer buffer = reply.CreateBufferedCopy(int.MaxValue);
Message copy = buffer.CreateMessage();
reply = buffer.CreateMessage();
string xml = copy.ToString();
if (xml.Contains("RegisterItemsResponse", StringComparison.OrdinalIgnoreCase)
|| xml.Contains("ReadResponse", StringComparison.OrdinalIgnoreCase)
|| xml.Contains("WriteBasicResponse", StringComparison.OrdinalIgnoreCase)
|| xml.Contains("PublishWriteCompleteResponse", StringComparison.OrdinalIgnoreCase))
{
trace("asb.reply=" + xml);
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
namespace MxAsbClient;
internal static class AsbPayloadDebug
{
public static byte[] SerializeItemsForDebug(string tag)
{
ItemIdentity[] items =
[
new ItemIdentity
{
Type = (ushort)ItemIdentityType.Name,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Name = tag,
ContextName = string.Empty,
},
];
using MemoryStream stream = new();
BinaryWriter writer = new(stream);
((IAsbCustomSerializableType)new ItemIdentity()).WriteArrayToStream(items, ref writer);
return stream.ToArray();
}
}
+147
View File
@@ -0,0 +1,147 @@
namespace MxAsbClient;
public enum AsbStatusElementType : ushort
{
OpcDaStatus = 1,
OpcUaStatus = 2,
OpcUaVendorStatus = 3,
ScadaStatus = 4,
MxStatusCategory = 5,
MxStatusDetail = 6,
MxQuality = 7,
Reserved1Status = 125,
Reserved2Status = 126,
Reserved3Status = 127,
}
public sealed record AsbStatusElement(AsbStatusElementType Type, ushort Value);
public sealed record AsbPublishedValue(
ulong ItemId,
string? ItemName,
ushort VariantType,
object? Value,
string Preview,
DateTime TimestampUtc,
bool TimestampSpecified,
ushort? Quality,
IReadOnlyList<AsbStatusElement> Status,
AsbStatus RawStatus,
Variant UserData)
{
public AsbStatusSummary StatusSummary => AsbResultMapper.ToStatusSummary(Status);
}
public sealed record AsbPublishResult(PublishResponse Response, IReadOnlyList<AsbPublishedValue> Values)
{
public AsbResultSummary Result => AsbResultMapper.ToSummary(Response.Result);
public bool HasValues => Values.Count > 0;
}
public static class AsbPublishMapper
{
public static IReadOnlyList<AsbPublishedValue> ToPublishedValues(
PublishResponse response,
IReadOnlyDictionary<ulong, string>? itemNamesById = null)
{
if (response.Values is null || response.Values.Length == 0)
{
return [];
}
AsbPublishedValue[] values = new AsbPublishedValue[response.Values.Length];
for (int i = 0; i < response.Values.Length; i++)
{
values[i] = ToPublishedValue(response.Values[i], itemNamesById);
}
return values;
}
public static AsbPublishedValue ToPublishedValue(
MonitoredItemValue itemValue,
IReadOnlyDictionary<ulong, string>? itemNamesById = null)
{
RuntimeValue runtime = itemValue.Value;
IReadOnlyList<AsbStatusElement> status = DecodeStatus(runtime.Status);
ushort? quality = status.FirstOrDefault(item => item.Type == AsbStatusElementType.MxQuality)?.Value;
string? itemName = string.IsNullOrWhiteSpace(itemValue.Item.Name)
? ResolveItemName(itemValue.Item.Id, itemNamesById)
: itemValue.Item.Name;
return new AsbPublishedValue(
itemValue.Item.Id,
itemName,
runtime.Value.Type,
MxAsbDataClient.DecodeVariant(runtime.Value),
MxAsbDataClient.FormatVariant(runtime.Value),
runtime.Timestamp.Kind == DateTimeKind.Utc ? runtime.Timestamp : runtime.Timestamp.ToUniversalTime(),
runtime.TimestampSpecified,
quality,
status,
runtime.Status,
itemValue.UserData);
}
public static IReadOnlyList<AsbStatusElement> DecodeStatus(AsbStatus status)
{
byte[] payload = status.Payload ?? [];
int length = status.Count > 0 ? Math.Min(status.Count, payload.Length) : payload.Length;
if (length == 0)
{
return [];
}
List<AsbStatusElement> elements = [];
int offset = 0;
while (offset < length)
{
byte marker = payload[offset++];
AsbStatusElementType type = (AsbStatusElementType)(marker & 0x7F);
if ((marker & 0x80) != 0)
{
elements.Add(new AsbStatusElement(type, 0));
continue;
}
if (offset + sizeof(ushort) > length)
{
break;
}
ushort value = BitConverter.ToUInt16(payload, offset);
offset += sizeof(ushort);
elements.Add(new AsbStatusElement(type, value));
}
return elements;
}
public static IReadOnlyDictionary<ulong, string> CreateItemNameMap(params ItemStatus[]?[] statusGroups)
{
Dictionary<ulong, string> names = [];
foreach (ItemStatus[]? statuses in statusGroups)
{
if (statuses is null)
{
continue;
}
foreach (ItemStatus status in statuses)
{
if (status.Item.IdSpecified && !string.IsNullOrWhiteSpace(status.Item.Name))
{
names[status.Item.Id] = status.Item.Name;
}
}
}
return names;
}
private static string? ResolveItemName(ulong id, IReadOnlyDictionary<ulong, string>? itemNamesById)
{
return itemNamesById is not null && itemNamesById.TryGetValue(id, out string? name) ? name : null;
}
}
+44
View File
@@ -0,0 +1,44 @@
namespace MxAsbClient;
public sealed record AsbReconnectOptions
{
public static AsbReconnectOptions Default { get; } = new();
public int MaxAttempts { get; init; } = 3;
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(500);
public bool CleanupCurrentConnection { get; init; } = true;
public AsbClientCleanupOptions CleanupOptions { get; init; } = AsbClientCleanupOptions.Default;
public void Validate()
{
if (MaxAttempts < 1)
{
throw new ArgumentOutOfRangeException(nameof(MaxAttempts), "Reconnect attempts must be at least one.");
}
if (Delay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(Delay), "Reconnect delay cannot be negative.");
}
ArgumentNullException.ThrowIfNull(CleanupOptions);
CleanupOptions.Validate();
}
}
public sealed record AsbReconnectAttempt(
int Attempt,
bool Succeeded,
Exception? Exception);
public sealed record AsbReconnectResult(
bool Succeeded,
MxAsbDataClient? Client,
AsbClientCleanupResult? CleanupResult,
IReadOnlyList<AsbReconnectAttempt> Attempts)
{
public Exception? LastException => Attempts.LastOrDefault(attempt => attempt.Exception is not null)?.Exception;
}
+67
View File
@@ -0,0 +1,67 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Win32;
using System.Numerics;
namespace MxAsbClient;
internal static class AsbRegistry
{
private const string Entropy = "wonderware";
private static string RegistryPath => Environment.Is64BitProcess
? @"SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices"
: @"SOFTWARE\ArchestrA\ArchestrAServices";
public static string GetDefaultSolutionName()
{
return Registry.GetValue($@"HKEY_LOCAL_MACHINE\{RegistryPath}", "DefaultASBSolution", string.Empty)?.ToString() ?? string.Empty;
}
public static string GetSolutionPassphrase(string? solutionName, Action<string>? trace = null)
{
trace?.Invoke("asb.stage=registry-solution");
string effectiveSolution = string.IsNullOrWhiteSpace(solutionName) ? GetDefaultSolutionName() : solutionName!;
if (string.IsNullOrWhiteSpace(effectiveSolution))
{
throw new InvalidOperationException("ASB default solution name was not found in the registry.");
}
trace?.Invoke("asb.stage=registry-open-solution");
using RegistryKey? key = Registry.LocalMachine.OpenSubKey($@"{RegistryPath}\{effectiveSolution}", writable: false);
if (key?.GetValue("sharedsecret") is not byte[] protectedBytes)
{
throw new InvalidOperationException($"ASB sharedsecret was not found for solution '{effectiveSolution}'.");
}
trace?.Invoke("asb.stage=registry-unprotect");
byte[] clear = ProtectedData.Unprotect(protectedBytes, Encoding.Unicode.GetBytes(Entropy), DataProtectionScope.LocalMachine);
trace?.Invoke("asb.stage=registry-passphrase-ready");
return Encoding.Unicode.GetString(clear);
}
public static AsbSolutionCryptoParameters GetCryptoParameters(string? solutionName)
{
string effectiveSolution = string.IsNullOrWhiteSpace(solutionName) ? GetDefaultSolutionName() : solutionName!;
using RegistryKey? key = Registry.LocalMachine.OpenSubKey($@"{RegistryPath}\{effectiveSolution}", writable: false);
if (key is null)
{
throw new InvalidOperationException($"ASB solution registry key was not found for '{effectiveSolution}'.");
}
string primeText = key.GetValue("Prime")?.ToString() ?? AsbSolutionCryptoParameters.DefaultPrimeText;
string generatorText = key.GetValue("Generator")?.ToString() ?? "22";
string hashAlgorithm = key.GetValue("HashAlgorthim")?.ToString() ?? "MD5";
int keySize = int.TryParse(key.GetValue("keySize")?.ToString(), out int parsedKeySize) ? parsedKeySize : 256;
return new AsbSolutionCryptoParameters(
BigInteger.Parse(primeText),
BigInteger.Parse(generatorText),
hashAlgorithm,
keySize);
}
}
internal sealed record AsbSolutionCryptoParameters(BigInteger Prime, BigInteger Generator, string HashAlgorithm, int KeySize)
{
public const string DefaultPrimeText = "179769313486231590770839156793787453197860296048756011706444423684197180216158519368947833795864925541502180565485980503646440548199239100050792877003355816639229553136239076508735759914822574862575007425302077447712589550957937778424442426617334727629299387668709205606050270810842907692932019128194";
}
+273
View File
@@ -0,0 +1,273 @@
namespace MxAsbClient;
public enum AsbErrorCode : ushort
{
Success = 0,
InvalidConnectionId = 1,
ApplicationAuthenticationError = 2,
UserAuthenticationError = 3,
UserAuthorizationError = 4,
NotSupportedOperation = 5,
MonitoredItemsNotFound = 6,
InvalidSubscriptionId = 7,
ItemAlreadyRegistered = 8,
ItemAlreadyDeletedOrDoesNotExist = 9,
InvalidMonitoredItems = 10,
OperationFailed = 11,
SpecificError = 12,
BadNoCommunication = 13,
BadNothingToDo = 14,
BadTooManyOperations = 15,
BadNodeIdInvalid = 16,
BrowseFailed = 17,
WriteFailedBadOutOfRange = 18,
WriteFailedBadTypeMismatch = 19,
WriteFailedBadDimensionMismatch = 20,
WriteFailedAccessDenied = 21,
WriteFailedSecuredWrite = 22,
WriteFailedVerifiedWrite = 23,
IndexOutOfRange = 24,
RequestTimedOut = 25,
DataTypeConversionNotSupported = 26,
ItemCannotBeRegisteredNoName = 27,
ItemCannotBeRegisteredNoId = 28,
ItemAlreadyBeingMonitored = 29,
SubscriptionIdAlreadyExist = 30,
OperationWouldBlock = 31,
PublishComplete = 32,
WriteFailedUserNotHavingAccessRights = 33,
WriteFailedVerifierNotHavingVerifyRights = 34,
ObjectNotInitialized = 128,
EndPointNotFound = 129,
ConnectionClosed = 130,
InvalidParameter = 131,
MemoryAllocationError = 132,
OperationNotComplete = 133,
FileOperationFailed = 256,
InvalidXmlFile = 272,
RecordLookupError = 288,
Unknown = ushort.MaxValue,
}
public enum AsbStatusQuality
{
Unknown = 0,
Bad = 1,
Uncertain = 2,
Good = 3,
}
public enum AsbMxStatusCategory
{
Unknown = -1,
Ok = 0,
Pending = 1,
Warning = 2,
CommunicationError = 3,
ConfigurationError = 4,
OperationalError = 5,
SecurityError = 6,
SoftwareError = 7,
OtherError = 8,
}
public enum AsbMxStatusDetail
{
Unknown = -1,
None = 0,
RequestTimedOut = 16,
PlatformCommunicationError = 17,
WriteAccessDenied = 33,
}
public sealed record AsbResultSummary(
AsbErrorCode Error,
int RawErrorCode,
uint Status,
uint SpecificErrorCode,
bool IsSuccess,
bool IsSuccessLike);
public sealed record AsbItemStatusSummary(
string? ItemName,
ulong ItemId,
AsbErrorCode Error,
ushort RawErrorCode,
bool IsSuccess,
IReadOnlyList<AsbStatusElement> Status)
{
public AsbStatusSummary StatusSummary { get; init; } = AsbResultMapper.ToStatusSummary(Status);
}
public sealed record AsbStatusSummary(
ushort? RawQuality,
AsbStatusQuality Quality,
ushort? RawCategory,
AsbMxStatusCategory Category,
ushort? RawDetail,
AsbMxStatusDetail Detail,
AsbErrorCode? StatusError,
bool IsGoodQuality,
bool IsSuccessLike,
IReadOnlyList<AsbStatusElement> Elements);
public static class AsbResultMapper
{
public static AsbResultSummary ToSummary(ArchestrAResult result)
{
AsbErrorCode error = result.ErrorCode is >= 0 and <= ushort.MaxValue
? ToErrorCode((ushort)result.ErrorCode)
: AsbErrorCode.Unknown;
bool isSuccess = error == AsbErrorCode.Success || result.Success;
return new AsbResultSummary(
error,
result.ErrorCode,
result.Status,
result.SpecificErrorCode,
isSuccess,
isSuccess || error == AsbErrorCode.PublishComplete);
}
public static AsbItemStatusSummary ToItemSummary(ItemStatus status)
{
AsbErrorCode error = ToErrorCode(status.ErrorCode);
IReadOnlyList<AsbStatusElement> elements = AsbPublishMapper.DecodeStatus(status.Status);
return new AsbItemStatusSummary(
status.Item.Name,
status.Item.Id,
error,
status.ErrorCode,
error == AsbErrorCode.Success,
elements);
}
public static IReadOnlyList<AsbItemStatusSummary> ToItemSummaries(ItemStatus[]? statuses)
{
if (statuses is null || statuses.Length == 0)
{
return [];
}
AsbItemStatusSummary[] summaries = new AsbItemStatusSummary[statuses.Length];
for (int i = 0; i < statuses.Length; i++)
{
summaries[i] = ToItemSummary(statuses[i]);
}
return summaries;
}
public static AsbErrorCode ToErrorCode(ushort errorCode)
{
return Enum.IsDefined(typeof(AsbErrorCode), errorCode)
? (AsbErrorCode)errorCode
: AsbErrorCode.Unknown;
}
public static AsbStatusSummary ToStatusSummary(AsbStatus status)
{
return ToStatusSummary(AsbPublishMapper.DecodeStatus(status));
}
public static AsbStatusSummary ToStatusSummary(IReadOnlyList<AsbStatusElement> elements)
{
ushort? rawQuality = FirstValue(elements, AsbStatusElementType.MxQuality);
ushort? rawCategory = FirstValue(elements, AsbStatusElementType.MxStatusCategory);
ushort? rawDetail = FirstValue(elements, AsbStatusElementType.MxStatusDetail);
AsbStatusQuality quality = ToQuality(rawQuality);
AsbMxStatusCategory category = ToMxStatusCategory(rawCategory);
AsbMxStatusDetail detail = ToMxStatusDetail(rawDetail);
AsbErrorCode? statusError = ToStatusError(category, detail);
bool isGoodQuality = quality == AsbStatusQuality.Good;
bool isSuccessLike = statusError == AsbErrorCode.Success
&& (quality == AsbStatusQuality.Good || quality == AsbStatusQuality.Unknown);
return new AsbStatusSummary(
rawQuality,
quality,
rawCategory,
category,
rawDetail,
detail,
statusError,
isGoodQuality,
isSuccessLike,
elements);
}
public static AsbStatusQuality ToQuality(ushort? quality)
{
if (quality is null)
{
return AsbStatusQuality.Unknown;
}
return (quality.Value & 0x00C0) switch
{
0x00C0 => AsbStatusQuality.Good,
0x0040 => AsbStatusQuality.Uncertain,
0x0000 => AsbStatusQuality.Bad,
_ => AsbStatusQuality.Unknown,
};
}
public static AsbMxStatusCategory ToMxStatusCategory(ushort? category)
{
return category switch
{
0 => AsbMxStatusCategory.Ok,
1 => AsbMxStatusCategory.Pending,
2 => AsbMxStatusCategory.Warning,
3 => AsbMxStatusCategory.CommunicationError,
4 => AsbMxStatusCategory.ConfigurationError,
5 => AsbMxStatusCategory.OperationalError,
6 => AsbMxStatusCategory.SecurityError,
7 => AsbMxStatusCategory.SoftwareError,
8 => AsbMxStatusCategory.OtherError,
_ => AsbMxStatusCategory.Unknown,
};
}
public static AsbMxStatusDetail ToMxStatusDetail(ushort? detail)
{
return detail switch
{
0 => AsbMxStatusDetail.None,
16 => AsbMxStatusDetail.RequestTimedOut,
17 => AsbMxStatusDetail.PlatformCommunicationError,
33 => AsbMxStatusDetail.WriteAccessDenied,
_ => AsbMxStatusDetail.Unknown,
};
}
private static AsbErrorCode? ToStatusError(AsbMxStatusCategory category, AsbMxStatusDetail detail)
{
if (category == AsbMxStatusCategory.Ok && detail == AsbMxStatusDetail.None)
{
return AsbErrorCode.Success;
}
return detail switch
{
AsbMxStatusDetail.RequestTimedOut => AsbErrorCode.RequestTimedOut,
AsbMxStatusDetail.PlatformCommunicationError => AsbErrorCode.BadNoCommunication,
AsbMxStatusDetail.WriteAccessDenied => AsbErrorCode.WriteFailedAccessDenied,
_ => null,
};
}
private static ushort? FirstValue(IReadOnlyList<AsbStatusElement> elements, AsbStatusElementType type)
{
foreach (AsbStatusElement element in elements)
{
if (element.Type == type)
{
return element.Value;
}
}
return null;
}
}
+49
View File
@@ -0,0 +1,49 @@
using System.Globalization;
using System.Xml.Linq;
using System.Xml.Serialization;
namespace MxAsbClient;
internal static class AsbSerialization
{
private static readonly Dictionary<Type, XmlSerializer> Serializers = [];
private static readonly object LockObject = new();
public static string ToXml(this object value)
{
if (value is null)
{
return string.Empty;
}
string text = string.Empty;
using (StringWriter writer = new(CultureInfo.CurrentCulture))
{
lock (LockObject)
{
Type type = value.GetType();
if (!Serializers.TryGetValue(type, out XmlSerializer? serializer))
{
serializer = new XmlSerializer(type, "urn:invensys.schemas");
Serializers.Add(type, serializer);
}
serializer.Serialize(writer, value);
text = writer.ToString();
}
}
using TextReader reader = new StringReader(text);
XElement root = XDocument.Load(reader).Root ?? throw new InvalidOperationException("Serialized ASB message had no root element.");
XAttribute? xsd = root.Attribute(XNamespace.Xmlns + "xsd");
XAttribute? xsi = root.Attribute(XNamespace.Xmlns + "xsi");
if (xsd is not null && xsi is not null)
{
root.ReplaceAttributes(xsi, xsd);
}
using StringWriter normalized = new(CultureInfo.CurrentCulture);
root.Save(normalized);
return normalized.ToString() ?? string.Empty;
}
}
+29
View File
@@ -0,0 +1,29 @@
namespace MxAsbClient;
public sealed record AsbSubscriptionOptions
{
public static AsbSubscriptionOptions Default { get; } = new();
public long MaxQueueSize { get; init; } = 128;
public ulong SampleInterval { get; init; } = 1000;
public void Validate()
{
if (MaxQueueSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(MaxQueueSize), "ASB subscription max queue size must be greater than zero.");
}
}
}
public sealed record AsbMonitoredItemOptions
{
public static AsbMonitoredItemOptions Default { get; } = new();
public ulong SampleInterval { get; init; } = 1000;
public bool Active { get; init; } = true;
public bool Buffered { get; init; }
}
+167
View File
@@ -0,0 +1,167 @@
using System.IO.Compression;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
namespace MxAsbClient;
internal sealed class AsbSystemAuthenticator
{
private static readonly byte[] PasswordSalt = Encoding.ASCII.GetBytes("ArchestrAService");
private readonly BigInteger dhPrime;
private readonly BigInteger dhGenerator;
private readonly string hashAlgorithm;
private readonly int keySize;
private readonly byte[] solutionPassphrase;
private readonly byte[] privateKey;
private readonly byte[] localPublicKey;
private byte[] remotePublicKey = [];
private ulong nextMessageNumber = 1;
public AsbSystemAuthenticator(string passphrase, AsbSolutionCryptoParameters cryptoParameters, Action<string>? trace = null)
{
dhPrime = cryptoParameters.Prime;
dhGenerator = cryptoParameters.Generator;
hashAlgorithm = cryptoParameters.HashAlgorithm;
keySize = cryptoParameters.KeySize;
trace?.Invoke("asb.stage=authenticator-passphrase-bytes");
solutionPassphrase = Encoding.UTF8.GetBytes(passphrase);
trace?.Invoke("asb.stage=authenticator-create-private");
BigInteger privateKeyValue = CreatePrivateKey();
trace?.Invoke("asb.stage=authenticator-private-ready");
privateKey = privateKeyValue.ToByteArray();
trace?.Invoke("asb.stage=authenticator-modpow");
localPublicKey = BigInteger.ModPow(dhGenerator, privateKeyValue, dhPrime).ToByteArray();
trace?.Invoke("asb.stage=authenticator-public-ready");
ConnectionId = Guid.NewGuid();
}
public Guid ConnectionId { get; }
public byte[] LocalPublicKey => localPublicKey;
public bool UseApolloSigning { get; private set; }
public void AcceptConnectResponse(ConnectResponse response)
{
remotePublicKey = response.ServicePublicKey?.Data ?? throw new InvalidOperationException("ASB connect response did not contain a service public key.");
UseApolloSigning = response.ConnectionLifetime?.Contains(":V2", StringComparison.OrdinalIgnoreCase) == true;
}
public AuthenticationData CreateAuthenticationData()
{
byte[] clear = [.. localPublicKey, .. remotePublicKey];
byte[] encrypted = Encrypt(clear, out byte[] iv);
return new AuthenticationData
{
Data = encrypted,
InitializationVector = iv,
};
}
public void Sign(ConnectedRequest request, bool forceHmac = false)
{
ConnectionValidator validator = new()
{
ConnectionId = ConnectionId,
MessageNumber = nextMessageNumber++,
MessageAuthenticationCode = [],
SignatureInitializationVector = [],
};
request.ConnectionValidator = validator;
using HMAC? hmac = CreateHmac(forceHmac);
if (hmac is null)
{
return;
}
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(request.ToXml()));
validator.MessageAuthenticationCode = Encrypt(hash, out byte[] iv);
validator.SignatureInitializationVector = iv;
}
private HMAC? CreateHmac(bool forceHmac)
{
return hashAlgorithm.ToLowerInvariant() switch
{
"md5" => new HMACMD5(CryptoKey),
"sha1" => new HMACSHA1(CryptoKey),
"sha512" => new HMACSHA512(CryptoKey),
_ => forceHmac ? new HMACSHA1(CryptoKey) : null,
};
}
private byte[] Encrypt(byte[] clear, out byte[] iv)
{
if (UseApolloSigning)
{
return EncryptApollo(clear, out iv);
}
return EncryptBaktun(clear, out iv);
}
private byte[] EncryptApollo(byte[] clear, out byte[] iv)
{
using Aes aes = Aes.Create();
aes.Key = DeriveAesKey();
iv = aes.IV;
using MemoryStream output = new();
using (CryptoStream crypto = new(output, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
crypto.Write(clear, 0, clear.Length);
}
return output.ToArray();
}
private byte[] EncryptBaktun(byte[] clear, out byte[] iv)
{
using Aes aes = Aes.Create();
aes.Key = DeriveAesKey();
iv = aes.IV;
using MemoryStream output = new();
using (CryptoStream crypto = new(output, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
using DeflateStream deflate = new(crypto, CompressionMode.Compress);
deflate.Write(clear, 0, clear.Length);
}
return output.ToArray();
}
private byte[] DeriveAesKey()
{
return Rfc2898DeriveBytes.Pbkdf2(
Convert.ToBase64String(CryptoKey),
PasswordSalt,
iterations: 1000,
HashAlgorithmName.SHA1,
outputLength: 16);
}
private byte[] CryptoKey
{
get
{
byte[] shared = BigInteger.ModPow(new BigInteger(remotePublicKey), new BigInteger(privateKey), dhPrime).ToByteArray();
return [.. shared, .. solutionPassphrase];
}
}
private BigInteger CreatePrivateKey()
{
byte[] bytes = new byte[(keySize / 8) + 1];
BigInteger value;
do
{
RandomNumberGenerator.Fill(bytes);
bytes[^1] = 0;
value = new BigInteger(bytes);
}
while (value <= BigInteger.Zero || value >= dhPrime - BigInteger.One);
return value;
}
}
+50
View File
@@ -0,0 +1,50 @@
namespace MxAsbClient;
public sealed record AsbWriteCompletionOptions
{
public static AsbWriteCompletionOptions Default { get; } = new();
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(5);
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(250);
public TimeSpan ReadbackDelay { get; init; } = TimeSpan.Zero;
public CancellationToken CancellationToken { get; init; }
public void ValidatePolling()
{
if (Timeout < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(Timeout), "Write completion timeout must not be negative.");
}
if (PollInterval <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(PollInterval), "Write completion poll interval must be greater than zero.");
}
}
public void ValidateReadback()
{
ValidatePolling();
if (ReadbackDelay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(ReadbackDelay), "Write completion readback delay must not be negative.");
}
}
}
public sealed record AsbWriteCompletionResult(
uint WriteHandle,
bool Completed,
bool TimedOut,
TimeSpan Elapsed,
int PollCount,
IReadOnlyList<PublishWriteCompleteResponse> Responses,
IReadOnlyList<ItemWriteComplete> CompleteWrites,
ItemWriteComplete? MatchingComplete);
public sealed record AsbWriteCompletionReadbackResult(
AsbWriteCompletionResult Completion,
ReadResponse? Readback);
+8
View File
@@ -0,0 +1,8 @@
namespace MxAsbClient;
public sealed record AsbWriteOptions
{
public uint WriteHandle { get; init; }
public string? Comment { get; init; }
}
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.7" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.7" />
<PackageReference Include="System.ServiceModel.NetTcp" Version="10.0.652802" />
<PackageReference Include="System.ServiceModel.Primitives" Version="10.0.652802" />
</ItemGroup>
</Project>
+295
View File
@@ -0,0 +1,295 @@
namespace MxAsbClient;
public sealed record MxAsbDataChangeEvent(
int ServerHandle,
int ItemHandle,
object? Value,
ushort Quality,
DateTime TimestampUtc,
IReadOnlyList<AsbStatusElement> Status)
{
public AsbStatusSummary StatusSummary => AsbResultMapper.ToStatusSummary(Status);
}
public sealed class MxAsbCompatibilityServer : IDisposable
{
private readonly object gate = new();
private readonly Dictionary<int, AsbServerSession> sessions = [];
private readonly Dictionary<int, AsbCompatibilityItem> items = [];
private int nextServerHandle = 1;
private int nextItemHandle = 1;
private bool disposed;
public event EventHandler<MxAsbDataChangeEvent>? DataChanged;
public int Register(string endpoint, string? solutionName = null, Action<string>? trace = null, bool dumpMessages = false)
{
return Register(new AsbConnectionOptions
{
Endpoint = endpoint,
SolutionName = solutionName,
Trace = trace,
DumpMessages = dumpMessages,
});
}
public int Register(AsbConnectionOptions options)
{
ObjectDisposedException.ThrowIf(disposed, this);
MxAsbDataClient client = MxAsbDataClient.Connect(options);
return RegisterClient(client);
}
private int RegisterClient(MxAsbDataClient client)
{
int serverHandle;
lock (gate)
{
serverHandle = nextServerHandle++;
sessions.Add(serverHandle, new AsbServerSession(serverHandle, client));
}
client.PublishedValueReceived += OnPublishedValueReceived;
return serverHandle;
}
public void Unregister(int serverHandle)
{
AsbServerSession session;
lock (gate)
{
session = GetSessionLocked(serverHandle);
sessions.Remove(serverHandle);
foreach (int itemHandle in items
.Where(pair => pair.Value.ServerHandle == serverHandle)
.Select(pair => pair.Key)
.ToArray())
{
items.Remove(itemHandle);
}
}
session.Client.PublishedValueReceived -= OnPublishedValueReceived;
session.Dispose();
}
public int AddItem(int serverHandle, string tag)
{
ObjectDisposedException.ThrowIf(disposed, this);
lock (gate)
{
GetSessionLocked(serverHandle);
int itemHandle = nextItemHandle++;
items.Add(itemHandle, new AsbCompatibilityItem(serverHandle, itemHandle, tag));
return itemHandle;
}
}
public void RemoveItem(int serverHandle, int itemHandle)
{
AsbServerSession session;
AsbCompatibilityItem item;
lock (gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
items.Remove(itemHandle);
}
if (session.SubscriptionId.HasValue && item.MonitoredItem.HasValue)
{
session.Client.DeleteMonitoredItems(session.SubscriptionId.Value, [item.MonitoredItem.Value]);
}
}
public void Advise(int serverHandle, int itemHandle, ulong sampleInterval = 1000, long maxQueueSize = 128)
{
Advise(
serverHandle,
itemHandle,
new AsbSubscriptionOptions
{
MaxQueueSize = maxQueueSize,
SampleInterval = sampleInterval,
},
new AsbMonitoredItemOptions
{
SampleInterval = sampleInterval,
});
}
public void Advise(
int serverHandle,
int itemHandle,
AsbSubscriptionOptions subscriptionOptions,
AsbMonitoredItemOptions monitoredItemOptions)
{
ObjectDisposedException.ThrowIf(disposed, this);
ArgumentNullException.ThrowIfNull(subscriptionOptions);
ArgumentNullException.ThrowIfNull(monitoredItemOptions);
subscriptionOptions.Validate();
AsbServerSession session;
AsbCompatibilityItem item;
lock (gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
}
if (!session.SubscriptionId.HasValue)
{
CreateSubscriptionResponse create = session.Client.CreateSubscription(subscriptionOptions);
EnsureSucceeded(create.Result, nameof(MxAsbDataClient.CreateSubscription));
session.SubscriptionId = create.SubscriptionId;
}
AddMonitoredItemsResponse add = session.Client.AddMonitoredItems(session.SubscriptionId.Value, [item.Tag], monitoredItemOptions);
EnsureSucceeded(add.Result, nameof(MxAsbDataClient.AddMonitoredItems));
ItemStatus? status = add.Status?.FirstOrDefault();
if (status.HasValue)
{
item.MonitoredItem = status.Value.Item;
lock (gate)
{
items[itemHandle] = item;
}
}
}
public AsbPublishResult Poll(int serverHandle)
{
ObjectDisposedException.ThrowIf(disposed, this);
AsbServerSession session;
lock (gate)
{
session = GetSessionLocked(serverHandle);
}
if (!session.SubscriptionId.HasValue)
{
throw new InvalidOperationException("The ASB server handle has no active subscription.");
}
return session.Client.PublishValues(session.SubscriptionId.Value);
}
public void Dispose()
{
if (disposed)
{
return;
}
AsbServerSession[] activeSessions;
lock (gate)
{
activeSessions = sessions.Values.ToArray();
sessions.Clear();
items.Clear();
}
foreach (AsbServerSession session in activeSessions)
{
session.Client.PublishedValueReceived -= OnPublishedValueReceived;
session.Dispose();
}
disposed = true;
}
private void OnPublishedValueReceived(object? sender, AsbPublishedValue value)
{
int serverHandle;
int itemHandle;
lock (gate)
{
AsbServerSession? session = sessions.Values.FirstOrDefault(candidate => ReferenceEquals(candidate.Client, sender));
if (session is null)
{
return;
}
AsbCompatibilityItem? item = items.Values.FirstOrDefault(candidate =>
candidate.ServerHandle == session.ServerHandle &&
candidate.MonitoredItem?.Id == value.ItemId);
if (item is null)
{
return;
}
serverHandle = session.ServerHandle;
itemHandle = item.ItemHandle;
}
DataChanged?.Invoke(
this,
new MxAsbDataChangeEvent(
serverHandle,
itemHandle,
value.Value,
value.Quality ?? 0,
value.TimestampUtc,
value.Status));
}
private AsbServerSession GetSessionLocked(int serverHandle)
{
if (!sessions.TryGetValue(serverHandle, out AsbServerSession? session))
{
throw new ArgumentException("Unknown ASB server handle.", nameof(serverHandle));
}
return session;
}
private AsbCompatibilityItem GetItemLocked(int serverHandle, int itemHandle)
{
if (!items.TryGetValue(itemHandle, out AsbCompatibilityItem? item) || item.ServerHandle != serverHandle)
{
throw new ArgumentException("Unknown ASB item handle.", nameof(itemHandle));
}
return item;
}
private static void EnsureSucceeded(ArchestrAResult result, string operation)
{
if (!result.Success)
{
throw new InvalidOperationException($"{operation} failed with error 0x{result.ErrorCode:X8}.");
}
}
private sealed class AsbServerSession(int serverHandle, MxAsbDataClient client) : IDisposable
{
public int ServerHandle { get; } = serverHandle;
public MxAsbDataClient Client { get; } = client;
public long? SubscriptionId { get; set; }
public void Dispose()
{
if (SubscriptionId.HasValue)
{
try
{
Client.DeleteSubscription(SubscriptionId.Value);
}
catch
{
// Best-effort cleanup; channel disposal below still releases the client.
}
}
Client.Dispose();
}
}
private sealed record AsbCompatibilityItem(
int ServerHandle,
int ItemHandle,
string Tag)
{
public ItemIdentity? MonitoredItem { get; set; }
}
}
+828
View File
@@ -0,0 +1,828 @@
using System.Diagnostics;
using System.Globalization;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace MxAsbClient;
public sealed class MxAsbDataClient : IDisposable
{
private const int InvalidConnectionId = 1;
private readonly object cleanupGate = new();
private readonly ChannelFactory<IAsbDataV2> factory;
private readonly IAsbDataV2 channel;
private readonly IClientChannel clientChannel;
private readonly AsbSystemAuthenticator authenticator;
private readonly string endpoint;
private readonly string? solutionName;
private readonly Action<string>? trace;
private readonly bool dumpMessages;
private readonly Dictionary<ulong, string> monitoredItemNamesById = [];
private AsbClientCleanupResult? cleanupResult;
private bool disposed;
private MxAsbDataClient(
ChannelFactory<IAsbDataV2> factory,
IAsbDataV2 channel,
AsbSystemAuthenticator authenticator,
string endpoint,
string? solutionName,
Action<string>? trace,
bool dumpMessages)
{
this.factory = factory;
this.channel = channel;
clientChannel = (IClientChannel)channel;
this.authenticator = authenticator;
this.endpoint = endpoint;
this.solutionName = solutionName;
this.trace = trace;
this.dumpMessages = dumpMessages;
}
public static MxAsbDataClient Connect(string endpoint, string? solutionName = null, Action<string>? trace = null, bool dumpMessages = false)
{
return Connect(new AsbConnectionOptions
{
Endpoint = endpoint,
SolutionName = solutionName,
Trace = trace,
DumpMessages = dumpMessages,
});
}
public static MxAsbDataClient Connect(AsbConnectionOptions options)
{
ArgumentNullException.ThrowIfNull(options);
options.Validate();
string endpoint = options.Endpoint;
string? solutionName = options.SolutionName;
Action<string>? trace = options.Trace;
bool dumpMessages = options.DumpMessages;
trace?.Invoke("asb.stage=read-passphrase");
string passphrase = AsbRegistry.GetSolutionPassphrase(solutionName, trace);
AsbSolutionCryptoParameters cryptoParameters = AsbRegistry.GetCryptoParameters(solutionName);
trace?.Invoke("asb.stage=create-authenticator");
AsbSystemAuthenticator authenticator = new(passphrase, cryptoParameters, trace);
trace?.Invoke("asb.stage=authenticator-ready");
NetTcpBinding binding = CreateBinding();
ChannelFactory<IAsbDataV2> factory = new(binding, new EndpointAddress(endpoint));
AsbDataCustomSerializer.Trace = dumpMessages ? trace : null;
int replacedSerializers = AsbCustomSerializerContractBehavior.ReplaceSerializer(factory.Endpoint.Contract);
trace?.Invoke($"asb.serializer.behaviors-replaced={replacedSerializers}");
if (trace is not null && dumpMessages)
{
factory.Endpoint.EndpointBehaviors.Add(new AsbMessageDumpBehavior(trace));
}
IAsbDataV2? channel = null;
IClientChannel? clientChannel = null;
try
{
trace?.Invoke("asb.stage=open-factory");
factory.Open();
channel = factory.CreateChannel();
trace?.Invoke("asb.stage=open-channel");
clientChannel = (IClientChannel)channel;
clientChannel.Open();
trace?.Invoke("asb.stage=connect");
ConnectResponse response = channel.Connect(new ConnectRequest
{
ConnectionId = authenticator.ConnectionId,
ConsumerPublicKey = new PublicKey { Data = authenticator.LocalPublicKey },
});
if (!response.Result.Success)
{
throw new InvalidOperationException($"ASB Connect failed with error 0x{response.Result.ErrorCode:X8}.");
}
authenticator.AcceptConnectResponse(response);
trace?.Invoke("asb.stage=authenticate-me");
AuthenticateMe authenticateMe = new()
{
ConsumerAuthenticationData = authenticator.CreateAuthenticationData(),
};
authenticator.Sign(authenticateMe, forceHmac: true);
channel.AuthenticateMe(authenticateMe);
trace?.Invoke("asb.stage=connected");
return new MxAsbDataClient(factory, channel, authenticator, endpoint, solutionName, trace, dumpMessages);
}
catch
{
trace?.Invoke("asb.stage=connect-cleanup");
if (clientChannel is not null)
{
AsbCommunicationCleanup.CloseOrAbort(clientChannel, "connect-channel", binding.CloseTimeout, trace);
}
AsbCommunicationCleanup.CloseOrAbort(factory, "connect-factory", binding.CloseTimeout, trace);
throw;
}
}
public event EventHandler<AsbPublishedValue>? PublishedValueReceived;
public AsbClientCleanupResult? LastCleanupResult => cleanupResult;
public bool IsDisposed => disposed;
public CommunicationState ChannelState => clientChannel.State;
public AsbReconnectResult Reconnect(AsbReconnectOptions? options = null)
{
options ??= AsbReconnectOptions.Default;
options.Validate();
AsbClientCleanupResult? currentCleanup = null;
if (options.CleanupCurrentConnection)
{
currentCleanup = Cleanup(options.CleanupOptions);
}
List<AsbReconnectAttempt> attempts = [];
for (int attempt = 1; attempt <= options.MaxAttempts; attempt++)
{
try
{
trace?.Invoke($"asb.reconnect.attempt={attempt}");
MxAsbDataClient client = Connect(endpoint, solutionName, trace, dumpMessages);
attempts.Add(new AsbReconnectAttempt(attempt, Succeeded: true, Exception: null));
return new AsbReconnectResult(Succeeded: true, client, currentCleanup, attempts);
}
catch (Exception ex)
{
attempts.Add(new AsbReconnectAttempt(attempt, Succeeded: false, ex));
trace?.Invoke($"asb.reconnect.failed={attempt}:{ex.GetType().Name}:{ex.Message}");
if (attempt < options.MaxAttempts && options.Delay > TimeSpan.Zero)
{
Thread.Sleep(options.Delay);
}
}
}
return new AsbReconnectResult(Succeeded: false, Client: null, currentCleanup, attempts);
}
public RegisterItemsResponse Register(string tag)
{
return RegisterMany([tag]);
}
public RegisterItemsResponse RegisterMany(IEnumerable<string> tags)
{
ArgumentNullException.ThrowIfNull(tags);
ItemIdentity[] items = CreateAbsoluteItems(tags);
RegisterItemsResponse response = RegisterOnce(items);
for (int attempt = 1; attempt < 5 && response.Result.ErrorCode == InvalidConnectionId; attempt++)
{
Thread.Sleep(TimeSpan.FromMilliseconds(100 * attempt));
response = RegisterOnce(items);
}
return response;
}
private RegisterItemsResponse RegisterOnce(string tag)
{
return RegisterOnce([CreateAbsoluteItem(tag)]);
}
private RegisterItemsResponse RegisterOnce(ItemIdentity[] items)
{
RegisterItemsRequest request = new()
{
Items = items,
RequireId = true,
RegisterOnly = true,
};
authenticator.Sign(request);
return channel.RegisterItems(request);
}
public UnregisterItemsResponse Unregister(string tag)
{
return Unregister(CreateAbsoluteItem(tag));
}
public UnregisterItemsResponse Unregister(ItemIdentity item)
{
return UnregisterMany([item]);
}
public UnregisterItemsResponse UnregisterMany(IEnumerable<ItemIdentity> items)
{
ArgumentNullException.ThrowIfNull(items);
UnregisterItemsRequest request = new()
{
Items = items.ToArray(),
};
authenticator.Sign(request);
return channel.UnregisterItems(request);
}
public ReadResponse Read(string tag)
{
return ReadMany([tag]);
}
public ReadResponse ReadMany(IEnumerable<string> tags)
{
ArgumentNullException.ThrowIfNull(tags);
ReadRequest request = new()
{
Items = CreateAbsoluteItems(tags),
};
authenticator.Sign(request);
return channel.Read(request);
}
public WriteResponse WriteInt32(string tag, int value, uint writeHandle)
{
return Write(tag, AsbVariantFactory.FromInt32(value), writeHandle, "MxAsbClient write-int");
}
public WriteResponse Write(string tag, Variant value, uint writeHandle, string? comment = null)
{
return Write(tag, value, new AsbWriteOptions
{
WriteHandle = writeHandle,
Comment = comment,
});
}
public WriteResponse Write(string tag, Variant value, AsbWriteOptions options)
{
ArgumentNullException.ThrowIfNull(options);
WriteBasicRequest request = new()
{
Items = [CreateAbsoluteItem(tag)],
Values =
[
new WriteValue
{
Value = value,
Comment = options.Comment ?? "MxAsbClient write",
},
],
WriteHandle = options.WriteHandle,
};
authenticator.Sign(request);
return channel.Write(request);
}
public PublishWriteCompleteResponse PublishWriteComplete()
{
PublishWriteCompleteRequest request = new();
authenticator.Sign(request);
return channel.PublishWriteComplete(request);
}
public AsbWriteCompletionResult WaitForWriteComplete(uint writeHandle, AsbWriteCompletionOptions? options = null)
{
options ??= AsbWriteCompletionOptions.Default;
options.ValidatePolling();
Stopwatch stopwatch = Stopwatch.StartNew();
List<PublishWriteCompleteResponse> responses = [];
List<ItemWriteComplete> completeWrites = [];
ItemWriteComplete? matchingComplete = null;
int pollCount = 0;
while (true)
{
options.CancellationToken.ThrowIfCancellationRequested();
PublishWriteCompleteResponse response = PublishWriteComplete();
pollCount++;
responses.Add(response);
ItemWriteComplete[] writes = response.CompleteWrites ?? [];
completeWrites.AddRange(writes);
foreach (ItemWriteComplete item in writes)
{
if (item.WriteHandleSpecified && item.WriteHandle == writeHandle)
{
matchingComplete = item;
break;
}
}
if (matchingComplete.HasValue)
{
stopwatch.Stop();
return new AsbWriteCompletionResult(
writeHandle,
Completed: true,
TimedOut: false,
stopwatch.Elapsed,
pollCount,
responses,
completeWrites,
matchingComplete);
}
TimeSpan remaining = options.Timeout - stopwatch.Elapsed;
if (remaining <= TimeSpan.Zero)
{
stopwatch.Stop();
return new AsbWriteCompletionResult(
writeHandle,
Completed: false,
TimedOut: true,
stopwatch.Elapsed,
pollCount,
responses,
completeWrites,
MatchingComplete: null);
}
TimeSpan delay = options.PollInterval < remaining ? options.PollInterval : remaining;
if (delay > TimeSpan.Zero)
{
if (options.CancellationToken.WaitHandle.WaitOne(delay))
{
throw new OperationCanceledException(options.CancellationToken);
}
}
}
}
public AsbWriteCompletionReadbackResult WaitForWriteCompleteAndRead(string tag, uint writeHandle, AsbWriteCompletionOptions? options = null)
{
options ??= AsbWriteCompletionOptions.Default;
options.ValidateReadback();
AsbWriteCompletionResult completion = WaitForWriteComplete(writeHandle, options);
if (!completion.Completed)
{
return new AsbWriteCompletionReadbackResult(completion, Readback: null);
}
if (options.ReadbackDelay > TimeSpan.Zero)
{
if (options.CancellationToken.WaitHandle.WaitOne(options.ReadbackDelay))
{
throw new OperationCanceledException(options.CancellationToken);
}
}
options.CancellationToken.ThrowIfCancellationRequested();
return new AsbWriteCompletionReadbackResult(completion, Read(tag));
}
public CreateSubscriptionResponse CreateSubscription(long maxQueueSize = 128, ulong sampleInterval = 1000)
{
return CreateSubscription(new AsbSubscriptionOptions
{
MaxQueueSize = maxQueueSize,
SampleInterval = sampleInterval,
});
}
public CreateSubscriptionResponse CreateSubscription(AsbSubscriptionOptions options)
{
ArgumentNullException.ThrowIfNull(options);
options.Validate();
CreateSubscriptionResponse response = CreateSubscriptionOnce(options.MaxQueueSize, options.SampleInterval);
for (int attempt = 1; attempt < 5 && response.Result.ErrorCode == InvalidConnectionId; attempt++)
{
Thread.Sleep(TimeSpan.FromMilliseconds(100 * attempt));
response = CreateSubscriptionOnce(options.MaxQueueSize, options.SampleInterval);
}
return response;
}
private CreateSubscriptionResponse CreateSubscriptionOnce(long maxQueueSize, ulong sampleInterval)
{
CreateSubscriptionRequest request = new()
{
MaxQueueSize = maxQueueSize,
SampleInterval = sampleInterval,
};
authenticator.Sign(request);
return channel.CreateSubscription(request);
}
public DeleteSubscriptionResponse DeleteSubscription(long subscriptionId)
{
DeleteSubscriptionRequest request = new()
{
SubscriptionId = subscriptionId,
};
authenticator.Sign(request);
return channel.DeleteSubscription(request);
}
public AddMonitoredItemsResponse AddMonitoredItems(long subscriptionId, IEnumerable<string> tags, ulong sampleInterval = 1000, bool active = true, bool buffered = false)
{
return AddMonitoredItems(subscriptionId, tags, new AsbMonitoredItemOptions
{
SampleInterval = sampleInterval,
Active = active,
Buffered = buffered,
});
}
public AddMonitoredItemsResponse AddMonitoredItems(long subscriptionId, IEnumerable<string> tags, AsbMonitoredItemOptions options)
{
ArgumentNullException.ThrowIfNull(tags);
ArgumentNullException.ThrowIfNull(options);
AddMonitoredItemsRequest request = new()
{
SubscriptionId = subscriptionId,
Items = tags.Select(tag => CreateMonitoredItem(CreateAbsoluteItem(tag), options.SampleInterval, options.Active, options.Buffered)).ToArray(),
RequireId = true,
};
authenticator.Sign(request);
AddMonitoredItemsResponse response = channel.AddMonitoredItems(request);
RememberItemNames(response.Status);
return response;
}
public DeleteMonitoredItemsResponse DeleteMonitoredItems(long subscriptionId, IEnumerable<ItemIdentity> items)
{
ArgumentNullException.ThrowIfNull(items);
ItemIdentity[] itemArray = items.ToArray();
DeleteMonitoredItemsRequest request = new()
{
SubscriptionId = subscriptionId,
Items = itemArray.Select(item => CreateMonitoredItem(item, 0, active: false, buffered: false)).ToArray(),
};
authenticator.Sign(request);
DeleteMonitoredItemsResponse response = channel.DeleteMonitoredItems(request);
ForgetItemNames(itemArray);
return response;
}
public PublishResponse Publish(long subscriptionId)
{
PublishRequest request = new()
{
SubscriptionId = subscriptionId,
};
authenticator.Sign(request);
return channel.Publish(request);
}
public AsbPublishResult PublishValues(long subscriptionId)
{
PublishResponse response = Publish(subscriptionId);
AsbPublishedValue[] values = AsbPublishMapper
.ToPublishedValues(response, monitoredItemNamesById)
.ToArray();
foreach (AsbPublishedValue value in values)
{
PublishedValueReceived?.Invoke(this, value);
}
return new AsbPublishResult(response, values);
}
public void Dispose()
{
Cleanup();
}
public AsbClientCleanupResult Cleanup(AsbClientCleanupOptions? options = null)
{
options ??= AsbClientCleanupOptions.Default;
options.Validate();
lock (cleanupGate)
{
if (disposed)
{
return cleanupResult ?? CreateAlreadyCleanedResult();
}
disposed = true;
trace?.Invoke("asb.stage=cleanup");
DisconnectCleanupResult disconnect = options.CancellationToken.IsCancellationRequested
? CreateCanceledDisconnectResult()
: SendDisconnectBestEffort(options);
CommunicationObjectCleanupResult channelResult = CleanupCommunicationObject(clientChannel, "channel", options);
CommunicationObjectCleanupResult factoryResult = CleanupCommunicationObject(factory, "factory", options);
cleanupResult = new AsbClientCleanupResult(
disconnect.Attempted,
disconnect.Sent,
disconnect.Failure,
channelResult,
factoryResult);
trace?.Invoke($"asb.cleanup.succeeded={cleanupResult.Succeeded}");
return cleanupResult;
}
}
private CommunicationObjectCleanupResult CleanupCommunicationObject(
ICommunicationObject communicationObject,
string name,
AsbClientCleanupOptions options)
{
if (options.CancellationToken.IsCancellationRequested)
{
trace?.Invoke($"asb.cleanup.{name}.canceled");
return AsbCommunicationCleanup.AbortOnly(communicationObject, name, trace);
}
return AsbCommunicationCleanup.CloseOrAbort(communicationObject, name, options.CloseTimeout, trace);
}
private static DisconnectCleanupResult CreateCanceledDisconnectResult()
{
string failure = AsbCommunicationCleanup.FormatCleanupFailure(
new OperationCanceledException("Cleanup cancellation requested before disconnect."));
return new DisconnectCleanupResult(Attempted: false, Sent: false, Failure: failure);
}
private DisconnectCleanupResult SendDisconnectBestEffort(AsbClientCleanupOptions options)
{
if (clientChannel.State is not CommunicationState.Opened)
{
trace?.Invoke($"asb.cleanup.disconnect.skipped-state={clientChannel.State}");
return new DisconnectCleanupResult(Attempted: false, Sent: false, Failure: null);
}
TimeSpan previousOperationTimeout = clientChannel.OperationTimeout;
try
{
clientChannel.OperationTimeout = options.DisconnectTimeout;
trace?.Invoke("asb.stage=disconnect");
Disconnect request = new()
{
ConsumerAuthenticationData = authenticator.CreateAuthenticationData(),
};
authenticator.Sign(request);
channel.Disconnect(request);
return new DisconnectCleanupResult(Attempted: true, Sent: true, Failure: null);
}
catch (Exception ex)
{
string failure = AsbCommunicationCleanup.FormatCleanupFailure(ex);
trace?.Invoke($"asb.cleanup.disconnect.failed={failure}");
return new DisconnectCleanupResult(Attempted: true, Sent: false, failure);
}
finally
{
clientChannel.OperationTimeout = previousOperationTimeout;
}
}
private static AsbClientCleanupResult CreateAlreadyCleanedResult()
{
CommunicationObjectCleanupResult skipped = new(
"unknown",
"Closed",
"Closed",
CloseAttempted: false,
Closed: true,
CloseFailure: null,
AbortAttempted: false,
Aborted: false,
AbortFailure: null);
return new AsbClientCleanupResult(
DisconnectAttempted: false,
DisconnectSent: false,
DisconnectFailure: null,
Channel: skipped with { Name = "channel" },
Factory: skipped with { Name = "factory" });
}
private static ItemIdentity CreateAbsoluteItem(string tag)
{
return new ItemIdentity
{
Type = (ushort)ItemIdentityType.Name,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Name = tag,
ContextName = string.Empty,
};
}
private static ItemIdentity[] CreateAbsoluteItems(IEnumerable<string> tags)
{
return tags.Select(CreateAbsoluteItem).ToArray();
}
private static MonitoredItem CreateMonitoredItem(ItemIdentity item, ulong sampleInterval, bool active, bool buffered)
{
return new MonitoredItem
{
Item = item,
SampleInterval = sampleInterval,
Active = active,
Buffered = buffered,
UserData = AsbVariantFactory.Empty,
ValueDeadband = AsbVariantFactory.Empty,
};
}
private void RememberItemNames(ItemStatus[]? statuses)
{
if (statuses is null)
{
return;
}
foreach (ItemStatus status in statuses)
{
if (status.Item.IdSpecified && !string.IsNullOrWhiteSpace(status.Item.Name))
{
monitoredItemNamesById[status.Item.Id] = status.Item.Name;
}
}
}
private void ForgetItemNames(IEnumerable<ItemIdentity> items)
{
foreach (ItemIdentity item in items)
{
if (item.IdSpecified)
{
monitoredItemNamesById.Remove(item.Id);
}
}
}
private static NetTcpBinding CreateBinding()
{
int max = int.MaxValue;
return new NetTcpBinding(SecurityMode.None)
{
TransferMode = TransferMode.Buffered,
MaxReceivedMessageSize = max,
MaxBufferSize = max,
MaxBufferPoolSize = long.MaxValue,
OpenTimeout = TimeSpan.FromSeconds(30),
ReceiveTimeout = TimeSpan.FromSeconds(30),
SendTimeout = TimeSpan.FromSeconds(30),
CloseTimeout = TimeSpan.FromSeconds(30),
ReaderQuotas =
{
MaxArrayLength = max,
MaxBytesPerRead = max,
MaxDepth = max,
MaxNameTableCharCount = max,
MaxStringContentLength = max,
},
ReliableSession =
{
InactivityTimeout = TimeSpan.FromSeconds(30),
},
};
}
public static string FormatVariant(Variant variant)
{
object? value = DecodeVariant(variant);
return value switch
{
null => string.Empty,
bool typed => typed.ToString(CultureInfo.InvariantCulture),
int typed => typed.ToString(CultureInfo.InvariantCulture),
float typed => typed.ToString(CultureInfo.InvariantCulture),
double typed => typed.ToString(CultureInfo.InvariantCulture),
DateTime typed => typed.ToString("O", CultureInfo.InvariantCulture),
TimeSpan typed => typed.ToString("c", CultureInfo.InvariantCulture),
int[] typed => string.Join(",", typed.Select(item => item.ToString(CultureInfo.InvariantCulture))),
bool[] typed => string.Join(",", typed.Select(item => item.ToString(CultureInfo.InvariantCulture))),
float[] typed => string.Join(",", typed.Select(item => item.ToString(CultureInfo.InvariantCulture))),
double[] typed => string.Join(",", typed.Select(item => item.ToString(CultureInfo.InvariantCulture))),
string[] typed => string.Join("|", typed),
DateTime[] typed => string.Join(",", typed.Select(item => item.ToString("O", CultureInfo.InvariantCulture))),
TimeSpan[] typed => string.Join(",", typed.Select(item => item.ToString("c", CultureInfo.InvariantCulture))),
string typed => typed,
byte[] typed => BitConverter.ToString(typed).Replace("-", string.Empty),
_ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
};
}
public static object? DecodeVariant(Variant variant)
{
byte[] payload = variant.Payload ?? [];
if (payload.Length == 0)
{
return variant.Type switch
{
(ushort)AsbDataType.TypeString => string.Empty,
(ushort)AsbDataType.TypeInt32Array => Array.Empty<int>(),
(ushort)AsbDataType.TypeBoolArray => Array.Empty<bool>(),
(ushort)AsbDataType.TypeFloatArray => Array.Empty<float>(),
(ushort)AsbDataType.TypeDoubleArray => Array.Empty<double>(),
(ushort)AsbDataType.TypeStringArray => Array.Empty<string>(),
(ushort)AsbDataType.TypeDateTimeArray => Array.Empty<DateTime>(),
(ushort)AsbDataType.TypeDurationArray => Array.Empty<TimeSpan>(),
_ => null,
};
}
return variant.Type switch
{
(ushort)AsbDataType.TypeBool when payload.Length >= 1 => payload[0] != 0,
(ushort)AsbDataType.TypeInt32 when payload.Length >= 4 => BitConverter.ToInt32(payload, 0),
(ushort)AsbDataType.TypeFloat when payload.Length >= 4 => BitConverter.ToSingle(payload, 0),
(ushort)AsbDataType.TypeDouble when payload.Length >= 8 => BitConverter.ToDouble(payload, 0),
(ushort)AsbDataType.TypeString => System.Text.Encoding.Unicode.GetString(payload),
(ushort)AsbDataType.TypeDateTime when payload.Length >= 8 => DateTime.FromFileTimeUtc(BitConverter.ToInt64(payload, 0)),
(ushort)AsbDataType.TypeDuration when payload.Length >= 8 => TimeSpan.FromTicks(BitConverter.ToInt64(payload, 0)),
(ushort)AsbDataType.TypeInt32Array => DecodeInt32Array(payload),
(ushort)AsbDataType.TypeBoolArray => payload.Select(item => item != 0).ToArray(),
(ushort)AsbDataType.TypeFloatArray => DecodeSingleArray(payload),
(ushort)AsbDataType.TypeDoubleArray => DecodeDoubleArray(payload),
(ushort)AsbDataType.TypeStringArray => DecodeStringArray(payload),
(ushort)AsbDataType.TypeDateTimeArray => DecodeDateTimeArray(payload),
(ushort)AsbDataType.TypeDurationArray => DecodeDurationArray(payload),
_ => payload,
};
}
private static int[] DecodeInt32Array(byte[] payload)
{
int[] values = new int[payload.Length / sizeof(int)];
for (int i = 0; i < values.Length; i++)
{
values[i] = BitConverter.ToInt32(payload, i * sizeof(int));
}
return values;
}
private static float[] DecodeSingleArray(byte[] payload)
{
float[] values = new float[payload.Length / sizeof(float)];
for (int i = 0; i < values.Length; i++)
{
values[i] = BitConverter.ToSingle(payload, i * sizeof(float));
}
return values;
}
private static double[] DecodeDoubleArray(byte[] payload)
{
double[] values = new double[payload.Length / sizeof(double)];
for (int i = 0; i < values.Length; i++)
{
values[i] = BitConverter.ToDouble(payload, i * sizeof(double));
}
return values;
}
private static string[] DecodeStringArray(byte[] payload)
{
List<string> values = [];
int offset = 0;
while (offset + sizeof(int) <= payload.Length)
{
int byteLength = BitConverter.ToInt32(payload, offset);
offset += sizeof(int);
if (byteLength < 0 || offset + byteLength > payload.Length)
{
break;
}
values.Add(byteLength == 0 ? string.Empty : System.Text.Encoding.Unicode.GetString(payload, offset, byteLength));
offset += byteLength;
}
return values.ToArray();
}
private static DateTime[] DecodeDateTimeArray(byte[] payload)
{
DateTime[] values = new DateTime[payload.Length / sizeof(long)];
for (int i = 0; i < values.Length; i++)
{
values[i] = DateTime.FromFileTimeUtc(BitConverter.ToInt64(payload, i * sizeof(long)));
}
return values;
}
private static TimeSpan[] DecodeDurationArray(byte[] payload)
{
TimeSpan[] values = new TimeSpan[payload.Length / sizeof(long)];
for (int i = 0; i < values.Length; i++)
{
values[i] = TimeSpan.FromTicks(BitConverter.ToInt64(payload, i * sizeof(long)));
}
return values;
}
private sealed record DisconnectCleanupResult(bool Attempted, bool Sent, string? Failure);
}
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MxAsbClient.Tests")]
[assembly: InternalsVisibleTo("MxAsbClient.Probe")]
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Reference Include="Interop.aaMxDataConsumer">
<HintPath>..\..\analysis\interop\Interop.aaMxDataConsumer.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
</Project>
+406
View File
@@ -0,0 +1,406 @@
using System;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Interop.aaMxDataConsumer;
namespace MxDataConsumerProbe;
internal static class Program
{
[STAThread]
private static int Main(string[] args)
{
if (HasFlag(args, "--probe-dataclient"))
{
return ProbeDataClient(args);
}
string tag = GetString(args, "--tag") ?? "TestChildObject.ScanState";
string context = GetString(args, "--context") ?? string.Empty;
int itemId = GetInt(args, "--item-id") ?? 1;
ulong userData = (ulong)(GetInt(args, "--user-data") ?? 0x4401);
int active = GetInt(args, "--active") ?? 0;
int pollCount = GetInt(args, "--poll-count") ?? 10;
int pollDelayMs = GetInt(args, "--poll-delay-ms") ?? 500;
int connectPollCount = GetInt(args, "--connect-poll-count") ?? Math.Max(pollCount, 12);
int connectPollDelayMs = GetInt(args, "--connect-poll-delay-ms") ?? Math.Max(pollDelayMs, 1000);
string[] namespaces = GetStrings(args, "--namespace");
if (namespaces.Length == 0)
{
namespaces =
[
"Default_ZB_MxDataProvider",
"Default_ZB2_MxDataProvider",
"net.tcp://localhost:4000/Default_ZB_MxDataProvider/IDataV3",
"net.tcp://localhost:4000/Default_ZB_MxDataProvider",
"net.tcp://localhost:9055/IDataV3SoapContract",
"Galaxy",
"ArchestrA",
"Lmx",
"localhost",
"ZB",
];
}
Console.WriteLine($"process=x64:{Environment.Is64BitProcess}");
Console.WriteLine($"tag={tag}");
Console.WriteLine($"context={context}");
var consumer = new DataConsumerClass();
var callback = new DataConsumerCallback();
consumer.RegisterCallback(callback);
try
{
int namespaceId = ResolveNamespace(consumer, namespaces);
Console.WriteLine($"namespace_id={namespaceId}");
Console.WriteLine($"namespace_callback_count={callback.NamespaceIds.Length}");
PrintConnectionState(consumer, namespaceId, "after_namespace");
PollConnection(consumer, namespaceId, connectPollCount, connectPollDelayMs);
consumer.ResolveReference(namespaceId, tag, context, itemId, userData);
Console.WriteLine("resolve_reference=sent");
PrintConnectionState(consumer, namespaceId, "after_resolve_reference");
PollRegistration(consumer, pollCount, pollDelayMs);
consumer.subscribe(namespaceId, itemId, userData, bActive: 1);
Console.WriteLine("subscribe=sent");
PrintConnectionState(consumer, namespaceId, "after_subscribe");
PollSubscription(consumer, pollCount, pollDelayMs);
consumer.ActivateSuspend(namespaceId, itemId, userData, active);
Console.WriteLine($"activate_suspend=sent active={active}");
PrintConnectionState(consumer, namespaceId, "after_activate_suspend");
PollActivateSuspend(consumer, pollCount, pollDelayMs);
return 0;
}
finally
{
try
{
consumer.UnregisterCallback();
}
catch (COMException ex)
{
Console.WriteLine($"unregister_callback_error=0x{ex.HResult:X8}");
}
}
}
private static int ProbeDataClient(string[] args)
{
string namespaceName = GetString(args, "--namespace") ?? "Galaxy";
string[] endpoints = GetStrings(args, "--endpoint");
if (endpoints.Length == 0)
{
endpoints =
[
"net.tcp://localhost:4000/Default_ZB_MxDataProvider/IDataV3",
"net.tcp://localhost:4000/Default_ZB_MxDataProvider",
"net.tcp://localhost:4000/IDataV3",
"net.tcp://localhost:4000/MxDataService1/IDataV3",
"net.tcp://localhost:9055/IDataV3SoapContract",
];
}
DataClientClass client;
try
{
client = new DataClientClass();
}
catch (COMException ex)
{
Console.WriteLine($"dataclient_create_error=0x{ex.HResult:X8}");
return 2;
}
client.Initialize(namespaceName);
Console.WriteLine($"dataclient_initialize={namespaceName}");
foreach (string endpoint in endpoints)
{
var token = new _IUserToken();
try
{
IArchestrAResult[] results = client.Connect2(endpoint, timeout: 5000, ref token, out uint clientId);
Console.WriteLine($"dataclient_connect endpoint={endpoint} client_id={clientId} results={results?.Length ?? 0}");
PrintArchestrAResults("dataclient_connect_result", results);
Console.WriteLine($"dataclient_connected={client.IsIDataClientConnected()}");
return 0;
}
catch (COMException ex)
{
Console.WriteLine($"dataclient_connect_error endpoint={endpoint} hresult=0x{ex.HResult:X8} last_error={SafeGetLastError(client)}");
}
}
return 2;
}
private static void PrintArchestrAResults(string prefix, IArchestrAResult[]? results)
{
if (results is null)
{
return;
}
for (int i = 0; i < results.Length; i++)
{
IArchestrAResult result = results[i];
try
{
Console.WriteLine($"{prefix}[{i}]=type:{result.GetType().FullName}");
}
catch (COMException ex)
{
Console.WriteLine($"{prefix}[{i}]_error=0x{ex.HResult:X8}");
}
}
}
private static string SafeGetLastError(DataClientClass client)
{
try
{
return client.GetLastError();
}
catch (COMException ex)
{
return $"<GetLastError failed 0x{ex.HResult:X8}>";
}
}
private static int ResolveNamespace(DataConsumerClass consumer, string[] namespaces)
{
foreach (string ns in namespaces)
{
try
{
int namespaceId = consumer.ResolveNamespace(ns);
Console.WriteLine($"resolve_namespace name={ns} id={namespaceId}");
return namespaceId;
}
catch (COMException ex)
{
Console.WriteLine($"resolve_namespace_error name={ns} hresult=0x{ex.HResult:X8}");
}
}
throw new InvalidOperationException("No candidate namespace resolved.");
}
private static void PollConnection(DataConsumerClass consumer, int namespaceId, int pollCount, int pollDelayMs)
{
for (int i = 0; i < pollCount; i++)
{
try
{
consumer.IsConnected(namespaceId, out int connected);
Console.WriteLine($"connection_poll[{i}]={connected}");
if (connected != 0)
{
return;
}
}
catch (COMException ex)
{
Console.WriteLine($"connection_poll_error[{i}]=0x{ex.HResult:X8}");
}
Thread.Sleep(pollDelayMs);
}
}
private static void PollRegistration(DataConsumerClass consumer, int pollCount, int pollDelayMs)
{
for (int i = 0; i < pollCount; i++)
{
uint result = consumer.ProcessRegistration2(
out int size,
out ItemRegistrationResponse[] responses,
out MxResultCode[] resultCodes);
Console.WriteLine($"registration_poll[{i}]=result:0x{result:X8} size:{size} responses:{responses?.Length ?? 0} result_codes:{resultCodes?.Length ?? 0}");
if (responses is { Length: > 0 } || resultCodes is { Length: > 0 })
{
PrintRegistration(responses);
PrintResultCodes("registration_result", resultCodes);
return;
}
Thread.Sleep(pollDelayMs);
}
}
private static void PrintConnectionState(DataConsumerClass consumer, int namespaceId, string label)
{
try
{
consumer.IsConnected(namespaceId, out int connected);
Console.WriteLine($"is_connected[{label}]={connected}");
}
catch (COMException ex)
{
Console.WriteLine($"is_connected_error[{label}]=0x{ex.HResult:X8}");
}
}
private static void PollSubscription(DataConsumerClass consumer, int pollCount, int pollDelayMs)
{
for (int i = 0; i < pollCount; i++)
{
uint result = consumer.ProcessSubscription2(
out int size,
out ItemSubscriptionResponse[] responses,
out MxResultCode[] resultCodes);
Console.WriteLine($"subscription_poll[{i}]=result:0x{result:X8} size:{size} responses:{responses?.Length ?? 0} result_codes:{resultCodes?.Length ?? 0}");
if (responses is { Length: > 0 } || resultCodes is { Length: > 0 })
{
PrintSubscription(responses);
PrintResultCodes("subscription_result", resultCodes);
return;
}
Thread.Sleep(pollDelayMs);
}
}
private static void PollActivateSuspend(DataConsumerClass consumer, int pollCount, int pollDelayMs)
{
for (int i = 0; i < pollCount; i++)
{
uint result = consumer.ProcessActivateSuspend2(
out int size,
out ItemActiveResponse[] responses,
out MxResultCode[] resultCodes);
Console.WriteLine($"activate_suspend_poll[{i}]=result:0x{result:X8} size:{size} responses:{responses?.Length ?? 0} result_codes:{resultCodes?.Length ?? 0}");
if (responses is { Length: > 0 } || resultCodes is { Length: > 0 })
{
PrintActive(responses);
PrintResultCodes("activate_suspend_result", resultCodes);
return;
}
Thread.Sleep(pollDelayMs);
}
}
private static void PrintRegistration(ItemRegistrationResponse[]? responses)
{
if (responses is null)
{
return;
}
for (int i = 0; i < responses.Length; i++)
{
ItemRegistrationResponse response = responses[i];
Console.WriteLine($"registration[{i}]=item:{response.ItemId} user_data:{response.userData} status:0x{response.Status:X8} write_capability:{response.WriteCapability}");
}
}
private static void PrintSubscription(ItemSubscriptionResponse[]? responses)
{
if (responses is null)
{
return;
}
for (int i = 0; i < responses.Length; i++)
{
ItemSubscriptionResponse response = responses[i];
Console.WriteLine($"subscription[{i}]=item:{response.ItemId} user_data:{response.userData} status:0x{response.Status:X8}");
}
}
private static void PrintActive(ItemActiveResponse[]? responses)
{
if (responses is null)
{
return;
}
for (int i = 0; i < responses.Length; i++)
{
ItemActiveResponse response = responses[i];
Console.WriteLine($"activate_suspend[{i}]=item:{response.ItemId} user_data:{response.userData} status:0x{response.Status:X8}");
}
}
private static void PrintResultCodes(string prefix, MxResultCode[]? resultCodes)
{
if (resultCodes is null)
{
return;
}
for (int i = 0; i < resultCodes.Length; i++)
{
MxResultCode resultCode = resultCodes[i];
Console.WriteLine($"{prefix}[{i}]=namespace:{resultCode.lNamespaceId} error:0x{resultCode.ErrorCode:X8} arch_error:0x{resultCode.ArchestrErrorCode:X8} arch_specific:0x{resultCode.ArchestrSpecific:X8} arch_status:0x{resultCode.ArchestrStatus:X8}");
}
}
private static int? GetInt(string[] args, string name)
{
string? raw = GetString(args, name);
if (raw is null)
{
return null;
}
return raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? int.Parse(raw.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture)
: int.Parse(raw, CultureInfo.InvariantCulture);
}
private static string? GetString(string[] args, string name)
{
string prefix = name + "=";
return args.FirstOrDefault(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))?.Substring(prefix.Length);
}
private static string[] GetStrings(string[] args, string name)
{
string prefix = name + "=";
return args
.Where(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(arg => arg.Substring(prefix.Length))
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
}
private static bool HasFlag(string[] args, string name)
{
return args.Any(arg => arg.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
internal sealed class DataConsumerCallback : IDataConsumerCallback
{
private readonly object _gate = new();
private readonly System.Collections.Generic.List<int> _namespaceIds = new();
public int[] NamespaceIds
{
get
{
lock (_gate)
{
return _namespaceIds.ToArray();
}
}
}
public void NamespaceResolved(int lNamespaceId)
{
lock (_gate)
{
_namespaceIds.Add(lNamespaceId);
}
Console.WriteLine($"callback_namespace_resolved={lNamespaceId}");
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MxNativeClient\MxNativeClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MxNativeClient\MxNativeClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</Project>
+658
View File
@@ -0,0 +1,658 @@
using MxNativeClient;
using MxNativeCodec;
RunObjRefParseTest();
RunOrpcStructureTests();
RunObjectExporterMessageTests();
RunNmxService2MessageTests();
RunNmxSvcCallbackMessageTests();
RunDceRpcAuthTrailerTests();
RunRemUnknownMessageTests();
RunDceRpcBindParseTest();
RunDceRpcRequestParseTest();
RunDceRpcResponseParseTest();
await RunGalaxyRepositoryResolverTestAsync();
await RunGalaxyRepositoryUserResolverTestAsync();
RunManagedNmxService2ClientTransferBodyTests();
RunCompatibilitySurfaceTests();
RunRecoveryPolicyTests();
RunOperationStatusTests();
Console.WriteLine("MxNativeClient protocol primitive tests passed.");
static void RunObjRefParseTest()
{
// Captured from: dotnet run MxNativeClient.Probe -- --dump-objref --objref-context=2
byte[] prefix = FromHex(
"4d 45 4f 57 01 00 00 00 00 00 00 00 00 00 00 00 c0 00 00 00 00 00 00 46 " +
"00 00 00 00 01 00 00 00 c2 5b ab 3b b5 d2 f0 ea 53 46 0b 86 8b 5f 57 39 " +
"20 f8 00 00 78 36 00 00 33 3c 4c 10 66 e0 ac 8b 95 00 7f 00");
var objRef = ComObjRef.Parse(prefix);
AssertEqual("OBJREF signature", 0x574F454Du, objRef.Signature);
AssertEqual("OBJREF flags", 1u, objRef.Flags);
AssertEqual("OBJREF iid", new Guid("00000000-0000-0000-C000-000000000046"), objRef.Iid);
AssertEqual("OBJREF OXID", 0xEAF0D2B53BAB5BC2ul, objRef.Oxid);
AssertEqual("OBJREF OID", 0x39575F8B860B4653ul, objRef.Oid);
AssertEqual("OBJREF entries", (ushort)149, objRef.DualStringEntries);
var std = StdObjRef.Parse(prefix.AsSpan(24, StdObjRef.EncodedLength));
AssertEqual("STD OXID", objRef.Oxid, std.Oxid);
AssertEqual("STD OID", objRef.Oid, std.Oid);
AssertEqual("STD IPID", objRef.Ipid, std.Ipid);
}
static void RunCompatibilitySurfaceTests()
{
Type serverType = typeof(MxNativeCompatibilityServer);
AssertNotNull(
"compat Write2 object timestamp overload",
serverType.GetMethod(
nameof(MxNativeCompatibilityServer.Write2),
[
typeof(int),
typeof(int),
typeof(object),
typeof(object),
typeof(int),
]));
AssertNotNull(
"compat WriteSecured2 object timestamp overload",
serverType.GetMethod(
nameof(MxNativeCompatibilityServer.WriteSecured2),
[
typeof(int),
typeof(int),
typeof(int),
typeof(int),
typeof(object),
typeof(object),
]));
AssertNotNull(
"compat recover method",
serverType.GetMethod(nameof(MxNativeCompatibilityServer.RecoverConnection), [typeof(int)]));
AssertNotNull(
"compat async recover method",
serverType.GetMethod(
nameof(MxNativeCompatibilityServer.RecoverConnectionAsync),
[typeof(int), typeof(MxNativeRecoveryPolicy), typeof(CancellationToken)]));
AssertNotNull("compat data-change event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.DataChanged)));
AssertNotNull("compat buffered data-change event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.BufferedDataChanged)));
AssertNotNull("compat write-complete event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.WriteCompleted)));
AssertNotNull("compat operation-complete event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.OperationCompleted)));
AssertNotNull("compat recovery-attempt event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.RecoveryAttemptStarted)));
AssertNotNull("compat recovery-failed event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.RecoveryAttemptFailed)));
AssertNotNull("compat recovery-completed event", serverType.GetEvent(nameof(MxNativeCompatibilityServer.RecoveryCompleted)));
AssertNotNull(
"session recover method",
typeof(MxNativeSession).GetMethod(nameof(MxNativeSession.RecoverConnection), Type.EmptyTypes));
AssertNotNull(
"session async recover method",
typeof(MxNativeSession).GetMethod(
nameof(MxNativeSession.RecoverConnectionAsync),
[typeof(MxNativeRecoveryPolicy), typeof(CancellationToken)]));
AssertNotNull("session recovery-attempt event", typeof(MxNativeSession).GetEvent(nameof(MxNativeSession.RecoveryAttemptStarted)));
AssertNotNull("session recovery-failed event", typeof(MxNativeSession).GetEvent(nameof(MxNativeSession.RecoveryAttemptFailed)));
AssertNotNull("session recovery-completed event", typeof(MxNativeSession).GetEvent(nameof(MxNativeSession.RecoveryCompleted)));
AssertNotNull(
"callback recovery marker",
typeof(MxNativeCallbackEvent).GetProperty(nameof(MxNativeCallbackEvent.IsDuringRecovery)));
AssertNotNull(
"operation status recovery marker",
typeof(MxNativeOperationStatusEvent).GetProperty(nameof(MxNativeOperationStatusEvent.IsDuringRecovery)));
AssertNotNull(
"reference registration recovery marker",
typeof(MxNativeReferenceRegistrationEvent).GetProperty(nameof(MxNativeReferenceRegistrationEvent.IsDuringRecovery)));
AssertNotNull(
"unparsed callback recovery marker",
typeof(MxNativeUnparsedCallbackEvent).GetProperty(nameof(MxNativeUnparsedCallbackEvent.IsDuringRecovery)));
AssertNotNull(
"compat data-change recovery marker",
typeof(MxNativeDataChangeEvent).GetProperty(nameof(MxNativeDataChangeEvent.IsDuringRecovery)));
AssertNotNull(
"compat write-complete recovery marker",
typeof(MxNativeWriteCompleteEvent).GetProperty(nameof(MxNativeWriteCompleteEvent.IsDuringRecovery)));
AssertNotNull(
"compat buffered data-change recovery marker",
typeof(MxNativeBufferedDataChangeEvent).GetProperty(nameof(MxNativeBufferedDataChangeEvent.IsDuringRecovery)));
AssertNotNull(
"managed nmx heartbeat method",
typeof(ManagedNmxService2Client).GetMethod(
nameof(ManagedNmxService2Client.SetHeartbeatSendInterval),
[typeof(int), typeof(int)]));
}
static void RunRecoveryPolicyTests()
{
new MxNativeRecoveryPolicy { MaxAttempts = 1, Delay = TimeSpan.Zero }.Validate();
AssertThrows<ArgumentOutOfRangeException>(
"recovery attempts validation",
() => new MxNativeRecoveryPolicy { MaxAttempts = 0 }.Validate());
AssertThrows<ArgumentOutOfRangeException>(
"recovery delay validation",
() => new MxNativeRecoveryPolicy { Delay = TimeSpan.FromMilliseconds(-1) }.Validate());
}
static void RunOperationStatusTests()
{
AssertEqual("completion-only 00 parses", true, NmxOperationStatusMessage.TryParseInner([0x00], out var completionOk));
AssertEqual("completion-only format", NmxOperationStatusFormat.CompletionOnly, completionOk.Format);
AssertEqual("completion-only 00 code", (byte)0x00, completionOk.CompletionCode);
AssertEqual("completion-only not mxaccess write complete", false, completionOk.IsMxAccessWriteComplete);
AssertEqual("completion-only 00 success", (short)0, completionOk.Status.Success);
AssertEqual("completion-only 00 category", MxStatusCategory.Unknown, completionOk.Status.Category);
AssertEqual("completion-only 00 detail", (short)0, completionOk.Status.Detail);
AssertEqual("completion-only 41 parses", true, NmxOperationStatusMessage.TryParseInner([0x41], out var completionBadType));
AssertEqual("completion-only 41 detail", (short)0x41, completionBadType.Status.Detail);
AssertEqual("completion-only 41 category", MxStatusCategory.Unknown, completionBadType.Status.Category);
AssertEqual("completion-only 41 not mxaccess write complete", false, completionBadType.IsMxAccessWriteComplete);
AssertEqual("completion-only ef parses", true, NmxOperationStatusMessage.TryParseInner([0xef], out var completionInternationalized));
AssertEqual("completion-only ef detail", (short)0xef, completionInternationalized.Status.Detail);
AssertEqual("completion-only ef not mxaccess write complete", false, completionInternationalized.IsMxAccessWriteComplete);
AssertEqual("status-word write complete parses", true, NmxOperationStatusMessage.TryParseInner([0x00, 0x00, 0x50, 0x80, 0x00], out var statusWord));
AssertEqual("status-word format", NmxOperationStatusFormat.StatusWord, statusWord.Format);
AssertEqual("status-word code", (ushort)0x8050, statusWord.StatusCode);
AssertEqual("status-word mxaccess write complete", true, statusWord.IsMxAccessWriteComplete);
AssertEqual("status-word status", MxStatus.WriteCompleteOk, statusWord.Status);
}
static void AssertThrows<TException>(string name, Action action)
where TException : Exception
{
try
{
action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException($"{name}: expected {typeof(TException).Name}.");
}
static void AssertNotNull(string name, object? actual)
{
if (actual is null)
{
throw new InvalidOperationException($"{name}: expected non-null value.");
}
}
static void RunOrpcStructureTests()
{
var cid = new Guid("01234567-89AB-CDEF-0123-456789ABCDEF");
var thisCall = OrpcThis.Create(cid);
byte[] encodedThis = thisCall.Encode();
AssertEqual("ORPCTHIS length", OrpcThis.EncodedLengthWithoutExtensions, encodedThis.Length);
AssertEqual("ORPCTHIS version", ComVersion.Version57, OrpcThis.Parse(encodedThis).Version);
AssertEqual("ORPCTHIS cid", cid, OrpcThis.Parse(encodedThis).Cid);
var thatCall = new OrpcThat(0, 0);
byte[] encodedThat = thatCall.Encode();
AssertEqual("ORPCTHAT length", OrpcThat.EncodedLengthWithoutExtensions, encodedThat.Length);
AssertEqual("ORPCTHAT flags", 0u, OrpcThat.Parse(encodedThat).Flags);
byte[] objRef = FromHex(
"4d 45 4f 57 01 00 00 00 00 00 00 00 00 00 00 00 c0 00 00 00 00 00 00 46 " +
"00 00 00 00 01 00 00 00 c2 5b ab 3b b5 d2 f0 ea 53 46 0b 86 8b 5f 57 39 " +
"20 f8 00 00 78 36 00 00 33 3c 4c 10 66 e0 ac 8b 95 00 7f 00");
var mip = new MInterfacePointer(objRef);
AssertBytes("MInterfacePointer payload", objRef, MInterfacePointer.Parse(mip.Encode()).ObjRefBytes);
}
static void RunRemUnknownMessageTests()
{
var ipid = new Guid("00007c14-3678-0000-fa76-a0a5cd73ac85");
var iid = NmxProcedureMetadata.INmxService2;
var cid = new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
byte[] request = RemUnknownMessages.EncodeRemQueryInterfaceRequest(ipid, iid, cid, publicRefs: 5);
AssertEqual("RemQI length", 76, request.Length);
AssertEqual("RemQI ORPCTHIS CID", cid, OrpcThis.Parse(request).Cid);
AssertEqual("RemQI source IPID", ipid, new Guid(request.AsSpan(OrpcThis.EncodedLengthWithoutExtensions, 16)));
AssertEqual("RemQI refs", 5u, ReadUInt32(request, 48));
AssertEqual("RemQI iid count", (ushort)1, ReadUInt16(request, 52));
AssertEqual("RemQI conformant max", 1u, ReadUInt32(request, 56));
AssertEqual("RemQI requested IID", iid, new Guid(request.AsSpan(60, 16)));
var std = new StdObjRef(0, 5, 0x0102030405060708, 0x1112131415161718, ipid);
byte[] resultBytes = new byte[RemQiResult.EncodedLength];
std.Encode().CopyTo(resultBytes.AsSpan(8));
var result = RemQiResult.Parse(resultBytes);
AssertEqual("RemQI result hr", 0, result.HResult);
AssertEqual("RemQI result ipid", ipid, result.StandardObjectReference.Ipid);
}
static void RunObjectExporterMessageTests()
{
byte[] expected = FromHex("c2 5b ab 3b b5 d2 f0 ea 01 00 00 00 01 00 00 00 07 00 00 00");
byte[] actual = ObjectExporterMessages.EncodeResolveOxidRequest(
0xEAF0D2B53BAB5BC2,
[ObjectExporterMessages.ProtseqNcacnIpTcp]);
AssertBytes("ResolveOxid request", expected, actual);
var failure = ObjectExporterMessages.ParseResolveOxidFailure(FromHex(
"00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05 00 00 00"));
AssertEqual("ResolveOxid unauth status", 5u, failure.ErrorStatus);
}
static void RunNmxService2MessageTests()
{
var cid = new Guid("11111111-2222-3333-4444-555555555555");
var orpcThis = OrpcThis.Create(cid);
byte[] getVersion = NmxService2Messages.EncodeGetPartnerVersionRequest(orpcThis, 1, 2, 0x7ffd);
AssertEqual("GetPartnerVersion opnum", (ushort)11, NmxService2Messages.GetPartnerVersionOpnum);
AssertEqual("GetPartnerVersion request length", 44, getVersion.Length);
AssertEqual("GetPartnerVersion cid", cid, OrpcThis.Parse(getVersion).Cid);
AssertEqual("GetPartnerVersion galaxy", 1u, ReadUInt32(getVersion, 32));
AssertEqual("GetPartnerVersion platform", 2u, ReadUInt32(getVersion, 36));
AssertEqual("GetPartnerVersion engine", 0x7ffdu, ReadUInt32(getVersion, 40));
byte[] response = FromHex("00 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00");
var parsed = NmxService2Messages.ParseGetPartnerVersionResponse(response);
AssertEqual("GetPartnerVersion response version", 6, parsed.PartnerVersion);
AssertEqual("GetPartnerVersion response hr", 0, parsed.HResult);
byte[] connect = NmxService2Messages.EncodeConnectRequest(orpcThis, 0x7fee, 1, 1, 0x7ffd);
AssertEqual("Connect request length", 48, connect.Length);
AssertEqual("Connect local engine", 0x7feeu, ReadUInt32(connect, 32));
byte[] subscriber = NmxService2Messages.EncodeSubscriberEngineRequest(orpcThis, 0x7fee, 1, 1, 0x7ffd);
AssertEqual("Subscriber request length", 48, subscriber.Length);
AssertEqual("Subscriber local engine", 0x7feeu, ReadUInt32(subscriber, 32));
AssertEqual("Subscriber remote engine", 0x7ffdu, ReadUInt32(subscriber, 44));
byte[] unregister = NmxService2Messages.EncodeUnregisterEngineRequest(orpcThis, 0x7fee);
AssertEqual("UnRegisterEngine request length", 36, unregister.Length);
AssertEqual("UnRegisterEngine local engine", 0x7feeu, ReadUInt32(unregister, 32));
byte[] heartbeat = NmxService2Messages.EncodeSetHeartbeatSendIntervalRequest(orpcThis, 5, 3);
AssertEqual("Heartbeat request length", 40, heartbeat.Length);
AssertEqual("Heartbeat ticks", 5u, ReadUInt32(heartbeat, 32));
AssertEqual("Heartbeat misses", 3u, ReadUInt32(heartbeat, 36));
byte[] transferBody = FromHex(
"01 00 00 00 00 00 00 00 00 00 9e 91 04 00 01 00 00 00 " +
"01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 " +
"00 00 02 02 00 00 30 75 00 00");
byte[] transfer = NmxService2Messages.EncodeTransferDataRequest(orpcThis, 1, 1, 2, transferBody);
AssertEqual("TransferData opnum", (ushort)6, NmxService2Messages.TransferDataOpnum);
AssertEqual("TransferData request length", 100, transfer.Length);
AssertEqual("TransferData galaxy", 1u, ReadUInt32(transfer, 32));
AssertEqual("TransferData platform", 1u, ReadUInt32(transfer, 36));
AssertEqual("TransferData engine", 2u, ReadUInt32(transfer, 40));
AssertEqual("TransferData lSize", 46u, ReadUInt32(transfer, 44));
AssertEqual("TransferData max count", 46u, ReadUInt32(transfer, 48));
AssertBytes("TransferData body", transferBody, transfer.AsSpan(52, transferBody.Length));
var hresult = NmxService2Messages.ParseHResultResponse(FromHex("00 00 00 00 00 00 00 00 00 00 00 00"));
AssertEqual("HRESULT response", 0, hresult.HResult);
byte[] bstr = NmxService2Messages.EncodeBstrUserMarshal("NmxComProxyWire5");
AssertBytes(
"BSTR user marshal",
FromHex(
"10 00 00 00 20 00 00 00 10 00 00 00 4e 00 6d 00 " +
"78 00 43 00 6f 00 6d 00 50 00 72 00 6f 00 78 00 " +
"79 00 57 00 69 00 72 00 65 00 35 00"),
bstr);
byte[] register = NmxService2Messages.EncodeRegisterEngine2Request(orpcThis, 0x7104, "NmxComProxyWire5", 6);
AssertEqual("RegisterEngine2 opnum", (ushort)10, NmxService2Messages.RegisterEngine2Opnum);
AssertEqual("RegisterEngine2 null callback length", 92, register.Length);
AssertEqual("RegisterEngine2 engine", 0x7104u, ReadUInt32(register, 32));
AssertEqual("RegisterEngine2 BSTR marker", 0x72657355u, ReadUInt32(register, 36));
AssertBytes("RegisterEngine2 BSTR", bstr, register.AsSpan(40, bstr.Length));
AssertEqual("RegisterEngine2 version", 6u, ReadUInt32(register, 40 + bstr.Length));
AssertEqual("RegisterEngine2 null callback", 0u, ReadUInt32(register, 44 + bstr.Length));
byte[] oddRegister = NmxService2Messages.EncodeRegisterEngine2Request(orpcThis, 0x7200, "MxManagedNullCallback", 6);
AssertEqual("RegisterEngine2 odd-name length", 104, oddRegister.Length);
AssertEqual("RegisterEngine2 odd-name marker", 0x72657355u, ReadUInt32(oddRegister, 36));
AssertEqual("RegisterEngine2 odd-name version", 6u, ReadUInt32(oddRegister, 96));
AssertEqual("RegisterEngine2 odd-name null callback", 0u, ReadUInt32(oddRegister, 100));
byte[] compactObjRef = new byte[68];
byte[] callbackRegister = NmxService2Messages.EncodeRegisterEngine2Request(orpcThis, 0x7108, "NmxCallbackX86", 6, compactObjRef);
AssertEqual("RegisterEngine2 callback marker", 0x00020000u, ReadUInt32(callbackRegister, 84));
AssertEqual("RegisterEngine2 callback size a", 68u, ReadUInt32(callbackRegister, 88));
AssertEqual("RegisterEngine2 callback size b", 68u, ReadUInt32(callbackRegister, 92));
}
static void RunNmxSvcCallbackMessageTests()
{
var cid = new Guid("22222222-3333-4444-5555-666666666666");
byte[] body = FromHex("01 02 03 04 05");
byte[] request = new byte[OrpcThis.EncodedLengthWithoutExtensions + 8 + body.Length + 3];
OrpcThis.Create(cid).Encode().CopyTo(request.AsSpan());
WriteUInt32(request, 32, (uint)body.Length);
WriteUInt32(request, 36, (uint)body.Length);
body.CopyTo(request.AsSpan(40));
var parsed = NmxSvcCallbackMessages.ParseCallbackRequest(request);
AssertEqual("callback iid", NmxProcedureMetadata.INmxSvcCallback, NmxSvcCallbackMessages.InterfaceId);
AssertEqual("callback data opnum", (ushort)3, NmxSvcCallbackMessages.DataReceivedOpnum);
AssertEqual("callback status opnum", (ushort)4, NmxSvcCallbackMessages.StatusReceivedOpnum);
AssertEqual("callback cid", cid, parsed.OrpcThis.Cid);
AssertBytes("callback body", body, parsed.Body);
byte[] response = NmxSvcCallbackMessages.EncodeCallbackResponse(0);
AssertEqual("callback response length", 12, response.Length);
AssertEqual("callback response hr", 0u, ReadUInt32(response, 8));
}
static void RunDceRpcAuthTrailerTests()
{
var trailer = new DceRpcAuthTrailer(DceRpcAuthType.WinNt, DceRpcAuthLevel.Connect, 0, 0, 79231);
byte[] token = [0x4e, 0x54, 0x4c, 0x4d];
var bind = new DceRpcBindPdu(
new DceRpcPduHeader(5, 0, DceRpcPacketType.Bind, 0x03, 0x10, 0, 0, 1),
4280,
4280,
0,
[
new DceRpcPresentationContext(
0,
new DceRpcSyntaxId(ObjectExporterMessages.IObjectExporter, 0, 0),
[DceRpcSyntaxId.Ndr20]),
]);
byte[] pdu = bind.EncodeWithAuth(trailer, token);
var header = DceRpcPduHeader.Parse(pdu);
AssertEqual("auth bind token len", (ushort)4, header.AuthLength);
var auth = DceRpcBindPdu.ReadAuthValue(pdu);
AssertEqual("auth trailer type", DceRpcAuthType.WinNt, auth.Trailer.AuthType);
AssertEqual("auth trailer level", DceRpcAuthLevel.Connect, auth.Trailer.AuthLevel);
AssertBytes("auth token", token, auth.Token.Span);
}
static void RunDceRpcBindParseTest()
{
byte[] bind = FromHex(
"05 00 0b 03 10 00 00 00 74 00 00 00 02 00 00 00 d0 16 d0 16 00 00 00 00 " +
"02 00 00 00 00 00 01 00 df 90 0c 4e 9d e3 64 41 a4 21 ac e8 94 84 c6 02 " +
"01 00 00 00 04 5d 88 8a eb 1c c9 11 9f e8 08 00 2b 10 48 60 02 00 00 00 " +
"01 00 01 00 df 90 0c 4e 9d e3 64 41 a4 21 ac e8 94 84 c6 02 01 00 00 00 " +
"2c 1c b7 6c 12 98 40 45 03 00 00 00 00 00 00 00 01 00 00 00");
var pdu = DceRpcBindPdu.Parse(bind);
AssertEqual("bind type", DceRpcPacketType.Bind, pdu.Header.PacketType);
AssertEqual("bind frag", (ushort)116, pdu.Header.FragmentLength);
AssertEqual("bind call", 2u, pdu.Header.CallId);
AssertEqual("bind contexts", 2, pdu.PresentationContexts.Count);
AssertEqual("bind context0", (ushort)0, pdu.PresentationContexts[0].ContextId);
AssertEqual("bind context0 syntax", new Guid("4E0C90DF-E39D-4164-A421-ACE89484C602"), pdu.PresentationContexts[0].AbstractSyntax.Uuid);
byte[] encoded = pdu.Encode();
AssertBytes("bind roundtrip", bind, encoded);
}
static void RunDceRpcRequestParseTest()
{
byte[] request = FromHex(
"05 00 00 03 10 00 00 00 28 00 00 00 02 00 00 00 10 00 00 00 00 00 00 00 " +
"8c 9e 28 85 62 f2 97 47 84 df 3e 4b a6 0e 98 7f");
var pdu = DceRpcRequestPdu.Parse(request);
AssertEqual("request type", DceRpcPacketType.Request, pdu.Header.PacketType);
AssertEqual("request call", 2u, pdu.Header.CallId);
AssertEqual("request context", (ushort)0, pdu.ContextId);
AssertEqual("request opnum", (ushort)0, pdu.Opnum);
AssertEqual("request stub length", 16, pdu.StubData.Length);
}
static void RunDceRpcResponseParseTest()
{
byte[] response = FromHex(
"05 00 02 03 10 00 00 00 2c 00 00 00 02 00 00 00 14 00 00 00 00 00 00 00 " +
"00 00 00 00 e5 96 74 5a a4 eb c2 4f b6 13 bf 8a c5 a8 3b 6e");
var pdu = DceRpcResponsePdu.Parse(response);
AssertEqual("response type", DceRpcPacketType.Response, pdu.Header.PacketType);
AssertEqual("response call", 2u, pdu.Header.CallId);
AssertEqual("response context", (ushort)0, pdu.ContextId);
AssertEqual("response stub length", 20, pdu.StubData.Length);
}
static async Task RunGalaxyRepositoryResolverTestAsync()
{
var resolver = new GalaxyRepositoryTagResolver();
GalaxyTagMetadata tag = await resolver.ResolveAsync("TestChildObject.TestInt");
AssertEqual("GR object", "TestChildObject", tag.ObjectTagName);
AssertEqual("GR attribute", "TestInt", tag.AttributeName);
AssertEqual("GR primitive", (string?)null, tag.PrimitiveName);
AssertEqual("GR platform", (ushort)1, tag.PlatformId);
AssertEqual("GR engine", (ushort)2, tag.EngineId);
AssertEqual("GR object id", (ushort)5, tag.ObjectId);
AssertEqual("GR primitive id", (short)2, tag.PrimitiveId);
AssertEqual("GR attribute id", (short)155, tag.AttributeId);
AssertEqual("GR property id", (short)10, tag.PropertyId);
AssertEqual("GR data type", (short)2, tag.MxDataType);
AssertEqual("GR is array", false, tag.IsArray);
AssertEqual("GR source", "dynamic", tag.AttributeSource);
AssertEqual("GR value kind", MxValueKind.Int32, tag.ToValueKind());
AssertEqual("GR supported value kind", true, tag.IsSupportedValueKind);
AssertEqual("GR try value kind", true, tag.TryGetValueKind(out var testIntKind));
AssertEqual("GR try value kind result", MxValueKind.Int32, testIntKind);
byte[] expected = FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00");
AssertBytes("GR synthesized handle", expected, tag.ToReferenceHandle().Encode());
GalaxyTagMetadata bufferedProperty = await resolver.ResolveAsync("TestChildObject.TestInt.property(buffer)");
AssertEqual("GR property(buffer) base attribute", "TestInt", bufferedProperty.AttributeName);
AssertEqual("GR property(buffer) id", GalaxyTagMetadata.BufferPropertyId, bufferedProperty.PropertyId);
AssertEqual("GR property(buffer) marker", true, bufferedProperty.IsBufferProperty);
AssertBytes(
"GR property(buffer) native literal handle",
FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 32 00 3e da 00 00"),
bufferedProperty.ToReferenceHandle().Encode());
GalaxyTagMetadata shortDesc = await resolver.ResolveAsync("TestChildObject.ShortDesc");
AssertEqual("GR ShortDesc category-independent property", (short)10, shortDesc.PropertyId);
AssertBytes(
"GR ShortDesc native value handle",
FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 65 00 0a 00 6d 02 00 00"),
shortDesc.ToReferenceHandle().Encode());
GalaxyTagMetadata secured = await resolver.ResolveAsync("TestMachine_001.ProtectedValue");
AssertEqual("GR secured object", "TestMachine_001", secured.ObjectTagName);
AssertEqual("GR secured attribute", "ProtectedValue", secured.AttributeName);
AssertEqual("GR secured object id", (ushort)6, secured.ObjectId);
AssertEqual("GR secured attr id", (short)166, secured.AttributeId);
AssertEqual("GR secured class", (short)2, secured.SecurityClassification);
AssertEqual("GR secured kind", MxValueKind.Boolean, secured.ToValueKind());
AssertBytes(
"GR secured handle",
FromHex("01 00 01 00 02 00 06 00 08 f4 02 00 a6 00 0a 00 bb 67 00 00"),
secured.ToReferenceHandle().Encode());
GalaxyTagMetadata verified = await resolver.ResolveAsync("TestMachine_001.ProtectedValue1");
AssertEqual("GR verified attr id", (short)167, verified.AttributeId);
AssertEqual("GR verified class", (short)3, verified.SecurityClassification);
AssertBytes(
"GR verified handle",
FromHex("01 00 01 00 02 00 06 00 08 f4 02 00 a7 00 0a 00 26 8a 00 00"),
verified.ToReferenceHandle().Encode());
GalaxyTagMetadata elapsed = await resolver.ResolveAsync("TestMachine_001.TestAlarm001.Alarm.TimeDeadband");
AssertEqual("GR dotted primitive object", "TestMachine_001", elapsed.ObjectTagName);
AssertEqual("GR dotted primitive", "TestAlarm001", elapsed.PrimitiveName);
AssertEqual("GR dotted primitive attribute", "Alarm.TimeDeadband", elapsed.AttributeName);
AssertEqual("GR dotted primitive data type", (short)MxDataType.ElapsedTime, elapsed.MxDataType);
AssertEqual("GR dotted primitive supported", false, elapsed.IsSupportedValueKind);
var elapsedProjection = elapsed.ProjectWriteValue(TimeSpan.FromSeconds(1));
AssertEqual("GR elapsed projection kind", MxValueKind.Int32, elapsedProjection.ValueKind);
AssertEqual("GR elapsed projection value", 1000, elapsedProjection.Value);
GalaxyTagMetadata internationalized = await resolver.ResolveAsync("DevAppEngine.Scheduler._EngUnitsMB");
AssertEqual("GR internationalized primitive", "Scheduler", internationalized.PrimitiveName);
AssertEqual("GR internationalized attribute", "_EngUnitsMB", internationalized.AttributeName);
AssertEqual("GR internationalized data type", (short)MxDataType.InternationalizedString, internationalized.MxDataType);
AssertEqual("GR internationalized supported", false, internationalized.IsSupportedValueKind);
var internationalizedProjection = internationalized.ProjectWriteValue("MB");
AssertEqual("GR internationalized projection kind", MxValueKind.String, internationalizedProjection.ValueKind);
AssertEqual("GR internationalized projection value", "MB", internationalizedProjection.Value);
IReadOnlyList<GalaxyTagMetadata> browsed = await resolver.BrowseAsync("TestChildObject", "Test%", maxRows: 25);
AssertEqual("GR browse includes TestInt", true, browsed.Any(item => item.ObjectTagName == "TestChildObject" && item.AttributeName == "TestInt"));
AssertEqual("GR browse max respected", true, browsed.Count <= 25);
}
static async Task RunGalaxyRepositoryUserResolverTestAsync()
{
var resolver = new GalaxyRepositoryUserResolver();
GalaxyUserProfile administrator = await resolver.ResolveByNameAsync("Administrator");
AssertEqual("GR user name", "Administrator", administrator.UserProfileName);
AssertEqual("GR user id", 2, administrator.UserProfileId);
AssertEqual("GR user guid", new Guid("9222FBBA-53F4-457E-8B37-C93A9A250B4A"), administrator.UserGuid);
AssertEqual("GR user group", "Default", administrator.DefaultSecurityGroup);
AssertEqual("GR user role administrator", true, administrator.Roles.Contains("Administrator"));
AssertEqual("GR user role default", true, administrator.Roles.Contains("Default"));
AssertEqual(
"GR user guid to profile id",
administrator.UserProfileId,
await resolver.ResolveUserProfileIdByGuidAsync(administrator.UserGuid));
}
static void RunManagedNmxService2ClientTransferBodyTests()
{
var tag = new GalaxyTagMetadata(
ObjectTagName: "TestChildObject",
AttributeName: "ShortDesc",
PrimitiveName: null,
PlatformId: 1,
EngineId: 2,
ObjectId: 5,
PrimitiveId: 2,
AttributeId: 101,
PropertyId: 10,
MxDataType: (short)MxDataType.InternationalizedString,
IsArray: false,
SecurityClassification: 1,
AttributeSource: "dynamic");
byte[] writeTransfer = ManagedNmxService2Client.EncodeWriteTransferBody(
localEngineId: 0x7ffa,
tag,
"hello-native",
writeIndex: 1,
clientToken: 0x0bff6894);
var writeEnvelope = NmxTransferEnvelope.Parse(writeTransfer);
AssertEqual("client write transfer kind", NmxTransferMessageKind.Write, writeEnvelope.MessageKind);
AssertEqual("client write target engine", 2, writeEnvelope.TargetEngineId);
AssertEqual("client write command", NmxWriteMessage.Command, writeEnvelope.InnerBody.Span[0]);
AssertEqual("client write value kind", NmxWriteMessage.GetWireKind(MxValueKind.String), writeEnvelope.InnerBody.Span[NmxWriteMessage.KindOffset]);
AssertBytes(
"client write native ShortDesc body",
FromHex("37 01 00 05 00 36 d7 02 00 65 00 0a 00 6d 02 00 00 05 1e 00 00 00 1a 00 00 00 68 00 65 00 6c 00 6c 00 6f 00 2d 00 6e 00 61 00 74 00 69 00 76 00 65 00 00 00 ff ff 00 00 00 00 00 00 00 00 94 68 ff 0b 01 00 00 00"),
writeEnvelope.InnerBody.Span);
Guid correlationId = new("cd9ccac0-6532-46b0-a585-a583b2e77a5d");
byte[] adviseTransfer = ManagedNmxService2Client.EncodeAdviseSupervisoryTransferBody(
localEngineId: 0x7ffb,
tag,
correlationId);
var adviseEnvelope = NmxTransferEnvelope.Parse(adviseTransfer);
AssertEqual("client advise transfer kind", NmxTransferMessageKind.ItemControl, adviseEnvelope.MessageKind);
var advise = NmxItemControlMessage.Parse(adviseEnvelope.InnerBody.Span);
AssertEqual("client advise command", NmxItemControlCommand.AdviseSupervisory, advise.Command);
AssertEqual("client advise correlation", correlationId, advise.ItemCorrelationId);
AssertEqual("client advise property", (short)10, advise.PropertyId);
AssertEqual(
"compat suppress empty internationalized string",
true,
MxNativeCompatibilityServer.ShouldSuppressCompatibilityDataChange(tag, string.Empty));
AssertEqual(
"compat keep populated internationalized string",
false,
MxNativeCompatibilityServer.ShouldSuppressCompatibilityDataChange(tag, "populated"));
AssertEqual(
"compat keep normal empty string",
false,
MxNativeCompatibilityServer.ShouldSuppressCompatibilityDataChange(tag with { MxDataType = (short)MxDataType.String }, string.Empty));
var securedTag = new GalaxyTagMetadata(
ObjectTagName: "TestMachine_001",
AttributeName: "ProtectedValue",
PrimitiveName: null,
PlatformId: 1,
EngineId: 2,
ObjectId: 6,
PrimitiveId: 2,
AttributeId: 166,
PropertyId: 10,
MxDataType: (short)MxDataType.Boolean,
IsArray: false,
SecurityClassification: 2,
AttributeSource: "dynamic");
byte[] securedTransfer = ManagedNmxService2Client.EncodeWriteSecured2TransferBody(
localEngineId: 0x7ffa,
securedTag,
true,
new DateTime(2026, 4, 25, 8, 30, 0),
"MxManagedSecured2",
currentUserId: 1,
verifierUserId: 0);
var securedEnvelope = NmxTransferEnvelope.Parse(securedTransfer);
AssertEqual("client secured2 transfer kind", NmxTransferMessageKind.Write, securedEnvelope.MessageKind);
AssertEqual("client secured2 command", NmxSecuredWrite2Message.Command, securedEnvelope.InnerBody.Span[0]);
AssertEqual("client secured2 bool kind", NmxWriteMessage.GetWireKind(MxValueKind.Boolean), securedEnvelope.InnerBody.Span[NmxWriteMessage.KindOffset]);
}
static byte[] FromHex(string hex)
{
string[] parts = hex.Split(' ', StringSplitOptions.RemoveEmptyEntries);
byte[] bytes = new byte[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
bytes[i] = Convert.ToByte(parts[i], 16);
}
return bytes;
}
static ushort ReadUInt16(ReadOnlySpan<byte> buffer, int offset)
{
return (ushort)(buffer[offset] | (buffer[offset + 1] << 8));
}
static uint ReadUInt32(ReadOnlySpan<byte> buffer, int offset)
{
return (uint)(buffer[offset]
| (buffer[offset + 1] << 8)
| (buffer[offset + 2] << 16)
| (buffer[offset + 3] << 24));
}
static void WriteUInt32(Span<byte> buffer, int offset, uint value)
{
buffer[offset] = (byte)value;
buffer[offset + 1] = (byte)(value >> 8);
buffer[offset + 2] = (byte)(value >> 16);
buffer[offset + 3] = (byte)(value >> 24);
}
static void AssertEqual<T>(string name, T expected, T actual)
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
{
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
}
}
static void AssertBytes(string name, ReadOnlySpan<byte> expected, ReadOnlySpan<byte> actual)
{
if (!expected.SequenceEqual(actual))
{
throw new InvalidOperationException($"{name}: byte mismatch.");
}
}
+145
View File
@@ -0,0 +1,145 @@
using System.Globalization;
namespace MxNativeClient;
public sealed record ComObjRef(
uint Signature,
uint Flags,
Guid Iid,
uint StandardFlags,
uint PublicRefs,
ulong Oxid,
ulong Oid,
Guid Ipid,
ushort DualStringEntries,
ushort DualStringSecurityOffset,
IReadOnlyList<ComDualStringEntry> DualStringEntriesDecoded)
{
public static ComObjRef Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 68)
{
throw new ArgumentException("OBJREF buffer is too short.", nameof(buffer));
}
ushort dualStringEntries = ReadUInt16(buffer, 64);
ushort securityOffset = ReadUInt16(buffer, 66);
return new ComObjRef(
Signature: ReadUInt32(buffer, 0),
Flags: ReadUInt32(buffer, 4),
Iid: new Guid(buffer.Slice(8, 16)),
StandardFlags: ReadUInt32(buffer, 24),
PublicRefs: ReadUInt32(buffer, 28),
Oxid: ReadUInt64(buffer, 32),
Oid: ReadUInt64(buffer, 40),
Ipid: new Guid(buffer.Slice(48, 16)),
DualStringEntries: dualStringEntries,
DualStringSecurityOffset: securityOffset,
DualStringEntriesDecoded: DecodeDualStringArray(buffer[68..], dualStringEntries, securityOffset));
}
public IEnumerable<string> ToDiagnosticLines()
{
yield return $"objref_signature=0x{Signature:X8}";
yield return $"objref_flags=0x{Flags:X8}";
yield return $"objref_iid={Iid}";
yield return $"std_flags=0x{StandardFlags:X8}";
yield return $"std_public_refs={PublicRefs}";
yield return $"std_oxid=0x{Oxid:X16}";
yield return $"std_oid=0x{Oid:X16}";
yield return $"std_ipid={Ipid}";
yield return $"dual_string_entries={DualStringEntries}";
yield return $"dual_string_security_offset={DualStringSecurityOffset}";
yield return $"dual_strings={string.Join("|", DualStringEntriesDecoded.Select(static entry => entry.ToDiagnosticString()))}";
}
private static IReadOnlyList<ComDualStringEntry> DecodeDualStringArray(ReadOnlySpan<byte> data, ushort entries, ushort securityOffset)
{
int count = Math.Min(entries, data.Length / 2);
var strings = new List<ComDualStringEntry>();
for (int i = 0; i < count;)
{
int entryStart = i;
ushort towerId = ReadUInt16(data, i * 2);
i++;
if (towerId == 0)
{
continue;
}
var text = new List<char>();
while (i < count)
{
ushort value = ReadUInt16(data, i * 2);
i++;
if (value == 0)
{
break;
}
if (value >= 0x20 && value <= 0x7e)
{
text.Add((char)value);
}
else
{
text.Add('<');
text.AddRange(value.ToString("x4", CultureInfo.InvariantCulture));
text.Add('>');
}
}
strings.Add(new ComDualStringEntry(
towerId,
ProtocolTowerName(towerId),
new string(text.ToArray()),
IsSecurityBinding: entryStart >= securityOffset));
}
return strings;
}
private static string ProtocolTowerName(ushort towerId)
{
return towerId switch
{
0x0007 => "ncacn_ip_tcp",
0x0008 => "ncadg_ip_udp",
0x0009 => "ncacn_np",
0x000f => "ncacn_spx",
0x0010 => "ncacn_nb_nb",
0x0016 => "ncadg_ip_udp_or_netbios",
0x001f => "ncalrpc",
_ => "unknown",
};
}
private static ushort ReadUInt16(ReadOnlySpan<byte> buffer, int offset)
{
return (ushort)(buffer[offset] | (buffer[offset + 1] << 8));
}
private static uint ReadUInt32(ReadOnlySpan<byte> buffer, int offset)
{
return (uint)(buffer[offset]
| (buffer[offset + 1] << 8)
| (buffer[offset + 2] << 16)
| (buffer[offset + 3] << 24));
}
private static ulong ReadUInt64(ReadOnlySpan<byte> buffer, int offset)
{
return ReadUInt32(buffer, offset) | ((ulong)ReadUInt32(buffer, offset + 4) << 32);
}
}
public sealed record ComDualStringEntry(ushort TowerId, string Protocol, string Value, bool IsSecurityBinding)
{
public string ToDiagnosticString()
{
string kind = IsSecurityBinding ? "security" : "string";
return $"{kind}:0x{TowerId:x4}:{Protocol}:{Value}";
}
}
+100
View File
@@ -0,0 +1,100 @@
using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;
namespace MxNativeClient;
public static class ComObjRefProvider
{
public const uint MarshalContextInProcess = 0;
public const uint MarshalContextLocal = 1;
public const uint MarshalContextDifferentMachine = 2;
public static byte[] MarshalActivatedIUnknownObjRef(string progId, uint destinationContext)
{
var type = Type.GetTypeFromProgID(progId, throwOnError: true)
?? throw new InvalidOperationException($"ProgID {progId} was not resolved.");
object instance = Activator.CreateInstance(type)
?? throw new InvalidOperationException($"ProgID {progId} activation returned null.");
try
{
return MarshalIUnknownObjRef(instance, destinationContext);
}
finally
{
if (Marshal.IsComObject(instance))
{
_ = Marshal.ReleaseComObject(instance);
}
}
}
public static byte[] MarshalIUnknownObjRef(object comObject, uint destinationContext)
{
return MarshalInterfaceObjRef(comObject, new Guid("00000000-0000-0000-C000-000000000046"), destinationContext);
}
public static byte[] MarshalInterfaceObjRef(object comObject, Guid iid, uint destinationContext)
{
IntPtr unknown = IntPtr.Zero;
ComTypes.IStream? stream = null;
try
{
unknown = Marshal.GetIUnknownForObject(comObject);
Marshal.ThrowExceptionForHR(CreateStreamOnHGlobal(IntPtr.Zero, true, out stream));
Marshal.ThrowExceptionForHR(CoMarshalInterface(stream, ref iid, unknown, destinationContext, IntPtr.Zero, 0));
Marshal.ThrowExceptionForHR(GetHGlobalFromStream(stream, out var hglobal));
nuint size = GlobalSize(hglobal);
IntPtr pointer = GlobalLock(hglobal);
if (pointer == IntPtr.Zero)
{
throw new InvalidOperationException("GlobalLock failed.");
}
try
{
byte[] buffer = new byte[(int)size];
Marshal.Copy(pointer, buffer, 0, buffer.Length);
return buffer;
}
finally
{
_ = GlobalUnlock(hglobal);
}
}
finally
{
if (unknown != IntPtr.Zero)
{
Marshal.Release(unknown);
}
if (stream is not null)
{
_ = Marshal.ReleaseComObject(stream);
}
}
}
[DllImport("ole32.dll")]
private static extern int CreateStreamOnHGlobal(IntPtr hGlobal, [MarshalAs(UnmanagedType.Bool)] bool deleteOnRelease, out ComTypes.IStream stream);
[DllImport("ole32.dll")]
private static extern int CoMarshalInterface(ComTypes.IStream stream, ref Guid iid, IntPtr unknown, uint destinationContext, IntPtr destinationContextPointer, uint marshalFlags);
[DllImport("ole32.dll")]
private static extern int GetHGlobalFromStream(ComTypes.IStream stream, out IntPtr hGlobal);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nuint GlobalSize(IntPtr hMem);
}
+135
View File
@@ -0,0 +1,135 @@
using System.Buffers.Binary;
using System.Buffers;
using System.Net;
using System.Net.Security;
namespace MxNativeClient;
public enum DceRpcAuthType : byte
{
None = 0,
GssNegotiate = 9,
WinNt = 10,
}
public enum DceRpcAuthLevel : byte
{
None = 1,
Connect = 2,
PacketIntegrity = 5,
PacketPrivacy = 6,
}
public sealed record DceRpcAuthTrailer(
DceRpcAuthType AuthType,
DceRpcAuthLevel AuthLevel,
byte AuthPadLength,
byte AuthReserved,
uint AuthContextId)
{
public const int Length = 8;
public static DceRpcAuthTrailer Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC auth trailer is too short.", nameof(buffer));
}
return new DceRpcAuthTrailer(
(DceRpcAuthType)buffer[0],
(DceRpcAuthLevel)buffer[1],
buffer[2],
buffer[3],
BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]));
}
public void WriteTo(Span<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC auth trailer buffer is too short.", nameof(buffer));
}
buffer[0] = (byte)AuthType;
buffer[1] = (byte)AuthLevel;
buffer[2] = AuthPadLength;
buffer[3] = AuthReserved;
BinaryPrimitives.WriteUInt32LittleEndian(buffer[4..8], AuthContextId);
}
}
public sealed record DceRpcAuthValue(DceRpcAuthTrailer Trailer, ReadOnlyMemory<byte> Token);
public sealed class SspiClientContext : IDisposable
{
private readonly NegotiateAuthentication _authentication;
public SspiClientContext(string package, string targetName, ProtectionLevel protectionLevel = ProtectionLevel.None)
{
var credential = CreateCredentialFromEnvironment();
_authentication = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions
{
Package = package,
TargetName = targetName,
Credential = credential ?? CredentialCache.DefaultNetworkCredentials,
RequiredProtectionLevel = protectionLevel,
});
}
public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingToken)
{
byte[]? input = incomingToken.IsEmpty ? Array.Empty<byte>() : incomingToken.ToArray();
byte[]? output = _authentication.GetOutgoingBlob(input, out var status);
if (status is not (NegotiateAuthenticationStatusCode.Completed or NegotiateAuthenticationStatusCode.ContinueNeeded))
{
throw new InvalidOperationException($"SSPI negotiation failed with {status}.");
}
return output ?? [];
}
public byte[] Wrap(ReadOnlySpan<byte> input, bool requestEncryption, out bool isEncrypted)
{
var writer = new ArrayBufferWriter<byte>(input.Length + 64);
var status = _authentication.Wrap(input, writer, requestEncryption, out isEncrypted);
if (status != NegotiateAuthenticationStatusCode.Completed)
{
throw new InvalidOperationException($"SSPI wrap failed with {status}.");
}
return writer.WrittenSpan.ToArray();
}
public byte[] Unwrap(ReadOnlySpan<byte> input, out bool wasEncrypted)
{
var writer = new ArrayBufferWriter<byte>(input.Length);
var status = _authentication.Unwrap(input, writer, out wasEncrypted);
if (status != NegotiateAuthenticationStatusCode.Completed)
{
throw new InvalidOperationException($"SSPI unwrap failed with {status}.");
}
return writer.WrittenSpan.ToArray();
}
public void Dispose()
{
_authentication.Dispose();
}
private static NetworkCredential? CreateCredentialFromEnvironment()
{
string? user = Environment.GetEnvironmentVariable("MX_RPC_USER");
string? password = Environment.GetEnvironmentVariable("MX_RPC_PASSWORD");
if (string.IsNullOrWhiteSpace(user) || password is null)
{
return null;
}
string? domain = Environment.GetEnvironmentVariable("MX_RPC_DOMAIN");
return string.IsNullOrWhiteSpace(domain)
? new NetworkCredential(user, password)
: new NetworkCredential(user, password, domain);
}
}
+380
View File
@@ -0,0 +1,380 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public enum DceRpcPacketType : byte
{
Request = 0,
Response = 2,
Fault = 3,
Bind = 11,
BindAck = 12,
AlterContext = 14,
AlterContextResponse = 15,
Auth3 = 16,
}
public readonly record struct DceRpcSyntaxId(Guid Uuid, ushort VersionMajor, ushort VersionMinor)
{
public static DceRpcSyntaxId Ndr20 { get; } = new(
new Guid("8A885D04-1CEB-11C9-9FE8-08002B104860"),
2,
0);
}
public sealed record DceRpcPresentationContext(
ushort ContextId,
DceRpcSyntaxId AbstractSyntax,
IReadOnlyList<DceRpcSyntaxId> TransferSyntaxes);
public sealed record DceRpcPduHeader(
byte Version,
byte VersionMinor,
DceRpcPacketType PacketType,
byte PacketFlags,
uint DataRepresentation,
ushort FragmentLength,
ushort AuthLength,
uint CallId)
{
public const int Length = 16;
public static DceRpcPduHeader Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC PDU header is too short.", nameof(buffer));
}
return new DceRpcPduHeader(
Version: buffer[0],
VersionMinor: buffer[1],
PacketType: (DceRpcPacketType)buffer[2],
PacketFlags: buffer[3],
DataRepresentation: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
FragmentLength: BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]),
AuthLength: BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]),
CallId: BinaryPrimitives.ReadUInt32LittleEndian(buffer[12..16]));
}
public void WriteTo(Span<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC PDU header buffer is too short.", nameof(buffer));
}
buffer[0] = Version;
buffer[1] = VersionMinor;
buffer[2] = (byte)PacketType;
buffer[3] = PacketFlags;
BinaryPrimitives.WriteUInt32LittleEndian(buffer[4..8], DataRepresentation);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], FragmentLength);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], AuthLength);
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], CallId);
}
}
public sealed record DceRpcRequestPdu(
DceRpcPduHeader Header,
uint AllocationHint,
ushort ContextId,
ushort Opnum,
ReadOnlyMemory<byte> StubData)
{
public static DceRpcRequestPdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType != DceRpcPacketType.Request)
{
throw new ArgumentException("PDU is not a request.", nameof(pdu));
}
if (span.Length < 24 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC request PDU is truncated.", nameof(pdu));
}
int trailerLength = header.AuthLength == 0 ? 0 : header.AuthLength + 8;
int stubLength = header.FragmentLength - 24 - trailerLength;
if (stubLength < 0)
{
throw new ArgumentException("DCE/RPC request PDU has invalid auth/trailer length.", nameof(pdu));
}
return new DceRpcRequestPdu(
Header: header,
AllocationHint: BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]),
ContextId: BinaryPrimitives.ReadUInt16LittleEndian(span[20..22]),
Opnum: BinaryPrimitives.ReadUInt16LittleEndian(span[22..24]),
StubData: pdu.Slice(24, stubLength));
}
public byte[] Encode()
{
int length = 24 + StubData.Length;
byte[] pdu = new byte[length];
var header = Header with
{
PacketType = DceRpcPacketType.Request,
FragmentLength = (ushort)length,
AuthLength = 0,
PacketFlags = Header.PacketFlags == 0 ? (byte)0x03 : Header.PacketFlags,
};
header.WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(16, 4), AllocationHint);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(20, 2), ContextId);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(22, 2), Opnum);
StubData.Span.CopyTo(pdu.AsSpan(24));
return pdu;
}
}
public sealed record DceRpcResponsePdu(
DceRpcPduHeader Header,
uint AllocationHint,
ushort ContextId,
byte CancelCount,
ReadOnlyMemory<byte> StubData)
{
public static DceRpcResponsePdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType != DceRpcPacketType.Response)
{
throw new ArgumentException("PDU is not a response.", nameof(pdu));
}
if (span.Length < 24 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC response PDU is truncated.", nameof(pdu));
}
int trailerLength = header.AuthLength == 0 ? 0 : header.AuthLength + 8;
int stubLength = header.FragmentLength - 24 - trailerLength;
if (stubLength < 0)
{
throw new ArgumentException("DCE/RPC response PDU has invalid auth/trailer length.", nameof(pdu));
}
return new DceRpcResponsePdu(
Header: header,
AllocationHint: BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]),
ContextId: BinaryPrimitives.ReadUInt16LittleEndian(span[20..22]),
CancelCount: span[22],
StubData: pdu.Slice(24, stubLength));
}
}
public sealed record DceRpcFaultPdu(
DceRpcPduHeader Header,
uint AllocationHint,
ushort ContextId,
byte CancelCount,
uint Status,
ReadOnlyMemory<byte> StubData)
{
public static DceRpcFaultPdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType != DceRpcPacketType.Fault)
{
throw new ArgumentException("PDU is not a fault.", nameof(pdu));
}
if (span.Length < 28 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC fault PDU is truncated.", nameof(pdu));
}
int trailerLength = header.AuthLength == 0 ? 0 : header.AuthLength + 8;
int stubLength = header.FragmentLength - 28 - trailerLength;
if (stubLength < 0)
{
throw new ArgumentException("DCE/RPC fault PDU has invalid auth/trailer length.", nameof(pdu));
}
return new DceRpcFaultPdu(
Header: header,
AllocationHint: BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]),
ContextId: BinaryPrimitives.ReadUInt16LittleEndian(span[20..22]),
CancelCount: span[22],
Status: BinaryPrimitives.ReadUInt32LittleEndian(span[24..28]),
StubData: pdu.Slice(28, stubLength));
}
}
public sealed record DceRpcBindPdu(
DceRpcPduHeader Header,
ushort MaxTransmitFragment,
ushort MaxReceiveFragment,
uint AssociationGroupId,
IReadOnlyList<DceRpcPresentationContext> PresentationContexts)
{
public static DceRpcBindPdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType is not (DceRpcPacketType.Bind or DceRpcPacketType.AlterContext))
{
throw new ArgumentException("PDU is not a bind or alter-context PDU.", nameof(pdu));
}
if (span.Length < 28 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC bind PDU is truncated.", nameof(pdu));
}
int contextCount = span[24];
int offset = 28;
var contexts = new List<DceRpcPresentationContext>(contextCount);
for (int i = 0; i < contextCount; i++)
{
if (offset + 24 > header.FragmentLength)
{
throw new ArgumentException("DCE/RPC bind context is truncated.", nameof(pdu));
}
ushort contextId = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset, 2));
byte transferSyntaxCount = span[offset + 2];
offset += 4;
var abstractSyntax = ReadSyntax(span, ref offset);
var transferSyntaxes = new List<DceRpcSyntaxId>(transferSyntaxCount);
for (int j = 0; j < transferSyntaxCount; j++)
{
transferSyntaxes.Add(ReadSyntax(span, ref offset));
}
contexts.Add(new DceRpcPresentationContext(contextId, abstractSyntax, transferSyntaxes));
}
return new DceRpcBindPdu(
Header: header,
MaxTransmitFragment: BinaryPrimitives.ReadUInt16LittleEndian(span[16..18]),
MaxReceiveFragment: BinaryPrimitives.ReadUInt16LittleEndian(span[18..20]),
AssociationGroupId: BinaryPrimitives.ReadUInt32LittleEndian(span[20..24]),
PresentationContexts: contexts);
}
public byte[] Encode()
{
int length = 28 + PresentationContexts.Sum(static context => 24 + 20 * context.TransferSyntaxes.Count);
byte[] pdu = new byte[length];
var header = Header with { FragmentLength = (ushort)length, AuthLength = 0 };
header.WriteTo(pdu);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(16, 2), MaxTransmitFragment);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(18, 2), MaxReceiveFragment);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(20, 4), AssociationGroupId);
pdu[24] = (byte)PresentationContexts.Count;
int offset = 28;
foreach (var context in PresentationContexts)
{
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset, 2), context.ContextId);
pdu[offset + 2] = (byte)context.TransferSyntaxes.Count;
offset += 4;
WriteSyntax(pdu.AsSpan(), ref offset, context.AbstractSyntax);
foreach (var transferSyntax in context.TransferSyntaxes)
{
WriteSyntax(pdu.AsSpan(), ref offset, transferSyntax);
}
}
return pdu;
}
public byte[] EncodeWithAuth(DceRpcAuthTrailer trailer, ReadOnlySpan<byte> authToken)
{
byte[] unauthenticated = Encode();
int padLength = Align(unauthenticated.Length, 4) - unauthenticated.Length;
int length = unauthenticated.Length + padLength + DceRpcAuthTrailer.Length + authToken.Length;
byte[] pdu = new byte[length];
unauthenticated.CopyTo(pdu.AsSpan());
var header = Header with
{
FragmentLength = (ushort)length,
AuthLength = (ushort)authToken.Length,
};
header.WriteTo(pdu);
var alignedTrailer = trailer with { AuthPadLength = (byte)padLength };
alignedTrailer.WriteTo(pdu.AsSpan(unauthenticated.Length + padLength, DceRpcAuthTrailer.Length));
authToken.CopyTo(pdu.AsSpan(length - authToken.Length));
return pdu;
}
public static byte[] EncodeAuth3(DceRpcPduHeader header, DceRpcAuthTrailer trailer, ReadOnlySpan<byte> authToken)
{
byte[] body = [(byte)' ', (byte)' ', (byte)' ', (byte)' '];
int padLength = Align(DceRpcPduHeader.Length + body.Length, 4) - (DceRpcPduHeader.Length + body.Length);
int length = DceRpcPduHeader.Length + body.Length + padLength + DceRpcAuthTrailer.Length + authToken.Length;
byte[] pdu = new byte[length];
var authHeader = header with
{
PacketType = DceRpcPacketType.Auth3,
FragmentLength = (ushort)length,
AuthLength = (ushort)authToken.Length,
};
authHeader.WriteTo(pdu);
body.CopyTo(pdu.AsSpan(DceRpcPduHeader.Length));
var alignedTrailer = trailer with { AuthPadLength = (byte)padLength };
alignedTrailer.WriteTo(pdu.AsSpan(DceRpcPduHeader.Length + body.Length + padLength, DceRpcAuthTrailer.Length));
authToken.CopyTo(pdu.AsSpan(length - authToken.Length));
return pdu;
}
public static DceRpcAuthValue ReadAuthValue(ReadOnlyMemory<byte> pdu)
{
var header = DceRpcPduHeader.Parse(pdu.Span);
if (header.AuthLength == 0)
{
throw new ArgumentException("PDU has no auth value.", nameof(pdu));
}
int trailerOffset = header.FragmentLength - header.AuthLength - DceRpcAuthTrailer.Length;
if (trailerOffset < DceRpcPduHeader.Length)
{
throw new ArgumentException("PDU auth trailer offset is invalid.", nameof(pdu));
}
return new DceRpcAuthValue(
DceRpcAuthTrailer.Parse(pdu.Span.Slice(trailerOffset, DceRpcAuthTrailer.Length)),
pdu.Slice(header.FragmentLength - header.AuthLength, header.AuthLength));
}
private static DceRpcSyntaxId ReadSyntax(ReadOnlySpan<byte> span, ref int offset)
{
if (offset + 20 > span.Length)
{
throw new ArgumentException("DCE/RPC syntax identifier is truncated.", nameof(span));
}
var syntax = new DceRpcSyntaxId(
new Guid(span.Slice(offset, 16)),
BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset + 16, 2)),
BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset + 18, 2)));
offset += 20;
return syntax;
}
private static void WriteSyntax(Span<byte> span, ref int offset, DceRpcSyntaxId syntax)
{
syntax.Uuid.TryWriteBytes(span.Slice(offset, 16));
BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(offset + 16, 2), syntax.VersionMajor);
BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(offset + 18, 2), syntax.VersionMinor);
offset += 20;
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
}
+420
View File
@@ -0,0 +1,420 @@
using System.Buffers.Binary;
using System.Net.Sockets;
using System.Net.Security;
namespace MxNativeClient;
public sealed class DceRpcTcpClient : IDisposable
{
private readonly string _host;
private readonly int _port;
private TcpClient? _client;
private NetworkStream? _stream;
private uint _nextCallId = 1;
private ushort _boundContextId;
private SspiClientContext? _sspi;
private ManagedNtlmClientContext? _managedNtlm;
private DceRpcAuthTrailer? _authTrailer;
private DceRpcAuthLevel _authLevel = DceRpcAuthLevel.None;
public DceRpcTcpClient(string host, int port)
{
_host = host;
_port = port;
}
public void Connect()
{
_client = new TcpClient();
_client.Connect(_host, _port);
_stream = _client.GetStream();
}
public DceRpcPduHeader Bind(Guid interfaceId, ushort versionMajor, ushort versionMinor)
{
EnsureConnected();
uint callId = _nextCallId++;
var pdu = new DceRpcBindPdu(
Header: CreateHeader(DceRpcPacketType.Bind, callId),
MaxTransmitFragment: 4280,
MaxReceiveFragment: 4280,
AssociationGroupId: 0,
PresentationContexts:
[
new DceRpcPresentationContext(
ContextId: 0,
AbstractSyntax: new DceRpcSyntaxId(interfaceId, versionMajor, versionMinor),
TransferSyntaxes: [DceRpcSyntaxId.Ndr20]),
]);
Write(pdu.Encode());
byte[] response = ReadPdu();
return DceRpcPduHeader.Parse(response);
}
public DceRpcPduHeader BindWithNtlmConnect(Guid interfaceId, ushort versionMajor, ushort versionMinor, string targetName = "localhost")
{
return BindWithNtlm(interfaceId, versionMajor, versionMinor, DceRpcAuthLevel.Connect, targetName);
}
public DceRpcPduHeader BindWithNtlmPacketIntegrity(Guid interfaceId, ushort versionMajor, ushort versionMinor, string targetName = "localhost")
{
return BindWithNtlm(interfaceId, versionMajor, versionMinor, DceRpcAuthLevel.PacketIntegrity, targetName);
}
public DceRpcPduHeader BindWithManagedNtlmPacketIntegrity(Guid interfaceId, ushort versionMajor, ushort versionMinor)
{
EnsureConnected();
_sspi?.Dispose();
_sspi = null;
_managedNtlm = ManagedNtlmClientContext.FromEnvironment();
byte[] type1 = _managedNtlm.CreateType1();
uint callId = _nextCallId++;
var pdu = new DceRpcBindPdu(
Header: CreateHeader(DceRpcPacketType.Bind, callId),
MaxTransmitFragment: 4280,
MaxReceiveFragment: 4280,
AssociationGroupId: 0,
PresentationContexts:
[
new DceRpcPresentationContext(
ContextId: 0,
AbstractSyntax: new DceRpcSyntaxId(interfaceId, versionMajor, versionMinor),
TransferSyntaxes: [DceRpcSyntaxId.Ndr20]),
]);
var trailer = new DceRpcAuthTrailer(
AuthType: DceRpcAuthType.WinNt,
AuthLevel: DceRpcAuthLevel.PacketIntegrity,
AuthPadLength: 0,
AuthReserved: 0,
AuthContextId: 79232);
Write(pdu.EncodeWithAuth(trailer, type1));
byte[] response = ReadPdu();
var responseHeader = DceRpcPduHeader.Parse(response);
var challenge = DceRpcBindPdu.ReadAuthValue(response);
byte[] type3 = _managedNtlm.CreateType3(challenge.Token.Span);
byte[] auth3 = DceRpcBindPdu.EncodeAuth3(
CreateHeader(DceRpcPacketType.Auth3, responseHeader.CallId),
trailer,
type3);
Write(auth3);
_boundContextId = 0;
_authTrailer = trailer;
_authLevel = DceRpcAuthLevel.PacketIntegrity;
return responseHeader;
}
public DceRpcPduHeader BindWithNtlm(Guid interfaceId, ushort versionMajor, ushort versionMinor, DceRpcAuthLevel authLevel, string targetName = "localhost")
{
EnsureConnected();
_sspi?.Dispose();
_managedNtlm = null;
_sspi = new SspiClientContext("NTLM", targetName, ProtectionLevelFor(authLevel));
byte[] type1 = _sspi.GetOutgoingBlob(ReadOnlySpan<byte>.Empty);
uint callId = _nextCallId++;
var pdu = new DceRpcBindPdu(
Header: CreateHeader(DceRpcPacketType.Bind, callId),
MaxTransmitFragment: 4280,
MaxReceiveFragment: 4280,
AssociationGroupId: 0,
PresentationContexts:
[
new DceRpcPresentationContext(
ContextId: 0,
AbstractSyntax: new DceRpcSyntaxId(interfaceId, versionMajor, versionMinor),
TransferSyntaxes: [DceRpcSyntaxId.Ndr20]),
]);
var trailer = new DceRpcAuthTrailer(
AuthType: DceRpcAuthType.WinNt,
AuthLevel: authLevel,
AuthPadLength: 0,
AuthReserved: 0,
AuthContextId: 79232);
Write(pdu.EncodeWithAuth(trailer, type1));
byte[] response = ReadPdu();
var responseHeader = DceRpcPduHeader.Parse(response);
var challenge = DceRpcBindPdu.ReadAuthValue(response);
byte[] type3 = _sspi.GetOutgoingBlob(challenge.Token.Span);
byte[] auth3 = DceRpcBindPdu.EncodeAuth3(
CreateHeader(DceRpcPacketType.Auth3, responseHeader.CallId),
trailer,
type3);
Write(auth3);
_boundContextId = 0;
_authTrailer = trailer;
_authLevel = authLevel;
return responseHeader;
}
public DceRpcResponsePdu Call(ushort contextId, ushort opnum, ReadOnlyMemory<byte> stubData)
{
return CallCore(contextId, opnum, stubData, objectUuid: null);
}
public DceRpcResponsePdu CallBoundObject(Guid objectUuid, ushort opnum, ReadOnlyMemory<byte> stubData)
{
return CallCore(_boundContextId, opnum, stubData, objectUuid);
}
private DceRpcResponsePdu CallCore(ushort contextId, ushort opnum, ReadOnlyMemory<byte> stubData, Guid? objectUuid)
{
EnsureConnected();
uint callId = _nextCallId++;
byte[] request = EncodeRequestBytes(CreateHeader(DceRpcPacketType.Request, callId), contextId, opnum, stubData, objectUuid);
Write(EncodeRequest(request));
byte[] response = ReadPdu();
var header = DceRpcPduHeader.Parse(response);
if (header.PacketType == DceRpcPacketType.Fault)
{
var fault = DceRpcFaultPdu.Parse(response);
throw new DceRpcFaultException(fault.Status);
}
return DceRpcResponsePdu.Parse(response);
}
public DceRpcResponsePdu CallBound(ushort opnum, ReadOnlyMemory<byte> stubData)
{
return Call(_boundContextId, opnum, stubData);
}
public void Dispose()
{
_sspi?.Dispose();
_stream?.Dispose();
_client?.Dispose();
}
private byte[] EncodeRequest(byte[] request)
{
if (_authLevel == DceRpcAuthLevel.PacketIntegrity)
{
return EncodePacketIntegrityRequest(request);
}
return request;
}
private byte[] EncodePacketIntegrityRequest(byte[] unauthenticated)
{
if (_authTrailer is null)
{
throw new InvalidOperationException("Packet-integrity auth was requested without an auth trailer.");
}
int padLength = Align(unauthenticated.Length, 4) - unauthenticated.Length;
int signatureLength = 16;
int length = unauthenticated.Length + padLength + DceRpcAuthTrailer.Length + signatureLength;
byte[] pdu = new byte[length];
unauthenticated.CopyTo(pdu.AsSpan());
if (padLength > 0)
{
pdu.AsSpan(unauthenticated.Length, padLength).Fill(0xbb);
}
var parsedHeader = DceRpcPduHeader.Parse(unauthenticated);
var header = parsedHeader with
{
PacketType = DceRpcPacketType.Request,
PacketFlags = parsedHeader.PacketFlags == 0 ? (byte)0x03 : parsedHeader.PacketFlags,
FragmentLength = (ushort)length,
AuthLength = (ushort)signatureLength,
};
header.WriteTo(pdu);
var trailer = _authTrailer with { AuthPadLength = (byte)padLength };
int trailerOffset = unauthenticated.Length + padLength;
trailer.WriteTo(pdu.AsSpan(trailerOffset, DceRpcAuthTrailer.Length));
pdu.AsSpan(length - signatureLength, signatureLength).Fill(0x20);
byte[] verifier;
if (_managedNtlm is not null)
{
verifier = _managedNtlm.Sign(pdu.AsSpan(0, length - signatureLength));
}
else if (_sspi is not null)
{
byte[] wrapped = _sspi.Wrap(pdu.AsSpan(0, length - signatureLength), requestEncryption: false, out _);
verifier = ExtractNtlmVerifier(wrapped, pdu.AsSpan(0, length - signatureLength));
}
else
{
throw new InvalidOperationException("Packet-integrity auth was requested without an auth context.");
}
verifier.CopyTo(pdu.AsSpan(length - signatureLength, signatureLength));
return pdu;
}
private static byte[] EncodeRequestBytes(
DceRpcPduHeader header,
ushort contextId,
ushort opnum,
ReadOnlyMemory<byte> stubData,
Guid? objectUuid)
{
int objectLength = objectUuid.HasValue ? 16 : 0;
int fixedOffset = DceRpcPduHeader.Length;
int stubOffset = fixedOffset + 8 + objectLength;
int length = stubOffset + stubData.Length;
byte[] pdu = new byte[length];
var requestHeader = header with
{
PacketType = DceRpcPacketType.Request,
FragmentLength = (ushort)length,
AuthLength = 0,
PacketFlags = (byte)((header.PacketFlags == 0 ? 0x03 : header.PacketFlags) | (objectUuid.HasValue ? 0x80 : 0x00)),
};
requestHeader.WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(fixedOffset, 4), (uint)stubData.Length);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(fixedOffset + 4, 2), contextId);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(fixedOffset + 6, 2), opnum);
if (objectUuid.HasValue)
{
objectUuid.Value.TryWriteBytes(pdu.AsSpan(fixedOffset + 8, 16));
}
stubData.Span.CopyTo(pdu.AsSpan(stubOffset));
return pdu;
}
private static byte[] ExtractNtlmVerifier(ReadOnlySpan<byte> wrapped, ReadOnlySpan<byte> signedInput)
{
const int signatureLength = 16;
TraceWrapShape(wrapped, signedInput, signatureLength);
if (wrapped.Length == signatureLength)
{
return wrapped.ToArray();
}
if (wrapped.Length == signedInput.Length + signatureLength)
{
if (wrapped[signatureLength..].SequenceEqual(signedInput))
{
return wrapped[..signatureLength].ToArray();
}
if (wrapped[..signedInput.Length].SequenceEqual(signedInput))
{
return wrapped[^signatureLength..].ToArray();
}
return wrapped[..signatureLength].ToArray();
}
throw new InvalidOperationException($"Unexpected SSPI wrap verifier length {wrapped.Length} for input length {signedInput.Length}.");
}
private static void TraceWrapShape(ReadOnlySpan<byte> wrapped, ReadOnlySpan<byte> signedInput, int signatureLength)
{
string? tracePath = Environment.GetEnvironmentVariable("MX_RPC_WRAP_TRACE");
if (string.IsNullOrWhiteSpace(tracePath))
{
return;
}
bool tokenThenInput = wrapped.Length >= signatureLength
&& wrapped.Length == signedInput.Length + signatureLength
&& wrapped[signatureLength..].SequenceEqual(signedInput);
bool inputThenToken = wrapped.Length >= signatureLength
&& wrapped.Length == signedInput.Length + signatureLength
&& wrapped[..signedInput.Length].SequenceEqual(signedInput);
string text =
$"signed_input_length={signedInput.Length}{Environment.NewLine}" +
$"wrapped_length={wrapped.Length}{Environment.NewLine}" +
$"token_then_input={tokenThenInput}{Environment.NewLine}" +
$"input_then_token={inputThenToken}{Environment.NewLine}" +
$"wrapped_first16={Convert.ToHexString(wrapped[..Math.Min(signatureLength, wrapped.Length)])}{Environment.NewLine}" +
$"wrapped_last16={Convert.ToHexString(wrapped[^Math.Min(signatureLength, wrapped.Length)..])}{Environment.NewLine}";
File.AppendAllText(tracePath, text);
}
private static DceRpcPduHeader CreateHeader(DceRpcPacketType packetType, uint callId)
{
return new DceRpcPduHeader(
Version: 5,
VersionMinor: 0,
PacketType: packetType,
PacketFlags: 0x03,
DataRepresentation: 0x10,
FragmentLength: 0,
AuthLength: 0,
CallId: callId);
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
private static ProtectionLevel ProtectionLevelFor(DceRpcAuthLevel authLevel)
{
return authLevel switch
{
DceRpcAuthLevel.PacketPrivacy => ProtectionLevel.EncryptAndSign,
DceRpcAuthLevel.PacketIntegrity => ProtectionLevel.Sign,
_ => ProtectionLevel.None,
};
}
private void Write(byte[] pdu)
{
EnsureConnected();
_stream!.Write(pdu);
_stream.Flush();
}
private byte[] ReadPdu()
{
EnsureConnected();
byte[] headerBytes = ReadExact(DceRpcPduHeader.Length);
var header = DceRpcPduHeader.Parse(headerBytes);
byte[] pdu = new byte[header.FragmentLength];
headerBytes.CopyTo(pdu, 0);
byte[] body = ReadExact(header.FragmentLength - DceRpcPduHeader.Length);
body.CopyTo(pdu.AsSpan(DceRpcPduHeader.Length));
return pdu;
}
private byte[] ReadExact(int length)
{
byte[] buffer = new byte[length];
int offset = 0;
while (offset < length)
{
int read = _stream!.Read(buffer, offset, length - offset);
if (read == 0)
{
throw new IOException("DCE/RPC socket closed while reading.");
}
offset += read;
}
return buffer;
}
private void EnsureConnected()
{
if (_stream is null)
{
throw new InvalidOperationException("DCE/RPC TCP client is not connected.");
}
}
}
public sealed class DceRpcFaultException : Exception
{
public DceRpcFaultException(uint status)
: base($"DCE/RPC fault 0x{status:x8}")
{
Status = status;
}
public uint Status { get; }
}
@@ -0,0 +1,433 @@
using Microsoft.Data.SqlClient;
using MxNativeCodec;
namespace MxNativeClient;
public sealed record GalaxyTagMetadata(
string ObjectTagName,
string AttributeName,
string? PrimitiveName,
ushort PlatformId,
ushort EngineId,
ushort ObjectId,
short PrimitiveId,
short AttributeId,
short PropertyId,
short MxDataType,
bool IsArray,
short SecurityClassification,
string AttributeSource)
{
public const short ValuePropertyId = 10;
public const short BufferPropertyId = 50;
public bool IsBufferProperty => PropertyId == BufferPropertyId;
public MxReferenceHandle ToReferenceHandle(byte galaxyId = 1)
{
return MxReferenceHandle.Create(
galaxyId,
PlatformId,
EngineId,
ObjectId,
ObjectTagName,
PrimitiveId,
AttributeId,
PropertyId,
AttributeName,
IsArray);
}
public MxValueKind ToValueKind()
{
return NmxWriteMessage.GetValueKind(MxDataType, IsArray);
}
public bool TryGetValueKind(out MxValueKind valueKind)
{
return NmxWriteMessage.TryGetValueKind(MxDataType, IsArray, out valueKind);
}
public bool IsSupportedValueKind => TryGetValueKind(out _);
public (MxValueKind ValueKind, object Value) ProjectWriteValue(object value)
{
if (TryGetValueKind(out MxValueKind valueKind))
{
return (valueKind, value);
}
if (IsArray)
{
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported MX array data type {MxDataType}.");
}
return (MxDataType, value) switch
{
((short)MxNativeCodec.MxDataType.ElapsedTime, TimeSpan timeSpan) => (MxValueKind.Int32, checked((int)timeSpan.TotalMilliseconds)),
((short)MxNativeCodec.MxDataType.ElapsedTime, _) => (MxValueKind.Int32, value),
((short)MxNativeCodec.MxDataType.InternationalizedString, _) => (MxValueKind.String, value),
_ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported MX data type {MxDataType}."),
};
}
}
public sealed class GalaxyRepositoryTagResolver
{
private const string DefaultConnectionString =
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True";
private readonly string _connectionString;
public GalaxyRepositoryTagResolver(string connectionString = DefaultConnectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_connectionString = connectionString;
}
public async Task<GalaxyTagMetadata> ResolveAsync(
string tagReference,
CancellationToken cancellationToken = default)
{
IReadOnlyList<ParsedTagReference> candidates = ParsedTagReference.ParseCandidates(tagReference);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
foreach (ParsedTagReference parsed in candidates)
{
await using var command = connection.CreateCommand();
command.CommandText = ResolveSql;
command.Parameters.AddWithValue("@objectTagName", parsed.ObjectTagName);
command.Parameters.AddWithValue("@attributeName", parsed.AttributeName);
command.Parameters.AddWithValue("@primitiveName", (object?)parsed.PrimitiveName ?? DBNull.Value);
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
GalaxyTagMetadata metadata = ReadMetadata(reader);
return parsed.PropertyIdOverride.HasValue
? metadata with { PropertyId = parsed.PropertyIdOverride.Value }
: metadata;
}
}
throw new InvalidOperationException($"Galaxy tag reference '{tagReference}' was not found in the deployed repository metadata.");
}
public async Task<IReadOnlyList<GalaxyTagMetadata>> BrowseAsync(
string objectTagLike = "%",
string attributeLike = "%",
int maxRows = 100,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(objectTagLike);
ArgumentException.ThrowIfNullOrWhiteSpace(attributeLike);
if (maxRows <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxRows), "Maximum row count must be positive.");
}
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = connection.CreateCommand();
command.CommandText = BrowseSql;
command.Parameters.AddWithValue("@objectTagLike", objectTagLike);
command.Parameters.AddWithValue("@attributeLike", attributeLike);
command.Parameters.AddWithValue("@maxRows", Math.Min(maxRows, 1000));
List<GalaxyTagMetadata> results = [];
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(ReadMetadata(reader));
}
return results;
}
private static GalaxyTagMetadata ReadMetadata(SqlDataReader reader)
{
return new GalaxyTagMetadata(
ObjectTagName: reader.GetString(0),
AttributeName: reader.GetString(1),
PrimitiveName: reader.IsDBNull(2) ? null : reader.GetString(2),
PlatformId: checked((ushort)reader.GetInt16(3)),
EngineId: checked((ushort)reader.GetInt16(4)),
ObjectId: checked((ushort)reader.GetInt16(5)),
PrimitiveId: reader.GetInt16(6),
AttributeId: reader.GetInt16(7),
PropertyId: checked((short)reader.GetInt32(8)),
MxDataType: reader.GetInt16(9),
IsArray: reader.GetBoolean(10),
SecurityClassification: reader.GetInt16(11),
AttributeSource: reader.GetString(12));
}
private sealed record ParsedTagReference(
string ObjectTagName,
string? PrimitiveName,
string AttributeName,
short? PropertyIdOverride)
{
public static IReadOnlyList<ParsedTagReference> ParseCandidates(string tagReference)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tagReference);
var property = ParsePropertySuffix(tagReference);
string[] parts = property.BaseReference.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length switch
{
2 => [new ParsedTagReference(parts[0], null, parts[1], property.PropertyIdOverride)],
>= 3 =>
[
new ParsedTagReference(parts[0], parts[1], string.Join('.', parts.Skip(2)), property.PropertyIdOverride),
new ParsedTagReference(parts[0], null, string.Join('.', parts.Skip(1)), property.PropertyIdOverride),
],
_ => throw new ArgumentException("Tag reference must be Object.Attribute or Object.Primitive.Attribute.", nameof(tagReference)),
};
}
private static (string BaseReference, short? PropertyIdOverride) ParsePropertySuffix(string tagReference)
{
const string bufferSuffix = ".property(buffer)";
if (tagReference.EndsWith(bufferSuffix, StringComparison.OrdinalIgnoreCase))
{
string baseReference = tagReference[..^bufferSuffix.Length];
if (string.IsNullOrWhiteSpace(baseReference))
{
throw new ArgumentException("Property references must include a base tag reference.", nameof(tagReference));
}
return (baseReference, GalaxyTagMetadata.BufferPropertyId);
}
return (tagReference, null);
}
}
private const string ResolveSql = """
;WITH deployed_package_chain AS (
SELECT
g.gobject_id,
p.package_id,
p.derived_from_package_id,
0 AS depth
FROM dbo.gobject g
INNER JOIN dbo.package p
ON p.package_id = g.deployed_package_id
WHERE g.is_template = 0
AND g.deployed_package_id <> 0
AND g.tag_name = @objectTagName
UNION ALL
SELECT
dpc.gobject_id,
p.package_id,
p.derived_from_package_id,
dpc.depth + 1
FROM deployed_package_chain dpc
INNER JOIN dbo.package p
ON p.package_id = dpc.derived_from_package_id
WHERE dpc.derived_from_package_id <> 0
AND dpc.depth < 10
),
ranked_dynamic AS (
SELECT
g.tag_name AS object_tag_name,
da.attribute_name,
CAST(NULL AS nvarchar(329)) AS primitive_name,
i.mx_platform_id,
i.mx_engine_id,
i.mx_object_id,
da.mx_primitive_id,
da.mx_attribute_id,
CAST(10 AS int) AS property_id,
da.mx_data_type,
da.is_array,
da.security_classification,
CAST(N'dynamic' AS nvarchar(16)) AS attribute_source,
ROW_NUMBER() OVER (
PARTITION BY dpc.gobject_id, da.attribute_name
ORDER BY dpc.depth
) AS rn
FROM deployed_package_chain dpc
INNER JOIN dbo.dynamic_attribute da
ON da.package_id = dpc.package_id
INNER JOIN dbo.gobject g
ON g.gobject_id = dpc.gobject_id
INNER JOIN dbo.instance i
ON i.gobject_id = g.gobject_id
WHERE da.attribute_name = @attributeName
AND @primitiveName IS NULL
),
primitive_attributes AS (
SELECT
g.tag_name AS object_tag_name,
ad.attribute_name,
NULLIF(pi.primitive_name, N'') AS primitive_name,
i.mx_platform_id,
i.mx_engine_id,
i.mx_object_id,
pi.mx_primitive_id,
ad.mx_attribute_id,
CAST(10 AS int) AS property_id,
ad.mx_data_type,
ad.is_array,
ad.security_classification,
CAST(N'primitive' AS nvarchar(16)) AS attribute_source,
1 AS rn
FROM dbo.gobject g
INNER JOIN dbo.instance i
ON i.gobject_id = g.gobject_id
INNER JOIN dbo.primitive_instance pi
ON pi.gobject_id = g.gobject_id
AND pi.package_id = g.deployed_package_id
AND pi.property_bitmask & 0x10 <> 0x10
INNER JOIN dbo.attribute_definition ad
ON ad.primitive_definition_id = pi.primitive_definition_id
WHERE g.tag_name = @objectTagName
AND ad.attribute_name = @attributeName
AND (
(@primitiveName IS NULL AND pi.primitive_name = N'')
OR (@primitiveName IS NOT NULL AND pi.primitive_name = @primitiveName)
)
)
SELECT TOP (1)
object_tag_name,
attribute_name,
primitive_name,
mx_platform_id,
mx_engine_id,
mx_object_id,
mx_primitive_id,
mx_attribute_id,
property_id,
mx_data_type,
is_array,
security_classification,
attribute_source
FROM (
SELECT * FROM ranked_dynamic WHERE rn = 1
UNION ALL
SELECT * FROM primitive_attributes
) resolved
ORDER BY CASE attribute_source WHEN N'dynamic' THEN 0 ELSE 1 END
""";
private const string BrowseSql = """
;WITH deployed_objects AS (
SELECT
g.gobject_id,
g.tag_name,
g.deployed_package_id,
i.mx_platform_id,
i.mx_engine_id,
i.mx_object_id
FROM dbo.gobject g
INNER JOIN dbo.instance i
ON i.gobject_id = g.gobject_id
WHERE g.is_template = 0
AND g.deployed_package_id <> 0
AND g.tag_name LIKE @objectTagLike
),
deployed_package_chain AS (
SELECT
d.gobject_id,
d.tag_name,
d.mx_platform_id,
d.mx_engine_id,
d.mx_object_id,
p.package_id,
p.derived_from_package_id,
0 AS depth
FROM deployed_objects d
INNER JOIN dbo.package p
ON p.package_id = d.deployed_package_id
UNION ALL
SELECT
dpc.gobject_id,
dpc.tag_name,
dpc.mx_platform_id,
dpc.mx_engine_id,
dpc.mx_object_id,
p.package_id,
p.derived_from_package_id,
dpc.depth + 1
FROM deployed_package_chain dpc
INNER JOIN dbo.package p
ON p.package_id = dpc.derived_from_package_id
WHERE dpc.derived_from_package_id <> 0
AND dpc.depth < 10
),
ranked_dynamic AS (
SELECT
dpc.tag_name AS object_tag_name,
da.attribute_name,
CAST(NULL AS nvarchar(329)) AS primitive_name,
dpc.mx_platform_id,
dpc.mx_engine_id,
dpc.mx_object_id,
da.mx_primitive_id,
da.mx_attribute_id,
CAST(10 AS int) AS property_id,
da.mx_data_type,
da.is_array,
da.security_classification,
CAST(N'dynamic' AS nvarchar(16)) AS attribute_source,
ROW_NUMBER() OVER (
PARTITION BY dpc.gobject_id, da.attribute_name
ORDER BY dpc.depth
) AS rn
FROM deployed_package_chain dpc
INNER JOIN dbo.dynamic_attribute da
ON da.package_id = dpc.package_id
WHERE da.attribute_name LIKE @attributeLike
),
primitive_attributes AS (
SELECT
d.tag_name AS object_tag_name,
ad.attribute_name,
NULLIF(pi.primitive_name, N'') AS primitive_name,
d.mx_platform_id,
d.mx_engine_id,
d.mx_object_id,
pi.mx_primitive_id,
ad.mx_attribute_id,
CAST(10 AS int) AS property_id,
ad.mx_data_type,
ad.is_array,
ad.security_classification,
CAST(N'primitive' AS nvarchar(16)) AS attribute_source,
1 AS rn
FROM deployed_objects d
INNER JOIN dbo.gobject g
ON g.gobject_id = d.gobject_id
INNER JOIN dbo.primitive_instance pi
ON pi.gobject_id = g.gobject_id
AND pi.package_id = g.deployed_package_id
AND pi.property_bitmask & 0x10 <> 0x10
INNER JOIN dbo.attribute_definition ad
ON ad.primitive_definition_id = pi.primitive_definition_id
WHERE ad.attribute_name LIKE @attributeLike
)
SELECT TOP (@maxRows)
object_tag_name,
attribute_name,
primitive_name,
mx_platform_id,
mx_engine_id,
mx_object_id,
mx_primitive_id,
mx_attribute_id,
property_id,
mx_data_type,
is_array,
security_classification,
attribute_source
FROM (
SELECT * FROM ranked_dynamic WHERE rn = 1
UNION ALL
SELECT * FROM primitive_attributes
) resolved
ORDER BY object_tag_name, primitive_name, attribute_name
""";
}
@@ -0,0 +1,149 @@
using Microsoft.Data.SqlClient;
namespace MxNativeClient;
public sealed record GalaxyUserProfile(
int UserProfileId,
string UserProfileName,
Guid UserGuid,
string DefaultSecurityGroup,
int? InTouchAccessLevel,
IReadOnlyList<string> Roles);
public sealed class GalaxyRepositoryUserResolver
{
private const string DefaultConnectionString =
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True";
private readonly string _connectionString;
public GalaxyRepositoryUserResolver(string connectionString = DefaultConnectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_connectionString = connectionString;
}
public async Task<int> ResolveUserProfileIdByGuidAsync(
Guid userGuid,
CancellationToken cancellationToken = default)
{
GalaxyUserProfile profile = await ResolveByGuidAsync(userGuid, cancellationToken).ConfigureAwait(false);
return profile.UserProfileId;
}
public async Task<GalaxyUserProfile> ResolveByGuidAsync(
Guid userGuid,
CancellationToken cancellationToken = default)
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = connection.CreateCommand();
command.CommandText = UserByGuidSql;
command.Parameters.AddWithValue("@userGuid", userGuid);
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new KeyNotFoundException($"Galaxy user GUID {userGuid} was not found in dbo.user_profile.");
}
return ReadProfile(reader);
}
public async Task<GalaxyUserProfile> ResolveByNameAsync(
string userName,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userName);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = connection.CreateCommand();
command.CommandText = UserByNameSql;
command.Parameters.AddWithValue("@userName", userName);
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new KeyNotFoundException($"Galaxy user {userName} was not found in dbo.user_profile.");
}
return ReadProfile(reader);
}
private static GalaxyUserProfile ReadProfile(SqlDataReader reader)
{
return new GalaxyUserProfile(
reader.GetInt32(0),
reader.GetString(1),
reader.GetGuid(2),
reader.GetString(3),
reader.IsDBNull(4) ? null : reader.GetInt32(4),
reader.IsDBNull(5) ? [] : ParseRoleBlob(reader.GetString(5)));
}
private static IReadOnlyList<string> ParseRoleBlob(string rolesText)
{
if (!rolesText.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || rolesText.Length <= 2)
{
return [];
}
byte[] bytes = Convert.FromHexString(rolesText[2..]);
List<string> roles = [];
for (int offset = 0; offset + 3 < bytes.Length; offset++)
{
List<char> chars = [];
int cursor = offset;
while (cursor + 1 < bytes.Length)
{
char c = (char)(bytes[cursor] | (bytes[cursor + 1] << 8));
if (c == '\0')
{
break;
}
if (c < 0x20 || c > 0x7e)
{
chars.Clear();
break;
}
chars.Add(c);
cursor += 2;
}
if (chars.Count < 2 || cursor + 1 >= bytes.Length || bytes[cursor] != 0 || bytes[cursor + 1] != 0)
{
continue;
}
string role = new(chars.ToArray());
if (!roles.Contains(role, StringComparer.OrdinalIgnoreCase))
{
roles.Add(role);
}
offset = cursor;
}
return roles;
}
private const string UserSelectSql = """
SELECT TOP (1)
user_profile_id,
user_profile_name,
user_guid,
default_security_group,
intouch_access_level,
CONVERT(nvarchar(max), roles) AS roles_text
FROM dbo.user_profile
""";
private const string UserByGuidSql = UserSelectSql + "\nWHERE user_guid = @userGuid\nORDER BY user_profile_id";
private const string UserByNameSql = UserSelectSql + "\nWHERE user_profile_name = @userName\nORDER BY user_profile_id";
}
@@ -0,0 +1,393 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
namespace MxNativeClient;
public sealed class ManagedCallbackExporter : IDisposable
{
private readonly TcpListener _listener;
private readonly CancellationTokenSource _cancellation = new();
private readonly Task _acceptLoop;
private readonly List<string> _events = [];
private readonly object _eventsLock = new();
private readonly ulong _oxid = RandomUInt64();
private readonly ulong _oid = RandomUInt64();
public ManagedCallbackExporter(int port = 0)
{
CallbackIpid = Guid.NewGuid();
RemUnknownIpid = Guid.NewGuid();
_listener = new TcpListener(IPAddress.Any, port);
_listener.Start();
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
_acceptLoop = Task.Run(AcceptLoopAsync);
}
public int Port { get; }
public Guid CallbackIpid { get; }
public Guid RemUnknownIpid { get; }
public IReadOnlyList<string> Events
{
get
{
lock (_eventsLock)
{
return _events.ToArray();
}
}
}
public byte[] CreateCallbackObjRef(string hostName)
{
return ComObjRefBuilder.CreateStandardObjRef(
NmxProcedureMetadata.INmxSvcCallback,
stdFlags: 0x280,
publicRefs: 5,
oxid: _oxid,
oid: _oid,
ipid: CallbackIpid,
stringBindings: [$"{hostName}[{Port}]"]);
}
public void Dispose()
{
_cancellation.Cancel();
_listener.Stop();
try
{
_acceptLoop.Wait(TimeSpan.FromSeconds(1));
}
catch
{
}
_cancellation.Dispose();
}
private async Task AcceptLoopAsync()
{
while (!_cancellation.IsCancellationRequested)
{
try
{
var client = await _listener.AcceptTcpClientAsync(_cancellation.Token).ConfigureAwait(false);
Record($"accept remote={client.Client.RemoteEndPoint}");
_ = Task.Run(() => ServeClientAsync(client), _cancellation.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (ObjectDisposedException)
{
break;
}
catch (Exception ex)
{
Record($"accept_error {ex.GetType().Name}: {ex.Message}");
}
}
}
private async Task ServeClientAsync(TcpClient client)
{
using (client)
{
NetworkStream stream = client.GetStream();
ushort currentContext = 0;
Guid currentInterface = Guid.Empty;
while (!_cancellation.IsCancellationRequested)
{
byte[]? pdu = await ReadPduAsync(stream, _cancellation.Token).ConfigureAwait(false);
if (pdu is null)
{
Record("client_closed");
return;
}
var header = DceRpcPduHeader.Parse(pdu);
Record($"pdu type={header.PacketType} flags=0x{header.PacketFlags:X2} frag={header.FragmentLength} auth={header.AuthLength} call={header.CallId}");
if (header.PacketType == DceRpcPacketType.Bind || header.PacketType == DceRpcPacketType.AlterContext)
{
var bind = DceRpcBindPdu.Parse(pdu);
currentContext = bind.PresentationContexts.Count == 0 ? (ushort)0 : bind.PresentationContexts[0].ContextId;
currentInterface = bind.PresentationContexts.Count == 0 ? Guid.Empty : bind.PresentationContexts[0].AbstractSyntax.Uuid;
Record($"bind context={currentContext} iid={currentInterface}");
await stream.WriteAsync(EncodeBindAck(header.CallId, currentContext), _cancellation.Token).ConfigureAwait(false);
continue;
}
if (header.PacketType == DceRpcPacketType.Request)
{
byte[] response = HandleRequest(pdu, header, currentContext, currentInterface);
await stream.WriteAsync(response, _cancellation.Token).ConfigureAwait(false);
continue;
}
if (header.PacketType == DceRpcPacketType.Auth3)
{
Record("auth3_ignored");
continue;
}
Record($"unhandled_pdu {header.PacketType}");
}
}
}
private byte[] HandleRequest(byte[] pdu, DceRpcPduHeader header, ushort currentContext, Guid currentInterface)
{
int fixedOffset = DceRpcPduHeader.Length;
uint allocationHint = BinaryPrimitives.ReadUInt32LittleEndian(pdu.AsSpan(fixedOffset, 4));
ushort contextId = BinaryPrimitives.ReadUInt16LittleEndian(pdu.AsSpan(fixedOffset + 4, 2));
ushort opnum = BinaryPrimitives.ReadUInt16LittleEndian(pdu.AsSpan(fixedOffset + 6, 2));
int stubOffset = fixedOffset + 8;
Guid? objectUuid = null;
if ((header.PacketFlags & 0x80) != 0)
{
objectUuid = new Guid(pdu.AsSpan(stubOffset, 16));
stubOffset += 16;
}
int trailerLength = header.AuthLength == 0 ? 0 : DceRpcAuthTrailer.Length + header.AuthLength;
int stubLength = header.FragmentLength - stubOffset - trailerLength;
ReadOnlySpan<byte> stub = pdu.AsSpan(stubOffset, Math.Max(0, stubLength));
Record($"request iid={currentInterface} context={contextId}/{currentContext} opnum={opnum} object={objectUuid} alloc={allocationHint} stub={stub.Length}");
if (currentInterface == RemUnknownMessages.IRemUnknown)
{
return opnum switch
{
RemUnknownMessages.RemQueryInterfaceOpnum => EncodeResponse(header.CallId, contextId, EncodeRemQueryInterfaceResponse(stub)),
RemUnknownMessages.RemAddRefOpnum => EncodeResponse(header.CallId, contextId, EncodeOrpcHResultResponse(0)),
RemUnknownMessages.RemReleaseOpnum => EncodeResponse(header.CallId, contextId, EncodeOrpcHResultResponse(0)),
_ => EncodeFault(header.CallId, contextId, 0x000006F7),
};
}
if (currentInterface == NmxSvcCallbackMessages.InterfaceId)
{
if (opnum is NmxSvcCallbackMessages.DataReceivedOpnum or NmxSvcCallbackMessages.StatusReceivedOpnum)
{
var parsed = NmxSvcCallbackMessages.ParseCallbackRequest(stub);
Record($"callback opnum={opnum} body={parsed.Body.Length}");
return EncodeResponse(header.CallId, contextId, NmxSvcCallbackMessages.EncodeCallbackResponse(0));
}
return EncodeFault(header.CallId, contextId, 0x000006F7);
}
return EncodeFault(header.CallId, contextId, 0x000006F7);
}
private byte[] EncodeRemQueryInterfaceResponse(ReadOnlySpan<byte> request)
{
Guid requestedIid = request.Length >= 76 ? new Guid(request.Slice(60, 16)) : Guid.Empty;
Record($"remqi requested={requestedIid}");
var std = new StdObjRef(0x280, 5, _oxid, _oid, requestedIid == RemUnknownMessages.IRemUnknown ? RemUnknownIpid : CallbackIpid);
int hr = requestedIid == RemUnknownMessages.IRemUnknown
|| requestedIid == NmxProcedureMetadata.INmxSvcCallback
|| requestedIid == new Guid("00000000-0000-0000-C000-000000000046")
? 0
: unchecked((int)0x80004002);
byte[] buffer = new byte[OrpcThat.EncodedLengthWithoutExtensions + 4 + 4 + RemQiResult.EncodedLength + 4];
int offset = 0;
new OrpcThat(0, 0).Encode().CopyTo(buffer.AsSpan(offset));
offset += OrpcThat.EncodedLengthWithoutExtensions;
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), 0x00020000);
offset += 4;
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), 1);
offset += 4;
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(offset, 4), hr);
offset += 8;
std.Encode().CopyTo(buffer.AsSpan(offset));
offset += StdObjRef.EncodedLength;
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), 0);
return buffer;
}
private static byte[] EncodeOrpcHResultResponse(int hresult)
{
byte[] buffer = new byte[OrpcThat.EncodedLengthWithoutExtensions + 4];
new OrpcThat(0, 0).Encode().CopyTo(buffer.AsSpan());
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(OrpcThat.EncodedLengthWithoutExtensions, 4), hresult);
return buffer;
}
private static byte[] EncodeBindAck(uint callId, ushort contextId)
{
const string secondaryAddress = "";
byte[] secondary = System.Text.Encoding.ASCII.GetBytes(secondaryAddress + "\0");
int secAddrLength = secondary.Length;
int secPad = Align(28 + 2 + secAddrLength, 4) - (28 + 2 + secAddrLength);
int resultOffset = 28 + 2 + secAddrLength + secPad;
int length = resultOffset + 4 + 24;
byte[] pdu = new byte[length];
new DceRpcPduHeader(5, 0, DceRpcPacketType.BindAck, 0x03, 0x10, (ushort)length, 0, callId).WriteTo(pdu);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(16, 2), 4280);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(18, 2), 4280);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(20, 4), 0x00005353);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(24, 2), (ushort)secAddrLength);
secondary.CopyTo(pdu.AsSpan(26));
int offset = resultOffset;
pdu[offset++] = 1;
pdu[offset++] = 0;
pdu[offset++] = 0;
pdu[offset++] = 0;
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset, 2), 0);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset + 2, 2), 0);
offset += 4;
DceRpcSyntaxId.Ndr20.Uuid.TryWriteBytes(pdu.AsSpan(offset, 16));
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset + 16, 2), DceRpcSyntaxId.Ndr20.VersionMajor);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset + 18, 2), DceRpcSyntaxId.Ndr20.VersionMinor);
_ = contextId;
return pdu;
}
private static byte[] EncodeResponse(uint callId, ushort contextId, byte[] stubData)
{
int length = 24 + stubData.Length;
byte[] pdu = new byte[length];
new DceRpcPduHeader(5, 0, DceRpcPacketType.Response, 0x03, 0x10, (ushort)length, 0, callId).WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(16, 4), (uint)stubData.Length);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(20, 2), contextId);
pdu[22] = 0;
pdu[23] = 0;
stubData.CopyTo(pdu.AsSpan(24));
return pdu;
}
private static byte[] EncodeFault(uint callId, ushort contextId, uint status)
{
byte[] pdu = new byte[28];
new DceRpcPduHeader(5, 0, DceRpcPacketType.Fault, 0x03, 0x10, (ushort)pdu.Length, 0, callId).WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(16, 4), 0);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(20, 2), contextId);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(24, 4), status);
return pdu;
}
private async Task<byte[]?> ReadPduAsync(NetworkStream stream, CancellationToken cancellationToken)
{
byte[] header = new byte[DceRpcPduHeader.Length];
if (!await ReadExactAsync(stream, header, cancellationToken).ConfigureAwait(false))
{
return null;
}
var parsed = DceRpcPduHeader.Parse(header);
byte[] pdu = new byte[parsed.FragmentLength];
header.CopyTo(pdu, 0);
if (!await ReadExactAsync(stream, pdu.AsMemory(DceRpcPduHeader.Length), cancellationToken).ConfigureAwait(false))
{
return null;
}
return pdu;
}
private static async Task<bool> ReadExactAsync(NetworkStream stream, Memory<byte> buffer, CancellationToken cancellationToken)
{
int offset = 0;
while (offset < buffer.Length)
{
int read = await stream.ReadAsync(buffer[offset..], cancellationToken).ConfigureAwait(false);
if (read == 0)
{
return false;
}
offset += read;
}
return true;
}
private void Record(string message)
{
lock (_eventsLock)
{
_events.Add($"{DateTimeOffset.UtcNow:O} {message}");
}
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
private static ulong RandomUInt64()
{
Span<byte> bytes = stackalloc byte[8];
System.Security.Cryptography.RandomNumberGenerator.Fill(bytes);
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
}
public static class ComObjRefBuilder
{
public static byte[] CreateStandardObjRef(
Guid iid,
uint stdFlags,
uint publicRefs,
ulong oxid,
ulong oid,
Guid ipid,
IReadOnlyList<string> stringBindings)
{
ushort securityOffset = (ushort)(stringBindings.Sum(binding => 1 + binding.Length + 1) + 1);
var words = new List<ushort>();
foreach (string binding in stringBindings)
{
words.Add(0x0007);
foreach (char ch in binding)
{
words.Add(ch);
}
words.Add(0);
}
words.Add(0);
foreach (ushort authenticationService in new ushort[] { 0x0009, 0x001e, 0x0010, 0x000a, 0x0016, 0x001f, 0x000e })
{
words.Add(authenticationService);
words.Add(0xffff);
words.Add(0);
}
words.Add(0);
ushort entries = (ushort)words.Count;
byte[] buffer = new byte[68 + entries * 2];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), 0x574F454D);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), 1);
iid.TryWriteBytes(buffer.AsSpan(8, 16));
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(24, 4), stdFlags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(28, 4), publicRefs);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(32, 8), oxid);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(40, 8), oid);
ipid.TryWriteBytes(buffer.AsSpan(48, 16));
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(64, 2), entries);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(66, 2), securityOffset);
int offset = 68;
foreach (ushort word in words)
{
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(offset, 2), word);
offset += 2;
}
return buffer;
}
}
@@ -0,0 +1,575 @@
using System.Globalization;
using System.Runtime.InteropServices;
using MxNativeCodec;
namespace MxNativeClient;
public enum DceRpcClientAuthentication
{
ManagedNtlm,
WindowsSspiNtlm,
}
public sealed class ManagedNmxService2Client : IDisposable
{
private readonly object _activatedComObject;
private readonly DceRpcTcpClient _serviceClient;
private readonly Guid _serviceIpid;
private bool _disposed;
private ManagedNmxService2Client(object activatedComObject, DceRpcTcpClient serviceClient, Guid serviceIpid)
{
_activatedComObject = activatedComObject;
_serviceClient = serviceClient;
_serviceIpid = serviceIpid;
}
public string Host { get; private init; } = string.Empty;
public int Port { get; private init; }
public static ManagedNmxService2Client Create(
DceRpcClientAuthentication authentication = DceRpcClientAuthentication.ManagedNtlm)
{
var type = Type.GetTypeFromProgID("NmxSvc.NmxService", throwOnError: true)
?? throw new InvalidOperationException("ProgID NmxSvc.NmxService was not resolved.");
object instance = Activator.CreateInstance(type)
?? throw new InvalidOperationException("ProgID NmxSvc.NmxService activation returned null.");
try
{
var resolved = ResolveService(instance, authentication);
var serviceClient = new DceRpcTcpClient(resolved.Host, resolved.Port);
serviceClient.Connect();
BindWithAuthentication(
serviceClient,
NmxService2Messages.InterfaceId,
authentication,
resolved.Host);
return new ManagedNmxService2Client(instance, serviceClient, resolved.ServiceIpid)
{
Host = resolved.Host,
Port = resolved.Port,
};
}
catch
{
if (Marshal.IsComObject(instance))
{
_ = Marshal.ReleaseComObject(instance);
}
throw;
}
}
public int GetPartnerVersion(int galaxyId, int platformId, int engineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeGetPartnerVersionRequest(
OrpcThis.Create(Guid.NewGuid()),
galaxyId,
platformId,
engineId);
var response = _serviceClient.CallBoundObject(_serviceIpid, NmxService2Messages.GetPartnerVersionOpnum, request);
var parsed = NmxService2Messages.ParseGetPartnerVersionResponse(response.StubData.Span);
ThrowIfFailed(parsed.HResult, nameof(GetPartnerVersion));
return parsed.PartnerVersion;
}
public int RegisterEngine2(int localEngineId, string engineName, int version, byte[] callbackObjRef)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeRegisterEngine2Request(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
engineName,
version,
callbackObjRef);
return CallForHResult(NmxService2Messages.RegisterEngine2Opnum, request);
}
public int RegisterEngine2WithoutCallback(int localEngineId, string engineName, int version)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeRegisterEngine2Request(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
engineName,
version,
callbackObjRef: null);
return CallForHResult(NmxService2Messages.RegisterEngine2Opnum, request);
}
public int UnregisterEngine(int localEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeUnregisterEngineRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId);
return CallForHResult(NmxService2Messages.UnregisterEngineOpnum, request);
}
public int Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeConnectRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
remoteGalaxyId,
remotePlatformId,
remoteEngineId);
return CallForHResult(NmxService2Messages.ConnectOpnum, request);
}
public int AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeSubscriberEngineRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
subscriberGalaxyId,
subscriberPlatformId,
subscriberEngineId);
return CallForHResult(NmxService2Messages.AddSubscriberEngineOpnum, request);
}
public int RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeSubscriberEngineRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
subscriberGalaxyId,
subscriberPlatformId,
subscriberEngineId);
return CallForHResult(NmxService2Messages.RemoveSubscriberEngineOpnum, request);
}
public int SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeSetHeartbeatSendIntervalRequest(
OrpcThis.Create(Guid.NewGuid()),
ticksPerBeat,
maxMissedTicks);
return CallForHResult(NmxService2Messages.SetHeartbeatSendIntervalOpnum, request);
}
public int TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, ReadOnlySpan<byte> messageBody)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ValidateTransferDataBody(messageBody, nameof(messageBody));
byte[] request = NmxService2Messages.EncodeTransferDataRequest(
OrpcThis.Create(Guid.NewGuid()),
remoteGalaxyId,
remotePlatformId,
remoteEngineId,
messageBody);
return CallForHResult(NmxService2Messages.TransferDataOpnum, request);
}
private static void ValidateTransferDataBody(ReadOnlySpan<byte> messageBody, string parameterName)
{
if (messageBody.IsEmpty)
{
throw new ArgumentException("TransferData body cannot be empty.", parameterName);
}
NmxTransferEnvelopeTemplate.FromObserved(messageBody);
if (messageBody.Length == NmxTransferEnvelopeTemplate.HeaderLength)
{
throw new ArgumentException("TransferData body must include an inner message after the 46-byte envelope.", parameterName);
}
}
internal static byte[] EncodeWriteTransferBody(
int localEngineId,
GalaxyTagMetadata tag,
object value,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
var projection = tag.ProjectWriteValue(value);
byte[] innerBody = NmxWriteMessage.Encode(
tag.ToReferenceHandle((byte)galaxyId),
projection.ValueKind,
projection.Value,
writeIndex,
clientToken);
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
internal static byte[] EncodeWrite2TransferBody(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
var projection = tag.ProjectWriteValue(value);
byte[] innerBody = NmxWriteMessage.EncodeTimestamped(
tag.ToReferenceHandle((byte)galaxyId),
projection.ValueKind,
projection.Value,
timestamp,
writeIndex,
clientToken);
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
internal static byte[] EncodeWriteSecured2TransferBody(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
string clientName,
int currentUserId,
int verifierUserId,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
var projection = tag.ProjectWriteValue(value);
byte[] innerBody = NmxSecuredWrite2Message.Encode(
tag.ToReferenceHandle((byte)galaxyId),
projection.ValueKind,
projection.Value,
timestamp,
clientName,
NmxSecuredWrite2Message.ResolveObservedUserToken(currentUserId),
NmxSecuredWrite2Message.ResolveObservedUserToken(verifierUserId),
writeIndex,
clientToken);
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
internal static byte[] EncodeAdviseSupervisoryTransferBody(
int localEngineId,
GalaxyTagMetadata tag,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
byte[] innerBody = NmxItemControlMessage.FromReferenceHandle(
NmxItemControlCommand.AdviseSupervisory,
itemCorrelationId,
tag.ToReferenceHandle((byte)galaxyId)).Encode();
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.ItemControl,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
public int Write(
int localEngineId,
GalaxyTagMetadata tag,
object value,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeWriteTransferBody(
localEngineId,
tag,
value,
writeIndex,
clientToken,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int Write2(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeWrite2TransferBody(
localEngineId,
tag,
value,
timestamp,
writeIndex,
clientToken,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int WriteSecured2(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
string clientName,
int currentUserId,
int verifierUserId,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeWriteSecured2TransferBody(
localEngineId,
tag,
value,
timestamp,
clientName,
currentUserId,
verifierUserId,
writeIndex,
clientToken,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int AdviseSupervisory(
int localEngineId,
GalaxyTagMetadata tag,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeAdviseSupervisoryTransferBody(
localEngineId,
tag,
itemCorrelationId,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int SendObservedPreAdviseMetadata(
int localEngineId,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] innerBody = NmxMetadataQueryMessage.EncodeObservedPreAdvise(itemCorrelationId);
byte[] transferBody = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Metadata,
localEngineId,
galaxyId,
targetPlatformId: 1,
targetEngineId: 1,
innerBody,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, 1, 1, transferBody);
}
public int RegisterReference(
int localEngineId,
GalaxyTagMetadata routeTag,
NmxReferenceRegistrationMessage message,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.ItemControl,
localEngineId,
galaxyId,
routeTag.PlatformId,
routeTag.EngineId,
message.Encode(),
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, routeTag.PlatformId, routeTag.EngineId, transferBody);
}
public int UnAdvise(
int localEngineId,
GalaxyTagMetadata tag,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] innerBody = NmxItemControlMessage.FromReferenceHandle(
NmxItemControlCommand.UnAdvise,
itemCorrelationId,
tag.ToReferenceHandle((byte)galaxyId)).Encode();
byte[] transferBody = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_serviceClient.Dispose();
if (Marshal.IsComObject(_activatedComObject))
{
_ = Marshal.ReleaseComObject(_activatedComObject);
}
_disposed = true;
}
private int CallForHResult(ushort opnum, byte[] request)
{
var response = _serviceClient.CallBoundObject(_serviceIpid, opnum, request);
var parsed = NmxService2Messages.ParseHResultResponse(response.StubData.Span);
return parsed.HResult;
}
private static (string Host, int Port, Guid ServiceIpid) ResolveService(
object instance,
DceRpcClientAuthentication authentication)
{
byte[] buffer = ComObjRefProvider.MarshalIUnknownObjRef(
instance,
ComObjRefProvider.MarshalContextDifferentMachine);
var objRef = ComObjRef.Parse(buffer);
var exporter = new ObjectExporterClient();
var oxidResponse = authentication == DceRpcClientAuthentication.ManagedNtlm
? exporter.ResolveOxidWithManagedNtlmPacketIntegrity(objRef.Oxid)
: exporter.ResolveOxidWithNtlmPacketIntegrity(objRef.Oxid);
var resolved = ObjectExporterMessages.ParseResolveOxidResult(oxidResponse.StubData.Span);
var endpoint = resolved.Bindings.First(binding => binding.TowerId == ObjectExporterMessages.ProtseqNcacnIpTcp);
string host = ParseBracketedHost(endpoint.Value);
int port = ParseBracketedPort(endpoint.Value);
using var client = new DceRpcTcpClient(host, port);
client.Connect();
BindWithAuthentication(client, RemUnknownMessages.IRemUnknown, authentication, host);
byte[] request = RemUnknownMessages.EncodeRemQueryInterfaceRequest(
objRef.Ipid,
NmxProcedureMetadata.INmxService2,
Guid.NewGuid());
var response = client.CallBoundObject(resolved.RemUnknownIpid, RemUnknownMessages.RemQueryInterfaceOpnum, request);
var parsed = RemUnknownMessages.ParseRemQueryInterfaceResponse(response.StubData.Span);
if (parsed.Result is null || parsed.Result.HResult != 0 || parsed.ErrorCode != 0)
{
throw new InvalidOperationException($"RemQueryInterface failed: hresult=0x{parsed.Result?.HResult ?? -1:X8}, error=0x{parsed.ErrorCode:X8}.");
}
return (host, port, parsed.Result.StandardObjectReference.Ipid);
}
private static void BindWithAuthentication(
DceRpcTcpClient client,
Guid interfaceId,
DceRpcClientAuthentication authentication,
string targetName)
{
if (authentication == DceRpcClientAuthentication.ManagedNtlm)
{
client.BindWithManagedNtlmPacketIntegrity(interfaceId, versionMajor: 0, versionMinor: 0);
return;
}
client.BindWithNtlmPacketIntegrity(interfaceId, versionMajor: 0, versionMinor: 0, targetName);
}
private static string ParseBracketedHost(string binding)
{
int open = binding.LastIndexOf('[');
if (open <= 0)
{
throw new FormatException($"Binding does not contain a bracketed host: {binding}");
}
return binding[..open];
}
private static int ParseBracketedPort(string binding)
{
int open = binding.LastIndexOf('[');
int close = binding.LastIndexOf(']');
if (open < 0 || close <= open)
{
throw new FormatException($"Binding does not contain a bracketed port: {binding}");
}
return int.Parse(binding.AsSpan(open + 1, close - open - 1), CultureInfo.InvariantCulture);
}
private static void ThrowIfFailed(int hresult, string operation)
{
if (hresult < 0)
{
Marshal.ThrowExceptionForHR(hresult);
}
if (hresult != 0)
{
throw new InvalidOperationException($"{operation} returned application status 0x{hresult:X8}.");
}
}
}
@@ -0,0 +1,389 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
namespace MxNativeClient;
public sealed class ManagedNtlmClientContext
{
private const uint NegotiateUnicode = 0x00000001;
private const uint RequestTarget = 0x00000004;
private const uint NegotiateSign = 0x00000010;
private const uint NegotiateSeal = 0x00000020;
private const uint NegotiateNtlm = 0x00000200;
private const uint NegotiateAlwaysSign = 0x00008000;
private const uint NegotiateExtendedSessionSecurity = 0x00080000;
private const uint NegotiateTargetInfo = 0x00800000;
private const uint NegotiateVersion = 0x02000000;
private const uint Negotiate128 = 0x20000000;
private const uint NegotiateKeyExchange = 0x40000000;
private const uint Negotiate56 = 0x80000000;
private readonly string _user;
private readonly string _password;
private readonly string _domain;
private readonly string _workstation;
private uint _flags;
private byte[] _exportedSessionKey = [];
private byte[] _clientSigningKey = [];
private Rc4? _clientSealingHandle;
private uint _sequence;
public ManagedNtlmClientContext(string user, string password, string domain, string? workstation = null)
{
_user = user;
_password = password;
_domain = domain;
_workstation = string.IsNullOrWhiteSpace(workstation) ? Environment.MachineName : workstation;
}
public static ManagedNtlmClientContext FromEnvironment()
{
string user = Environment.GetEnvironmentVariable("MX_RPC_USER")
?? throw new InvalidOperationException("MX_RPC_USER is required for managed NTLM.");
string password = Environment.GetEnvironmentVariable("MX_RPC_PASSWORD")
?? throw new InvalidOperationException("MX_RPC_PASSWORD is required for managed NTLM.");
string domain = Environment.GetEnvironmentVariable("MX_RPC_DOMAIN") ?? string.Empty;
return new ManagedNtlmClientContext(user, password, domain);
}
public byte[] CreateType1()
{
_flags = NegotiateKeyExchange
| NegotiateSign
| NegotiateAlwaysSign
| NegotiateSeal
| NegotiateTargetInfo
| NegotiateNtlm
| NegotiateExtendedSessionSecurity
| NegotiateUnicode
| RequestTarget
| Negotiate128
| Negotiate56;
byte[] message = new byte[32];
Encoding.ASCII.GetBytes("NTLMSSP\0").CopyTo(message, 0);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(8, 4), 1);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(12, 4), _flags);
return message;
}
public byte[] CreateType3(ReadOnlySpan<byte> type2)
{
var challenge = NtlmChallenge.Parse(type2);
_flags &= challenge.Flags;
byte[] clientChallenge = RandomNumberGenerator.GetBytes(8);
byte[] targetInfo = BuildTargetInfo(challenge.TargetInfo);
byte[] responseKeyNt = HmacMd5(NtHash(_password), Encoding.Unicode.GetBytes(_user.ToUpperInvariant() + _domain));
byte[] temp = BuildNtlmV2Temp(clientChallenge, targetInfo);
byte[] ntProof = HmacMd5(responseKeyNt, Combine(challenge.ServerChallenge, temp));
byte[] ntResponse = Combine(ntProof, temp);
byte[] lmResponse = Combine(HmacMd5(responseKeyNt, Combine(challenge.ServerChallenge, clientChallenge)), clientChallenge);
byte[] sessionBaseKey = HmacMd5(responseKeyNt, ntProof);
_exportedSessionKey = RandomNumberGenerator.GetBytes(16);
byte[] encryptedSessionKey = new Rc4(sessionBaseKey).Transform(_exportedSessionKey);
_clientSigningKey = SignKey(_exportedSessionKey, clientMode: true);
_clientSealingHandle = new Rc4(SealKey(_exportedSessionKey, clientMode: true));
_sequence = 0;
byte[] domain = Encoding.Unicode.GetBytes(_domain);
byte[] user = Encoding.Unicode.GetBytes(_user);
byte[] workstation = Encoding.Unicode.GetBytes(_workstation);
const int headerLength = 64;
int payloadLength = lmResponse.Length + ntResponse.Length + domain.Length + user.Length + workstation.Length + encryptedSessionKey.Length;
byte[] message = new byte[headerLength + payloadLength];
Encoding.ASCII.GetBytes("NTLMSSP\0").CopyTo(message, 0);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(8, 4), 3);
int offset = headerLength;
WriteSecurityBuffer(message, 12, lmResponse, ref offset);
WriteSecurityBuffer(message, 20, ntResponse, ref offset);
WriteSecurityBuffer(message, 28, domain, ref offset);
WriteSecurityBuffer(message, 36, user, ref offset);
WriteSecurityBuffer(message, 44, workstation, ref offset);
WriteSecurityBuffer(message, 52, encryptedSessionKey, ref offset);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(60, 4), _flags);
return message;
}
public byte[] Sign(ReadOnlySpan<byte> message)
{
if (_clientSealingHandle is null || _clientSigningKey.Length == 0)
{
throw new InvalidOperationException("NTLM context has not completed Type3 negotiation.");
}
byte[] sequenceBytes = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(sequenceBytes, _sequence);
byte[] digest = HmacMd5(_clientSigningKey, Combine(sequenceBytes, message.ToArray()));
byte[] checksum = _clientSealingHandle.Transform(digest[..8]);
byte[] signature = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(signature.AsSpan(0, 4), 1);
checksum.CopyTo(signature.AsSpan(4, 8));
BinaryPrimitives.WriteUInt32LittleEndian(signature.AsSpan(12, 4), _sequence);
_sequence++;
return signature;
}
private static byte[] BuildNtlmV2Temp(byte[] clientChallenge, byte[] targetInfo)
{
byte[] temp = new byte[28 + targetInfo.Length];
temp[0] = 1;
temp[1] = 1;
BinaryPrimitives.WriteInt64LittleEndian(temp.AsSpan(8, 8), DateTimeOffset.UtcNow.ToFileTime());
clientChallenge.CopyTo(temp.AsSpan(16, 8));
targetInfo.CopyTo(temp.AsSpan(28));
return temp;
}
private static byte[] BuildTargetInfo(ReadOnlySpan<byte> original)
{
var pairs = AvPair.ParseAll(original);
byte[]? dnsHost = pairs.FirstOrDefault(static pair => pair.Id == 3)?.Value;
if (dnsHost is not null)
{
byte[] prefix = Encoding.Unicode.GetBytes("cifs/");
pairs.RemoveAll(static pair => pair.Id == 9);
pairs.Add(new AvPair(9, Combine(prefix, dnsHost)));
}
if (!pairs.Any(static pair => pair.Id == 7))
{
byte[] timestamp = new byte[8];
BinaryPrimitives.WriteInt64LittleEndian(timestamp, DateTimeOffset.UtcNow.ToFileTime());
pairs.Add(new AvPair(7, timestamp));
}
using var stream = new MemoryStream();
Span<byte> header = stackalloc byte[4];
foreach (var pair in pairs.Where(static pair => pair.Id != 0))
{
BinaryPrimitives.WriteUInt16LittleEndian(header[..2], pair.Id);
BinaryPrimitives.WriteUInt16LittleEndian(header[2..], (ushort)pair.Value.Length);
stream.Write(header);
stream.Write(pair.Value);
}
stream.Write(new byte[4]);
return stream.ToArray();
}
private static byte[] SignKey(byte[] sessionKey, bool clientMode)
{
string magic = clientMode
? "session key to client-to-server signing key magic constant\0"
: "session key to server-to-client signing key magic constant\0";
return MD5.HashData(Combine(sessionKey, Encoding.ASCII.GetBytes(magic)));
}
private static byte[] SealKey(byte[] sessionKey, bool clientMode)
{
string magic = clientMode
? "session key to client-to-server sealing key magic constant\0"
: "session key to server-to-client sealing key magic constant\0";
return MD5.HashData(Combine(sessionKey, Encoding.ASCII.GetBytes(magic)));
}
private static byte[] NtHash(string password)
{
return Md4.Hash(Encoding.Unicode.GetBytes(password));
}
private static byte[] HmacMd5(byte[] key, byte[] data)
{
using var hmac = new HMACMD5(key);
return hmac.ComputeHash(data);
}
private static byte[] Combine(params byte[][] parts)
{
byte[] combined = new byte[parts.Sum(static part => part.Length)];
int offset = 0;
foreach (byte[] part in parts)
{
part.CopyTo(combined.AsSpan(offset));
offset += part.Length;
}
return combined;
}
private static void WriteSecurityBuffer(byte[] message, int descriptorOffset, byte[] value, ref int payloadOffset)
{
BinaryPrimitives.WriteUInt16LittleEndian(message.AsSpan(descriptorOffset, 2), (ushort)value.Length);
BinaryPrimitives.WriteUInt16LittleEndian(message.AsSpan(descriptorOffset + 2, 2), (ushort)value.Length);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(descriptorOffset + 4, 4), (uint)payloadOffset);
value.CopyTo(message.AsSpan(payloadOffset));
payloadOffset += value.Length;
}
private sealed record NtlmChallenge(uint Flags, byte[] ServerChallenge, byte[] TargetInfo)
{
public static NtlmChallenge Parse(ReadOnlySpan<byte> message)
{
if (message.Length < 48 || !message[..8].SequenceEqual(Encoding.ASCII.GetBytes("NTLMSSP\0")))
{
throw new ArgumentException("NTLM challenge is truncated or invalid.", nameof(message));
}
int targetInfoLength = BinaryPrimitives.ReadUInt16LittleEndian(message.Slice(40, 2));
int targetInfoOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(message.Slice(44, 4));
if (targetInfoOffset < 0 || targetInfoOffset + targetInfoLength > message.Length)
{
throw new ArgumentException("NTLM challenge target-info buffer is invalid.", nameof(message));
}
return new NtlmChallenge(
BinaryPrimitives.ReadUInt32LittleEndian(message.Slice(20, 4)),
message.Slice(24, 8).ToArray(),
message.Slice(targetInfoOffset, targetInfoLength).ToArray());
}
}
private sealed record AvPair(ushort Id, byte[] Value)
{
public static List<AvPair> ParseAll(ReadOnlySpan<byte> buffer)
{
var pairs = new List<AvPair>();
int offset = 0;
while (offset + 4 <= buffer.Length)
{
ushort id = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(offset, 2));
ushort length = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(offset + 2, 2));
offset += 4;
if (id == 0)
{
break;
}
if (offset + length > buffer.Length)
{
throw new ArgumentException("NTLM AV pair buffer is truncated.", nameof(buffer));
}
pairs.Add(new AvPair(id, buffer.Slice(offset, length).ToArray()));
offset += length;
}
return pairs;
}
}
private sealed class Rc4
{
private readonly byte[] _s = new byte[256];
private int _i;
private int _j;
public Rc4(byte[] key)
{
for (int i = 0; i < 256; i++)
{
_s[i] = (byte)i;
}
int j = 0;
for (int i = 0; i < 256; i++)
{
j = (j + _s[i] + key[i % key.Length]) & 0xff;
(_s[i], _s[j]) = (_s[j], _s[i]);
}
}
public byte[] Transform(ReadOnlySpan<byte> input)
{
byte[] output = new byte[input.Length];
for (int k = 0; k < input.Length; k++)
{
_i = (_i + 1) & 0xff;
_j = (_j + _s[_i]) & 0xff;
(_s[_i], _s[_j]) = (_s[_j], _s[_i]);
byte keyByte = _s[(_s[_i] + _s[_j]) & 0xff];
output[k] = (byte)(input[k] ^ keyByte);
}
return output;
}
}
private static class Md4
{
public static byte[] Hash(byte[] input)
{
uint a = 0x67452301;
uint b = 0xefcdab89;
uint c = 0x98badcfe;
uint d = 0x10325476;
byte[] padded = Pad(input);
Span<uint> x = stackalloc uint[16];
for (int offset = 0; offset < padded.Length; offset += 64)
{
uint aa = a;
uint bb = b;
uint cc = c;
uint dd = d;
for (int i = 0; i < 16; i++)
{
x[i] = BinaryPrimitives.ReadUInt32LittleEndian(padded.AsSpan(offset + i * 4, 4));
}
Round1(ref a, b, c, d, x[0], 3); Round1(ref d, a, b, c, x[1], 7); Round1(ref c, d, a, b, x[2], 11); Round1(ref b, c, d, a, x[3], 19);
Round1(ref a, b, c, d, x[4], 3); Round1(ref d, a, b, c, x[5], 7); Round1(ref c, d, a, b, x[6], 11); Round1(ref b, c, d, a, x[7], 19);
Round1(ref a, b, c, d, x[8], 3); Round1(ref d, a, b, c, x[9], 7); Round1(ref c, d, a, b, x[10], 11); Round1(ref b, c, d, a, x[11], 19);
Round1(ref a, b, c, d, x[12], 3); Round1(ref d, a, b, c, x[13], 7); Round1(ref c, d, a, b, x[14], 11); Round1(ref b, c, d, a, x[15], 19);
Round2(ref a, b, c, d, x[0], 3); Round2(ref d, a, b, c, x[4], 5); Round2(ref c, d, a, b, x[8], 9); Round2(ref b, c, d, a, x[12], 13);
Round2(ref a, b, c, d, x[1], 3); Round2(ref d, a, b, c, x[5], 5); Round2(ref c, d, a, b, x[9], 9); Round2(ref b, c, d, a, x[13], 13);
Round2(ref a, b, c, d, x[2], 3); Round2(ref d, a, b, c, x[6], 5); Round2(ref c, d, a, b, x[10], 9); Round2(ref b, c, d, a, x[14], 13);
Round2(ref a, b, c, d, x[3], 3); Round2(ref d, a, b, c, x[7], 5); Round2(ref c, d, a, b, x[11], 9); Round2(ref b, c, d, a, x[15], 13);
Round3(ref a, b, c, d, x[0], 3); Round3(ref d, a, b, c, x[8], 9); Round3(ref c, d, a, b, x[4], 11); Round3(ref b, c, d, a, x[12], 15);
Round3(ref a, b, c, d, x[2], 3); Round3(ref d, a, b, c, x[10], 9); Round3(ref c, d, a, b, x[6], 11); Round3(ref b, c, d, a, x[14], 15);
Round3(ref a, b, c, d, x[1], 3); Round3(ref d, a, b, c, x[9], 9); Round3(ref c, d, a, b, x[5], 11); Round3(ref b, c, d, a, x[13], 15);
Round3(ref a, b, c, d, x[3], 3); Round3(ref d, a, b, c, x[11], 9); Round3(ref c, d, a, b, x[7], 11); Round3(ref b, c, d, a, x[15], 15);
a += aa;
b += bb;
c += cc;
d += dd;
}
byte[] hash = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(0, 4), a);
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(4, 4), b);
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(8, 4), c);
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(12, 4), d);
return hash;
}
private static byte[] Pad(byte[] input)
{
ulong bitLength = (ulong)input.Length * 8;
int paddedLength = input.Length + 1;
while (paddedLength % 64 != 56)
{
paddedLength++;
}
byte[] padded = new byte[paddedLength + 8];
input.CopyTo(padded.AsSpan());
padded[input.Length] = 0x80;
BinaryPrimitives.WriteUInt64LittleEndian(padded.AsSpan(paddedLength, 8), bitLength);
return padded;
}
private static uint F(uint x, uint y, uint z) => (x & y) | (~x & z);
private static uint G(uint x, uint y, uint z) => (x & y) | (x & z) | (y & z);
private static uint H(uint x, uint y, uint z) => x ^ y ^ z;
private static void Round1(ref uint a, uint b, uint c, uint d, uint x, int s) => a = BitOperations.RotateLeft(a + F(b, c, d) + x, s);
private static void Round2(ref uint a, uint b, uint c, uint d, uint x, int s) => a = BitOperations.RotateLeft(a + G(b, c, d) + x + 0x5a827999, s);
private static void Round3(ref uint a, uint b, uint c, uint d, uint x, int s) => a = BitOperations.RotateLeft(a + H(b, c, d) + x + 0x6ed9eba1, s);
}
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MxNativeCodec\MxNativeCodec.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,910 @@
using System.Globalization;
using MxNativeCodec;
namespace MxNativeClient;
public sealed record MxNativeDataChangeEvent(
int ServerHandle,
int ItemHandle,
object? Value,
ushort Quality,
DateTime TimestampUtc,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeWriteCompleteEvent(
int ServerHandle,
int ItemHandle,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeOperationCompleteEvent(
int ServerHandle,
int ItemHandle,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeBufferedDataChangeEvent(
int ServerHandle,
int ItemHandle,
short MxDataType,
IReadOnlyList<object?> Values,
IReadOnlyList<ushort> Qualities,
IReadOnlyList<DateTime> TimestampsUtc,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeCompatibilityRecoveryAttemptEvent(
int ServerHandle,
int Attempt,
int MaxAttempts);
public sealed record MxNativeCompatibilityRecoveryFailureEvent(
int ServerHandle,
int Attempt,
int MaxAttempts,
Exception Exception,
bool WillRetry);
public sealed record MxNativeCompatibilityRecoveryCompletedEvent(
int ServerHandle,
int Attempt,
int MaxAttempts);
public sealed class MxNativeCompatibilityServer : IDisposable
{
private readonly object _gate = new();
private readonly Dictionary<int, MxNativeSession> _sessions = [];
private readonly Dictionary<int, CompatibilityItem> _items = [];
private readonly Dictionary<int, int> _bufferedUpdateIntervals = [];
private readonly Dictionary<int, int> _nextUserHandles = [];
private readonly Dictionary<int, Queue<int>> _pendingWriteItems = [];
private int _nextServerHandle = 1;
private int _nextItemHandle = 1;
private bool _disposed;
public event EventHandler<MxNativeDataChangeEvent>? DataChanged;
public event EventHandler<MxNativeBufferedDataChangeEvent>? BufferedDataChanged;
public event EventHandler<MxNativeWriteCompleteEvent>? WriteCompleted;
#pragma warning disable CS0067 // OperationComplete has the MXAccess event shape, but no firing path is modeled until captures define its trigger.
public event EventHandler<MxNativeOperationCompleteEvent>? OperationCompleted;
#pragma warning restore CS0067
public event EventHandler<MxNativeCompatibilityRecoveryAttemptEvent>? RecoveryAttemptStarted;
public event EventHandler<MxNativeCompatibilityRecoveryFailureEvent>? RecoveryAttemptFailed;
public event EventHandler<MxNativeCompatibilityRecoveryCompletedEvent>? RecoveryCompleted;
public int Register(string clientName)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var session = MxNativeSession.Open(new MxNativeClientOptions
{
EngineName = string.IsNullOrWhiteSpace(clientName) ? $"MxNativeClient.{Environment.ProcessId}" : clientName,
});
int serverHandle;
lock (_gate)
{
serverHandle = _nextServerHandle++;
_sessions.Add(serverHandle, session);
_nextUserHandles.Add(serverHandle, 1);
_pendingWriteItems.Add(serverHandle, new Queue<int>());
}
session.CallbackReceived += (_, evt) => OnCallbackReceived(serverHandle, evt);
session.OperationStatusReceived += (_, evt) => OnOperationStatusReceived(serverHandle, evt);
session.RecoveryAttemptStarted += (_, evt) => RecoveryAttemptStarted?.Invoke(
this,
new MxNativeCompatibilityRecoveryAttemptEvent(serverHandle, evt.Attempt, evt.MaxAttempts));
session.RecoveryAttemptFailed += (_, evt) => RecoveryAttemptFailed?.Invoke(
this,
new MxNativeCompatibilityRecoveryFailureEvent(
serverHandle,
evt.Attempt,
evt.MaxAttempts,
evt.Exception,
evt.WillRetry));
session.RecoveryCompleted += (_, evt) => RecoveryCompleted?.Invoke(
this,
new MxNativeCompatibilityRecoveryCompletedEvent(serverHandle, evt.Attempt, evt.MaxAttempts));
return serverHandle;
}
public void Unregister(int serverHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
foreach (int itemHandle in _items
.Where(pair => pair.Value.ServerHandle == serverHandle)
.Select(pair => pair.Key)
.ToArray())
{
_items.Remove(itemHandle);
}
_sessions.Remove(serverHandle);
_bufferedUpdateIntervals.Remove(serverHandle);
_nextUserHandles.Remove(serverHandle);
_pendingWriteItems.Remove(serverHandle);
}
session.Dispose();
}
public void RecoverConnection(int serverHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
session.RecoverConnection();
}
public async Task RecoverConnectionAsync(
int serverHandle,
MxNativeRecoveryPolicy? policy = null,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
await session.RecoverConnectionAsync(policy, cancellationToken).ConfigureAwait(false);
}
public int AddItem(int serverHandle, string itemDefinition)
{
return AddItemAsync(serverHandle, itemDefinition).GetAwaiter().GetResult();
}
public async Task<int> AddItemAsync(
int serverHandle,
string itemDefinition,
CancellationToken cancellationToken = default)
{
return await AddItemCoreAsync(serverHandle, itemDefinition, itemContext: null, cancellationToken).ConfigureAwait(false);
}
private async Task<int> AddItemCoreAsync(
int serverHandle,
string itemDefinition,
string? itemContext,
CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
GalaxyTagMetadata? metadata;
string resolvedReference;
try
{
(metadata, resolvedReference) = await ResolveWithOptionalContextAsync(
session,
itemDefinition,
itemContext,
cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException ex) when (IsMissingReferenceResolution(ex))
{
metadata = null;
resolvedReference = string.IsNullOrWhiteSpace(itemContext)
? itemDefinition
: CombineItemContext(itemDefinition, itemContext);
}
lock (_gate)
{
int itemHandle = _nextItemHandle++;
_items.Add(
itemHandle,
metadata is null
? new CompatibilityItem(
serverHandle,
resolvedReference,
metadata: null,
isInvalidReference: true)
: new CompatibilityItem(serverHandle, resolvedReference, metadata));
return itemHandle;
}
}
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
{
return AddItemCoreAsync(serverHandle, itemDefinition, itemContext, CancellationToken.None).GetAwaiter().GetResult();
}
public void RemoveItem(int serverHandle, int itemHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
_items.Remove(itemHandle);
}
if (item.Subscription is not null)
{
session.Unsubscribe(item.Subscription.CorrelationId);
}
}
public void Advise(int serverHandle, int itemHandle)
{
AdviseAsync(serverHandle, itemHandle).GetAwaiter().GetResult();
}
public Task AdviseSupervisoryAsync(int serverHandle, int itemHandle, CancellationToken cancellationToken = default)
{
return AdviseAsync(serverHandle, itemHandle, cancellationToken);
}
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
Advise(serverHandle, itemHandle);
}
public async Task AdviseAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
bool fireInvalidReference = false;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.Subscription is not null)
{
return;
}
if (item.IsInvalidReference)
{
item.IsAdvisedInvalidReference = true;
fireInvalidReference = true;
}
}
if (fireInvalidReference)
{
ThreadPool.QueueUserWorkItem(_ => FireInvalidReferenceDataChange(serverHandle, itemHandle));
return;
}
if (item.Metadata is null)
{
throw new InvalidOperationException("Valid compatibility items must carry Galaxy metadata.");
}
MxNativeSubscription subscription = item.IsBuffered
? await session.RegisterBufferedItemAsync(
item.ItemDefinition,
item.ItemContext,
itemHandle,
cancellationToken).ConfigureAwait(false)
: await session.SubscribeAsync(item.TagReference, cancellationToken).ConfigureAwait(false);
lock (_gate)
{
if (_items.TryGetValue(itemHandle, out CompatibilityItem? current))
{
current.Subscription = subscription;
}
}
}
public void UnAdvise(int serverHandle, int itemHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.IsInvalidReference)
{
item.IsAdvisedInvalidReference = false;
return;
}
if (item.Subscription is null)
{
return;
}
}
session.Unsubscribe(item.Subscription.CorrelationId);
lock (_gate)
{
if (_items.TryGetValue(itemHandle, out CompatibilityItem? current))
{
current.Subscription = null;
}
}
}
public void Write(int serverHandle, int itemHandle, object value, int userId = 0)
{
WriteAsync(serverHandle, itemHandle, value).GetAwaiter().GetResult();
}
public async Task WriteAsync(
int serverHandle,
int itemHandle,
object value,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.IsInvalidReference)
{
return;
}
if (item.IsBuffered)
{
throw new ArgumentException("Normal Write is not valid for buffered item handles.", nameof(itemHandle));
}
if (item.Subscription is null)
{
throw new ArgumentException("Write requires an advised item handle.", nameof(itemHandle));
}
if (item.Metadata?.IsBufferProperty == true)
{
return;
}
GetPendingWriteItemsLocked(serverHandle).Enqueue(itemHandle);
}
try
{
await session.WriteAsync(item.TagReference, value, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch
{
lock (_gate)
{
RemovePendingWriteItemLocked(serverHandle, itemHandle);
}
throw;
}
}
public void Write2(int serverHandle, int itemHandle, object value, DateTime timestamp, int userId = 0)
{
Write2Async(serverHandle, itemHandle, value, timestamp).GetAwaiter().GetResult();
}
public void Write2(int serverHandle, int itemHandle, object value, object timestamp, int userId = 0)
{
Write2(serverHandle, itemHandle, value, CoerceWriteTimestamp(timestamp), userId);
}
public async Task Write2Async(
int serverHandle,
int itemHandle,
object value,
DateTime timestamp,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.IsInvalidReference)
{
return;
}
if (item.IsBuffered)
{
throw new ArgumentException("Normal Write2 is not valid for buffered item handles.", nameof(itemHandle));
}
if (item.Subscription is null)
{
throw new ArgumentException("Write2 requires an advised item handle.", nameof(itemHandle));
}
if (item.Metadata?.IsBufferProperty == true)
{
return;
}
GetPendingWriteItemsLocked(serverHandle).Enqueue(itemHandle);
}
try
{
await session.Write2Async(item.TagReference, value, timestamp, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch
{
lock (_gate)
{
RemovePendingWriteItemLocked(serverHandle, itemHandle);
}
throw;
}
}
public Task Write2Async(
int serverHandle,
int itemHandle,
object value,
object timestamp,
CancellationToken cancellationToken = default)
{
return Write2Async(serverHandle, itemHandle, value, CoerceWriteTimestamp(timestamp), cancellationToken);
}
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value)
{
throw new NotSupportedException("Dedicated WriteSecured parity is not implemented because current MXAccess captures do not emit a successful NMX secured-write body.");
}
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, DateTime timestamp)
{
WriteSecured2Async(serverHandle, itemHandle, value, timestamp, currentUserId, verifierUserId).GetAwaiter().GetResult();
}
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, object timestamp)
{
WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, CoerceWriteTimestamp(timestamp));
}
public async Task WriteSecured2Async(int serverHandle, int itemHandle, object value, DateTime timestamp, int currentUserId, int verifierUserId = 0)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
}
await session.WriteSecured2Async(
item.TagReference,
value,
timestamp,
currentUserId,
verifierUserId).ConfigureAwait(false);
}
public Task WriteSecured2Async(
int serverHandle,
int itemHandle,
object value,
object timestamp,
int currentUserId,
int verifierUserId = 0)
{
return WriteSecured2Async(
serverHandle,
itemHandle,
value,
CoerceWriteTimestamp(timestamp),
currentUserId,
verifierUserId);
}
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(verifyUser);
_ = verifyUserPassword;
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
return AllocateUserHandleLocked(serverHandle);
}
}
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
}
if (!Guid.TryParse(userIdGuid, out Guid parsedGuid))
{
throw new ArgumentException("ArchestrA user ID must be a GUID string.", nameof(userIdGuid));
}
lock (_gate)
{
return AllocateUserHandleLocked(serverHandle);
}
}
public void Suspend(int serverHandle, int itemHandle, out MxStatus status)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
CompatibilityItem item = GetItemLocked(serverHandle, itemHandle);
if (item.Subscription is null)
{
status = new MxStatus(0, MxStatusCategory.Unknown, MxStatusSource.Unknown, 0);
throw new ArgumentException("Suspend requires an advised item handle.", nameof(itemHandle));
}
}
status = MxStatus.SuspendPending;
}
public void Activate(int serverHandle, int itemHandle, out MxStatus status)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
CompatibilityItem item = GetItemLocked(serverHandle, itemHandle);
if (item.Subscription is null)
{
status = new MxStatus(0, MxStatusCategory.Unknown, MxStatusSource.Unknown, 0);
throw new ArgumentException("Activate requires an advised item handle.", nameof(itemHandle));
}
}
status = MxStatus.ActivateOk;
}
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
return AddBufferedItemAsync(serverHandle, itemDefinition, itemContext).GetAwaiter().GetResult();
}
public async Task<int> AddBufferedItemAsync(
int serverHandle,
string itemDefinition,
string itemContext,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
(GalaxyTagMetadata metadata, string routeReference) = await ResolveWithOptionalContextAsync(
session,
itemDefinition,
itemContext,
cancellationToken).ConfigureAwait(false);
lock (_gate)
{
int itemHandle = _nextItemHandle++;
_items.Add(
itemHandle,
new CompatibilityItem(
serverHandle,
routeReference,
metadata,
isBuffered: true,
itemDefinition: itemDefinition,
itemContext: itemContext));
return itemHandle;
}
}
public void SetBufferedUpdateInterval(int serverHandle, int updateInterval)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (updateInterval <= 0)
{
throw new ArgumentOutOfRangeException(nameof(updateInterval), updateInterval, "Buffered update interval must be positive.");
}
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
_bufferedUpdateIntervals[serverHandle] = ((updateInterval + 99) / 100) * 100;
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
MxNativeSession[] sessions;
lock (_gate)
{
sessions = _sessions.Values.ToArray();
_sessions.Clear();
_items.Clear();
_bufferedUpdateIntervals.Clear();
_nextUserHandles.Clear();
_pendingWriteItems.Clear();
}
foreach (MxNativeSession session in sessions)
{
session.Dispose();
}
_disposed = true;
}
private void OnCallbackReceived(int serverHandle, MxNativeCallbackEvent evt)
{
int itemHandle;
CompatibilityItem? item;
lock (_gate)
{
var matched = _items.FirstOrDefault(pair =>
pair.Value.ServerHandle == serverHandle &&
pair.Value.Subscription?.CorrelationId == evt.Message.ItemCorrelationId);
itemHandle = matched.Key;
item = matched.Value;
}
if (itemHandle == 0 || item is null)
{
return;
}
if (evt.Record.Value is null)
{
return;
}
if (item.IsBuffered)
{
BufferedDataChanged?.Invoke(
this,
new MxNativeBufferedDataChangeEvent(
serverHandle,
itemHandle,
item.Metadata?.MxDataType ?? 0,
[evt.Record.Value],
[evt.Record.Quality],
[evt.Record.TimestampUtc],
[evt.Status],
evt.IsDuringRecovery));
return;
}
if (item.Metadata is not null && ShouldSuppressCompatibilityDataChange(item.Metadata, evt.Record.Value))
{
return;
}
DataChanged?.Invoke(
this,
new MxNativeDataChangeEvent(
serverHandle,
itemHandle,
evt.Record.Value,
evt.Record.Quality,
evt.Record.TimestampUtc,
[evt.Status],
evt.IsDuringRecovery));
}
internal static bool ShouldSuppressCompatibilityDataChange(GalaxyTagMetadata metadata, object value)
{
return metadata.MxDataType == (short)MxDataType.InternationalizedString
&& value is string text
&& text.Length == 0;
}
private void FireInvalidReferenceDataChange(int serverHandle, int itemHandle)
{
DataChanged?.Invoke(
this,
new MxNativeDataChangeEvent(
serverHandle,
itemHandle,
Value: null,
Quality: 0,
TimestampUtc: DateTime.UtcNow,
[MxStatus.InvalidReferenceConfiguration]));
}
private void OnOperationStatusReceived(int serverHandle, MxNativeOperationStatusEvent evt)
{
int itemHandle;
lock (_gate)
{
if (!_pendingWriteItems.TryGetValue(serverHandle, out Queue<int>? pendingItems)
|| !pendingItems.TryDequeue(out itemHandle))
{
return;
}
}
if (!evt.Message.IsMxAccessWriteComplete)
{
return;
}
WriteCompleted?.Invoke(
this,
new MxNativeWriteCompleteEvent(serverHandle, itemHandle, [evt.Message.Status], evt.IsDuringRecovery));
}
private MxNativeSession GetSessionLocked(int serverHandle)
{
if (!_sessions.TryGetValue(serverHandle, out MxNativeSession? session))
{
throw new ArgumentException("Unknown MX native server handle.", nameof(serverHandle));
}
return session;
}
private CompatibilityItem GetItemLocked(int serverHandle, int itemHandle)
{
if (!_items.TryGetValue(itemHandle, out CompatibilityItem? item) || item.ServerHandle != serverHandle)
{
throw new ArgumentException("Unknown MX native item handle.", nameof(itemHandle));
}
return item;
}
private int AllocateUserHandleLocked(int serverHandle)
{
int next = _nextUserHandles.TryGetValue(serverHandle, out int value) ? value : 1;
_nextUserHandles[serverHandle] = next + 1;
return next;
}
private Queue<int> GetPendingWriteItemsLocked(int serverHandle)
{
if (!_pendingWriteItems.TryGetValue(serverHandle, out Queue<int>? pendingItems))
{
pendingItems = new Queue<int>();
_pendingWriteItems.Add(serverHandle, pendingItems);
}
return pendingItems;
}
private static bool IsMissingReferenceResolution(InvalidOperationException ex)
{
return ex.Message.Contains("was not found in the deployed repository metadata", StringComparison.Ordinal);
}
private static string CombineItemContext(string itemDefinition, string itemContext)
{
if (string.IsNullOrWhiteSpace(itemContext))
{
return itemDefinition;
}
string trimmedContext = itemContext.TrimEnd('.');
string trimmedDefinition = itemDefinition.TrimStart('.');
return trimmedDefinition.StartsWith(trimmedContext + ".", StringComparison.OrdinalIgnoreCase)
? trimmedDefinition
: $"{trimmedContext}.{trimmedDefinition}";
}
private static async Task<(GalaxyTagMetadata Metadata, string ResolvedReference)> ResolveWithOptionalContextAsync(
MxNativeSession session,
string itemDefinition,
string? itemContext,
CancellationToken cancellationToken)
{
try
{
GalaxyTagMetadata metadata = await session.ResolveTagAsync(itemDefinition, cancellationToken).ConfigureAwait(false);
return (metadata, itemDefinition);
}
catch (Exception ex) when (!string.IsNullOrWhiteSpace(itemContext) && CanRetryWithItemContext(ex))
{
string combinedReference = CombineItemContext(itemDefinition, itemContext);
GalaxyTagMetadata metadata = await session.ResolveTagAsync(combinedReference, cancellationToken).ConfigureAwait(false);
return (metadata, combinedReference);
}
}
private static bool CanRetryWithItemContext(Exception ex)
{
return ex is ArgumentException
|| (ex is InvalidOperationException invalidOperation && IsMissingReferenceResolution(invalidOperation));
}
private static DateTime CoerceWriteTimestamp(object timestamp)
{
return timestamp switch
{
DateTime dateTime => dateTime,
DateTimeOffset dateTimeOffset => dateTimeOffset.UtcDateTime,
double oaDate => DateTime.FromOADate(oaDate),
float oaDate => DateTime.FromOADate(oaDate),
decimal oaDate => DateTime.FromOADate((double)oaDate),
string text when DateTime.TryParse(
text,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal,
out DateTime parsed) => parsed,
_ => throw new ArgumentException(
"Timestamp must be a DateTime, DateTimeOffset, OLE Automation date, or invariant parseable date/time string.",
nameof(timestamp)),
};
}
private void RemovePendingWriteItemLocked(int serverHandle, int itemHandle)
{
if (!_pendingWriteItems.TryGetValue(serverHandle, out Queue<int>? pendingItems)
|| pendingItems.Count == 0)
{
return;
}
int pendingCount = pendingItems.Count;
bool removed = false;
for (int i = 0; i < pendingCount; i++)
{
int pendingItem = pendingItems.Dequeue();
if (!removed && pendingItem == itemHandle)
{
removed = true;
continue;
}
pendingItems.Enqueue(pendingItem);
}
}
private sealed class CompatibilityItem(
int serverHandle,
string tagReference,
GalaxyTagMetadata? metadata,
bool isBuffered = false,
bool isInvalidReference = false,
string itemDefinition = "",
string itemContext = "")
{
public int ServerHandle { get; } = serverHandle;
public string TagReference { get; } = tagReference;
public GalaxyTagMetadata? Metadata { get; } = metadata;
public bool IsBuffered { get; } = isBuffered;
public bool IsInvalidReference { get; } = isInvalidReference;
public string ItemDefinition { get; } = itemDefinition;
public string ItemContext { get; } = itemContext;
public MxNativeSubscription? Subscription { get; set; }
public bool IsAdvisedInvalidReference { get; set; }
}
}
+669
View File
@@ -0,0 +1,669 @@
using System.Runtime.InteropServices;
using System.Threading;
using MxNativeCodec;
namespace MxNativeClient;
public sealed record MxNativeClientOptions
{
public int LocalEngineId { get; init; } = GenerateDefaultLocalEngineId();
public string EngineName { get; init; } = $"MxNativeClient.{Environment.ProcessId}";
public int PartnerVersion { get; init; } = 6;
public int GalaxyId { get; init; } = 1;
public int SourcePlatformId { get; init; } = 1;
public DceRpcClientAuthentication Authentication { get; init; } = DceRpcClientAuthentication.ManagedNtlm;
public int? HeartbeatTicksPerBeat { get; init; }
public int HeartbeatMaxMissedTicks { get; init; } = 3;
private static int GenerateDefaultLocalEngineId()
{
return 0x7000 + (Environment.ProcessId & 0x0fff);
}
}
public sealed record MxNativeRecoveryPolicy
{
public static MxNativeRecoveryPolicy SingleAttempt { get; } = new();
public int MaxAttempts { get; init; } = 1;
public TimeSpan Delay { get; init; } = TimeSpan.Zero;
public void Validate()
{
if (MaxAttempts < 1)
{
throw new ArgumentOutOfRangeException(nameof(MaxAttempts), "Recovery attempts must be at least one.");
}
if (Delay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(Delay), "Recovery delay cannot be negative.");
}
}
}
public sealed record MxNativeRecoveryAttemptEvent(int Attempt, int MaxAttempts);
public sealed record MxNativeRecoveryFailureEvent(
int Attempt,
int MaxAttempts,
Exception Exception,
bool WillRetry);
public sealed record MxNativeRecoveryCompletedEvent(int Attempt, int MaxAttempts);
public sealed record MxNativeSubscription(
Guid CorrelationId,
string TagReference,
GalaxyTagMetadata Metadata,
bool IsBuffered = false,
string BufferedItemDefinition = "",
string BufferedItemContext = "",
int BufferedItemHandle = 0);
public sealed record MxNativeCallbackEvent(
NmxSubscriptionMessage Message,
NmxSubscriptionRecord Record,
byte[] RawBody,
bool IsDuringRecovery = false)
{
public MxStatus Status => Record.ToDataChangeStatus();
}
public sealed record MxNativeOperationStatusEvent(
NmxOperationStatusMessage Message,
byte[] RawBody,
bool IsDuringRecovery = false);
public sealed record MxNativeReferenceRegistrationEvent(
NmxReferenceRegistrationResultMessage Message,
byte[] RawBody,
bool IsDuringRecovery = false);
public sealed record MxNativeUnparsedCallbackEvent(
byte[] RawBody,
string ParseError,
bool IsDuringRecovery = false);
public sealed class MxNativeSession : IDisposable
{
private readonly MxNativeClientOptions _options;
private readonly GalaxyRepositoryTagResolver _resolver;
private readonly NmxCallbackSink _callback;
private readonly GCHandle _callbackHandle;
private readonly Dictionary<Guid, MxNativeSubscription> _subscriptions = [];
private readonly HashSet<PublisherEndpoint> _publisherEndpoints = [];
private ManagedNmxService2Client _service;
private int _recoveryActive;
private bool _disposed;
private MxNativeSession(
MxNativeClientOptions options,
ManagedNmxService2Client service,
GalaxyRepositoryTagResolver resolver,
NmxCallbackSink callback,
GCHandle callbackHandle)
{
_options = options;
_service = service;
_resolver = resolver;
_callback = callback;
_callbackHandle = callbackHandle;
_callback.DataReceived += OnCallbackReceived;
_callback.StatusReceived += OnCallbackReceived;
}
public event EventHandler<MxNativeCallbackEvent>? CallbackReceived;
public event EventHandler<MxNativeOperationStatusEvent>? OperationStatusReceived;
public event EventHandler<MxNativeReferenceRegistrationEvent>? ReferenceRegistrationReceived;
public event EventHandler<MxNativeUnparsedCallbackEvent>? UnparsedCallbackReceived;
public event EventHandler<MxNativeRecoveryAttemptEvent>? RecoveryAttemptStarted;
public event EventHandler<MxNativeRecoveryFailureEvent>? RecoveryAttemptFailed;
public event EventHandler<MxNativeRecoveryCompletedEvent>? RecoveryCompleted;
public IReadOnlyCollection<MxNativeSubscription> Subscriptions => _subscriptions.Values;
public static MxNativeSession Open(MxNativeClientOptions? options = null)
{
options ??= new MxNativeClientOptions();
var callback = new NmxCallbackSink();
var callbackHandle = GCHandle.Alloc(callback);
try
{
return new MxNativeSession(
options,
CreateRegisteredService(options, callback),
new GalaxyRepositoryTagResolver(),
callback,
callbackHandle);
}
catch
{
callbackHandle.Free();
throw;
}
}
public Task<GalaxyTagMetadata> ResolveTagAsync(string tagReference, CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _resolver.ResolveAsync(tagReference, cancellationToken);
}
public Task<IReadOnlyList<GalaxyTagMetadata>> BrowseAsync(
string objectTagLike = "%",
string attributeLike = "%",
int maxRows = 100,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _resolver.BrowseAsync(objectTagLike, attributeLike, maxRows, cancellationToken);
}
public async Task WriteAsync(
string tagReference,
object value,
int writeIndex = 1,
uint clientToken = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsureSucceeded(
_service.Write(
_options.LocalEngineId,
tag,
value,
writeIndex,
clientToken,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.Write));
}
public async Task Write2Async(
string tagReference,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsureSucceeded(
_service.Write2(
_options.LocalEngineId,
tag,
value,
timestamp,
writeIndex,
clientToken,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.Write2));
}
public Task WriteSecuredAsync(
string tagReference,
object value,
int currentUserId,
int verifierUserId = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
throw new NotSupportedException(
"Dedicated MXAccess WriteSecured parity is not implemented. Captures 036, 038, and 039 show the installed MXAccess WriteSecured method returning 0x80004021 before emitting an NMX write body; normal Write to secured-classified public tags is supported through WriteAsync.");
}
public async Task WriteSecured2Async(
string tagReference,
object value,
DateTime timestamp,
int currentUserId,
int verifierUserId = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsureSucceeded(
_service.WriteSecured2(
_options.LocalEngineId,
tag,
value,
timestamp,
_options.EngineName,
currentUserId,
verifierUserId,
writeIndex: 1,
clientToken: 0,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.WriteSecured2));
}
public async Task<MxNativeSubscription> SubscribeAsync(
string tagReference,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsurePublisherConnected(tag);
var subscription = new MxNativeSubscription(Guid.NewGuid(), tagReference, tag);
EnsureSucceeded(
_service.AdviseSupervisory(
_options.LocalEngineId,
tag,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.AdviseSupervisory));
_subscriptions.Add(subscription.CorrelationId, subscription);
return subscription;
}
public async Task<MxNativeSubscription> RegisterBufferedItemAsync(
string itemDefinition,
string itemContext,
int itemHandle,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
string routeReference = string.IsNullOrWhiteSpace(itemContext)
? itemDefinition
: $"{itemContext.TrimEnd('.')}.{itemDefinition.TrimStart('.')}";
GalaxyTagMetadata routeTag = await ResolveTagAsync(routeReference, cancellationToken).ConfigureAwait(false);
EnsurePublisherConnected(routeTag);
var subscription = new MxNativeSubscription(
Guid.NewGuid(),
routeReference,
routeTag,
IsBuffered: true,
BufferedItemDefinition: itemDefinition,
BufferedItemContext: itemContext,
BufferedItemHandle: itemHandle);
var message = new NmxReferenceRegistrationMessage(
itemHandle,
subscription.CorrelationId,
NmxReferenceRegistrationMessage.ToBufferedItemDefinition(itemDefinition),
itemContext,
Subscribe: true);
EnsureSucceeded(
_service.RegisterReference(
_options.LocalEngineId,
routeTag,
message,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.RegisterReference));
_subscriptions.Add(subscription.CorrelationId, subscription);
return subscription;
}
public async Task<object?> ReadAsync(
string tagReference,
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(timeout), "Read timeout must be positive.");
}
using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutSource.CancelAfter(timeout);
var completion = new TaskCompletionSource<object?>(
TaskCreationOptions.RunContinuationsAsynchronously);
MxNativeSubscription? subscription = null;
using CancellationTokenRegistration cancellation = timeoutSource.Token.Register(
static state => ((TaskCompletionSource<object?>)state!).TrySetCanceled(),
completion);
EventHandler<MxNativeCallbackEvent> handler = (_, evt) =>
{
if (subscription is null ||
evt.Message.ItemCorrelationId != subscription.CorrelationId ||
evt.Record.Value is null)
{
return;
}
completion.TrySetResult(evt.Record.Value);
};
CallbackReceived += handler;
try
{
subscription = await SubscribeAsync(tagReference, timeoutSource.Token).ConfigureAwait(false);
return await completion.Task.ConfigureAwait(false);
}
finally
{
CallbackReceived -= handler;
if (subscription is not null && _subscriptions.ContainsKey(subscription.CorrelationId))
{
Unsubscribe(subscription.CorrelationId);
}
}
}
public void Unsubscribe(Guid correlationId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_subscriptions.Remove(correlationId, out MxNativeSubscription? subscription))
{
return;
}
if (!subscription.IsBuffered)
{
EnsureSucceeded(
_service.UnAdvise(
_options.LocalEngineId,
subscription.Metadata,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.UnAdvise));
}
}
public void RecoverConnection()
{
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
RecoveryAttemptStarted?.Invoke(this, new MxNativeRecoveryAttemptEvent(1, 1));
RecoverConnectionCore();
RecoveryCompleted?.Invoke(this, new MxNativeRecoveryCompletedEvent(1, 1));
}
catch (Exception ex) when (IsRecoverableRecoveryException(ex))
{
RecoveryAttemptFailed?.Invoke(this, new MxNativeRecoveryFailureEvent(1, 1, ex, WillRetry: false));
throw;
}
}
public async Task RecoverConnectionAsync(
MxNativeRecoveryPolicy? policy = null,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
policy ??= MxNativeRecoveryPolicy.SingleAttempt;
policy.Validate();
Exception? lastError = null;
for (int attempt = 1; attempt <= policy.MaxAttempts; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();
RecoveryAttemptStarted?.Invoke(this, new MxNativeRecoveryAttemptEvent(attempt, policy.MaxAttempts));
try
{
RecoverConnectionCore();
RecoveryCompleted?.Invoke(this, new MxNativeRecoveryCompletedEvent(attempt, policy.MaxAttempts));
return;
}
catch (Exception ex) when (IsRecoverableRecoveryException(ex))
{
lastError = ex;
bool willRetry = attempt < policy.MaxAttempts;
RecoveryAttemptFailed?.Invoke(
this,
new MxNativeRecoveryFailureEvent(attempt, policy.MaxAttempts, ex, willRetry));
if (!willRetry)
{
break;
}
if (policy.Delay > TimeSpan.Zero)
{
await Task.Delay(policy.Delay, cancellationToken).ConfigureAwait(false);
}
}
}
throw new InvalidOperationException(
$"MX native recovery failed after {policy.MaxAttempts} attempt(s).",
lastError);
}
private void RecoverConnectionCore()
{
Interlocked.Increment(ref _recoveryActive);
try
{
ManagedNmxService2Client replacement = CreateRegisteredService(_options, _callback);
try
{
foreach (PublisherEndpoint endpoint in _publisherEndpoints)
{
ConnectPublisher(replacement, endpoint);
}
foreach (MxNativeSubscription subscription in _subscriptions.Values)
{
ReAdviseSubscription(replacement, subscription);
}
}
catch
{
replacement.Dispose();
throw;
}
ManagedNmxService2Client oldService = _service;
_service = replacement;
oldService.Dispose();
}
finally
{
Interlocked.Decrement(ref _recoveryActive);
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (MxNativeSubscription subscription in _subscriptions.Values.ToArray())
{
if (!subscription.IsBuffered)
{
TryCleanup(() => _service.UnAdvise(
_options.LocalEngineId,
subscription.Metadata,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId));
}
}
foreach (PublisherEndpoint publisherEndpoint in _publisherEndpoints)
{
TryCleanup(() => _service.RemoveSubscriberEngine(
publisherEndpoint.EngineId,
_options.GalaxyId,
_options.SourcePlatformId,
_options.LocalEngineId));
}
TryCleanup(() => _service.UnregisterEngine(_options.LocalEngineId));
_service.Dispose();
if (_callbackHandle.IsAllocated)
{
_callbackHandle.Free();
}
_disposed = true;
}
private void EnsurePublisherConnected(GalaxyTagMetadata tag)
{
var endpoint = new PublisherEndpoint(tag.PlatformId, tag.EngineId);
if (_publisherEndpoints.Contains(endpoint))
{
return;
}
ConnectPublisher(_service, endpoint);
_publisherEndpoints.Add(endpoint);
}
private void ConnectPublisher(ManagedNmxService2Client service, PublisherEndpoint endpoint)
{
EnsureSucceeded(
service.Connect(_options.LocalEngineId, _options.GalaxyId, endpoint.PlatformId, endpoint.EngineId),
nameof(ManagedNmxService2Client.Connect));
EnsureSucceeded(
service.AddSubscriberEngine(endpoint.EngineId, _options.GalaxyId, _options.SourcePlatformId, _options.LocalEngineId),
nameof(ManagedNmxService2Client.AddSubscriberEngine));
}
private void ReAdviseSubscription(ManagedNmxService2Client service, MxNativeSubscription subscription)
{
if (subscription.IsBuffered)
{
var message = new NmxReferenceRegistrationMessage(
subscription.BufferedItemHandle,
subscription.CorrelationId,
NmxReferenceRegistrationMessage.ToBufferedItemDefinition(subscription.BufferedItemDefinition),
subscription.BufferedItemContext,
Subscribe: true);
EnsureSucceeded(
service.RegisterReference(
_options.LocalEngineId,
subscription.Metadata,
message,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.RegisterReference));
return;
}
EnsureSucceeded(
service.AdviseSupervisory(
_options.LocalEngineId,
subscription.Metadata,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.AdviseSupervisory));
}
private void OnCallbackReceived(object? sender, NmxServiceMessage message)
{
bool isDuringRecovery = Volatile.Read(ref _recoveryActive) > 0;
if (NmxOperationStatusMessage.TryParseProcessDataReceivedBody(message.Body, out var operationStatus))
{
OperationStatusReceived?.Invoke(
this,
new MxNativeOperationStatusEvent(operationStatus, message.Body, isDuringRecovery));
return;
}
if (NmxReferenceRegistrationResultMessage.TryParseProcessDataReceivedBody(message.Body, out var registrationResult))
{
ReferenceRegistrationReceived?.Invoke(
this,
new MxNativeReferenceRegistrationEvent(registrationResult!, message.Body, isDuringRecovery));
return;
}
NmxSubscriptionMessage parsed;
try
{
parsed = NmxSubscriptionMessage.ParseProcessDataReceivedBody(message.Body);
}
catch (ArgumentException ex)
{
UnparsedCallbackReceived?.Invoke(
this,
new MxNativeUnparsedCallbackEvent(message.Body, ex.Message, isDuringRecovery));
return;
}
foreach (NmxSubscriptionRecord record in parsed.Records)
{
CallbackReceived?.Invoke(this, new MxNativeCallbackEvent(parsed, record, message.Body, isDuringRecovery));
}
}
private static void EnsureSucceeded(int hresult, string operation)
{
if (hresult == 0)
{
return;
}
if (hresult < 0)
{
Marshal.ThrowExceptionForHR(hresult);
}
throw new InvalidOperationException($"{operation} returned application status 0x{hresult:X8}.");
}
private static ManagedNmxService2Client CreateRegisteredService(MxNativeClientOptions options, NmxCallbackSink callback)
{
byte[] callbackObjRef = ComObjRefProvider.MarshalInterfaceObjRef(
callback,
NmxProcedureMetadata.INmxSvcCallback,
ComObjRefProvider.MarshalContextDifferentMachine);
ManagedNmxService2Client service = ManagedNmxService2Client.Create(options.Authentication);
try
{
EnsureSucceeded(
service.RegisterEngine2(options.LocalEngineId, options.EngineName, options.PartnerVersion, callbackObjRef),
nameof(ManagedNmxService2Client.RegisterEngine2));
if (options.HeartbeatTicksPerBeat is { } heartbeatTicks)
{
EnsureSucceeded(
service.SetHeartbeatSendInterval(heartbeatTicks, options.HeartbeatMaxMissedTicks),
nameof(ManagedNmxService2Client.SetHeartbeatSendInterval));
}
return service;
}
catch
{
service.Dispose();
throw;
}
}
private static void TryCleanup(Func<int> operation)
{
try
{
_ = operation();
}
catch (Exception ex) when (ex is InvalidOperationException or COMException or ObjectDisposedException)
{
}
}
private static bool IsRecoverableRecoveryException(Exception ex)
{
return ex is InvalidOperationException or COMException or IOException or ObjectDisposedException;
}
private readonly record struct PublisherEndpoint(int PlatformId, int EngineId);
}
+39
View File
@@ -0,0 +1,39 @@
using System.Runtime.InteropServices;
namespace MxNativeClient;
public sealed record NmxServiceMessage(byte[] Body);
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public sealed unsafe class NmxCallbackSink : INmxSvcCallback
{
public event EventHandler<NmxServiceMessage>? DataReceived;
public event EventHandler<NmxServiceMessage>? StatusReceived;
public void DataReceivedRaw(int bufferSize, ref sbyte dataBuffer)
{
DataReceived?.Invoke(this, new NmxServiceMessage(CopyBuffer(bufferSize, ref dataBuffer)));
}
public void StatusReceivedRaw(int bufferSize, ref sbyte statusBuffer)
{
StatusReceived?.Invoke(this, new NmxServiceMessage(CopyBuffer(bufferSize, ref statusBuffer)));
}
private static byte[] CopyBuffer(int bufferSize, ref sbyte firstByte)
{
if (bufferSize <= 0)
{
return [];
}
var output = new byte[bufferSize];
fixed (sbyte* source = &firstByte)
{
new ReadOnlySpan<byte>(source, bufferSize).CopyTo(output);
}
return output;
}
}
+92
View File
@@ -0,0 +1,92 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MxNativeClient;
[ComImport]
[Guid("AE24BD51-2E80-44CC-905B-E5446C942BEB")]
[ClassInterface(ClassInterfaceType.None)]
internal sealed class NmxServiceClass : INmxService2
{
public extern void RegisterEngine(int engineId, string engineName, INmxSvcCallback callback);
public extern void UnRegisterEngine(int engineId);
public extern void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
public extern void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
public extern void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
public extern void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
public extern void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
public extern void RegisterEngine2(int engineId, string engineName, int version, INmxSvcCallback callback);
public extern void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("575008DB-845D-46C6-A906-F6F8CA86F315")]
internal interface INmxService
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RegisterEngine(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void UnRegisterEngine(int engineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("2630A513-A974-4B1A-8025-457A9A7C56B8")]
internal interface INmxService2 : INmxService
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void RegisterEngine(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void UnRegisterEngine(int engineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RegisterEngine2(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, int version, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
}
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("B49F92F7-C748-4169-8ECA-A0670B012746")]
public interface INmxSvcCallback
{
[PreserveSig]
void DataReceivedRaw(int bufferSize, ref sbyte dataBuffer);
[PreserveSig]
void StatusReceivedRaw(int bufferSize, ref sbyte statusBuffer);
}
+115
View File
@@ -0,0 +1,115 @@
namespace MxNativeClient;
public static class NmxProcedureMetadata
{
public static readonly Guid INmxService2 = new("2630A513-A974-4B1A-8025-457A9A7C56B8");
public static readonly Guid INmxSvcCallback = new("B49F92F7-C748-4169-8ECA-A0670B012746");
public static readonly NdrProcedureDescriptor RegisterEngine = new(
InterfaceId: INmxService2,
Name: nameof(RegisterEngine),
Opnum: 3,
X86StackSize: 20,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 4);
public static readonly NdrProcedureDescriptor UnRegisterEngine = new(
InterfaceId: INmxService2,
Name: nameof(UnRegisterEngine),
Opnum: 4,
X86StackSize: 12,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 2);
public static readonly NdrProcedureDescriptor Connect = new(
InterfaceId: INmxService2,
Name: nameof(Connect),
Opnum: 5,
X86StackSize: 24,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor TransferData = new(
InterfaceId: INmxService2,
Name: nameof(TransferData),
Opnum: 6,
X86StackSize: 28,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 6);
public static readonly NdrProcedureDescriptor AddSubscriberEngine = new(
InterfaceId: INmxService2,
Name: nameof(AddSubscriberEngine),
Opnum: 7,
X86StackSize: 24,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor RemoveSubscriberEngine = new(
InterfaceId: INmxService2,
Name: nameof(RemoveSubscriberEngine),
Opnum: 8,
X86StackSize: 24,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor SetHeartbeatSendInterval = new(
InterfaceId: INmxService2,
Name: nameof(SetHeartbeatSendInterval),
Opnum: 9,
X86StackSize: 16,
ClientBufferSize: 16,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 3);
public static readonly NdrProcedureDescriptor RegisterEngine2 = new(
InterfaceId: INmxService2,
Name: nameof(RegisterEngine2),
Opnum: 10,
X86StackSize: 24,
ClientBufferSize: 16,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor GetPartnerVersion = new(
InterfaceId: INmxService2,
Name: nameof(GetPartnerVersion),
Opnum: 11,
X86StackSize: 24,
ClientBufferSize: 24,
ServerBufferSize: 36,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor DataReceived = new(
InterfaceId: INmxSvcCallback,
Name: nameof(DataReceived),
Opnum: 3,
X86StackSize: 16,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 3);
public static readonly NdrProcedureDescriptor StatusReceived = new(
InterfaceId: INmxSvcCallback,
Name: nameof(StatusReceived),
Opnum: 4,
X86StackSize: 16,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 3);
}
public sealed record NdrProcedureDescriptor(
Guid InterfaceId,
string Name,
int Opnum,
int X86StackSize,
int ClientBufferSize,
int ServerBufferSize,
int ParameterCountIncludingReturn);
+216
View File
@@ -0,0 +1,216 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeClient;
public sealed record NmxGetPartnerVersionResult(OrpcThat OrpcThat, int PartnerVersion, int HResult);
public sealed record NmxHResultResponse(OrpcThat OrpcThat, int HResult);
public static class NmxService2Messages
{
public static Guid ClassIdNmxService { get; } = new("AE24BD51-2E80-44CC-905B-E5446C942BEB");
public static Guid InterfaceId { get; } = NmxProcedureMetadata.INmxService2;
public const ushort RegisterEngineOpnum = 3;
public const ushort UnregisterEngineOpnum = 4;
public const ushort ConnectOpnum = 5;
public const ushort TransferDataOpnum = 6;
public const ushort AddSubscriberEngineOpnum = 7;
public const ushort RemoveSubscriberEngineOpnum = 8;
public const ushort SetHeartbeatSendIntervalOpnum = 9;
public const ushort RegisterEngine2Opnum = 10;
public const ushort GetPartnerVersionOpnum = 11;
public static byte[] EncodeGetPartnerVersionRequest(
OrpcThis orpcThis,
int galaxyId,
int platformId,
int engineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 12];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), galaxyId);
WriteInt32(buffer.AsSpan(36, 4), platformId);
WriteInt32(buffer.AsSpan(40, 4), engineId);
return buffer;
}
public static NmxGetPartnerVersionResult ParseGetPartnerVersionResponse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThat.EncodedLengthWithoutExtensions + 8)
{
throw new ArgumentException("GetPartnerVersion response is too short.", nameof(buffer));
}
return new NmxGetPartnerVersionResult(
OrpcThat.Parse(buffer),
ReadInt32(buffer[8..12]),
ReadInt32(buffer[12..16]));
}
public static byte[] EncodeConnectRequest(
OrpcThis orpcThis,
int localEngineId,
int remoteGalaxyId,
int remotePlatformId,
int remoteEngineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 16];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), localEngineId);
WriteInt32(buffer.AsSpan(36, 4), remoteGalaxyId);
WriteInt32(buffer.AsSpan(40, 4), remotePlatformId);
WriteInt32(buffer.AsSpan(44, 4), remoteEngineId);
return buffer;
}
public static byte[] EncodeSubscriberEngineRequest(
OrpcThis orpcThis,
int localEngineId,
int subscriberGalaxyId,
int subscriberPlatformId,
int subscriberEngineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 16];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), localEngineId);
WriteInt32(buffer.AsSpan(36, 4), subscriberGalaxyId);
WriteInt32(buffer.AsSpan(40, 4), subscriberPlatformId);
WriteInt32(buffer.AsSpan(44, 4), subscriberEngineId);
return buffer;
}
public static byte[] EncodeUnregisterEngineRequest(
OrpcThis orpcThis,
int localEngineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 4];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), localEngineId);
return buffer;
}
public static byte[] EncodeSetHeartbeatSendIntervalRequest(
OrpcThis orpcThis,
int ticksPerBeat,
int maxMissedTicks)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 8];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), ticksPerBeat);
WriteInt32(buffer.AsSpan(36, 4), maxMissedTicks);
return buffer;
}
public static byte[] EncodeTransferDataRequest(
OrpcThis orpcThis,
int remoteGalaxyId,
int remotePlatformId,
int remoteEngineId,
ReadOnlySpan<byte> messageBody)
{
int bodyOffset = OrpcThis.EncodedLengthWithoutExtensions + 20;
int paddedLength = Align(bodyOffset + messageBody.Length, 4);
byte[] buffer = new byte[paddedLength];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), remoteGalaxyId);
WriteInt32(buffer.AsSpan(36, 4), remotePlatformId);
WriteInt32(buffer.AsSpan(40, 4), remoteEngineId);
WriteInt32(buffer.AsSpan(44, 4), messageBody.Length);
WriteInt32(buffer.AsSpan(48, 4), messageBody.Length);
messageBody.CopyTo(buffer.AsSpan(bodyOffset));
return buffer;
}
public static byte[] EncodeRegisterEngine2Request(
OrpcThis orpcThis,
int localEngineId,
string engineName,
int version,
byte[]? callbackObjRef = null)
{
byte[] bstr = EncodeBstrUserMarshal(engineName);
byte[] callback = callbackObjRef is null
? EncodeNullInterfacePointer()
: EncodeInterfacePointer(callbackObjRef);
int bstrOffset = OrpcThis.EncodedLengthWithoutExtensions + 8;
int versionOffset = Align(bstrOffset + bstr.Length, 4);
int length = Align(versionOffset + 4 + callback.Length, 4);
byte[] buffer = new byte[length];
orpcThis.Encode().CopyTo(buffer.AsSpan());
int offset = OrpcThis.EncodedLengthWithoutExtensions;
WriteInt32(buffer.AsSpan(offset, 4), localEngineId);
offset += 4;
WriteInt32(buffer.AsSpan(offset, 4), 0x72657355);
offset += 4;
bstr.CopyTo(buffer.AsSpan(offset));
offset = versionOffset;
WriteInt32(buffer.AsSpan(offset, 4), version);
offset += 4;
callback.CopyTo(buffer.AsSpan(offset));
return buffer;
}
public static byte[] EncodeBstrUserMarshal(string value)
{
byte[] utf16 = Encoding.Unicode.GetBytes(value);
if ((utf16.Length % 2) != 0)
{
throw new ArgumentException("BSTR payload must be UTF-16.", nameof(value));
}
int charCount = utf16.Length / 2;
byte[] buffer = new byte[12 + utf16.Length];
WriteInt32(buffer.AsSpan(0, 4), charCount);
WriteInt32(buffer.AsSpan(4, 4), utf16.Length);
WriteInt32(buffer.AsSpan(8, 4), charCount);
utf16.CopyTo(buffer.AsSpan(12));
return buffer;
}
public static NmxHResultResponse ParseHResultResponse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThat.EncodedLengthWithoutExtensions + 4)
{
throw new ArgumentException("HRESULT response is too short.", nameof(buffer));
}
return new NmxHResultResponse(
OrpcThat.Parse(buffer),
ReadInt32(buffer[8..12]));
}
private static int ReadInt32(ReadOnlySpan<byte> buffer)
{
return BinaryPrimitives.ReadInt32LittleEndian(buffer);
}
private static void WriteInt32(Span<byte> buffer, int value)
{
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
private static byte[] EncodeNullInterfacePointer()
{
return [0, 0, 0, 0];
}
private static byte[] EncodeInterfacePointer(byte[] objRef)
{
int length = Align(12 + objRef.Length, 4);
byte[] buffer = new byte[length];
WriteInt32(buffer.AsSpan(0, 4), 0x00020000);
WriteInt32(buffer.AsSpan(4, 4), objRef.Length);
WriteInt32(buffer.AsSpan(8, 4), objRef.Length);
objRef.CopyTo(buffer.AsSpan(12));
return buffer;
}
}
+112
View File
@@ -0,0 +1,112 @@
using System.Runtime.InteropServices;
using MxNativeCodec;
namespace MxNativeClient;
public sealed class NmxServiceClient : IDisposable
{
public const int ObservedNmxVersion = 30000;
private readonly INmxService2 _service;
private readonly object _comObject;
private int? _registeredEngineId;
private bool _disposed;
private NmxServiceClient(INmxService2 service)
{
_service = service;
_comObject = service;
}
public static NmxServiceClient Create()
{
var service = (INmxService2)new NmxServiceClass();
return new NmxServiceClient(service);
}
public void RegisterEngine(int engineId, string engineName, NmxCallbackSink callback, int version = ObservedNmxVersion)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.RegisterEngine2(engineId, engineName, version, callback);
_registeredEngineId = engineId;
}
public void UnregisterEngine()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_registeredEngineId is not { } engineId)
{
return;
}
_service.UnRegisterEngine(engineId);
_registeredEngineId = null;
}
public int GetPartnerVersion(int galaxyId, int platformId, int engineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.GetPartnerVersion(galaxyId, platformId, engineId, out var version);
return version;
}
public void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.Connect(localEngineId, remoteGalaxyId, remotePlatformId, remoteEngineId);
}
public void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.SetHeartbeatSendInterval(ticksPerBeat, maxMissedTicks);
}
public void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, ReadOnlySpan<byte> messageBody)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ValidateTransferDataBody(messageBody, nameof(messageBody));
var copy = messageBody.ToArray();
_service.TransferData(remoteGalaxyId, remotePlatformId, remoteEngineId, copy.Length, ref copy[0]);
}
public void Dispose()
{
if (_disposed)
{
return;
}
try
{
if (_registeredEngineId is not null)
{
UnregisterEngine();
}
}
finally
{
if (Marshal.IsComObject(_comObject))
{
Marshal.ReleaseComObject(_comObject);
}
_disposed = true;
}
}
private static void ValidateTransferDataBody(ReadOnlySpan<byte> messageBody, string parameterName)
{
if (messageBody.IsEmpty)
{
throw new ArgumentException("TransferData body cannot be empty.", parameterName);
}
NmxTransferEnvelopeTemplate.FromObserved(messageBody);
if (messageBody.Length == NmxTransferEnvelopeTemplate.HeaderLength)
{
throw new ArgumentException("TransferData body must include an inner message after the 46-byte envelope.", parameterName);
}
}
}
@@ -0,0 +1,45 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public sealed record NmxCallbackRequest(OrpcThis OrpcThis, byte[] Body);
public static class NmxSvcCallbackMessages
{
public static Guid InterfaceId { get; } = NmxProcedureMetadata.INmxSvcCallback;
public const ushort DataReceivedOpnum = 3;
public const ushort StatusReceivedOpnum = 4;
public static NmxCallbackRequest ParseCallbackRequest(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThis.EncodedLengthWithoutExtensions + 8)
{
throw new ArgumentException("Callback request is too short.", nameof(buffer));
}
var orpcThis = OrpcThis.Parse(buffer);
int size = BinaryPrimitives.ReadInt32LittleEndian(buffer.Slice(OrpcThis.EncodedLengthWithoutExtensions, 4));
int maxCount = BinaryPrimitives.ReadInt32LittleEndian(buffer.Slice(OrpcThis.EncodedLengthWithoutExtensions + 4, 4));
if (size < 0 || maxCount < size)
{
throw new ArgumentException("Callback request has invalid array size metadata.", nameof(buffer));
}
int bodyOffset = OrpcThis.EncodedLengthWithoutExtensions + 8;
if (bodyOffset + size > buffer.Length)
{
throw new ArgumentException("Callback request byte array is truncated.", nameof(buffer));
}
return new NmxCallbackRequest(orpcThis, buffer.Slice(bodyOffset, size).ToArray());
}
public static byte[] EncodeCallbackResponse(int hresult)
{
byte[] buffer = new byte[OrpcThat.EncodedLengthWithoutExtensions + sizeof(int)];
new OrpcThat(0, 0).Encode().CopyTo(buffer.AsSpan());
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(OrpcThat.EncodedLengthWithoutExtensions, sizeof(int)), hresult);
return buffer;
}
}
@@ -0,0 +1,82 @@
namespace MxNativeClient;
public sealed class ObjectExporterClient
{
private readonly string _host;
private readonly int _port;
public ObjectExporterClient(string host = "127.0.0.1", int port = 135)
{
_host = host;
_port = port;
}
public ResolveOxidFailure ResolveOxidUnauthenticated(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.Bind(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
var response = client.Call(contextId: 0, ObjectExporterMessages.ResolveOxidOpnum, request);
return ObjectExporterMessages.ParseResolveOxidFailure(response.StubData.Span);
}
public DceRpcResponsePdu ResolveOxidWithNtlmConnect(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.BindWithNtlmConnect(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
return client.CallBound(ObjectExporterMessages.ResolveOxidOpnum, request);
}
public DceRpcResponsePdu ResolveOxidWithNtlmPacketIntegrity(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.BindWithNtlmPacketIntegrity(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0, targetName: _host);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
return client.CallBound(ObjectExporterMessages.ResolveOxidOpnum, request);
}
public DceRpcResponsePdu ResolveOxidWithManagedNtlmPacketIntegrity(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.BindWithManagedNtlmPacketIntegrity(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
return client.CallBound(ObjectExporterMessages.ResolveOxidOpnum, request);
}
}
@@ -0,0 +1,141 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public static class ObjectExporterMessages
{
public static readonly Guid IObjectExporter = new("99FCFEC4-5260-101B-BBCB-00AA0021347A");
public const ushort ResolveOxidOpnum = 0;
public const ushort SimplePingOpnum = 1;
public const ushort ComplexPingOpnum = 2;
public const ushort ServerAliveOpnum = 3;
public const ushort ResolveOxid2Opnum = 4;
public const ushort ServerAlive2Opnum = 5;
public const ushort ProtseqNcacnIpTcp = 0x0007;
public const ushort ProtseqNcalRpc = 0x001f;
public static byte[] EncodeResolveOxidRequest(ulong oxid, IReadOnlyList<ushort> requestedProtseqs)
{
if (requestedProtseqs.Count == 0)
{
throw new ArgumentException("At least one protocol sequence is required.", nameof(requestedProtseqs));
}
int length = 8 + 2 + 2 + 4 + requestedProtseqs.Count * sizeof(ushort);
length = Align(length, 4);
byte[] buffer = new byte[length];
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(0, 8), oxid);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(8, 2), (ushort)requestedProtseqs.Count);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(12, 4), (uint)requestedProtseqs.Count);
for (int i = 0; i < requestedProtseqs.Count; i++)
{
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(16 + i * sizeof(ushort), sizeof(ushort)), requestedProtseqs[i]);
}
return buffer;
}
public static ResolveOxidFailure ParseResolveOxidFailure(ReadOnlySpan<byte> responseStub)
{
if (responseStub.Length < 4)
{
throw new ArgumentException("ResolveOxid response stub is too short.", nameof(responseStub));
}
return new ResolveOxidFailure(BinaryPrimitives.ReadUInt32LittleEndian(responseStub[^4..]));
}
public static ResolveOxidResult ParseResolveOxidResult(ReadOnlySpan<byte> responseStub)
{
if (responseStub.Length < 32)
{
throw new ArgumentException("ResolveOxid response stub is too short.", nameof(responseStub));
}
uint referentId = BinaryPrimitives.ReadUInt32LittleEndian(responseStub[0..4]);
if (referentId == 0)
{
uint nullStatus = BinaryPrimitives.ReadUInt32LittleEndian(responseStub[^4..]);
return new ResolveOxidResult([], Guid.Empty, 0, nullStatus);
}
uint maxCount = BinaryPrimitives.ReadUInt32LittleEndian(responseStub[4..8]);
ushort entries = BinaryPrimitives.ReadUInt16LittleEndian(responseStub[8..10]);
ushort securityOffset = BinaryPrimitives.ReadUInt16LittleEndian(responseStub[10..12]);
if (maxCount < entries)
{
throw new ArgumentException("ResolveOxid DUALSTRINGARRAY max count is smaller than entry count.", nameof(responseStub));
}
int arrayOffset = 12;
int arrayBytes = checked((int)maxCount * sizeof(ushort));
if (arrayOffset + arrayBytes > responseStub.Length)
{
throw new ArgumentException("ResolveOxid DUALSTRINGARRAY is truncated.", nameof(responseStub));
}
var decoded = DecodeDualStringArray(responseStub.Slice(arrayOffset, entries * sizeof(ushort)), entries, securityOffset);
int offset = Align(arrayOffset + arrayBytes, 4);
if (offset + 24 > responseStub.Length)
{
throw new ArgumentException("ResolveOxid trailing fields are truncated.", nameof(responseStub));
}
return new ResolveOxidResult(
decoded,
new Guid(responseStub.Slice(offset, 16)),
BinaryPrimitives.ReadUInt32LittleEndian(responseStub.Slice(offset + 16, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(responseStub.Slice(offset + 20, 4)));
}
private static IReadOnlyList<ComDualStringEntry> DecodeDualStringArray(ReadOnlySpan<byte> data, ushort entries, ushort securityOffset)
{
var strings = new List<ComDualStringEntry>();
for (int i = 0; i < entries;)
{
int entryStart = i;
ushort towerId = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(i * 2, 2));
i++;
if (towerId == 0)
{
continue;
}
var text = new List<char>();
while (i < entries)
{
ushort value = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(i * 2, 2));
i++;
if (value == 0)
{
break;
}
text.Add(value >= 0x20 && value <= 0x7e ? (char)value : '?');
}
strings.Add(new ComDualStringEntry(
towerId,
towerId == ProtseqNcacnIpTcp ? "ncacn_ip_tcp" : $"protseq_0x{towerId:x4}",
new string(text.ToArray()),
IsSecurityBinding: entryStart >= securityOffset));
}
return strings;
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
}
public sealed record ResolveOxidFailure(uint ErrorStatus);
public sealed record ResolveOxidResult(
IReadOnlyList<ComDualStringEntry> Bindings,
Guid RemUnknownIpid,
uint AuthnHint,
uint ErrorStatus);
+140
View File
@@ -0,0 +1,140 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public readonly record struct ComVersion(ushort Major, ushort Minor)
{
public static ComVersion Version57 { get; } = new(5, 7);
}
public sealed record OrpcThis(
ComVersion Version,
uint Flags,
uint Reserved1,
Guid Cid,
uint ExtensionsReferentId)
{
public const int EncodedLengthWithoutExtensions = 32;
public static OrpcThis Create(Guid cid, ComVersion? version = null)
{
return new OrpcThis(version ?? ComVersion.Version57, 0, 0, cid, 0);
}
public static OrpcThis Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLengthWithoutExtensions)
{
throw new ArgumentException("ORPCTHIS buffer is too short.", nameof(buffer));
}
return new OrpcThis(
Version: new ComVersion(
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4])),
Flags: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
Reserved1: BinaryPrimitives.ReadUInt32LittleEndian(buffer[8..12]),
Cid: new Guid(buffer.Slice(12, 16)),
ExtensionsReferentId: BinaryPrimitives.ReadUInt32LittleEndian(buffer[28..32]));
}
public byte[] Encode()
{
byte[] buffer = new byte[EncodedLengthWithoutExtensions];
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), Version.Major);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), Version.Minor);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), Flags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(8, 4), Reserved1);
Cid.TryWriteBytes(buffer.AsSpan(12, 16));
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(28, 4), ExtensionsReferentId);
return buffer;
}
}
public sealed record OrpcThat(uint Flags, uint ExtensionsReferentId)
{
public const int EncodedLengthWithoutExtensions = 8;
public static OrpcThat Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLengthWithoutExtensions)
{
throw new ArgumentException("ORPCTHAT buffer is too short.", nameof(buffer));
}
return new OrpcThat(
Flags: BinaryPrimitives.ReadUInt32LittleEndian(buffer[0..4]),
ExtensionsReferentId: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]));
}
public byte[] Encode()
{
byte[] buffer = new byte[EncodedLengthWithoutExtensions];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), Flags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), ExtensionsReferentId);
return buffer;
}
}
public sealed record MInterfacePointer(byte[] ObjRefBytes)
{
public byte[] Encode()
{
byte[] buffer = new byte[sizeof(uint) + ObjRefBytes.Length];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, sizeof(uint)), (uint)ObjRefBytes.Length);
ObjRefBytes.CopyTo(buffer.AsSpan(sizeof(uint)));
return buffer;
}
public static MInterfacePointer Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < sizeof(uint))
{
throw new ArgumentException("MInterfacePointer buffer is too short.", nameof(buffer));
}
uint size = BinaryPrimitives.ReadUInt32LittleEndian(buffer[..sizeof(uint)]);
if (size > buffer.Length - sizeof(uint))
{
throw new ArgumentException("MInterfacePointer OBJREF payload is truncated.", nameof(buffer));
}
return new MInterfacePointer(buffer.Slice(sizeof(uint), (int)size).ToArray());
}
public ComObjRef ParseObjRef()
{
return ComObjRef.Parse(ObjRefBytes);
}
}
public sealed record StdObjRef(uint Flags, uint PublicRefs, ulong Oxid, ulong Oid, Guid Ipid)
{
public const int EncodedLength = 40;
public static StdObjRef Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLength)
{
throw new ArgumentException("STDOBJREF buffer is too short.", nameof(buffer));
}
return new StdObjRef(
Flags: BinaryPrimitives.ReadUInt32LittleEndian(buffer[0..4]),
PublicRefs: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
Oxid: BinaryPrimitives.ReadUInt64LittleEndian(buffer[8..16]),
Oid: BinaryPrimitives.ReadUInt64LittleEndian(buffer[16..24]),
Ipid: new Guid(buffer.Slice(24, 16)));
}
public byte[] Encode()
{
byte[] buffer = new byte[EncodedLength];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), Flags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), PublicRefs);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(8, 8), Oxid);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(16, 8), Oid);
Ipid.TryWriteBytes(buffer.AsSpan(24, 16));
return buffer;
}
}
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MxNativeClient.Tests")]
+79
View File
@@ -0,0 +1,79 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public static class RemUnknownMessages
{
public static readonly Guid IRemUnknown = new("00000131-0000-0000-C000-000000000046");
public const ushort RemQueryInterfaceOpnum = 3;
public const ushort RemAddRefOpnum = 4;
public const ushort RemReleaseOpnum = 5;
public static byte[] EncodeRemQueryInterfaceRequest(Guid sourceIpid, Guid requestedIid, Guid causalityId, uint publicRefs = 5)
{
var orpcThis = OrpcThis.Create(causalityId).Encode();
byte[] body = new byte[orpcThis.Length + 16 + 4 + 4 + 4 + 16];
int offset = 0;
orpcThis.CopyTo(body.AsSpan(offset));
offset += orpcThis.Length;
sourceIpid.TryWriteBytes(body.AsSpan(offset, 16));
offset += 16;
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset, 4), publicRefs);
offset += 4;
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset, 2), 1);
offset += 2;
body[offset++] = 0xce;
body[offset++] = 0xce; // NDR alignment before the conformant IID array max count.
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset, 4), 1);
offset += 4;
requestedIid.TryWriteBytes(body.AsSpan(offset, 16));
return body;
}
public static RemQueryInterfaceResponse ParseRemQueryInterfaceResponse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThat.EncodedLengthWithoutExtensions + 4 + RemQiResult.EncodedLength + 4)
{
throw new ArgumentException("RemQueryInterface response is too short.", nameof(buffer));
}
var orpcThat = OrpcThat.Parse(buffer);
uint referentId = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(OrpcThat.EncodedLengthWithoutExtensions, 4));
int offset = OrpcThat.EncodedLengthWithoutExtensions + 4;
RemQiResult? result = null;
if (referentId != 0)
{
offset += 4; // Conformant array max count for the REMQIRESULT result array.
result = RemQiResult.Parse(buffer[offset..]);
}
if (result is not null)
{
offset += RemQiResult.EncodedLength;
}
uint errorCode = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(offset, 4));
return new RemQueryInterfaceResponse(orpcThat, result, errorCode);
}
}
public sealed record RemQueryInterfaceResponse(OrpcThat OrpcThat, RemQiResult? Result, uint ErrorCode);
public sealed record RemQiResult(int HResult, StdObjRef StandardObjectReference)
{
public const int EncodedLength = sizeof(int) + sizeof(int) + StdObjRef.EncodedLength;
public static RemQiResult Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLength)
{
throw new ArgumentException("REMQIRESULT buffer is too short.", nameof(buffer));
}
return new RemQiResult(
HResult: BinaryPrimitives.ReadInt32LittleEndian(buffer[..4]),
StandardObjectReference: StdObjRef.Parse(buffer[8..]));
}
}
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MxNativeCodec\MxNativeCodec.csproj" />
</ItemGroup>
</Project>
+860
View File
@@ -0,0 +1,860 @@
using System.Buffers.Binary;
using MxNativeCodec;
RunRoundTrip("int", MxValueKind.Int32, 109,
"37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 6d 00 00 00 ff ff 00 00 00 00 00 00 00 00 c9 14 b1 08 01 00 00 00");
RunRoundTrip("bool", MxValueKind.Boolean, true,
"37 01 00 05 00 36 d7 02 00 9a 00 0a 00 fa 7d 00 00 01 ff ff ff 00 00 00 00 00 00 00 00 e1 d8 b5 08 01 00 00 00");
RunRoundTrip("float", MxValueKind.Float32, 1.25f,
"37 01 00 05 00 36 d7 02 00 9c 00 0a 00 a6 ed 00 00 03 00 00 a0 3f ff ff 00 00 00 00 00 00 00 00 64 6f b6 08 01 00 00 00");
RunRoundTrip("double", MxValueKind.Float64, 1.125d,
"37 01 00 05 00 36 d7 02 00 9d 00 0a 00 1e 95 00 00 04 00 00 00 00 00 00 f2 3f ff ff 00 00 00 00 00 00 00 00 bf 04 b7 08 01 00 00 00");
RunRoundTrip("string", MxValueKind.String, "AlphaMX",
"37 01 00 05 00 36 d7 02 00 9e 00 0a 00 1a 94 00 00 05 14 00 00 00 10 00 00 00 41 00 6c 00 70 00 68 00 61 00 4d 00 58 00 00 00 ff ff 00 00 00 00 00 00 00 00 3d a1 b7 08 01 00 00 00");
RunRoundTrip("elapsed int caller", MxValueKind.Int32, 1000,
"37 01 00 06 00 08 f4 7e 00 7d 00 0a 00 ac 59 00 00 02 e8 03 00 00 ff ff 00 00 00 00 00 00 00 00 e0 bf fe 0b 01 00 00 00");
RunRoundTrip("internationalized string caller", MxValueKind.String, "hello-native",
"37 01 00 05 00 36 d7 02 00 65 00 0a 00 6d 02 00 00 05 1e 00 00 00 1a 00 00 00 68 00 65 00 6c 00 6c 00 6f 00 2d 00 6e 00 61 00 74 00 69 00 76 00 65 00 00 00 ff ff 00 00 00 00 00 00 00 00 94 68 ff 0b 01 00 00 00");
RunRoundTrip("datetime", MxValueKind.DateTime, new DateTime(2026, 4, 25, 2, 30, 0),
"37 01 00 05 00 36 d7 02 00 9f 00 0a 00 62 49 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 32 00 3a 00 33 00 30 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 ff ff 00 00 00 00 00 00 00 00 58 94 b8 08 01 00 00 00");
RunRoundTrip("write2 int timestamp", MxValueKind.Int32, 114,
"37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 72 00 00 00 00 00 00 72 68 3a 83 d4 dc 01 20 2e d8 08 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 7, 15, 0, DateTimeKind.Utc));
RunTimestampedGenerated("write2 bool timestamp", MxValueKind.Boolean, false,
"37 01 00 05 00 36 d7 02 00 9a 00 0a 00 fa 7d 00 00 01 00 00 00 00 d3 c1 2f ab d4 dc 01 c9 dd a5 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 1, 2));
RunRoundTrip("write2 float timestamp", MxValueKind.Float32, 6.25f,
"37 01 00 05 00 36 d7 02 00 9c 00 0a 00 a6 ed 00 00 03 00 00 c8 40 00 00 80 af 1d 54 ab d4 dc 01 94 c4 a5 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 2, 3));
RunRoundTrip("write2 double timestamp", MxValueKind.Float64, 7.125d,
"37 01 00 05 00 36 d7 02 00 9d 00 0a 00 1e 95 00 00 04 00 00 00 00 00 80 1c 40 00 00 00 8c 79 78 ab d4 dc 01 a3 56 a6 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 3, 4));
RunRoundTrip("write2 string timestamp", MxValueKind.String, "Write2Alpha",
"37 01 00 05 00 36 d7 02 00 9e 00 0a 00 1a 94 00 00 05 1c 00 00 00 18 00 00 00 57 00 72 00 69 00 74 00 65 00 32 00 41 00 6c 00 70 00 68 00 61 00 00 00 00 00 80 68 d5 9c ab d4 dc 01 9f 6e a6 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 4, 5));
RunRoundTrip("int[]", MxValueKind.Int32Array, new[] { 201, 202, 203, 204, 205, 206, 207, 208, 209, 210 },
"37 01 00 05 00 36 d7 02 00 a4 00 0a 00 60 57 ff ff 42 00 00 00 00 0a 00 04 00 00 00 c9 00 00 00 ca 00 00 00 cb 00 00 00 cc 00 00 00 cd 00 00 00 ce 00 00 00 cf 00 00 00 d0 00 00 00 d1 00 00 00 d2 00 00 00 ff ff 00 00 00 00 00 00 00 00 4d c3 c4 08 01 00 00 00");
RunRoundTrip("bool[]", MxValueKind.BooleanArray, new[] { true, true, false, false, true, true, false, false, true, true },
"37 01 00 05 00 36 d7 02 00 a0 00 0a 00 fa 89 ff ff 41 00 00 00 00 0a 00 02 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff ff ff 00 00 00 00 00 00 00 00 a8 7f c5 08 01 00 00 00");
RunRoundTrip("bool[] mxaccess safearray projection", MxValueKind.BooleanArray, new[] { true, true, false, false, false, false, true, true, true, true },
"37 01 00 05 00 36 d7 02 00 a0 00 0a 00 fa 89 ff ff 41 00 00 00 00 0a 00 02 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 ac ec 08 0c 01 00 00 00");
RunRoundTrip("float[]", MxValueKind.Float32Array, new[] { 1.25f, 2.5f, 3.75f, 4.25f, 5.5f, 6.75f, 7.25f, 8.5f, 9.75f, 10.25f },
"37 01 00 05 00 36 d7 02 00 a3 00 0a 00 95 f1 ff ff 43 00 00 00 00 0a 00 04 00 00 00 00 00 a0 3f 00 00 20 40 00 00 70 40 00 00 88 40 00 00 b0 40 00 00 d8 40 00 00 e8 40 00 00 08 41 00 00 1c 41 00 00 24 41 ff ff 00 00 00 00 00 00 00 00 0c f4 c5 08 01 00 00 00");
RunRoundTrip("double[]", MxValueKind.Float64Array, new[] { 1.125d, 2.25d, 3.5d, 4.625d, 5.75d, 6.875d, 7.0d, 8.125d, 9.25d, 10.375d },
"37 01 00 05 00 36 d7 02 00 a2 00 0a 00 11 0e ff ff 44 00 00 00 00 0a 00 08 00 00 00 00 00 00 00 00 00 f2 3f 00 00 00 00 00 00 02 40 00 00 00 00 00 00 0c 40 00 00 00 00 00 80 12 40 00 00 00 00 00 00 17 40 00 00 00 00 00 80 1b 40 00 00 00 00 00 00 1c 40 00 00 00 00 00 40 20 40 00 00 00 00 00 80 22 40 00 00 00 00 00 c0 24 40 ff ff 00 00 00 00 00 00 00 00 cf 68 c6 08 01 00 00 00");
RunRoundTrip("string[]", MxValueKind.StringArray, new[] { "A01", "B02", "C03", "D04", "E05", "F06", "G07", "H08", "I09", "J10" },
"37 01 00 05 00 36 d7 02 00 a5 00 0a 00 5d 4b ff ff 45 00 00 00 00 0a 00 04 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 41 00 30 00 31 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 42 00 30 00 32 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 43 00 30 00 33 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 44 00 30 00 34 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 45 00 30 00 35 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 46 00 30 00 36 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 47 00 30 00 37 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 48 00 30 00 38 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 49 00 30 00 39 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 4a 00 31 00 30 00 00 00 ff ff 00 00 00 00 00 00 00 00 c3 da c6 08 01 00 00 00");
RunRoundTrip("write2 int[] timestamp", MxValueKind.Int32Array, new[] { 301, 302, 303, 304, 305, 306, 307, 308, 309, 310 },
"37 01 00 05 00 36 d7 02 00 a4 00 0a 00 60 57 ff ff 42 00 00 00 00 0a 00 04 00 00 00 2d 01 00 00 2e 01 00 00 2f 01 00 00 30 01 00 00 31 01 00 00 32 01 00 00 33 01 00 00 34 01 00 00 35 01 00 00 36 01 00 00 00 00 80 93 fc 76 ac d4 dc 01 43 35 ac 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 10, 11));
RunRoundTrip("write2 bool[] timestamp", MxValueKind.BooleanArray, new[] { true, true, false, false, true, true, false, false, true, true },
"37 01 00 05 00 36 d7 02 00 a0 00 0a 00 fa 89 ff ff 41 00 00 00 00 0a 00 02 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 70 58 9b ac d4 dc 01 8b 1c ac 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 11, 12));
RunRoundTrip("write2 string[] timestamp", MxValueKind.StringArray, new[] { "AA1", "BB2", "CC3", "DD4", "EE5", "FF6", "GG7", "HH8", "II9", "JJ10" },
"37 01 00 05 00 36 d7 02 00 a5 00 0a 00 5d 4b ff ff 45 00 00 00 00 0a 00 04 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 41 00 41 00 31 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 42 00 42 00 32 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 43 00 43 00 33 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 44 00 44 00 34 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 45 00 45 00 35 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 46 00 46 00 36 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 47 00 47 00 37 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 48 00 48 00 38 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 49 00 49 00 39 00 00 00 13 00 00 00 05 0e 00 00 00 0a 00 00 00 4a 00 4a 00 31 00 30 00 00 00 00 00 80 4c b4 bf ac d4 dc 01 10 a3 ac 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 12, 13));
RunRoundTrip("write2 float[] timestamp", MxValueKind.Float32Array, new[] { 1.5f, 2.5f, 3.5f, 4.5f, 5.5f, 6.5f, 7.5f, 8.5f, 9.5f, 10.5f },
"37 01 00 05 00 36 d7 02 00 a3 00 0a 00 95 f1 ff ff 43 00 00 00 00 0a 00 04 00 00 00 00 00 c0 3f 00 00 20 40 00 00 60 40 00 00 90 40 00 00 b0 40 00 00 d0 40 00 00 f0 40 00 00 08 41 00 00 18 41 00 00 28 41 00 00 00 29 10 e4 ac d4 dc 01 fa 6a af 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 13, 14));
RunRoundTrip("write2 double[] timestamp", MxValueKind.Float64Array, new[] { 1.5d, 2.5d, 3.5d, 4.5d, 5.5d, 6.5d, 7.5d, 8.5d, 9.5d, 10.5d },
"37 01 00 05 00 36 d7 02 00 a2 00 0a 00 11 0e ff ff 44 00 00 00 00 0a 00 08 00 00 00 00 00 00 00 00 00 f8 3f 00 00 00 00 00 00 04 40 00 00 00 00 00 00 0c 40 00 00 00 00 00 00 12 40 00 00 00 00 00 00 16 40 00 00 00 00 00 00 1a 40 00 00 00 00 00 00 1e 40 00 00 00 00 00 00 21 40 00 00 00 00 00 00 23 40 00 00 00 00 00 00 25 40 00 00 80 05 6c 08 ad d4 dc 01 42 52 af 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 14, 15));
RunRoundTrip("write2 datetime[] timestamp", MxValueKind.DateTimeArray, new[]
{
new DateTime(2026, 4, 25, 9, 0, 0),
new DateTime(2026, 4, 25, 9, 1, 0),
new DateTime(2026, 4, 25, 9, 2, 0),
new DateTime(2026, 4, 25, 9, 3, 0),
new DateTime(2026, 4, 25, 9, 4, 0),
new DateTime(2026, 4, 25, 9, 5, 0),
new DateTime(2026, 4, 25, 9, 6, 0),
new DateTime(2026, 4, 25, 9, 7, 0),
new DateTime(2026, 4, 25, 9, 8, 0),
new DateTime(2026, 4, 25, 9, 9, 0),
},
"37 01 00 05 00 36 d7 02 00 a1 00 0a 00 1b df ff ff 45 00 00 00 00 0a 00 04 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 30 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 31 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 32 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 33 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 34 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 35 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 36 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 37 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 38 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 33 00 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 39 00 3a 00 30 00 39 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 00 00 00 e2 c7 2c ad d4 dc 01 35 07 b1 0b 01 00 00 00",
timestamp: new DateTime(2026, 4, 25, 8, 15, 16));
RunTransferEnvelopeRoundTrip(
"int transfer envelope",
"37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 17 cd 5b 07 ff ff 00 00 00 00 00 00 00 00 24 3c ed 08 01 00 00 00",
"01 00 28 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 17 cd 5b 07 ff ff 00 00 00 00 00 00 00 00 24 3c ed 08 01 00 00 00");
RunObservedFrameTests();
RunItemControlMessageTests();
RunMxReferenceHandleTests();
RunGeneratedTransferEnvelopeTests();
RunReferenceRegistrationMessageTests();
RunProtectedWriteGenerationTests();
RunSecuredWrite2GenerationTests();
RunMxStatusDetailTests();
RunMxDataTypeSupportTests();
Console.WriteLine("MxNativeCodec observed write-body round trips passed.");
static void RunRoundTrip(string name, MxValueKind kind, object value, string hex, DateTime? timestamp = null)
{
byte[] observed = FromHex(hex);
var template = ObservedWriteBodyTemplate.FromObserved(kind, observed);
object decoded = template.Decode(observed);
AssertValue(name, value, decoded);
AssertEqual(name + " write index", 1, template.DecodeWriteIndex(observed));
byte[] encoded = template.Encode(value, writeIndex: 1);
AssertBytes(name, observed, encoded);
if (timestamp.HasValue)
{
var handle = HandleFromObservedWriteBody(observed);
uint clientToken = BinaryPrimitives.ReadUInt32LittleEndian(observed.AsSpan(observed.Length - 8, sizeof(uint)));
byte[] generated = NmxWriteMessage.EncodeTimestamped(
handle,
kind,
decoded,
timestamp.Value,
template.DecodeWriteIndex(observed),
clientToken);
AssertBytes(name + " generated", observed, generated);
}
else
{
var handle = HandleFromObservedWriteBody(observed);
uint clientToken = BinaryPrimitives.ReadUInt32LittleEndian(observed.AsSpan(observed.Length - 8, sizeof(uint)));
byte[] generated = NmxWriteMessage.Encode(
handle,
kind,
decoded,
template.DecodeWriteIndex(observed),
clientToken);
AssertBytes(name + " generated", observed, generated);
}
}
static void RunTimestampedGenerated(string name, MxValueKind kind, object value, string hex, DateTime timestamp)
{
byte[] observed = FromHex(hex);
var handle = HandleFromObservedWriteBody(observed);
uint clientToken = BinaryPrimitives.ReadUInt32LittleEndian(observed.AsSpan(observed.Length - 8, sizeof(uint)));
int writeIndex = BinaryPrimitives.ReadInt32LittleEndian(observed.AsSpan(observed.Length - sizeof(int), sizeof(int)));
byte[] generated = NmxWriteMessage.EncodeTimestamped(
handle,
kind,
value,
timestamp,
writeIndex,
clientToken);
AssertBytes(name + " generated", observed, generated);
}
static void RunTransferEnvelopeRoundTrip(string name, string innerHex, string transferHex)
{
byte[] inner = FromHex(innerHex);
byte[] transfer = FromHex(transferHex);
var template = NmxTransferEnvelopeTemplate.FromObserved(transfer);
AssertBytes(name + " decoded inner", inner, template.DecodeInner(transfer).Span);
AssertBytes(name, transfer, template.Encode(inner));
}
static void RunObservedFrameTests()
{
byte[] adviseTransfer = FromHex(
"01 00 27 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 " +
"1f 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 00 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00");
var adviseEnvelope = NmxObservedEnvelope.ParseTransferDataBody(adviseTransfer);
AssertEqual("advise declared inner", 39, adviseEnvelope.DeclaredInnerLength);
AssertEqual("advise actual inner", 39, adviseEnvelope.ActualInnerLength);
var advise = NmxObservedMessage.Parse(adviseEnvelope.InnerBody.Span);
AssertEqual("advise command", (byte)0x1f, advise.Command);
AssertEqual("advise command name", "AdviseSupervisory", advise.CommandName);
AssertEqual("advise version major", (byte)1, advise.VersionMajor);
AssertEqual("advise correlation", new Guid("cd9ccac0-6532-46b0-a585-a583b2e77a5d"), advise.ItemCorrelationId);
byte[] statusReceived = FromHex(
"6c 00 00 00 01 00 3e 00 00 00 00 00 00 00 5d 89 05 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"32 01 00 01 00 00 00 db 4e 14 ba e9 25 85 47 81 e4 e0 e7 1a d2 b1 aa c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 03 00 00 00 03 00 00 00 c0 00 60 9a 60 95 84 d4 dc 01 02");
var statusEnvelope = NmxObservedEnvelope.ParseProcessDataReceivedBody(statusReceived);
AssertEqual("status total prefix", 108, statusEnvelope.TotalLengthPrefix!.Value);
AssertEqual("status declared inner", 62, statusEnvelope.DeclaredInnerLength);
AssertEqual("status actual inner", 58, statusEnvelope.ActualInnerLength);
var status = NmxObservedMessage.Parse(statusEnvelope.InnerBody.Span);
AssertEqual("status command", (byte)0x32, status.Command);
AssertEqual("status command name", "SubscriptionStatus", status.CommandName);
var typedStatus = NmxSubscriptionMessage.ParseProcessDataReceivedBody(statusReceived);
AssertEqual("typed status command", NmxSubscriptionMessage.SubscriptionStatusCommand, typedStatus.Command);
AssertEqual("typed status count", 1, typedStatus.RecordCount);
AssertEqual("typed status operation id", new Guid("ba144edb-25e9-4785-81e4-e0e71ad2b1aa"), typedStatus.OperationId);
AssertEqual("typed status item correlation", new Guid("cd9ccac0-6532-46b0-a585-a583b2e77a5d"), typedStatus.ItemCorrelationId!.Value);
AssertEqual("typed status record count", 1, typedStatus.Records.Count);
AssertEqual("typed status status", 3, typedStatus.Records[0].Status);
AssertEqual("typed status detail", 3, typedStatus.Records[0].DetailStatus!.Value);
AssertEqual("typed status quality", (ushort)0x00c0, typedStatus.Records[0].Quality);
AssertEqual("typed status wire kind", (byte)0x02, typedStatus.Records[0].WireKind);
AssertEqual("typed status value", null, typedStatus.Records[0].Value);
AssertEqual("typed status mx status", MxStatus.DataChangeOk, typedStatus.Records[0].ToDataChangeStatus());
byte[] dataUpdateReceived = FromHex(
"01 00 2a 00 00 00 00 00 00 00 cd 86 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 1c c8 68 ce e5 13 e8 48 89 87 21 91 a8 dc 42 93 03 00 00 00 c0 00 00 72 68 3a 83 d4 dc 01 02 72 00 00 00");
var dataUpdateEnvelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(dataUpdateReceived);
AssertEqual("data update has no length prefix", false, dataUpdateEnvelope.HasLengthPrefix);
AssertEqual("data update declared inner", 42, dataUpdateEnvelope.DeclaredInnerLength);
AssertEqual("data update actual inner", 42, dataUpdateEnvelope.ActualInnerLength);
var dataUpdate = NmxSubscriptionMessage.ParseProcessDataReceivedBody(dataUpdateReceived);
AssertEqual("data update command", NmxSubscriptionMessage.DataUpdateCommand, dataUpdate.Command);
AssertEqual("data update count", 1, dataUpdate.RecordCount);
AssertEqual("data update operation id", new Guid("ce68c81c-13e5-48e8-8987-2191a8dc4293"), dataUpdate.OperationId);
AssertEqual("data update item correlation", null, dataUpdate.ItemCorrelationId);
AssertEqual("data update status", 3, dataUpdate.Records[0].Status);
AssertEqual("data update detail", null, dataUpdate.Records[0].DetailStatus);
AssertEqual("data update quality", (ushort)0x00c0, dataUpdate.Records[0].Quality);
AssertEqual("data update timestamp", new DateTime(2026, 4, 25, 7, 15, 0, DateTimeKind.Utc), dataUpdate.Records[0].TimestampUtc);
AssertEqual("data update wire kind", (byte)0x02, dataUpdate.Records[0].WireKind);
AssertEqual("data update value", 114, dataUpdate.Records[0].Value!);
AssertEqual("data update mx status", MxStatus.DataChangeOk, dataUpdate.Records[0].ToDataChangeStatus());
byte[] writeCompleteReceived = FromHex(
"01 00 05 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 02 02 00 00 30 75 00 00 00 00 50 80 00");
AssertEqual(
"write complete operation parse",
true,
NmxOperationStatusMessage.TryParseProcessDataReceivedBody(writeCompleteReceived, out var writeComplete));
AssertEqual("write complete format", NmxOperationStatusFormat.StatusWord, writeComplete.Format);
AssertEqual("write complete command", (byte)0x00, writeComplete.Command);
AssertEqual("write complete status code", (ushort)0x8050, writeComplete.StatusCode);
AssertEqual("write complete completion code", (byte)0x00, writeComplete.CompletionCode);
AssertEqual("write complete mx status", MxStatus.WriteCompleteOk, writeComplete.Status);
AssertEqual("write complete mxaccess event", true, writeComplete.IsMxAccessWriteComplete);
byte[] writeWrongTypeStatusReceived = FromHex(
"33 00 00 00 01 00 05 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 f9 7f 00 00 02 02 00 00 30 75 00 00 41");
AssertEqual(
"write wrong-type status parse",
true,
NmxOperationStatusMessage.TryParseProcessDataReceivedBody(writeWrongTypeStatusReceived, out var writeWrongTypeStatus));
AssertEqual("write wrong-type format", NmxOperationStatusFormat.CompletionOnly, writeWrongTypeStatus.Format);
AssertEqual("write wrong-type status code", (ushort)0, writeWrongTypeStatus.StatusCode);
AssertEqual("write wrong-type completion code", (byte)0x41, writeWrongTypeStatus.CompletionCode);
AssertEqual("write wrong-type mx status success", (short)0, writeWrongTypeStatus.Status.Success);
AssertEqual("write wrong-type mx status detail", (short)0x41, writeWrongTypeStatus.Status.Detail);
AssertEqual("write wrong-type mxaccess event", false, writeWrongTypeStatus.IsMxAccessWriteComplete);
byte[] writeCompletionOnlyOkReceived = FromHex(
"33 00 00 00 01 00 05 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 f9 7f 00 00 02 02 00 00 30 75 00 00 00");
AssertEqual(
"write completion-only ok parse",
true,
NmxOperationStatusMessage.TryParseProcessDataReceivedBody(writeCompletionOnlyOkReceived, out var writeCompletionOnlyOk));
AssertEqual("write completion-only ok format", NmxOperationStatusFormat.CompletionOnly, writeCompletionOnlyOk.Format);
AssertEqual("write completion-only ok completion code", (byte)0x00, writeCompletionOnlyOk.CompletionCode);
AssertEqual("write completion-only ok mxaccess event", false, writeCompletionOnlyOk.IsMxAccessWriteComplete);
byte[] statusWithValueReceived = FromHex(
"01 00 3e 00 00 00 00 00 00 00 ca 86 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"32 01 00 01 00 00 00 1c c8 68 ce e5 13 e8 48 89 87 21 91 a8 dc 42 93 b0 b8 80 11 ae ae f8 40 9a c9 8b 3c 9e 79 f4 a4 03 00 00 00 03 00 00 00 c0 00 90 06 82 68 7b d4 dc 01 02 6f 00 00 00");
var statusWithValue = NmxSubscriptionMessage.ParseProcessDataReceivedBody(statusWithValueReceived);
AssertEqual("status with value command", NmxSubscriptionMessage.SubscriptionStatusCommand, statusWithValue.Command);
AssertEqual("status with value count", 1, statusWithValue.RecordCount);
AssertEqual("status with value operation id", new Guid("ce68c81c-13e5-48e8-8987-2191a8dc4293"), statusWithValue.OperationId);
AssertEqual("status with value item correlation", new Guid("1180b8b0-aeae-40f8-9ac9-8b3c9e79f4a4"), statusWithValue.ItemCorrelationId!.Value);
AssertEqual("status with value status", 3, statusWithValue.Records[0].Status);
AssertEqual("status with value detail", 3, statusWithValue.Records[0].DetailStatus!.Value);
AssertEqual("status with value quality", (ushort)0x00c0, statusWithValue.Records[0].Quality);
AssertEqual("status with value wire kind", (byte)0x02, statusWithValue.Records[0].WireKind);
AssertEqual("status with value value", 111, statusWithValue.Records[0].Value!);
byte[] multiRecordStatusReceived = FromHex(
"01 00 69 00 00 00 00 00 00 00 ef e4 08 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"32 01 00 02 00 00 00 13 27 1c 4c c8 d3 95 40 b9 dd 17 80 70 23 4a 9d 42 95 8e 96 10 37 0c 4a b8 f8 79 bf ac 46 b8 e4 01 00 00 00 03 00 00 00 c0 00 20 2e 5a 46 28 d3 dc 01 06 0a 00 00 00 00 a0 41 c3 55 bd dc 01 00 00 02 00 00 00 03 00 00 00 c0 00 80 18 5b 46 28 d3 dc 01 06 0a 00 00 00 80 c1 75 25 a5 bd dc 01 00 00");
var multiRecordStatus = NmxSubscriptionMessage.ParseProcessDataReceivedBody(multiRecordStatusReceived);
AssertEqual("multi status count", 2, multiRecordStatus.RecordCount);
AssertEqual("multi status parsed records", 2, multiRecordStatus.Records.Count);
AssertEqual("multi status first status", 1, multiRecordStatus.Records[0].Status);
AssertEqual("multi status second status", 2, multiRecordStatus.Records[1].Status);
AssertEqual("multi status first kind", (byte)0x06, multiRecordStatus.Records[0].WireKind);
AssertEqual("multi status second kind", (byte)0x06, multiRecordStatus.Records[1].WireKind);
AssertEqual("multi status first value type", true, multiRecordStatus.Records[0].Value is DateTime);
AssertEqual("multi status second value type", true, multiRecordStatus.Records[1].Value is DateTime);
byte[] boolUpdateReceived = FromHex(
"01 00 27 00 00 00 00 00 00 00 f9 74 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 88 b0 59 be 24 f0 f8 43 a7 89 b6 95 d8 11 13 9e 03 00 00 00 c0 00 f0 58 d4 21 7c d4 dc 01 01 ff");
AssertCallbackValue("bool update", boolUpdateReceived, true);
byte[] floatUpdateReceived = FromHex(
"01 00 2a 00 00 00 00 00 00 00 4d 75 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 e1 29 46 93 4a c7 6d 43 b8 9b ad df bc ee f8 61 03 00 00 00 c0 00 10 8f cb 38 7c d4 dc 01 03 00 00 a0 3f");
AssertCallbackValue("float update", floatUpdateReceived, 1.25f);
byte[] doubleUpdateReceived = FromHex(
"01 00 2e 00 00 00 00 00 00 00 a1 75 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 3b 39 15 ab 40 13 c5 4c 8d 6c b6 74 e5 9a db bf 03 00 00 00 c0 00 20 16 af 4f 7c d4 dc 01 04 00 00 00 00 00 00 f2 3f");
AssertCallbackValue("double update", doubleUpdateReceived, 1.125d);
byte[] stringUpdateReceived = FromHex(
"01 00 3e 00 00 00 00 00 00 00 f8 75 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 e1 40 1f 90 80 e4 7d 43 bf ab 6f a2 ea 23 97 ed 03 00 00 00 c0 00 10 5c 75 67 7c d4 dc 01 05 14 00 00 00 10 00 00 00 41 00 6c 00 70 00 68 00 61 00 4d 00 58 00 00 00");
AssertCallbackValue("string update", stringUpdateReceived, "AlphaMX");
byte[] stringStatusReceived = FromHex(
"01 00 60 00 00 00 00 00 00 00 f5 75 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"32 01 00 01 00 00 00 e1 40 1f 90 80 e4 7d 43 bf ab 6f a2 ea 23 97 ed b2 36 c6 09 ef 0d 00 43 9e 1d 5e d9 16 c8 7c cc 03 00 00 00 03 00 00 00 c0 00 b0 a0 e2 57 47 bd dc 01 05 22 00 00 00 1e 00 00 00 48 00 65 00 6c 00 6c 00 6f 00 46 00 72 00 6f 00 6d 00 4f 00 70 00 63 00 55 00 61 00 00 00");
AssertCallbackValue("string status", stringStatusReceived, "HelloFromOpcUa");
byte[] compactEmptyStringStatusReceived = FromHex(
"32 01 00 01 00 00 00 bd 26 31 07 1d d2 76 4d b4 48 29 dd a2 53 1e 29 e6 5c 35 81 5e f9 79 44 b3 5a d0 5d 9f c6 60 a8 03 00 00 00 00 00 00 00 c0 00 30 69 98 49 28 d3 dc 01 05 04 00 00 00");
var compactEmptyStringStatus = NmxSubscriptionMessage.ParseInner(compactEmptyStringStatusReceived);
AssertEqual("compact empty string kind", (byte)0x05, compactEmptyStringStatus.Records[0].WireKind);
AssertEqual("compact empty string value", string.Empty, compactEmptyStringStatus.Records[0].Value!);
byte[] elapsedTimeStatusReceived = FromHex(
"32 01 00 01 00 00 00 00 c0 ac 3a 1a 83 b4 48 88 e6 43 56 87 4b 87 97 45 d3 97 30 68 7f b9 48 b8 c4 bf cd 99 a7 a3 51 03 00 00 00 00 00 00 00 c0 00 90 5c d6 49 28 d3 dc 01 07 00 00 00 00");
var elapsedTimeStatus = NmxSubscriptionMessage.ParseInner(elapsedTimeStatusReceived);
AssertEqual("elapsed time kind", (byte)0x07, elapsedTimeStatus.Records[0].WireKind);
AssertEqual("elapsed time value", TimeSpan.Zero, elapsedTimeStatus.Records[0].Value!);
byte[] nonZeroElapsedTimeStatusReceived = FromHex(
"70 00 00 00 01 00 42 00 00 00 00 00 00 00 cd 89 05 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"32 01 00 01 00 00 00 80 d0 62 c4 97 13 d4 43 a0 bc 8b a5 a6 e2 69 86 7c 32 dd c9 b6 70 af 4a 89 a5 18 67 bc 51 66 71 03 00 00 00 00 00 00 00 c0 00 90 5c d6 49 28 d3 dc 01 07 00 e4 0b 54");
var nonZeroElapsedTimeStatus = NmxSubscriptionMessage.ParseProcessDataReceivedBody(nonZeroElapsedTimeStatusReceived);
AssertEqual("nonzero elapsed time kind", (byte)0x07, nonZeroElapsedTimeStatus.Records[0].WireKind);
AssertEqual("nonzero elapsed time value", TimeSpan.FromMilliseconds(0x540be400), nonZeroElapsedTimeStatus.Records[0].Value!);
byte[] intArrayUpdateReceived = FromHex(
"01 00 58 00 00 00 00 00 00 00 c0 7c 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 46 5a 30 af 6a 87 65 46 ac c5 4d 98 f2 e2 12 fe 03 00 00 00 c0 00 d0 ba 8f 68 7e d4 dc 01 42 00 00 00 00 0a 00 04 00 00 00 c9 00 00 00 ca 00 00 00 cb 00 00 00 cc 00 00 00 cd 00 00 00 ce 00 00 00 cf 00 00 00 d0 00 00 00 d1 00 00 00 d2 00 00 00");
AssertCallbackValue("int array update", intArrayUpdateReceived, new[] { 201, 202, 203, 204, 205, 206, 207, 208, 209, 210 });
byte[] boolArrayUpdateReceived = FromHex(
"01 00 44 00 00 00 00 00 00 00 23 7d 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 f6 8c cc e9 ef 7e 36 4e ab 35 e3 d0 27 51 db 55 03 00 00 00 c0 00 20 73 4c 85 7e d4 dc 01 41 00 00 00 00 0a 00 02 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff");
AssertCallbackValue("bool array update", boolArrayUpdateReceived, new[] { true, true, false, false, true, true, false, false, true, true });
byte[] floatArrayUpdateReceived = FromHex(
"01 00 58 00 00 00 00 00 00 00 62 7d 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 43 cd bf c8 a6 e1 a7 44 b7 14 99 a0 52 81 ec 43 03 00 00 00 c0 00 b0 47 0d 97 7e d4 dc 01 43 00 00 00 00 0a 00 04 00 00 00 00 00 a0 3f 00 00 20 40 00 00 70 40 00 00 88 40 00 00 b0 40 00 00 d8 40 00 00 e8 40 00 00 08 41 00 00 1c 41 00 00 24 41");
AssertCallbackValue("float array update", floatArrayUpdateReceived, new[] { 1.25f, 2.5f, 3.75f, 4.25f, 5.5f, 6.75f, 7.25f, 8.5f, 9.75f, 10.25f });
byte[] doubleArrayUpdateReceived = FromHex(
"01 00 80 00 00 00 00 00 00 00 a0 7d 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 eb 4d ab 7d 2e d8 02 49 b9 04 34 db b0 c9 41 06 03 00 00 00 c0 00 b0 bc cc a8 7e d4 dc 01 44 00 00 00 00 0a 00 08 00 00 00 00 00 00 00 00 00 f2 3f 00 00 00 00 00 00 02 40 00 00 00 00 00 00 0c 40 00 00 00 00 00 80 12 40 00 00 00 00 00 00 17 40 00 00 00 00 00 80 1b 40 00 00 00 00 00 00 1c 40 00 00 00 00 00 40 20 40 00 00 00 00 00 80 22 40 00 00 00 00 00 c0 24 40");
AssertCallbackValue("double array update", doubleArrayUpdateReceived, new[] { 1.125d, 2.25d, 3.5d, 4.625d, 5.75d, 6.875d, 7.0d, 8.125d, 9.25d, 10.375d });
byte[] dateTimeArrayUpdateReceived = FromHex(
"01 00 a8 00 00 00 00 00 00 00 e4 7e 04 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 02 00 00 30 75 00 00 " +
"33 01 00 01 00 00 00 c4 10 20 21 f5 14 45 42 aa 5a 7e 63 fc e1 d7 72 03 00 00 00 c0 00 40 85 bb 06 7f d4 dc 01 46 00 00 00 00 0a 00 0c 00 00 00 00 58 f7 21 81 d4 dc 01 00 00 00 00 00 9e ba 45 81 d4 dc 01 00 00 00 00 00 e4 7d 69 81 d4 dc 01 00 00 00 00 00 2a 41 8d 81 d4 dc 01 00 00 00 00 00 70 04 b1 81 d4 dc 01 00 00 00 00 00 b6 c7 d4 81 d4 dc 01 00 00 00 00 00 fc 8a f8 81 d4 dc 01 00 00 00 00 00 42 4e 1c 82 d4 dc 01 00 00 00 00 00 88 11 40 82 d4 dc 01 00 00 00 00 00 ce d4 63 82 d4 dc 01 00 00 00 00");
var dateTimeArrayUpdate = NmxSubscriptionMessage.ParseProcessDataReceivedBody(dateTimeArrayUpdateReceived);
AssertEqual("datetime array update value type", true, dateTimeArrayUpdate.Records[0].Value is DateTime[]);
AssertEqual("datetime array update count", 10, ((DateTime[])dateTimeArrayUpdate.Records[0].Value!).Length);
byte[] metadataTransfer = FromHex(
"01 00 3a 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 " +
"17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 d0 fc 40 09 1f 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 00 00 01 00 00 00 " +
"17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 50 03 41 09 20 01 00 02 00 00 00");
var metadataEnvelope = NmxObservedEnvelope.ParseTransferDataBody(metadataTransfer);
var metadata = NmxObservedMessage.Parse(metadataEnvelope.InnerBody.Span);
AssertEqual("metadata command", (byte)0x17, metadata.Command);
AssertEqual("metadata command name", "MetadataQuery", metadata.CommandName);
AssertEqual("metadata string 1", true, metadata.Strings.Any(s => s.Value == "DevPlatform.GR.TimeOfLastDeploy"));
AssertEqual("metadata string 2", true, metadata.Strings.Any(s => s.Value == "DevPlatform.GR.TimeOfLastConfigChange"));
AssertBytes(
"observed pre-advise metadata encode",
metadataEnvelope.InnerBody.ToArray(),
NmxMetadataQueryMessage.EncodeObservedPreAdvise(new Guid("cd9ccac0-6532-46b0-a585-a583b2e77a5d")));
byte[] metadataResponseReceived = FromHex(
"C2 02 00 00 01 00 94 02 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 24 72 00 00 02 02 00 00 30 75 00 00 " +
"40 1F 50 80 08 A6 00 00 00 40 00 00 91 44 00 65 00 76 00 50 00 6C 00 61 00 74 00 66 00 6F 00 72 00 6D 00 2E 00 47 00 52 00 2E 00 54 00 69 00 6D 00 65 00 4F 00 66 00 4C 00 61 00 73 00 74 00 44 00 65 00 70 00 6C 00 6F 00 79 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6C 00 61 00 74 00 66 00 6F 00 72 00 6D 00 00 00 28 00 00 00 47 00 52 00 2E 00 54 00 69 00 6D 00 65 00 4F 00 66 00 4C 00 61 00 73 00 74 00 44 00 65 00 70 00 6C 00 6F 00 79 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 F2 9A 00 6A 00 0A 00 5F F1 00 00 01 6C 00 00 00 41 00 6E 00 20 00 69 00 6E 00 74 00 65 00 72 00 6E 00 61 00 6C 00 20 00 65 00 72 00 72 00 6F 00 72 00 20 00 6F 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6E 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6E 00 74 00 69 00 6D 00 65 00 20 00 4F 00 62 00 6A 00 65 00 63 00 74 00 00 00 1F 00 00 50 80 01 00 01 00 01 00 30 75 00 00 D3 57 0D 95 6B 8F 7C 43 A6 09 63 4D 53 53 A2 C2 66 6B F0 7D 78 52 AC 46 82 E5 AF 0A CF 91 34 F5 40 1F 50 80 08 BE 00 00 00 4C 00 00 91 44 00 65 00 76 00 50 00 6C 00 61 00 74 00 66 00 6F 00 72 00 6D 00 2E 00 47 00 52 00 2E 00 54 00 69 00 6D 00 65 00 4F 00 66 00 4C 00 61 00 73 00 74 00 43 00 6F 00 6E 00 66 00 69 00 67 00 43 00 68 00 61 00 6E 00 67 00 65 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6C 00 61 00 74 00 66 00 6F 00 72 00 6D 00 00 00 34 00 00 00 47 00 52 00 2E 00 54 00 69 00 6D 00 65 00 4F 00 66 00 4C 00 61 00 73 00 74 00 43 00 6F 00 6E 00 66 00 69 00 67 00 43 00 68 00 61 00 6E 00 67 00 65 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 F2 9A 00 6B 00 0A 00 87 3A 00 00 01 6C 00 00 00 41 00 6E 00 20 00 69 00 6E 00 74 00 65 00 72 00 6E 00 61 00 6C 00 20 00 65 00 72 00 72 00 6F 00 72 00 20 00 6F 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6E 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6E 00 74 00 69 00 6D 00 65 00 20 00 4F 00 62 00 6A 00 65 00 63 00 74 00 00 00 20 00 00 50 80 01 00 01 00 01 00");
var metadataResponse = NmxObservedMessage.Parse(metadataResponseReceived.AsSpan(46));
AssertEqual("metadata response command", (byte)0x40, metadataResponse.Command);
AssertEqual("metadata response command name", "MetadataResponse", metadataResponse.CommandName);
AssertEqual("metadata response string 1", true, metadataResponse.Strings.Any(s => s.Value == "DevPlatform.GR.TimeOfLastDeploy"));
AssertEqual("metadata response string 2", true, metadataResponse.Strings.Any(s => s.Value == "DevPlatform.GR.TimeOfLastConfigChange"));
AssertEqual("metadata response error string", true, metadataResponse.Strings.Any(s => s.Value == "An internal error occurred in the Base Runtime Object"));
}
static void RunItemControlMessageTests()
{
RunItemControlRoundTrip(
"TestInt advise",
"1f 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 00 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00",
NmxItemControlCommand.AdviseSupervisory,
new Guid("cd9ccac0-6532-46b0-a585-a583b2e77a5d"),
5,
0xd736,
2,
155,
10,
0xda3e,
0);
RunItemControlRoundTrip(
"TestInt unadvise",
"21 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00",
NmxItemControlCommand.UnAdvise,
new Guid("cd9ccac0-6532-46b0-a585-a583b2e77a5d"),
5,
0xd736,
2,
155,
10,
0xda3e,
0);
RunItemControlRoundTrip(
"TestBool advise",
"1f 01 00 46 86 88 6b e0 16 13 4f 9a 88 2b bc 8f ae 13 04 00 00 05 00 36 d7 02 00 9a 00 0a 00 fa 7d 00 00 03 00 00 00",
NmxItemControlCommand.AdviseSupervisory,
new Guid("6b888646-16e0-4f13-9a88-2bbc8fae1304"),
5,
0xd736,
2,
154,
10,
0x7dfa,
0);
RunItemControlRoundTrip(
"TestString advise",
"1f 01 00 7d 4e 8d dc 01 db 02 41 9e 5d a4 f3 44 10 c4 73 00 00 05 00 36 d7 02 00 9e 00 0a 00 1a 94 00 00 03 00 00 00",
NmxItemControlCommand.AdviseSupervisory,
new Guid("dc8d4e7d-db01-4102-9e5d-a4f34410c473"),
5,
0xd736,
2,
158,
10,
0x941a,
0);
}
static void RunMxStatusDetailTests()
{
var denied = new MxStatus(
Success: 0,
Category: MxStatusCategory.SecurityError,
DetectedBy: MxStatusSource.RespondingAutomationObject,
Detail: 33);
AssertEqual("status detail denied", "Write access denied", denied.DetailText!);
AssertEqual("status detail secured", "Secured Write", MxStatusDetails.GetKnownText(56)!);
AssertEqual("status detail conversion", "Conversion to intended data type is not supported", MxStatusDetails.GetKnownText(541)!);
AssertEqual("status detail configure classification", "Object must be offscan to modify attributes that have an MxSecurityConfigure security classification", MxStatusDetails.GetKnownText(8017)!);
AssertEqual("status detail unknown", null, MxStatusDetails.GetKnownText(999));
AssertEqual("invalid reference status success", (short)0, MxStatus.InvalidReferenceConfiguration.Success);
AssertEqual("invalid reference status category", MxStatusCategory.ConfigurationError, MxStatus.InvalidReferenceConfiguration.Category);
AssertEqual("invalid reference status source", MxStatusSource.RequestingLmx, MxStatus.InvalidReferenceConfiguration.DetectedBy);
AssertEqual("invalid reference status detail", (short)6, MxStatus.InvalidReferenceConfiguration.Detail);
}
static void RunMxDataTypeSupportTests()
{
AssertEqual("int type kind supported", true, NmxWriteMessage.TryGetValueKind((short)MxDataType.Integer, isArray: false, out var intKind));
AssertEqual("int type kind", MxValueKind.Int32, intKind);
AssertEqual("int array type kind supported", true, NmxWriteMessage.TryGetValueKind((short)MxDataType.Integer, isArray: true, out var intArrayKind));
AssertEqual("int array type kind", MxValueKind.Int32Array, intArrayKind);
AssertEqual("elapsed unsupported", false, NmxWriteMessage.TryGetValueKind((short)MxDataType.ElapsedTime, isArray: false, out _));
AssertEqual("internationalized string unsupported", false, NmxWriteMessage.TryGetValueKind((short)MxDataType.InternationalizedString, isArray: false, out _));
}
static void RunMxReferenceHandleTests()
{
byte[] observedHandle = FromHex("01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00");
var handle = MxReferenceHandle.Parse(observedHandle);
AssertEqual("handle platform", (ushort)1, handle.PlatformId);
AssertEqual("handle engine", (ushort)2, handle.EngineId);
AssertEqual("handle object id", (ushort)5, handle.ObjectId);
AssertEqual("handle object signature", (ushort)0xd736, handle.ObjectSignature);
AssertEqual("handle primitive id", (short)2, handle.PrimitiveId);
AssertEqual("handle attribute id", (short)155, handle.AttributeId);
AssertEqual("handle property id", (short)10, handle.PropertyId);
AssertEqual("handle attribute signature", (ushort)0xda3e, handle.AttributeSignature);
AssertEqual("handle attribute index", (short)0, handle.AttributeIndex);
AssertBytes("handle encode", observedHandle, handle.Encode());
AssertEqual("object signature crc", (ushort)0xd736, MxReferenceHandle.ComputeNameSignature("TestChildObject"));
AssertEqual("int signature crc", (ushort)0xda3e, MxReferenceHandle.ComputeNameSignature("TestInt"));
AssertEqual("bool signature crc", (ushort)0x7dfa, MxReferenceHandle.ComputeNameSignature("TestBool"));
AssertEqual("string signature crc", (ushort)0x941a, MxReferenceHandle.ComputeNameSignature("TestString"));
AssertEqual("int array signature crc", (ushort)0x5760, MxReferenceHandle.ComputeNameSignature("TestIntArray"));
var synthesized = MxReferenceHandle.Create(
galaxyId: 1,
platformId: 1,
engineId: 2,
objectId: 5,
objectTagName: "TestChildObject",
primitiveId: 2,
attributeId: 155,
propertyId: 10,
attributeName: "TestInt",
isArray: false);
AssertEqual("synthesized handle", handle, synthesized);
var advise = NmxItemControlMessage.FromReferenceHandle(
NmxItemControlCommand.AdviseSupervisory,
new Guid("e56c1aff-d3a3-4a1d-921f-f12e18f0d95d"),
handle);
AssertEqual("handle to advise object id", handle.ObjectId, advise.ObjectId);
AssertEqual("handle to advise object signature", handle.ObjectSignature, advise.ObjectSignature);
AssertEqual("handle to advise primitive", handle.PrimitiveId, advise.PrimitiveId);
AssertEqual("handle to advise attr id", handle.AttributeId, advise.AttributeId);
AssertEqual("handle to advise property id", handle.PropertyId, advise.PropertyId);
AssertEqual("handle to advise attr signature", handle.AttributeSignature, advise.AttributeSignature);
AssertEqual("handle to advise attr index", handle.AttributeIndex, advise.AttributeIndex);
AssertEqual("advise to handle", handle, advise.ToReferenceHandle(engineId: 2));
AssertEqual("GR bool kind", MxValueKind.Boolean, NmxWriteMessage.GetValueKind(1, isArray: false));
AssertEqual("GR int kind", MxValueKind.Int32, NmxWriteMessage.GetValueKind(2, isArray: false));
AssertEqual("GR float kind", MxValueKind.Float32, NmxWriteMessage.GetValueKind(3, isArray: false));
AssertEqual("GR double kind", MxValueKind.Float64, NmxWriteMessage.GetValueKind(4, isArray: false));
AssertEqual("GR string kind", MxValueKind.String, NmxWriteMessage.GetValueKind(5, isArray: false));
AssertEqual("GR datetime kind", MxValueKind.DateTime, NmxWriteMessage.GetValueKind(6, isArray: false));
AssertEqual("GR int array kind", MxValueKind.Int32Array, NmxWriteMessage.GetValueKind(2, isArray: true));
}
static void RunGeneratedTransferEnvelopeTests()
{
byte[] inner = FromHex(
"1f 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 00 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00");
byte[] expected = FromHex(
"01 00 27 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 " +
"1f 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 00 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00");
byte[] generated = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.ItemControl,
localEngineId: 0x7ffb,
targetGalaxyId: 1,
targetPlatformId: 1,
targetEngineId: 2,
inner);
AssertBytes("generated item-control transfer", expected, generated);
var parsed = NmxTransferEnvelope.Parse(generated);
AssertEqual("transfer kind", NmxTransferMessageKind.ItemControl, parsed.MessageKind);
AssertEqual("transfer source galaxy", 1, parsed.SourceGalaxyId);
AssertEqual("transfer source platform", 1, parsed.SourcePlatformId);
AssertEqual("transfer local engine", 0x7ffb, parsed.LocalEngineId);
AssertEqual("transfer target galaxy", 1, parsed.TargetGalaxyId);
AssertEqual("transfer target platform", 1, parsed.TargetPlatformId);
AssertEqual("transfer target engine", 2, parsed.TargetEngineId);
AssertEqual("transfer timeout", 30000, parsed.TimeoutMilliseconds);
AssertBytes("transfer inner", inner, parsed.InnerBody.Span);
byte[] unadviseInner = FromHex(
"21 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00");
byte[] expectedUnadvise = FromHex(
"01 00 25 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 " +
"21 01 00 c0 ca 9c cd 32 65 b0 46 a5 85 a5 83 b2 e7 7a 5d 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 03 00 00 00");
AssertBytes(
"generated unadvise transfer",
expectedUnadvise,
NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId: 0x7ffb,
targetGalaxyId: 1,
targetPlatformId: 1,
targetEngineId: 2,
unadviseInner));
}
static void RunReferenceRegistrationMessageTests()
{
byte[] observedNormal = FromHex(
"10 01 00 02 00 00 00 35 bd 0b 74 63 e8 54 41 ad 3a a0 68 61 a6 f9 9c ff ff 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10 00 00 81 54 00 65 00 73 00 74 00 49 00 6e 00 74 00 00 00 00 00 00 00 00 00 00 00 20 00 00 00 54 00 65 00 73 00 74 00 43 00 68 00 69 00 6c 00 64 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01");
var normal = NmxReferenceRegistrationMessage.Parse(observedNormal);
AssertEqual("reference registration handle", 2, normal.ItemHandle);
AssertEqual("reference registration guid", new Guid("740bbd35-e863-4154-ad3a-a06861a6f99c"), normal.ItemCorrelationId);
AssertEqual("reference registration item", "TestInt", normal.ItemDefinition);
AssertEqual("reference registration context", "TestChildObject", normal.ItemContext);
AssertEqual("reference registration subscribe", true, normal.Subscribe);
AssertBytes("reference registration encode", observedNormal, normal.Encode());
byte[] observedBuffered = FromHex(
"10 01 00 01 00 00 00 90 29 63 fc 46 69 16 4d bc 65 19 f6 d3 06 07 37 ff ff 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 32 00 00 81 54 00 65 00 73 00 74 00 49 00 6e 00 74 00 2e 00 70 00 72 00 6f 00 70 00 65 00 72 00 74 00 79 00 28 00 62 00 75 00 66 00 66 00 65 00 72 00 29 00 00 00 00 00 00 00 00 00 00 00 20 00 00 00 54 00 65 00 73 00 74 00 43 00 68 00 69 00 6c 00 64 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01");
var buffered = NmxReferenceRegistrationMessage.Parse(observedBuffered);
AssertEqual("buffered reference registration handle", 1, buffered.ItemHandle);
AssertEqual("buffered reference registration guid", new Guid("fc632990-6946-4d16-bc65-19f6d3060737"), buffered.ItemCorrelationId);
AssertEqual("buffered reference registration item", "TestInt.property(buffer)", buffered.ItemDefinition);
AssertEqual("buffered reference registration context", "TestChildObject", buffered.ItemContext);
AssertEqual("buffered helper", "TestInt.property(buffer)", NmxReferenceRegistrationMessage.ToBufferedItemDefinition("TestInt"));
AssertBytes("buffered reference registration encode", observedBuffered, buffered.Encode());
byte[] observedBufferedWithContext = FromHex(
"10 01 00 01 00 00 00 66 9b 96 d6 a8 42 20 4b 9e 9b dc 3e 2b 84 4e 21 ff ff 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 44 00 00 81 54 00 65 00 73 00 74 00 48 00 69 00 73 00 74 00 6f 00 72 00 79 00 56 00 61 00 6c 00 75 00 65 00 2e 00 70 00 72 00 6f 00 70 00 65 00 72 00 74 00 79 00 28 00 62 00 75 00 66 00 66 00 65 00 72 00 29 00 00 00 00 00 00 00 00 00 00 00 20 00 00 00 54 00 65 00 73 00 74 00 4d 00 61 00 63 00 68 00 69 00 6e 00 65 00 5f 00 30 00 30 00 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01");
var bufferedWithContext = NmxReferenceRegistrationMessage.Parse(observedBufferedWithContext);
AssertEqual("context buffered registration handle", 1, bufferedWithContext.ItemHandle);
AssertEqual("context buffered registration guid", new Guid("d6969b66-42a8-4b20-9e9b-dc3e2b844e21"), bufferedWithContext.ItemCorrelationId);
AssertEqual("context buffered registration item", "TestHistoryValue.property(buffer)", bufferedWithContext.ItemDefinition);
AssertEqual("context buffered registration context", "TestMachine_001", bufferedWithContext.ItemContext);
AssertBytes("context buffered registration encode", observedBufferedWithContext, bufferedWithContext.Encode());
byte[] transfer = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.ItemControl,
localEngineId: 0x7ff6,
targetGalaxyId: 1,
targetPlatformId: 1,
targetEngineId: 2,
observedBuffered);
AssertEqual("buffered registration transfer kind", NmxTransferMessageKind.ItemControl, NmxTransferEnvelope.Parse(transfer).MessageKind);
byte[] observedNormalResult = FromHex(
"11 01 00 02 00 00 00 35 bd 0b 74 63 e8 54 41 ad 3a a0 68 61 a6 f9 9c 00 a0 41 c3 55 bd dc 01 80 c1 75 25 a5 bd dc 01 01 08 56 00 00 00 10 00 00 81 54 00 65 00 73 00 74 00 49 00 6e 00 74 00 00 00 02 00 00 00 00 00 00 00 00 00 20 00 00 00 54 00 65 00 73 00 74 00 43 00 68 00 69 00 6c 00 64 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00");
var normalResult = NmxReferenceRegistrationResultMessage.Parse(observedNormalResult);
AssertEqual("normal result handle", 2, normalResult.ItemHandle);
AssertEqual("normal result item", "TestInt", normalResult.ItemDefinition);
AssertEqual("normal result type", 2, normalResult.MxDataType);
AssertEqual("normal result context", "TestChildObject", normalResult.ItemContext);
AssertEqual("normal result status category byte", (byte)1, normalResult.StatusCategory);
AssertEqual("normal result status detail byte", (byte)8, normalResult.StatusDetail);
byte[] observedBufferedResult = FromHex(
"11 01 00 01 00 00 00 90 29 63 fc 46 69 16 4d bc 65 19 f6 d3 06 07 37 00 a0 41 c3 55 bd dc 01 80 c1 75 25 a5 bd dc 01 01 08 78 00 00 00 32 00 00 81 54 00 65 00 73 00 74 00 49 00 6e 00 74 00 2e 00 70 00 72 00 6f 00 70 00 65 00 72 00 74 00 79 00 28 00 62 00 75 00 66 00 66 00 65 00 72 00 29 00 00 00 02 00 00 00 00 00 00 00 00 00 20 00 00 00 54 00 65 00 73 00 74 00 43 00 68 00 69 00 6c 00 64 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00");
var bufferedResult = NmxReferenceRegistrationResultMessage.Parse(observedBufferedResult);
AssertEqual("buffered result handle", 1, bufferedResult.ItemHandle);
AssertEqual("buffered result item", "TestInt.property(buffer)", bufferedResult.ItemDefinition);
AssertEqual("buffered result type", 2, bufferedResult.MxDataType);
AssertEqual("buffered result context", "TestChildObject", bufferedResult.ItemContext);
byte[] observedBufferedContextResult = FromHex(
"11 01 00 01 00 00 00 66 9b 96 d6 a8 42 20 4b 9e 9b dc 3e 2b 84 4e 21 00 a0 41 c3 55 bd dc 01 80 c1 75 25 a5 bd dc 01 01 08 8a 00 00 00 44 00 00 81 54 00 65 00 73 00 74 00 48 00 69 00 73 00 74 00 6f 00 72 00 79 00 56 00 61 00 6c 00 75 00 65 00 2e 00 70 00 72 00 6f 00 70 00 65 00 72 00 74 00 79 00 28 00 62 00 75 00 66 00 66 00 65 00 72 00 29 00 00 00 02 00 00 00 00 00 00 00 00 00 20 00 00 00 54 00 65 00 73 00 74 00 4d 00 61 00 63 00 68 00 69 00 6e 00 65 00 5f 00 30 00 30 00 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00");
var bufferedContextResult = NmxReferenceRegistrationResultMessage.Parse(observedBufferedContextResult);
AssertEqual("context buffered result handle", 1, bufferedContextResult.ItemHandle);
AssertEqual("context buffered result item", "TestHistoryValue.property(buffer)", bufferedContextResult.ItemDefinition);
AssertEqual("context buffered result type", 2, bufferedContextResult.MxDataType);
AssertEqual("context buffered result context", "TestMachine_001", bufferedContextResult.ItemContext);
}
static void RunProtectedWriteGenerationTests()
{
byte[] securedWrite = FromHex(
"37 01 00 06 00 08 f4 02 00 a6 00 0a 00 bb 67 00 00 01 ff ff ff 00 00 00 00 00 00 00 00 a1 fa d6 08 01 00 00 00");
var securedHandle = HandleFromObservedWriteBody(securedWrite);
AssertEqual("secured object signature", (ushort)0xf408, securedHandle.ObjectSignature);
AssertEqual("secured attr signature", (ushort)0x67bb, securedHandle.AttributeSignature);
AssertEqual("secured object signature crc", securedHandle.ObjectSignature, MxReferenceHandle.ComputeNameSignature("TestMachine_001"));
AssertEqual("secured attr signature crc", securedHandle.AttributeSignature, MxReferenceHandle.ComputeNameSignature("ProtectedValue"));
AssertBytes(
"secured generated bool write",
securedWrite,
NmxWriteMessage.Encode(
securedHandle,
MxValueKind.Boolean,
true,
writeIndex: 1,
clientToken: BinaryPrimitives.ReadUInt32LittleEndian(securedWrite.AsSpan(securedWrite.Length - 8, sizeof(uint)))));
byte[] verifiedWrite = FromHex(
"37 01 00 06 00 08 f4 02 00 a7 00 0a 00 26 8a 00 00 01 ff ff ff 00 00 00 00 00 00 00 00 67 64 d7 08 01 00 00 00");
var verifiedHandle = HandleFromObservedWriteBody(verifiedWrite);
AssertEqual("verified object signature", (ushort)0xf408, verifiedHandle.ObjectSignature);
AssertEqual("verified attr signature", (ushort)0x8a26, verifiedHandle.AttributeSignature);
AssertEqual("verified attr signature crc", verifiedHandle.AttributeSignature, MxReferenceHandle.ComputeNameSignature("ProtectedValue1"));
AssertBytes(
"verified generated bool write",
verifiedWrite,
NmxWriteMessage.Encode(
verifiedHandle,
MxValueKind.Boolean,
true,
writeIndex: 1,
clientToken: BinaryPrimitives.ReadUInt32LittleEndian(verifiedWrite.AsSpan(verifiedWrite.Length - 8, sizeof(uint)))));
}
static void RunSecuredWrite2GenerationTests()
{
byte[] secured = FromHex(
"38 01 00 06 00 08 f4 02 00 a6 00 0a 00 bb 67 00 00 01 ff 00 00 00 f6 bf e8 1b d5 dc 01 07 b9 a9 f4 72 6e ae 48 83 b5 bb de 91 8c 89 0f 7e 00 00 00 4d 00 78 00 46 00 72 00 69 00 64 00 61 00 54 00 72 00 61 00 63 00 65 00 2d 00 31 00 31 00 35 00 2d 00 66 00 72 00 69 00 64 00 61 00 2d 00 77 00 72 00 69 00 74 00 65 00 2d 00 73 00 65 00 63 00 75 00 72 00 65 00 64 00 32 00 2d 00 61 00 75 00 74 00 68 00 2d 00 70 00 72 00 6f 00 74 00 65 00 63 00 74 00 65 00 64 00 76 00 61 00 6c 00 75 00 65 00 2d 00 74 00 72 00 75 00 65 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 92 f4 cc 0c 01 00 00 00");
AssertSecuredWrite2(
"secured WriteSecured2 bool",
secured,
MxValueKind.Boolean,
true,
"MxFridaTrace-115-frida-write-secured2-auth-protectedvalue-true",
verifierToken: new byte[NmxSecuredWrite2Message.AuthenticatorTokenLength]);
byte[] verified = FromHex(
"38 01 00 06 00 08 f4 02 00 a7 00 0a 00 26 8a 00 00 01 ff 00 00 00 41 76 50 1c d5 dc 01 07 b9 a9 f4 72 6e ae 48 83 b5 bb de 91 8c 89 0f 88 00 00 00 4d 00 78 00 46 00 72 00 69 00 64 00 61 00 54 00 72 00 61 00 63 00 65 00 2d 00 31 00 31 00 36 00 2d 00 66 00 72 00 69 00 64 00 61 00 2d 00 77 00 72 00 69 00 74 00 65 00 2d 00 73 00 65 00 63 00 75 00 72 00 65 00 64 00 32 00 2d 00 61 00 75 00 74 00 68 00 2d 00 76 00 65 00 72 00 69 00 66 00 69 00 65 00 64 00 2d 00 70 00 72 00 6f 00 74 00 65 00 63 00 74 00 65 00 64 00 76 00 61 00 6c 00 75 00 65 00 31 00 00 00 07 b9 a9 f4 72 6e ae 48 83 b5 bb de 91 8c 89 0f ff ff 1d 9d cf 0c 01 00 00 00");
AssertSecuredWrite2(
"verified WriteSecured2 bool",
verified,
MxValueKind.Boolean,
true,
"MxFridaTrace-116-frida-write-secured2-auth-verified-protectedvalue1",
verifierToken: NmxSecuredWrite2Message.ObservedAuthenticatedUserToken);
byte[] securedInt = FromHex(
"38 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 09 03 00 00 00 00 80 8d 64 10 1f d5 dc 01 07 b9 a9 f4 72 6e ae 48 83 b5 bb de 91 8c 89 0f 66 00 00 00 4d 00 78 00 46 00 72 00 69 00 64 00 61 00 54 00 72 00 61 00 63 00 65 00 2d 00 31 00 31 00 37 00 2d 00 66 00 72 00 69 00 64 00 61 00 2d 00 77 00 72 00 69 00 74 00 65 00 2d 00 73 00 65 00 63 00 75 00 72 00 65 00 64 00 32 00 2d 00 61 00 75 00 74 00 68 00 2d 00 74 00 65 00 73 00 74 00 69 00 6e 00 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff 2a a4 e1 0c 01 00 00 00");
AssertSecuredWrite2(
"secured WriteSecured2 int",
securedInt,
MxValueKind.Int32,
777,
"MxFridaTrace-117-frida-write-secured2-auth-testint",
verifierToken: new byte[NmxSecuredWrite2Message.AuthenticatorTokenLength]);
}
static void AssertSecuredWrite2(
string name,
byte[] observed,
MxValueKind valueKind,
object value,
string clientName,
ReadOnlySpan<byte> verifierToken)
{
var handle = HandleFromObservedWriteBody(observed);
int prefixLength = valueKind == MxValueKind.Boolean ? 21 : 24;
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(observed.AsSpan(prefixLength, sizeof(long)));
uint clientToken = BinaryPrimitives.ReadUInt32LittleEndian(observed.AsSpan(observed.Length - 8, sizeof(uint)));
int writeIndex = BinaryPrimitives.ReadInt32LittleEndian(observed.AsSpan(observed.Length - sizeof(int), sizeof(int)));
byte[] generated = NmxSecuredWrite2Message.Encode(
handle,
valueKind,
value,
DateTime.FromFileTime(fileTime),
clientName,
NmxSecuredWrite2Message.ObservedAuthenticatedUserToken,
verifierToken,
writeIndex,
clientToken);
AssertBytes(name, observed, generated);
}
static void RunItemControlRoundTrip(
string name,
string hex,
NmxItemControlCommand command,
Guid correlationId,
ushort objectId,
ushort objectSignature,
short primitiveId,
short attributeId,
short propertyId,
ushort attributeSignature,
short attributeIndex)
{
byte[] observed = FromHex(hex);
var parsed = NmxItemControlMessage.Parse(observed);
AssertEqual(name + " command", command, parsed.Command);
AssertEqual(name + " correlation", correlationId, parsed.ItemCorrelationId);
AssertEqual(name + " object id", objectId, parsed.ObjectId);
AssertEqual(name + " object signature", objectSignature, parsed.ObjectSignature);
AssertEqual(name + " primitive id", primitiveId, parsed.PrimitiveId);
AssertEqual(name + " attribute id", attributeId, parsed.AttributeId);
AssertEqual(name + " property id", propertyId, parsed.PropertyId);
AssertEqual(name + " attribute signature", attributeSignature, parsed.AttributeSignature);
AssertEqual(name + " attribute index", attributeIndex, parsed.AttributeIndex);
AssertEqual(name + " tail", 3u, parsed.Tail);
AssertBytes(name + " encode", observed, parsed.Encode());
}
static void AssertCallbackValue(string name, byte[] processDataBody, object expected)
{
var callback = NmxSubscriptionMessage.ParseProcessDataReceivedBody(processDataBody);
AssertEqual(name + " record count", 1, callback.Records.Count);
AssertEqual(name + " quality", (ushort)0x00c0, callback.Records[0].Quality);
AssertValue(name + " value", expected, callback.Records[0].Value!);
}
static byte[] FromHex(string hex)
{
string[] parts = hex.Split(' ', StringSplitOptions.RemoveEmptyEntries);
byte[] bytes = new byte[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
bytes[i] = Convert.ToByte(parts[i], 16);
}
return bytes;
}
static MxReferenceHandle HandleFromObservedWriteBody(ReadOnlySpan<byte> body)
{
return new MxReferenceHandle(
GalaxyId: 1,
PlatformId: 1,
EngineId: 2,
ObjectId: BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(3, sizeof(ushort))),
ObjectSignature: BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(5, sizeof(ushort))),
PrimitiveId: BinaryPrimitives.ReadInt16LittleEndian(body.Slice(7, sizeof(short))),
AttributeId: BinaryPrimitives.ReadInt16LittleEndian(body.Slice(9, sizeof(short))),
PropertyId: BinaryPrimitives.ReadInt16LittleEndian(body.Slice(11, sizeof(short))),
AttributeSignature: BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(13, sizeof(ushort))),
AttributeIndex: BinaryPrimitives.ReadInt16LittleEndian(body.Slice(15, sizeof(short))));
}
static void AssertValue(string name, object expected, object actual)
{
bool equals = expected switch
{
float f => actual is float a && Math.Abs(f - a) < 0.0001f,
double d => actual is double a && Math.Abs(d - a) < 0.0000001d,
DateTime d => actual is DateTime a && d == a,
bool[] expectedValues => actual is bool[] actualValues && expectedValues.SequenceEqual(actualValues),
int[] expectedValues => actual is int[] actualValues && expectedValues.SequenceEqual(actualValues),
float[] expectedValues => actual is float[] actualValues && expectedValues.Zip(actualValues, static (e, a) => Math.Abs(e - a) < 0.0001f).All(static x => x),
double[] expectedValues => actual is double[] actualValues && expectedValues.Zip(actualValues, static (e, a) => Math.Abs(e - a) < 0.0000001d).All(static x => x),
string[] expectedValues => actual is string[] actualValues && expectedValues.SequenceEqual(actualValues),
DateTime[] expectedValues => actual is DateTime[] actualValues && expectedValues.SequenceEqual(actualValues),
_ => Equals(expected, actual),
};
if (!equals)
{
throw new InvalidOperationException($"{name}: expected decoded value {expected}, got {actual}.");
}
}
static void AssertEqual<T>(string name, T expected, T actual)
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
{
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
}
}
static void AssertBytes(string name, ReadOnlySpan<byte> expected, ReadOnlySpan<byte> actual)
{
if (!expected.SequenceEqual(actual))
{
int diff = 0;
while (diff < expected.Length && diff < actual.Length && expected[diff] == actual[diff])
{
diff++;
}
string expectedWindow = Convert.ToHexString(expected.Slice(diff, Math.Min(24, expected.Length - diff))).ToLowerInvariant();
string actualWindow = Convert.ToHexString(actual.Slice(diff, Math.Min(24, actual.Length - diff))).ToLowerInvariant();
throw new InvalidOperationException($"{name}: encoded bytes do not match observed bytes at offset {diff}; expected length {expected.Length}, actual length {actual.Length}; expected {expectedWindow}; actual {actualWindow}.");
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace MxNativeCodec;
public enum MxDataType : short
{
Unknown = -1,
NoData = 0,
Boolean = 1,
Integer = 2,
Float = 3,
Double = 4,
String = 5,
Time = 6,
ElapsedTime = 7,
ReferenceType = 8,
StatusType = 9,
Enum = 10,
SecurityClassificationEnum = 11,
DataQualityType = 12,
QualifiedEnum = 13,
QualifiedStruct = 14,
InternationalizedString = 15,
BigString = 16,
End = 17,
}
+8
View File
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
+120
View File
@@ -0,0 +1,120 @@
using System.Buffers.Binary;
namespace MxNativeCodec;
public readonly record struct MxReferenceHandle(
byte GalaxyId,
ushort PlatformId,
ushort EngineId,
ushort ObjectId,
ushort ObjectSignature,
short PrimitiveId,
short AttributeId,
short PropertyId,
ushort AttributeSignature,
short AttributeIndex)
{
public const int EncodedLength = 20;
private const ushort Crc16IbmPolynomial = 0xa001;
public ushort Reserved0 => 0;
public static MxReferenceHandle Create(
byte galaxyId,
ushort platformId,
ushort engineId,
ushort objectId,
string objectTagName,
short primitiveId,
short attributeId,
short propertyId,
string attributeName,
bool isArray)
{
return new MxReferenceHandle(
galaxyId,
platformId,
engineId,
objectId,
ComputeNameSignature(objectTagName),
primitiveId,
attributeId,
propertyId,
ComputeNameSignature(attributeName),
isArray ? (short)-1 : (short)0);
}
public static ushort ComputeNameSignature(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ushort crc = 0;
foreach (char character in name.ToLowerInvariant())
{
crc = UpdateCrc16Ibm(crc, (byte)character);
crc = UpdateCrc16Ibm(crc, (byte)(character >> 8));
}
return crc;
}
public static MxReferenceHandle Parse(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != EncodedLength)
{
throw new ArgumentException($"MX reference handle must be {EncodedLength} bytes.", nameof(bytes));
}
return new MxReferenceHandle(
GalaxyId: bytes[0],
PlatformId: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(2, sizeof(ushort))),
EngineId: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(4, sizeof(ushort))),
ObjectId: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(6, sizeof(ushort))),
ObjectSignature: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(8, sizeof(ushort))),
PrimitiveId: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(10, sizeof(short))),
AttributeId: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(12, sizeof(short))),
PropertyId: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(14, sizeof(short))),
AttributeSignature: BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(16, sizeof(ushort))),
AttributeIndex: BinaryPrimitives.ReadInt16LittleEndian(bytes.Slice(18, sizeof(short))));
}
public byte[] Encode()
{
byte[] bytes = new byte[EncodedLength];
WriteTo(bytes);
return bytes;
}
public void WriteTo(Span<byte> destination)
{
if (destination.Length < EncodedLength)
{
throw new ArgumentException($"Destination must be at least {EncodedLength} bytes.", nameof(destination));
}
destination[0] = GalaxyId;
destination[1] = 0;
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(2, sizeof(ushort)), PlatformId);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(4, sizeof(ushort)), EngineId);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(6, sizeof(ushort)), ObjectId);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(8, sizeof(ushort)), ObjectSignature);
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(10, sizeof(short)), PrimitiveId);
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(12, sizeof(short)), AttributeId);
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(14, sizeof(short)), PropertyId);
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(16, sizeof(ushort)), AttributeSignature);
BinaryPrimitives.WriteInt16LittleEndian(destination.Slice(18, sizeof(short)), AttributeIndex);
}
private static ushort UpdateCrc16Ibm(ushort crc, byte value)
{
crc ^= value;
for (int bit = 0; bit < 8; bit++)
{
crc = (ushort)(((crc & 1) != 0)
? ((crc >> 1) ^ Crc16IbmPolynomial)
: (crc >> 1));
}
return crc;
}
}
+126
View File
@@ -0,0 +1,126 @@
namespace MxNativeCodec;
public enum MxStatusCategory : short
{
Unknown = -1,
Ok = 0,
Pending = 1,
Warning = 2,
CommunicationError = 3,
ConfigurationError = 4,
OperationalError = 5,
SecurityError = 6,
SoftwareError = 7,
OtherError = 8,
}
public enum MxStatusSource : short
{
Unknown = -1,
RequestingLmx = 0,
RespondingLmx = 1,
RequestingNmx = 2,
RespondingNmx = 3,
RequestingAutomationObject = 4,
RespondingAutomationObject = 5,
}
public sealed record MxStatus(
short Success,
MxStatusCategory Category,
MxStatusSource DetectedBy,
short Detail)
{
public string? DetailText => MxStatusDetails.GetKnownText(Detail);
public static MxStatus DataChangeOk { get; } = new(
Success: -1,
Category: MxStatusCategory.Ok,
DetectedBy: MxStatusSource.RequestingLmx,
Detail: 0);
public static MxStatus WriteCompleteOk { get; } = new(
Success: -1,
Category: MxStatusCategory.Ok,
DetectedBy: MxStatusSource.RespondingAutomationObject,
Detail: 0);
public static MxStatus SuspendPending { get; } = new(
Success: -1,
Category: MxStatusCategory.Pending,
DetectedBy: MxStatusSource.RequestingLmx,
Detail: 0);
public static MxStatus ActivateOk { get; } = new(
Success: -1,
Category: MxStatusCategory.Ok,
DetectedBy: MxStatusSource.RequestingLmx,
Detail: 0);
public static MxStatus InvalidReferenceConfiguration { get; } = new(
Success: 0,
Category: MxStatusCategory.ConfigurationError,
DetectedBy: MxStatusSource.RequestingLmx,
Detail: 6);
}
public static class MxStatusDetails
{
private static readonly IReadOnlyDictionary<short, string> KnownDetails = new Dictionary<short, string>
{
[16] = "Request timed out",
[17] = "Platform communication error",
[18] = "Invalid platform ID",
[19] = "Invalid engine ID",
[20] = "Engine communication error",
[21] = "Invalid reference",
[22] = "No Galaxy Repository",
[23] = "Invalid object ID",
[24] = "Object signature mismatch",
[25] = "Invalid primitive ID",
[26] = "Invalid attribute ID",
[27] = "Invalid property ID",
[28] = "Index out of range",
[29] = "Data out of range",
[30] = "Incorrect data type",
[31] = "Attribute not readable",
[32] = "Attribute not writeable",
[33] = "Write access denied",
[34] = "Unknown error",
[35] = "detected by",
[36] = "Wrong data type",
[37] = "Wrong number of dimensions",
[38] = "Invalid index",
[39] = "Index out of order",
[40] = "Dimension does not exist",
[41] = "Conversion not supported",
[42] = "Unable to convert string",
[43] = "Overflow",
[44] = "Attribute signature mismatch",
[45] = "Resolving local portion of reference",
[46] = "Resolving global portion of reference",
[47] = "Nmx version mismatch",
[48] = "Nmx command not valid",
[49] = "Lmx version mismatch",
[50] = "Lmx command not valid",
[51] = "However, the object could not be put On Scan - Permission to modify \"Operate\" attributes is required",
[52] = "Unable to resolve reference for 'set' request because Galaxy Repository is busy performing a 'Deploy/Undeploy' operation",
[53] = "Too many outstanding pending requests to engine",
[54] = "Object Initializing",
[55] = "Engine Initializing",
[56] = "Secured Write",
[57] = "Verified Write",
[58] = "No Alarm Ack Privilege",
[59] = "Alarm Acked Already",
[60] = "User did not have the necessary permissions to write",
[61] = "Verifier did not have the necessary permissions to verify",
[541] = "Conversion to intended data type is not supported",
[542] = "Unable to convert the input string to intended data type",
[8017] = "Object must be offscan to modify attributes that have an MxSecurityConfigure security classification",
};
public static string? GetKnownText(short detail)
{
return KnownDetails.TryGetValue(detail, out string? text) ? text : null;
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace MxNativeCodec;
public enum MxValueKind
{
Boolean,
Int32,
Float32,
Float64,
String,
DateTime,
ElapsedTime,
BooleanArray,
Int32Array,
Float32Array,
Float64Array,
StringArray,
DateTimeArray,
}
+154
View File
@@ -0,0 +1,154 @@
using System.Buffers.Binary;
namespace MxNativeCodec;
public enum NmxItemControlCommand : byte
{
Advise = 0x1f,
AdviseSupervisory = 0x1f,
UnAdvise = 0x21,
}
public sealed record NmxItemControlMessage(
NmxItemControlCommand Command,
Guid ItemCorrelationId,
ushort ObjectId,
ushort ObjectSignature,
short PrimitiveId,
short AttributeId,
short PropertyId,
ushort AttributeSignature,
short AttributeIndex,
uint Tail)
{
private const ushort Version = 1;
private const int HeaderLength = 3;
private const int GuidLength = 16;
private const int AdviseExtraLength = 2;
private const int PayloadLength = 18;
public static int GetEncodedLength(NmxItemControlCommand command)
{
return HeaderLength
+ GuidLength
+ (command == NmxItemControlCommand.AdviseSupervisory ? AdviseExtraLength : 0)
+ PayloadLength;
}
public static NmxItemControlMessage Parse(ReadOnlySpan<byte> body)
{
if (body.Length < HeaderLength + GuidLength + PayloadLength)
{
throw new ArgumentException("NMX item-control body is too short.", nameof(body));
}
var command = (NmxItemControlCommand)body[0];
if (command is not (NmxItemControlCommand.AdviseSupervisory or NmxItemControlCommand.UnAdvise))
{
throw new ArgumentException($"Unsupported item-control command 0x{body[0]:X2}.", nameof(body));
}
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(1, sizeof(ushort)));
if (version != Version)
{
throw new ArgumentException($"Unsupported item-control version {version}.", nameof(body));
}
int expectedLength = GetEncodedLength(command);
if (body.Length != expectedLength)
{
throw new ArgumentException($"Unexpected item-control body length {body.Length}; expected {expectedLength}.", nameof(body));
}
int offset = HeaderLength;
var itemCorrelationId = new Guid(body.Slice(offset, GuidLength));
offset += GuidLength;
if (command == NmxItemControlCommand.AdviseSupervisory)
{
offset += AdviseExtraLength;
}
return new NmxItemControlMessage(
command,
itemCorrelationId,
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset, sizeof(ushort))),
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset + 2, sizeof(ushort))),
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 4, sizeof(short))),
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 6, sizeof(short))),
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 8, sizeof(short))),
BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset + 10, sizeof(ushort))),
BinaryPrimitives.ReadInt16LittleEndian(body.Slice(offset + 12, sizeof(short))),
BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(offset + 14, sizeof(uint))));
}
public static NmxItemControlMessage FromReferenceHandle(
NmxItemControlCommand command,
Guid itemCorrelationId,
MxReferenceHandle referenceHandle,
uint tail = 3)
{
return new NmxItemControlMessage(
command,
itemCorrelationId,
referenceHandle.ObjectId,
referenceHandle.ObjectSignature,
referenceHandle.PrimitiveId,
referenceHandle.AttributeId,
referenceHandle.PropertyId,
referenceHandle.AttributeSignature,
referenceHandle.AttributeIndex,
tail);
}
public MxReferenceHandle ToReferenceHandle(
byte galaxyId = 1,
ushort platformId = 1,
ushort engineId = 1)
{
return new MxReferenceHandle(
galaxyId,
platformId,
engineId,
ObjectId,
ObjectSignature,
PrimitiveId,
AttributeId,
PropertyId,
AttributeSignature,
AttributeIndex);
}
public byte[] Encode()
{
byte[] body = new byte[GetEncodedLength(Command)];
body[0] = (byte)Command;
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
int offset = HeaderLength;
ItemCorrelationId.TryWriteBytes(body.AsSpan(offset, GuidLength));
offset += GuidLength;
if (Command == NmxItemControlCommand.AdviseSupervisory)
{
offset += AdviseExtraLength;
}
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset, sizeof(ushort)), ObjectId);
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset + 2, sizeof(ushort)), ObjectSignature);
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 4, sizeof(short)), PrimitiveId);
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 6, sizeof(short)), AttributeId);
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 8, sizeof(short)), PropertyId);
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset + 10, sizeof(ushort)), AttributeSignature);
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset + 12, sizeof(short)), AttributeIndex);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset + 14, sizeof(uint)), Tail);
return body;
}
public NmxItemControlMessage ToUnAdvise()
{
return this with { Command = NmxItemControlCommand.UnAdvise };
}
public NmxItemControlMessage ToAdviseSupervisory()
{
return this with { Command = NmxItemControlCommand.AdviseSupervisory };
}
}
@@ -0,0 +1,15 @@
namespace MxNativeCodec;
public static class NmxMetadataQueryMessage
{
private const int PreAdviseCorrelationOffset = 0x8a;
public static byte[] EncodeObservedPreAdvise(Guid itemCorrelationId)
{
byte[] body = Convert.FromHexString(
"17010001010001000000650071000a0000000000086a0000004000008144006500760050006c006100740066006f0072006d002e00470052002e00540069006d0065004f0066004c006100730074004400650070006c006f00790000000200000000000200000000000200000000000101000100010000000000000000000000000001d0fc40091f0100c0ca9ccd3265b046a585a583b2e77a5d000001000000" +
"17010001010001000000650071000a000000000008760000004c00008144006500760050006c006100740066006f0072006d002e00470052002e00540069006d0065004f0066004c0061007300740043006f006e006600690067004300680061006e0067006500000002000000000002000000000002000000000001010001000100000000000000000000000000015003410920010002000000");
itemCorrelationId.TryWriteBytes(body.AsSpan(PreAdviseCorrelationOffset, 16));
return body;
}
}
+192
View File
@@ -0,0 +1,192 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeCodec;
public sealed record NmxObservedEnvelope(
bool HasLengthPrefix,
int? TotalLengthPrefix,
int DeclaredInnerLength,
int ActualInnerLength,
ReadOnlyMemory<byte> Header,
ReadOnlyMemory<byte> InnerBody)
{
public const int HeaderLength = 46;
public const int InnerLengthOffset = 2;
public static NmxObservedEnvelope ParseTransferDataBody(ReadOnlyMemory<byte> body)
{
if (body.Length < HeaderLength)
{
throw new ArgumentException("NMX TransferData body is too short.", nameof(body));
}
int declaredInnerLength = BinaryPrimitives.ReadInt32LittleEndian(body.Span.Slice(InnerLengthOffset, sizeof(int)));
int actualInnerLength = body.Length - HeaderLength;
if (declaredInnerLength != actualInnerLength)
{
throw new ArgumentException("NMX TransferData inner length does not match body size.", nameof(body));
}
return new NmxObservedEnvelope(
false,
null,
declaredInnerLength,
actualInnerLength,
body[..HeaderLength],
body.Slice(HeaderLength, actualInnerLength));
}
public static NmxObservedEnvelope ParseProcessDataReceivedBody(ReadOnlyMemory<byte> body)
{
if (body.Length < sizeof(int) + HeaderLength)
{
throw new ArgumentException("NMX ProcessDataReceived body is too short.", nameof(body));
}
int totalLengthPrefix = BinaryPrimitives.ReadInt32LittleEndian(body.Span[..sizeof(int)]);
if (totalLengthPrefix != body.Length)
{
throw new ArgumentException("NMX ProcessDataReceived length prefix does not match body size.", nameof(body));
}
int headerOffset = sizeof(int);
int declaredInnerLength = BinaryPrimitives.ReadInt32LittleEndian(
body.Span.Slice(headerOffset + InnerLengthOffset, sizeof(int)));
int actualInnerLength = declaredInnerLength - sizeof(int);
if (actualInnerLength < 0 || headerOffset + HeaderLength + actualInnerLength != body.Length)
{
throw new ArgumentException("NMX ProcessDataReceived inner length does not match body size.", nameof(body));
}
return new NmxObservedEnvelope(
true,
totalLengthPrefix,
declaredInnerLength,
actualInnerLength,
body.Slice(headerOffset, HeaderLength),
body.Slice(headerOffset + HeaderLength, actualInnerLength));
}
public static NmxObservedEnvelope ParseProcessDataReceivedBodyFlexible(ReadOnlyMemory<byte> body)
{
if (body.Length >= sizeof(int) + HeaderLength)
{
int totalLengthPrefix = BinaryPrimitives.ReadInt32LittleEndian(body.Span[..sizeof(int)]);
if (totalLengthPrefix == body.Length)
{
return ParseProcessDataReceivedBody(body);
}
}
if (body.Length < HeaderLength)
{
throw new ArgumentException("NMX ProcessDataReceived body is too short.", nameof(body));
}
int declaredInnerLength = BinaryPrimitives.ReadInt32LittleEndian(body.Span.Slice(InnerLengthOffset, sizeof(int)));
int actualInnerLength = body.Length - HeaderLength;
if (declaredInnerLength != actualInnerLength)
{
throw new ArgumentException("NMX ProcessDataReceived header inner length does not match body size.", nameof(body));
}
return new NmxObservedEnvelope(
false,
null,
declaredInnerLength,
actualInnerLength,
body[..HeaderLength],
body.Slice(HeaderLength, actualInnerLength));
}
}
public sealed record NmxObservedString(int Offset, string Value);
public sealed record NmxObservedMessage(
byte Command,
string CommandName,
byte VersionMajor,
byte VersionMinor,
Guid? ItemCorrelationId,
IReadOnlyList<NmxObservedString> Strings)
{
public static NmxObservedMessage Parse(ReadOnlySpan<byte> body)
{
if (body.Length < 3)
{
throw new ArgumentException("NMX message body is too short.", nameof(body));
}
byte command = body[0];
Guid? itemCorrelationId = null;
if (command is 0x1f or 0x21 && body.Length >= 19)
{
itemCorrelationId = new Guid(body.Slice(3, 16));
}
return new NmxObservedMessage(
command,
GetCommandName(command),
body[1],
body[2],
itemCorrelationId,
ExtractUtf16Strings(body));
}
private static string GetCommandName(byte command)
{
return command switch
{
0x17 => "MetadataQuery",
0x1f => "AdviseSupervisory",
0x21 => "UnAdvise",
0x32 => "SubscriptionStatus",
0x33 => "DataUpdate",
0x37 => "Write",
0x40 => "MetadataResponse",
_ => $"Unknown0x{command:X2}",
};
}
private static IReadOnlyList<NmxObservedString> ExtractUtf16Strings(ReadOnlySpan<byte> body)
{
List<NmxObservedString> strings = [];
int offset = 0;
while (offset + 8 <= body.Length)
{
int start = offset;
int chars = 0;
while (offset + 1 < body.Length)
{
byte lo = body[offset];
byte hi = body[offset + 1];
if (lo == 0 && hi == 0)
{
break;
}
if (hi != 0 || lo < 0x20 || lo > 0x7e)
{
chars = 0;
break;
}
chars++;
offset += 2;
}
if (chars >= 3 && offset + 1 < body.Length && body[offset] == 0 && body[offset + 1] == 0)
{
string value = Encoding.Unicode.GetString(body.Slice(start, chars * 2));
strings.Add(new NmxObservedString(start, value));
offset += 2;
continue;
}
offset = start + 1;
}
return strings;
}
}
@@ -0,0 +1,77 @@
namespace MxNativeCodec;
public enum NmxOperationStatusFormat
{
CompletionOnly,
StatusWord,
}
public sealed record NmxOperationStatusMessage(
NmxOperationStatusFormat Format,
byte Command,
ushort StatusCode,
byte CompletionCode,
MxStatus Status)
{
public bool IsMxAccessWriteComplete => Format == NmxOperationStatusFormat.StatusWord
&& StatusCode == 0x8050
&& CompletionCode == 0x00;
public static bool TryParseProcessDataReceivedBody(ReadOnlyMemory<byte> body, out NmxOperationStatusMessage message)
{
try
{
var envelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(body);
return TryParseInner(envelope.InnerBody.Span, out message);
}
catch (ArgumentException)
{
message = null!;
return false;
}
}
public static bool TryParseInner(ReadOnlySpan<byte> inner, out NmxOperationStatusMessage message)
{
if (inner.Length == 1)
{
byte completionCode = inner[0];
message = new NmxOperationStatusMessage(
Format: NmxOperationStatusFormat.CompletionOnly,
Command: 0,
StatusCode: 0,
CompletionCode: completionCode,
Status: CreateUnpromotedCompletionStatus(completionCode));
return true;
}
if (inner.Length == 5 && inner[0] == 0x00 && inner[1] == 0x00)
{
ushort statusCode = (ushort)(inner[2] | (inner[3] << 8));
byte completionCode = inner[4];
message = new NmxOperationStatusMessage(
Format: NmxOperationStatusFormat.StatusWord,
Command: inner[0],
StatusCode: statusCode,
CompletionCode: completionCode,
Status: statusCode == 0x8050 && completionCode == 0x00 ? MxStatus.WriteCompleteOk : new MxStatus(
Success: 0,
Category: MxStatusCategory.Unknown,
DetectedBy: MxStatusSource.Unknown,
Detail: completionCode == 0x00 ? unchecked((short)statusCode) : completionCode));
return true;
}
message = null!;
return false;
}
private static MxStatus CreateUnpromotedCompletionStatus(byte completionCode)
{
return new MxStatus(
Success: 0,
Category: MxStatusCategory.Unknown,
DetectedBy: MxStatusSource.Unknown,
Detail: completionCode);
}
}
@@ -0,0 +1,142 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeCodec;
public sealed record NmxReferenceRegistrationMessage(
int ItemHandle,
Guid ItemCorrelationId,
string ItemDefinition,
string ItemContext,
bool Subscribe)
{
private const byte Command = 0x10;
private const ushort Version = 1;
private const int HeaderLength = 55;
private const int ItemStringReservedLength = 8;
private const int TailLength = 20;
public static NmxReferenceRegistrationMessage Parse(ReadOnlySpan<byte> body)
{
if (body.Length < HeaderLength + ItemStringReservedLength + TailLength)
{
throw new ArgumentException("NMX reference-registration body is too short.", nameof(body));
}
if (body[0] != Command)
{
throw new ArgumentException($"Unsupported reference-registration command 0x{body[0]:X2}.", nameof(body));
}
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(1, sizeof(ushort)));
if (version != Version)
{
throw new ArgumentException($"Unsupported reference-registration version {version}.", nameof(body));
}
int itemHandle = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(3, sizeof(int)));
var correlationId = new Guid(body.Slice(7, 16));
int offset = HeaderLength;
string itemDefinition = ReadRegisteredString(body, ref offset, taggedLength: true);
if (body.Slice(offset, ItemStringReservedLength).IndexOfAnyExcept((byte)0) >= 0)
{
throw new ArgumentException("Unexpected nonzero reference-registration item string reserved bytes.", nameof(body));
}
offset += ItemStringReservedLength;
string itemContext = ReadRegisteredString(body, ref offset, taggedLength: false);
if (body.Length - offset != TailLength)
{
throw new ArgumentException($"Unexpected reference-registration tail length {body.Length - offset}.", nameof(body));
}
if (body.Slice(offset, TailLength - 1).IndexOfAnyExcept((byte)0) >= 0)
{
throw new ArgumentException("Unexpected nonzero reference-registration tail bytes.", nameof(body));
}
return new NmxReferenceRegistrationMessage(
itemHandle,
correlationId,
itemDefinition,
itemContext,
body[offset + TailLength - 1] != 0);
}
public byte[] Encode()
{
byte[] itemDefinitionBytes = EncodeNullTerminatedUtf16(ItemDefinition);
byte[] itemContextBytes = EncodeNullTerminatedUtf16(ItemContext);
byte[] body = new byte[
HeaderLength
+ sizeof(int)
+ itemDefinitionBytes.Length
+ ItemStringReservedLength
+ sizeof(int)
+ itemContextBytes.Length
+ TailLength];
body[0] = Command;
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(3, sizeof(int)), ItemHandle);
ItemCorrelationId.TryWriteBytes(body.AsSpan(7, 16));
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(23, sizeof(short)), -1);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(27, sizeof(int)), 1);
int offset = HeaderLength;
WriteRegisteredString(body, ref offset, itemDefinitionBytes, taggedLength: true);
offset += ItemStringReservedLength;
WriteRegisteredString(body, ref offset, itemContextBytes, taggedLength: false);
body[offset + TailLength - 1] = Subscribe ? (byte)1 : (byte)0;
return body;
}
public static string ToBufferedItemDefinition(string itemDefinition)
{
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
return itemDefinition.EndsWith(".property(buffer)", StringComparison.OrdinalIgnoreCase)
? itemDefinition
: itemDefinition + ".property(buffer)";
}
private static string ReadRegisteredString(ReadOnlySpan<byte> body, ref int offset, bool taggedLength)
{
int rawLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
int byteLength = taggedLength ? rawLength & 0x00FF_FFFF : rawLength;
if (taggedLength && (rawLength & unchecked((int)0xFF00_0000)) != unchecked((int)0x8100_0000))
{
throw new ArgumentException("Reference-registration item definition is missing the observed 0x81 string marker.");
}
offset += sizeof(int);
if (byteLength < 2 || byteLength % 2 != 0 || offset + byteLength > body.Length)
{
throw new ArgumentException($"Invalid reference-registration string byte length {byteLength}.");
}
string value = Encoding.Unicode.GetString(body.Slice(offset, byteLength - 2));
if (body[offset + byteLength - 2] != 0 || body[offset + byteLength - 1] != 0)
{
throw new ArgumentException("Reference-registration string is not null terminated.");
}
offset += byteLength;
return value;
}
private static void WriteRegisteredString(byte[] body, ref int offset, byte[] value, bool taggedLength)
{
int rawLength = taggedLength ? value.Length | unchecked((int)0x8100_0000) : value.Length;
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(offset, sizeof(int)), rawLength);
offset += sizeof(int);
value.CopyTo(body.AsSpan(offset));
offset += value.Length;
}
private static byte[] EncodeNullTerminatedUtf16(string value)
{
return Encoding.Unicode.GetBytes(value + '\0');
}
}
@@ -0,0 +1,120 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeCodec;
public sealed record NmxReferenceRegistrationResultMessage(
int ItemHandle,
Guid ItemCorrelationId,
DateTime FirstTimestampUtc,
DateTime SecondTimestampUtc,
byte StatusCategory,
byte StatusDetail,
string ItemDefinition,
int MxDataType,
string ItemContext)
{
private const byte Command = 0x11;
private const ushort Version = 1;
private const int HeaderLength = 45;
private const int BetweenStringsLength = 10;
private const int TailLength = 16;
public static NmxReferenceRegistrationResultMessage Parse(ReadOnlySpan<byte> body)
{
if (body.Length < HeaderLength + TailLength)
{
throw new ArgumentException("NMX reference-registration result body is too short.", nameof(body));
}
if (body[0] != Command)
{
throw new ArgumentException($"Unsupported reference-registration result command 0x{body[0]:X2}.", nameof(body));
}
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(1, sizeof(ushort)));
if (version != Version)
{
throw new ArgumentException($"Unsupported reference-registration result version {version}.", nameof(body));
}
int blockLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(41, sizeof(int)));
if (blockLength != body.Length - 41)
{
throw new ArgumentException("Reference-registration result block length does not match body size.", nameof(body));
}
int offset = HeaderLength;
string itemDefinition = ReadRegisteredString(body, ref offset, taggedLength: true);
int mxDataType = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
offset += sizeof(int);
if (body.Slice(offset, BetweenStringsLength - sizeof(int)).IndexOfAnyExcept((byte)0) >= 0)
{
throw new ArgumentException("Unexpected nonzero reference-registration result reserved bytes.", nameof(body));
}
offset += BetweenStringsLength - sizeof(int);
string itemContext = ReadRegisteredString(body, ref offset, taggedLength: false);
if (body.Length - offset != TailLength)
{
throw new ArgumentException($"Unexpected reference-registration result tail length {body.Length - offset}.", nameof(body));
}
if (body.Slice(offset, TailLength).IndexOfAnyExcept((byte)0) >= 0)
{
throw new ArgumentException("Unexpected nonzero reference-registration result tail bytes.", nameof(body));
}
return new NmxReferenceRegistrationResultMessage(
BinaryPrimitives.ReadInt32LittleEndian(body.Slice(3, sizeof(int))),
new Guid(body.Slice(7, 16)),
DateTime.FromFileTimeUtc(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(23, sizeof(long)))),
DateTime.FromFileTimeUtc(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(31, sizeof(long)))),
body[39],
body[40],
itemDefinition,
mxDataType,
itemContext);
}
public static bool TryParseProcessDataReceivedBody(ReadOnlyMemory<byte> processDataBody, out NmxReferenceRegistrationResultMessage? message)
{
try
{
var envelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(processDataBody);
message = Parse(envelope.InnerBody.Span);
return true;
}
catch (ArgumentException)
{
message = null;
return false;
}
}
private static string ReadRegisteredString(ReadOnlySpan<byte> body, ref int offset, bool taggedLength)
{
int rawLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
int byteLength = taggedLength ? rawLength & 0x00FF_FFFF : rawLength;
if (taggedLength && (rawLength & unchecked((int)0xFF00_0000)) != unchecked((int)0x8100_0000))
{
throw new ArgumentException("Reference-registration result item definition is missing the observed 0x81 string marker.");
}
offset += sizeof(int);
if (byteLength < 2 || byteLength % 2 != 0 || offset + byteLength > body.Length)
{
throw new ArgumentException($"Invalid reference-registration result string byte length {byteLength}.");
}
string value = Encoding.Unicode.GetString(body.Slice(offset, byteLength - 2));
if (body[offset + byteLength - 2] != 0 || body[offset + byteLength - 1] != 0)
{
throw new ArgumentException("Reference-registration result string is not null terminated.");
}
offset += byteLength;
return value;
}
}
@@ -0,0 +1,105 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeCodec;
public static class NmxSecuredWrite2Message
{
public const byte Command = 0x38;
public const ushort Version = 1;
public const int AuthenticatorTokenLength = 16;
public static readonly byte[] ObservedAuthenticatedUserToken =
[
0x07, 0xb9, 0xa9, 0xf4,
0x72, 0x6e, 0xae, 0x48,
0x83, 0xb5, 0xbb, 0xde,
0x91, 0x8c, 0x89, 0x0f,
];
public static byte[] Encode(
MxReferenceHandle referenceHandle,
MxValueKind valueKind,
object value,
DateTime timestamp,
string clientName,
ReadOnlySpan<byte> currentUserToken,
ReadOnlySpan<byte> verifierUserToken,
int writeIndex = 1,
uint clientToken = 0)
{
if (currentUserToken.Length != AuthenticatorTokenLength)
{
throw new ArgumentException("Current user token must be 16 bytes.", nameof(currentUserToken));
}
if (verifierUserToken.Length != AuthenticatorTokenLength)
{
throw new ArgumentException("Verifier user token must be 16 bytes.", nameof(verifierUserToken));
}
byte[] timestampedPrefix = NmxWriteMessage.EncodeTimestamped(
referenceHandle,
valueKind,
value,
timestamp,
writeIndex,
clientToken: 0);
timestampedPrefix[0] = Command;
byte[] clientNameBytes = Encoding.Unicode.GetBytes((clientName ?? string.Empty) + '\0');
int prefixLength = timestampedPrefix.Length - sizeof(uint) - sizeof(int);
byte[] body = new byte[prefixLength + AuthenticatorTokenLength + sizeof(int) + clientNameBytes.Length + AuthenticatorTokenLength + sizeof(short) + sizeof(uint) + sizeof(int)];
timestampedPrefix.AsSpan(0, prefixLength).CopyTo(body);
int offset = prefixLength;
currentUserToken.CopyTo(body.AsSpan(offset, AuthenticatorTokenLength));
offset += AuthenticatorTokenLength;
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(offset, sizeof(int)), clientNameBytes.Length);
offset += sizeof(int);
clientNameBytes.CopyTo(body.AsSpan(offset));
offset += clientNameBytes.Length;
verifierUserToken.CopyTo(body.AsSpan(offset, AuthenticatorTokenLength));
offset += AuthenticatorTokenLength;
BinaryPrimitives.WriteInt16LittleEndian(body.AsSpan(offset, sizeof(short)), -1);
offset += sizeof(short);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset, sizeof(uint)), clientToken);
offset += sizeof(uint);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(offset, sizeof(int)), writeIndex);
return body;
}
public static byte[] EncodeBoolean(
MxReferenceHandle referenceHandle,
bool value,
DateTime timestamp,
string clientName,
ReadOnlySpan<byte> currentUserToken,
ReadOnlySpan<byte> verifierUserToken,
int writeIndex = 1,
uint clientToken = 0)
{
return Encode(
referenceHandle,
MxValueKind.Boolean,
value,
timestamp,
clientName,
currentUserToken,
verifierUserToken,
writeIndex,
clientToken);
}
public static byte[] ResolveObservedUserToken(int userId)
{
return userId == 0
? new byte[AuthenticatorTokenLength]
: ObservedAuthenticatedUserToken.ToArray();
}
public static string FormatToken(ReadOnlySpan<byte> token)
{
return Convert.ToHexString(token).ToLowerInvariant();
}
}
+428
View File
@@ -0,0 +1,428 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeCodec;
public sealed record NmxCallbackValue(
byte WireKind,
MxValueKind? ValueKind,
object? Value,
int EncodedLength);
public sealed record NmxSubscriptionRecord(
int Status,
int? DetailStatus,
ushort Quality,
DateTime TimestampUtc,
byte WireKind,
object? Value,
int Offset,
int Length)
{
public MxStatus ToDataChangeStatus()
{
return MxStatus.DataChangeOk;
}
}
public sealed record NmxSubscriptionMessage(
byte Command,
ushort Version,
int RecordCount,
Guid OperationId,
Guid? ItemCorrelationId,
IReadOnlyList<NmxSubscriptionRecord> Records)
{
public const byte SubscriptionStatusCommand = 0x32;
public const byte DataUpdateCommand = 0x33;
public static NmxSubscriptionMessage ParseProcessDataReceivedBody(ReadOnlyMemory<byte> body)
{
var envelope = NmxObservedEnvelope.ParseProcessDataReceivedBodyFlexible(body);
return ParseInner(envelope.InnerBody.Span);
}
public static NmxSubscriptionMessage ParseInner(ReadOnlySpan<byte> inner)
{
if (inner.Length < 23)
{
throw new ArgumentException("NMX subscription callback body is too short.", nameof(inner));
}
byte command = inner[0];
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(inner.Slice(1, sizeof(ushort)));
int recordCount = BinaryPrimitives.ReadInt32LittleEndian(inner.Slice(3, sizeof(int)));
var operationId = new Guid(inner.Slice(7, 16));
return command switch
{
SubscriptionStatusCommand => ParseSubscriptionStatus(inner, version, recordCount, operationId),
DataUpdateCommand => ParseDataUpdate(inner, version, recordCount, operationId),
_ => throw new ArgumentException($"Unsupported NMX subscription callback command 0x{command:X2}.", nameof(inner)),
};
}
private static NmxSubscriptionMessage ParseDataUpdate(
ReadOnlySpan<byte> inner,
ushort version,
int recordCount,
Guid operationId)
{
if (recordCount != 1)
{
throw new ArgumentException("Observed NMX DataUpdate callback parser currently supports one record per body.", nameof(inner));
}
const int recordOffset = 23;
var record = ParseRecord(inner, recordOffset, hasDetailStatus: false);
return new NmxSubscriptionMessage(
DataUpdateCommand,
version,
recordCount,
operationId,
null,
[record]);
}
private static NmxSubscriptionMessage ParseSubscriptionStatus(
ReadOnlySpan<byte> inner,
ushort version,
int recordCount,
Guid operationId)
{
if (inner.Length < 39)
{
throw new ArgumentException("NMX SubscriptionStatus callback body is too short.", nameof(inner));
}
var itemCorrelationId = new Guid(inner.Slice(23, 16));
int offset = 39;
List<NmxSubscriptionRecord> records = [];
for (int i = 0; i < recordCount; i++)
{
var record = ParseRecord(inner, offset, hasDetailStatus: true);
records.Add(record);
offset += record.Length;
}
return new NmxSubscriptionMessage(
SubscriptionStatusCommand,
version,
recordCount,
operationId,
itemCorrelationId,
records);
}
private static NmxSubscriptionRecord ParseRecord(ReadOnlySpan<byte> body, int offset, bool hasDetailStatus)
{
int minimumLength = hasDetailStatus ? 19 : 15;
if (offset < 0 || offset + minimumLength > body.Length)
{
throw new ArgumentException("NMX subscription record is too short.", nameof(body));
}
int start = offset;
int status = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
offset += sizeof(int);
int? detailStatus = null;
if (hasDetailStatus)
{
detailStatus = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, sizeof(int)));
offset += sizeof(int);
}
ushort quality = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(offset, sizeof(ushort)));
offset += sizeof(ushort);
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(body.Slice(offset, sizeof(long)));
offset += sizeof(long);
byte wireKind = body[offset++];
var value = DecodeValue(wireKind, body[offset..]);
offset += value.EncodedLength;
return new NmxSubscriptionRecord(
status,
detailStatus,
quality,
DateTime.FromFileTimeUtc(fileTime),
wireKind,
value.Value,
start,
offset - start);
}
private static NmxCallbackValue DecodeValue(byte wireKind, ReadOnlySpan<byte> body)
{
if (body.Length == 0)
{
return new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0);
}
return wireKind switch
{
0x01 when body.Length >= 1 => new NmxCallbackValue(wireKind, MxValueKind.Boolean, body[0] != 0, 1),
0x02 when body.Length >= sizeof(int) => new NmxCallbackValue(wireKind, MxValueKind.Int32, BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]), sizeof(int)),
0x03 when body.Length >= sizeof(float) => new NmxCallbackValue(wireKind, MxValueKind.Float32, BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)])), sizeof(float)),
0x04 when body.Length >= sizeof(double) => new NmxCallbackValue(wireKind, MxValueKind.Float64, BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(body[..sizeof(long)])), sizeof(double)),
0x05 => DecodeStringValue(wireKind, body),
0x06 => DecodeDateTimeValue(wireKind, body),
0x07 => DecodeElapsedTimeValue(wireKind, body),
0x41 or 0x42 or 0x43 or 0x44 or 0x45 or 0x46 => DecodeArrayValue(wireKind, body),
_ => new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0),
};
}
private static NmxCallbackValue DecodeStringValue(byte wireKind, ReadOnlySpan<byte> body)
{
if (body.Length < sizeof(int))
{
return new NmxCallbackValue(wireKind, MxValueKind.String, null, 0);
}
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]);
if (recordLength == sizeof(int))
{
return new NmxCallbackValue(wireKind, MxValueKind.String, string.Empty, sizeof(int));
}
if (body.Length < 8)
{
return new NmxCallbackValue(wireKind, MxValueKind.String, null, 0);
}
int textByteLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(4, sizeof(int)));
if (recordLength < 8 || textByteLength < 0 || recordLength != textByteLength + 4 || body.Length < 8 + textByteLength)
{
return new NmxCallbackValue(wireKind, MxValueKind.String, null, 0);
}
ReadOnlySpan<byte> textBytes = body.Slice(8, textByteLength);
if (textBytes.Length >= 2 && textBytes[^2] == 0 && textBytes[^1] == 0)
{
textBytes = textBytes[..^2];
}
string value = Encoding.Unicode.GetString(textBytes);
return new NmxCallbackValue(wireKind, MxValueKind.String, value, 8 + textByteLength);
}
private static NmxCallbackValue DecodeDateTimeValue(byte wireKind, ReadOnlySpan<byte> body)
{
if (body.Length >= 14)
{
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]);
if (recordLength >= 10 && body.Length >= sizeof(int) + recordLength)
{
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(body.Slice(sizeof(int), sizeof(long)));
if (TryFromFileTimeUtc(fileTime, out DateTime timestamp))
{
return new NmxCallbackValue(
wireKind,
MxValueKind.DateTime,
timestamp,
sizeof(int) + recordLength);
}
return new NmxCallbackValue(wireKind, MxValueKind.DateTime, null, sizeof(int) + recordLength);
}
}
if (body.Length >= sizeof(long))
{
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(body[..sizeof(long)]);
if (TryFromFileTimeUtc(fileTime, out DateTime timestamp))
{
return new NmxCallbackValue(wireKind, MxValueKind.DateTime, timestamp, sizeof(long));
}
}
return new NmxCallbackValue(wireKind, MxValueKind.DateTime, null, 0);
}
private static NmxCallbackValue DecodeElapsedTimeValue(byte wireKind, ReadOnlySpan<byte> body)
{
if (body.Length < sizeof(int))
{
return new NmxCallbackValue(wireKind, MxValueKind.ElapsedTime, null, 0);
}
int milliseconds = BinaryPrimitives.ReadInt32LittleEndian(body[..sizeof(int)]);
return new NmxCallbackValue(wireKind, MxValueKind.ElapsedTime, TimeSpan.FromMilliseconds(milliseconds), sizeof(int));
}
private static NmxCallbackValue DecodeArrayValue(byte wireKind, ReadOnlySpan<byte> body)
{
const int arrayHeaderLength = 10;
if (body.Length < arrayHeaderLength)
{
return new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0);
}
ushort count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(4, sizeof(ushort)));
int elementWidth = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(6, sizeof(int)));
ReadOnlySpan<byte> values = body[arrayHeaderLength..];
return wireKind switch
{
0x41 => DecodeBooleanArray(wireKind, count, elementWidth, body.Length, values),
0x42 => DecodeInt32Array(wireKind, count, elementWidth, body.Length, values),
0x43 => DecodeFloat32Array(wireKind, count, elementWidth, body.Length, values),
0x44 => DecodeFloat64Array(wireKind, count, elementWidth, body.Length, values),
0x45 => DecodeStringArray(wireKind, count, values),
0x46 => DecodeDateTimeArray(wireKind, count, elementWidth, body.Length, values),
_ => new NmxCallbackValue(wireKind, ToValueKindOrNull(wireKind), null, 0),
};
}
private static NmxCallbackValue DecodeBooleanArray(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
{
if (elementWidth != sizeof(short) || values.Length < count * elementWidth)
{
return new NmxCallbackValue(wireKind, MxValueKind.BooleanArray, null, 0);
}
bool[] decoded = new bool[count];
for (int i = 0; i < count; i++)
{
decoded[i] = BinaryPrimitives.ReadInt16LittleEndian(values.Slice(i * elementWidth, sizeof(short))) != 0;
}
return new NmxCallbackValue(wireKind, MxValueKind.BooleanArray, decoded, 10 + count * elementWidth);
}
private static NmxCallbackValue DecodeInt32Array(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
{
if (elementWidth != sizeof(int) || values.Length < count * elementWidth)
{
return new NmxCallbackValue(wireKind, MxValueKind.Int32Array, null, 0);
}
int[] decoded = new int[count];
for (int i = 0; i < count; i++)
{
decoded[i] = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(i * elementWidth, sizeof(int)));
}
return new NmxCallbackValue(wireKind, MxValueKind.Int32Array, decoded, 10 + count * elementWidth);
}
private static NmxCallbackValue DecodeFloat32Array(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
{
if (elementWidth != sizeof(float) || values.Length < count * elementWidth)
{
return new NmxCallbackValue(wireKind, MxValueKind.Float32Array, null, 0);
}
float[] decoded = new float[count];
for (int i = 0; i < count; i++)
{
decoded[i] = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(values.Slice(i * elementWidth, sizeof(int))));
}
return new NmxCallbackValue(wireKind, MxValueKind.Float32Array, decoded, 10 + count * elementWidth);
}
private static NmxCallbackValue DecodeFloat64Array(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
{
if (elementWidth != sizeof(double) || values.Length < count * elementWidth)
{
return new NmxCallbackValue(wireKind, MxValueKind.Float64Array, null, 0);
}
double[] decoded = new double[count];
for (int i = 0; i < count; i++)
{
decoded[i] = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(values.Slice(i * elementWidth, sizeof(long))));
}
return new NmxCallbackValue(wireKind, MxValueKind.Float64Array, decoded, 10 + count * elementWidth);
}
private static NmxCallbackValue DecodeDateTimeArray(byte wireKind, ushort count, int elementWidth, int bodyLength, ReadOnlySpan<byte> values)
{
if (elementWidth != 12 || values.Length < count * elementWidth)
{
return new NmxCallbackValue(wireKind, MxValueKind.DateTimeArray, null, 0);
}
DateTime[] decoded = new DateTime[count];
for (int i = 0; i < count; i++)
{
long fileTime = BinaryPrimitives.ReadInt64LittleEndian(values.Slice(i * elementWidth, sizeof(long)));
decoded[i] = DateTime.FromFileTimeUtc(fileTime);
}
return new NmxCallbackValue(wireKind, MxValueKind.DateTimeArray, decoded, 10 + count * elementWidth);
}
private static NmxCallbackValue DecodeStringArray(byte wireKind, ushort count, ReadOnlySpan<byte> values)
{
string[] decoded = new string[count];
int offset = 0;
for (int i = 0; i < count; i++)
{
if (offset + 13 > values.Length)
{
return new NmxCallbackValue(wireKind, MxValueKind.StringArray, null, 0);
}
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(offset, sizeof(int)));
byte elementKind = values[offset + 4];
int textRecordLength = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(offset + 5, sizeof(int)));
int textByteLength = BinaryPrimitives.ReadInt32LittleEndian(values.Slice(offset + 9, sizeof(int)));
if (recordLength < 9 || elementKind != 0x05 || textRecordLength != textByteLength + sizeof(int) || recordLength != 1 + sizeof(int) + sizeof(int) + textByteLength || offset + 13 + textByteLength > values.Length)
{
return new NmxCallbackValue(wireKind, MxValueKind.StringArray, null, 0);
}
ReadOnlySpan<byte> textBytes = values.Slice(offset + 13, textByteLength);
if (textBytes.Length >= 2 && textBytes[^2] == 0 && textBytes[^1] == 0)
{
textBytes = textBytes[..^2];
}
decoded[i] = Encoding.Unicode.GetString(textBytes);
offset += 13 + textByteLength;
}
return new NmxCallbackValue(wireKind, MxValueKind.StringArray, decoded, 10 + offset);
}
private static MxValueKind? ToValueKindOrNull(byte wireKind)
{
return wireKind switch
{
0x01 => MxValueKind.Boolean,
0x02 => MxValueKind.Int32,
0x03 => MxValueKind.Float32,
0x04 => MxValueKind.Float64,
0x05 => MxValueKind.String,
0x06 => MxValueKind.DateTime,
0x07 => MxValueKind.ElapsedTime,
0x41 => MxValueKind.BooleanArray,
0x42 => MxValueKind.Int32Array,
0x43 => MxValueKind.Float32Array,
0x44 => MxValueKind.Float64Array,
0x45 => MxValueKind.StringArray,
0x46 => MxValueKind.DateTimeArray,
_ => null,
};
}
private static bool TryFromFileTimeUtc(long fileTime, out DateTime timestamp)
{
try
{
timestamp = DateTime.FromFileTimeUtc(fileTime);
return true;
}
catch (ArgumentOutOfRangeException)
{
timestamp = default;
return false;
}
}
}
+104
View File
@@ -0,0 +1,104 @@
using System.Buffers.Binary;
namespace MxNativeCodec;
public enum NmxTransferMessageKind : int
{
Metadata = 1,
ItemControl = 2,
Write = 3,
}
public sealed record NmxTransferEnvelope(
NmxTransferMessageKind MessageKind,
int SourceGalaxyId,
int SourcePlatformId,
int LocalEngineId,
int TargetGalaxyId,
int TargetPlatformId,
int TargetEngineId,
int TimeoutMilliseconds,
ReadOnlyMemory<byte> InnerBody)
{
public const int HeaderLength = 46;
private const ushort Version = 1;
private const int InnerLengthOffset = 2;
private const int ReservedOffset = 6;
private const int MessageKindOffset = 10;
private const int SourceGalaxyOffset = 14;
private const int SourcePlatformOffset = 18;
private const int LocalEngineOffset = 22;
private const int TargetGalaxyOffset = 26;
private const int TargetPlatformOffset = 30;
private const int TargetEngineOffset = 34;
private const int ProtocolMarkerOffset = 38;
private const int TimeoutOffset = 42;
private const int ProtocolMarker = 0x0201;
private const int DefaultTimeoutMilliseconds = 30000;
public static NmxTransferEnvelope Parse(ReadOnlyMemory<byte> transferBody)
{
if (transferBody.Length < HeaderLength)
{
throw new ArgumentException("NMX TransferData body is too short.", nameof(transferBody));
}
ReadOnlySpan<byte> span = transferBody.Span;
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(span[..sizeof(ushort)]);
if (version != Version)
{
throw new ArgumentException($"Unsupported NMX TransferData version {version}.", nameof(transferBody));
}
int innerLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(InnerLengthOffset, sizeof(int)));
if (innerLength != transferBody.Length - HeaderLength)
{
throw new ArgumentException("NMX TransferData inner length does not match the body size.", nameof(transferBody));
}
int protocolMarker = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(ProtocolMarkerOffset, sizeof(int)));
if (protocolMarker != ProtocolMarker)
{
throw new ArgumentException($"Unsupported NMX TransferData protocol marker 0x{protocolMarker:X8}.", nameof(transferBody));
}
return new NmxTransferEnvelope(
(NmxTransferMessageKind)BinaryPrimitives.ReadInt32LittleEndian(span.Slice(MessageKindOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(SourceGalaxyOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(SourcePlatformOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(LocalEngineOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TargetGalaxyOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TargetPlatformOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TargetEngineOffset, sizeof(int))),
BinaryPrimitives.ReadInt32LittleEndian(span.Slice(TimeoutOffset, sizeof(int))),
transferBody[HeaderLength..]);
}
public static byte[] Encode(
NmxTransferMessageKind messageKind,
int localEngineId,
int targetGalaxyId,
int targetPlatformId,
int targetEngineId,
ReadOnlySpan<byte> innerBody,
int sourceGalaxyId = 1,
int sourcePlatformId = 1,
int timeoutMilliseconds = DefaultTimeoutMilliseconds)
{
byte[] transferBody = new byte[HeaderLength + innerBody.Length];
BinaryPrimitives.WriteUInt16LittleEndian(transferBody.AsSpan(0, sizeof(ushort)), Version);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(InnerLengthOffset, sizeof(int)), innerBody.Length);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(ReservedOffset, sizeof(int)), 0);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(MessageKindOffset, sizeof(int)), (int)messageKind);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(SourceGalaxyOffset, sizeof(int)), sourceGalaxyId);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(SourcePlatformOffset, sizeof(int)), sourcePlatformId);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(LocalEngineOffset, sizeof(int)), localEngineId);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TargetGalaxyOffset, sizeof(int)), targetGalaxyId);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TargetPlatformOffset, sizeof(int)), targetPlatformId);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TargetEngineOffset, sizeof(int)), targetEngineId);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(ProtocolMarkerOffset, sizeof(int)), ProtocolMarker);
BinaryPrimitives.WriteInt32LittleEndian(transferBody.AsSpan(TimeoutOffset, sizeof(int)), timeoutMilliseconds);
innerBody.CopyTo(transferBody.AsSpan(HeaderLength));
return transferBody;
}
}
@@ -0,0 +1,57 @@
using System.Buffers.Binary;
namespace MxNativeCodec;
public sealed class NmxTransferEnvelopeTemplate
{
public const int HeaderLength = 46;
public const int InnerLengthOffset = 2;
private readonly byte[] _header;
private NmxTransferEnvelopeTemplate(byte[] header)
{
_header = header;
}
public static NmxTransferEnvelopeTemplate FromObserved(ReadOnlySpan<byte> observedTransferBody)
{
if (observedTransferBody.Length < HeaderLength)
{
throw new ArgumentException("Observed TransferData body is too short.", nameof(observedTransferBody));
}
int innerLength = BinaryPrimitives.ReadInt32LittleEndian(observedTransferBody.Slice(InnerLengthOffset, sizeof(int)));
if (innerLength != observedTransferBody.Length - HeaderLength)
{
throw new ArgumentException("Observed TransferData body does not contain the expected inner length.", nameof(observedTransferBody));
}
return new NmxTransferEnvelopeTemplate(observedTransferBody[..HeaderLength].ToArray());
}
public byte[] Encode(ReadOnlySpan<byte> innerPutRequestBody)
{
byte[] body = new byte[HeaderLength + innerPutRequestBody.Length];
_header.CopyTo(body, 0);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(InnerLengthOffset, sizeof(int)), innerPutRequestBody.Length);
innerPutRequestBody.CopyTo(body.AsSpan(HeaderLength));
return body;
}
public ReadOnlyMemory<byte> DecodeInner(ReadOnlyMemory<byte> transferBody)
{
if (transferBody.Length < HeaderLength)
{
throw new ArgumentException("TransferData body is too short.", nameof(transferBody));
}
int innerLength = BinaryPrimitives.ReadInt32LittleEndian(transferBody.Span.Slice(InnerLengthOffset, sizeof(int)));
if (innerLength != transferBody.Length - HeaderLength)
{
throw new ArgumentException("TransferData body inner length does not match the body size.", nameof(transferBody));
}
return transferBody[HeaderLength..];
}
}
+394
View File
@@ -0,0 +1,394 @@
using System.Buffers.Binary;
using System.Globalization;
using System.Text;
namespace MxNativeCodec;
public static class NmxWriteMessage
{
public const byte Command = 0x37;
public const ushort Version = 1;
public const int HandleProjectionOffset = 3;
public const int HandleProjectionLength = 14;
public const int KindOffset = 17;
public static byte[] Encode(
MxReferenceHandle referenceHandle,
MxValueKind valueKind,
object value,
int writeIndex = 1,
uint clientToken = 0)
{
byte[] valueBytes = EncodeValue(valueKind, value);
byte[] body = valueKind switch
{
MxValueKind.Boolean => CreateBoolean(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
MxValueKind.Int32 or MxValueKind.Float32 => CreateFixed(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
MxValueKind.Float64 => CreateFixed(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
MxValueKind.String or MxValueKind.DateTime => CreateVariable(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array => CreateArray(referenceHandle, valueKind, valueBytes, GetArrayCount(value), GetArrayElementWidth(valueKind), writeIndex, clientToken),
MxValueKind.StringArray or MxValueKind.DateTimeArray => CreateArray(referenceHandle, valueKind, valueBytes, GetArrayCount(value), elementWidth: 4, writeIndex, clientToken),
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
};
return body;
}
public static byte[] EncodeTimestamped(
MxReferenceHandle referenceHandle,
MxValueKind valueKind,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0)
{
byte[] valueBytes = EncodeValue(valueKind, value);
byte[] body = valueKind switch
{
MxValueKind.Boolean => CreateBooleanTimestamped(referenceHandle, value, timestamp, writeIndex, clientToken),
MxValueKind.Int32 or MxValueKind.Float32 => CreateFixedTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
MxValueKind.Float64 => CreateFixedTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
MxValueKind.String or MxValueKind.DateTime => CreateVariableTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array => CreateArrayTimestamped(referenceHandle, valueKind, valueBytes, GetArrayCount(value), GetArrayElementWidth(valueKind), timestamp, writeIndex, clientToken),
MxValueKind.StringArray or MxValueKind.DateTimeArray => CreateArrayTimestamped(referenceHandle, valueKind, valueBytes, GetArrayCount(value), elementWidth: 4, timestamp, writeIndex, clientToken),
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
};
return body;
}
public static MxValueKind GetValueKind(short mxDataType, bool isArray)
{
if (TryGetValueKind(mxDataType, isArray, out MxValueKind valueKind))
{
return valueKind;
}
throw new ArgumentOutOfRangeException(nameof(mxDataType), $"Unsupported MX data type {mxDataType} with isArray={isArray}.");
}
public static bool TryGetValueKind(short mxDataType, bool isArray, out MxValueKind valueKind)
{
return (mxDataType, isArray) switch
{
(1, false) => Return(MxValueKind.Boolean, out valueKind),
(2, false) => Return(MxValueKind.Int32, out valueKind),
(3, false) => Return(MxValueKind.Float32, out valueKind),
(4, false) => Return(MxValueKind.Float64, out valueKind),
(5, false) => Return(MxValueKind.String, out valueKind),
(6, false) => Return(MxValueKind.DateTime, out valueKind),
(1, true) => Return(MxValueKind.BooleanArray, out valueKind),
(2, true) => Return(MxValueKind.Int32Array, out valueKind),
(3, true) => Return(MxValueKind.Float32Array, out valueKind),
(4, true) => Return(MxValueKind.Float64Array, out valueKind),
(5, true) => Return(MxValueKind.StringArray, out valueKind),
(6, true) => Return(MxValueKind.DateTimeArray, out valueKind),
_ => Return(default, out valueKind, success: false),
};
}
private static bool Return(MxValueKind source, out MxValueKind target, bool success = true)
{
target = source;
return success;
}
public static byte GetWireKind(MxValueKind valueKind)
{
return valueKind switch
{
MxValueKind.Boolean => 0x01,
MxValueKind.Int32 => 0x02,
MxValueKind.Float32 => 0x03,
MxValueKind.Float64 => 0x04,
MxValueKind.String or MxValueKind.DateTime => 0x05,
MxValueKind.BooleanArray => 0x41,
MxValueKind.Int32Array => 0x42,
MxValueKind.Float32Array => 0x43,
MxValueKind.Float64Array => 0x44,
MxValueKind.StringArray or MxValueKind.DateTimeArray => 0x45,
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
};
}
private static byte[] CreateFixed(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
WriteNormalSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), writeIndex, clientToken);
return body;
}
private static byte[] CreateBoolean(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 11 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
WriteBooleanSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), writeIndex, clientToken);
return body;
}
private static byte[] CreateBooleanTimestamped(MxReferenceHandle referenceHandle, object value, DateTime timestamp, int writeIndex, uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + sizeof(byte) + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, MxValueKind.Boolean);
body[KindOffset + 1] = Convert.ToBoolean(value, CultureInfo.InvariantCulture) ? (byte)0xff : (byte)0x00;
WriteTimestampedSuffix(body.AsSpan(KindOffset + 2), timestamp, writeIndex, clientToken);
return body;
}
private static byte[] CreateFixedTimestamped(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, DateTime timestamp, int writeIndex, uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
WriteTimestampedSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), timestamp, writeIndex, clientToken);
return body;
}
private static byte[] CreateVariable(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + sizeof(int) + sizeof(int) + valueBytes.Length + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, sizeof(int)), valueBytes.Length + sizeof(int));
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, sizeof(int)), valueBytes.Length);
valueBytes.CopyTo(body.AsSpan(26));
WriteNormalSuffix(body.AsSpan(26 + valueBytes.Length), writeIndex, clientToken);
return body;
}
private static byte[] CreateVariableTimestamped(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, DateTime timestamp, int writeIndex, uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + sizeof(int) + sizeof(int) + valueBytes.Length + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, sizeof(int)), valueBytes.Length + sizeof(int));
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, sizeof(int)), valueBytes.Length);
valueBytes.CopyTo(body.AsSpan(26));
WriteTimestampedSuffix(body.AsSpan(26 + valueBytes.Length), timestamp, writeIndex, clientToken);
return body;
}
private static byte[] CreateArray(
MxReferenceHandle referenceHandle,
MxValueKind valueKind,
byte[] valueBytes,
int count,
ushort elementWidth,
int writeIndex,
uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + 10 + valueBytes.Length + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, sizeof(ushort)), checked((ushort)count));
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(24, sizeof(ushort)), elementWidth);
valueBytes.CopyTo(body.AsSpan(28));
WriteNormalSuffix(body.AsSpan(28 + valueBytes.Length), writeIndex, clientToken);
return body;
}
private static byte[] CreateArrayTimestamped(
MxReferenceHandle referenceHandle,
MxValueKind valueKind,
byte[] valueBytes,
int count,
ushort elementWidth,
DateTime timestamp,
int writeIndex,
uint clientToken)
{
byte[] body = new byte[KindOffset + 1 + 10 + valueBytes.Length + 14 + sizeof(int)];
WriteCommonPrefix(body, referenceHandle, valueKind);
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, sizeof(ushort)), checked((ushort)count));
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(24, sizeof(ushort)), elementWidth);
valueBytes.CopyTo(body.AsSpan(28));
WriteTimestampedSuffix(body.AsSpan(28 + valueBytes.Length), timestamp, writeIndex, clientToken);
return body;
}
private static void WriteCommonPrefix(byte[] body, MxReferenceHandle referenceHandle, MxValueKind valueKind)
{
body[0] = Command;
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
referenceHandle.Encode().AsSpan(6, HandleProjectionLength).CopyTo(body.AsSpan(HandleProjectionOffset, HandleProjectionLength));
body[KindOffset] = GetWireKind(valueKind);
}
private static void WriteNormalSuffix(Span<byte> suffixAndIndex, int writeIndex, uint clientToken)
{
if (suffixAndIndex.Length != 18)
{
throw new ArgumentException("Normal write suffix span must include the 14-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
}
BinaryPrimitives.WriteInt16LittleEndian(suffixAndIndex[..sizeof(short)], -1);
BinaryPrimitives.WriteUInt64LittleEndian(suffixAndIndex.Slice(2, sizeof(ulong)), 0);
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(10, sizeof(uint)), clientToken);
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(14, sizeof(int)), writeIndex);
}
private static void WriteBooleanSuffix(Span<byte> suffixAndIndex, int writeIndex, uint clientToken)
{
if (suffixAndIndex.Length != 15)
{
throw new ArgumentException("Boolean write suffix span must include the 11-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
}
suffixAndIndex[..7].Clear();
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(7, sizeof(uint)), clientToken);
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(11, sizeof(int)), writeIndex);
}
private static void WriteTimestampedSuffix(Span<byte> suffixAndIndex, DateTime timestamp, int writeIndex, uint clientToken)
{
if (suffixAndIndex.Length != 18)
{
throw new ArgumentException("Timestamped write suffix span must include the 14-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
}
BinaryPrimitives.WriteInt16LittleEndian(suffixAndIndex[..sizeof(short)], 0);
BinaryPrimitives.WriteInt64LittleEndian(suffixAndIndex.Slice(2, sizeof(long)), timestamp.ToFileTime());
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(10, sizeof(uint)), clientToken);
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(14, sizeof(int)), writeIndex);
}
private static byte[] EncodeValue(MxValueKind valueKind, object value)
{
return valueKind switch
{
MxValueKind.Boolean => Convert.ToBoolean(value, CultureInfo.InvariantCulture) ? [0xff, 0xff, 0xff, 0x00] : [0x00, 0xff, 0xff, 0x00],
MxValueKind.Int32 => EncodeInt32(Convert.ToInt32(value, CultureInfo.InvariantCulture)),
MxValueKind.Float32 => EncodeFloat32(Convert.ToSingle(value, CultureInfo.InvariantCulture)),
MxValueKind.Float64 => EncodeFloat64(Convert.ToDouble(value, CultureInfo.InvariantCulture)),
MxValueKind.String => EncodeUtf16String(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
MxValueKind.DateTime => EncodeUtf16String(FormatDateTime((DateTime)value)),
MxValueKind.BooleanArray => EncodeBooleanArray((bool[])value),
MxValueKind.Int32Array => EncodeInt32Array((int[])value),
MxValueKind.Float32Array => EncodeFloat32Array((float[])value),
MxValueKind.Float64Array => EncodeFloat64Array((double[])value),
MxValueKind.StringArray => EncodeVariableArray(((string[])value).Select(static value => value ?? string.Empty)),
MxValueKind.DateTimeArray => EncodeVariableArray(((DateTime[])value).Select(FormatDateTime)),
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
};
}
private static byte[] EncodeInt32(int value)
{
byte[] bytes = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return bytes;
}
private static byte[] EncodeFloat32(float value)
{
byte[] bytes = new byte[sizeof(float)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, BitConverter.SingleToInt32Bits(value));
return bytes;
}
private static byte[] EncodeFloat64(double value)
{
byte[] bytes = new byte[sizeof(double)];
BinaryPrimitives.WriteInt64LittleEndian(bytes, BitConverter.DoubleToInt64Bits(value));
return bytes;
}
private static byte[] EncodeUtf16String(string value)
{
byte[] textBytes = Encoding.Unicode.GetBytes(value);
byte[] bytes = new byte[textBytes.Length + 2];
textBytes.CopyTo(bytes, 0);
return bytes;
}
private static byte[] EncodeBooleanArray(bool[] values)
{
byte[] bytes = new byte[values.Length * sizeof(short)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt16LittleEndian(bytes.AsSpan(i * sizeof(short), sizeof(short)), values[i] ? (short)-1 : (short)0);
}
return bytes;
}
private static byte[] EncodeInt32Array(int[] values)
{
byte[] bytes = new byte[values.Length * sizeof(int)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(int), sizeof(int)), values[i]);
}
return bytes;
}
private static byte[] EncodeFloat32Array(float[] values)
{
byte[] bytes = new byte[values.Length * sizeof(float)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(float), sizeof(float)), BitConverter.SingleToInt32Bits(values[i]));
}
return bytes;
}
private static byte[] EncodeFloat64Array(double[] values)
{
byte[] bytes = new byte[values.Length * sizeof(double)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(i * sizeof(double), sizeof(double)), BitConverter.DoubleToInt64Bits(values[i]));
}
return bytes;
}
private static byte[] EncodeVariableArray(IEnumerable<string> values)
{
using var stream = new MemoryStream();
foreach (string value in values)
{
byte[] textBytes = EncodeUtf16String(value);
byte[] header = new byte[13];
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(0, 4), 1 + sizeof(int) + sizeof(int) + textBytes.Length);
header[4] = 0x05;
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(5, 4), textBytes.Length + sizeof(int));
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(9, 4), textBytes.Length);
stream.Write(header);
stream.Write(textBytes);
}
return stream.ToArray();
}
private static int GetArrayCount(object value)
{
return value switch
{
bool[] values => values.Length,
int[] values => values.Length,
float[] values => values.Length,
double[] values => values.Length,
string[] values => values.Length,
DateTime[] values => values.Length,
_ => throw new ArgumentException("Value is not a supported MX array.", nameof(value)),
};
}
private static ushort GetArrayElementWidth(MxValueKind valueKind)
{
return valueKind switch
{
MxValueKind.BooleanArray => sizeof(short),
MxValueKind.Int32Array => sizeof(int),
MxValueKind.Float32Array => sizeof(float),
MxValueKind.Float64Array => sizeof(double),
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
};
}
private static string FormatDateTime(DateTime value)
{
return value.ToString("M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture);
}
}
@@ -0,0 +1,412 @@
using System.Buffers.Binary;
using System.Globalization;
using System.Text;
namespace MxNativeCodec;
public sealed class ObservedWriteBodyTemplate
{
public const int FixedValueOffset = 18;
public const int VariableValueOffset = 26;
public const int ArrayValueOffset = 28;
private readonly byte[] _prefix;
private readonly byte[] _suffixBeforeWriteIndex;
private readonly MxValueKind _kind;
private ObservedWriteBodyTemplate(MxValueKind kind, byte[] prefix, byte[] suffixBeforeWriteIndex)
{
_kind = kind;
_prefix = prefix;
_suffixBeforeWriteIndex = suffixBeforeWriteIndex;
}
public MxValueKind Kind => _kind;
public static ObservedWriteBodyTemplate FromObserved(MxValueKind kind, ReadOnlySpan<byte> observedBody)
{
if (observedBody.Length < 24)
{
throw new ArgumentException("Observed body is too short.", nameof(observedBody));
}
return kind switch
{
MxValueKind.Boolean => CreateFixed(kind, observedBody, valueWidth: 4),
MxValueKind.Int32 => CreateFixed(kind, observedBody, valueWidth: 4),
MxValueKind.Float32 => CreateFixed(kind, observedBody, valueWidth: 4),
MxValueKind.Float64 => CreateFixed(kind, observedBody, valueWidth: 8),
MxValueKind.String => CreateVariable(kind, observedBody),
MxValueKind.DateTime => CreateVariable(kind, observedBody),
MxValueKind.BooleanArray => CreateArray(kind, observedBody),
MxValueKind.Int32Array => CreateArray(kind, observedBody),
MxValueKind.Float32Array => CreateArray(kind, observedBody),
MxValueKind.Float64Array => CreateArray(kind, observedBody),
MxValueKind.StringArray => CreateArray(kind, observedBody),
MxValueKind.DateTimeArray => CreateArray(kind, observedBody),
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
};
}
public byte[] Encode(object value, int writeIndex)
{
byte[] valueBytes = EncodeValue(value);
byte[] body = new byte[_prefix.Length + valueBytes.Length + _suffixBeforeWriteIndex.Length + sizeof(int)];
_prefix.CopyTo(body, 0);
valueBytes.CopyTo(body, _prefix.Length);
_suffixBeforeWriteIndex.CopyTo(body, _prefix.Length + valueBytes.Length);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(body.Length - sizeof(int)), writeIndex);
PatchVariableLengths(body, valueBytes.Length);
PatchArrayDescriptor(body, valueBytes.Length);
return body;
}
public object Decode(ReadOnlySpan<byte> body)
{
return _kind switch
{
MxValueKind.Boolean => DecodeBoolean(body),
MxValueKind.Int32 => BinaryPrimitives.ReadInt32LittleEndian(body.Slice(FixedValueOffset, 4)),
MxValueKind.Float32 => BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(FixedValueOffset, 4))),
MxValueKind.Float64 => BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(FixedValueOffset, 8))),
MxValueKind.String => DecodeUtf16String(body),
MxValueKind.DateTime => DecodeDateTime(body),
MxValueKind.BooleanArray => DecodeBooleanArray(body),
MxValueKind.Int32Array => DecodeInt32Array(body),
MxValueKind.Float32Array => DecodeFloat32Array(body),
MxValueKind.Float64Array => DecodeFloat64Array(body),
MxValueKind.StringArray => DecodeVariableArray(body, DecodeVariableArrayString),
MxValueKind.DateTimeArray => DecodeVariableArray(body, DecodeVariableArrayDateTime),
_ => throw new InvalidOperationException($"Unsupported value kind {_kind}."),
};
}
public int DecodeWriteIndex(ReadOnlySpan<byte> body)
{
if (body.Length < sizeof(int))
{
throw new ArgumentException("Body is too short.", nameof(body));
}
return BinaryPrimitives.ReadInt32LittleEndian(body[^sizeof(int)..]);
}
private static ObservedWriteBodyTemplate CreateFixed(MxValueKind kind, ReadOnlySpan<byte> body, int valueWidth)
{
int suffixStart = FixedValueOffset + valueWidth;
int suffixLength = body.Length - suffixStart - sizeof(int);
if (suffixLength < 0)
{
throw new ArgumentException("Observed fixed-width body is too short.", nameof(body));
}
return new ObservedWriteBodyTemplate(
kind,
body[..FixedValueOffset].ToArray(),
body.Slice(suffixStart, suffixLength).ToArray());
}
private static ObservedWriteBodyTemplate CreateVariable(MxValueKind kind, ReadOnlySpan<byte> body)
{
if (body.Length < VariableValueOffset + sizeof(int))
{
throw new ArgumentException("Observed variable-width body is too short.", nameof(body));
}
int valueByteLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(22, 4));
int suffixStart = VariableValueOffset + valueByteLength;
int suffixLength = body.Length - suffixStart - sizeof(int);
if (valueByteLength < 2 || suffixLength < 0)
{
throw new ArgumentException("Observed variable-width body has invalid lengths.", nameof(body));
}
return new ObservedWriteBodyTemplate(
kind,
body[..VariableValueOffset].ToArray(),
body.Slice(suffixStart, suffixLength).ToArray());
}
private static ObservedWriteBodyTemplate CreateArray(MxValueKind kind, ReadOnlySpan<byte> body)
{
if (body.Length < ArrayValueOffset + 18)
{
throw new ArgumentException("Observed array body is too short.", nameof(body));
}
int suffixStart = body.Length - 18;
return new ObservedWriteBodyTemplate(
kind,
body[..ArrayValueOffset].ToArray(),
body.Slice(suffixStart, 14).ToArray());
}
private byte[] EncodeValue(object value)
{
return _kind switch
{
MxValueKind.Boolean => EncodeBoolean(Convert.ToBoolean(value, CultureInfo.InvariantCulture)),
MxValueKind.Int32 => EncodeInt32(Convert.ToInt32(value, CultureInfo.InvariantCulture)),
MxValueKind.Float32 => EncodeFloat32(Convert.ToSingle(value, CultureInfo.InvariantCulture)),
MxValueKind.Float64 => EncodeFloat64(Convert.ToDouble(value, CultureInfo.InvariantCulture)),
MxValueKind.String => EncodeUtf16String(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
MxValueKind.DateTime => EncodeUtf16String(FormatObservedDateTime((DateTime)value)),
MxValueKind.BooleanArray => EncodeBooleanArray((bool[])value),
MxValueKind.Int32Array => EncodeInt32Array((int[])value),
MxValueKind.Float32Array => EncodeFloat32Array((float[])value),
MxValueKind.Float64Array => EncodeFloat64Array((double[])value),
MxValueKind.StringArray => EncodeVariableArray(((string[])value).Select(static s => s ?? string.Empty)),
MxValueKind.DateTimeArray => EncodeVariableArray(((DateTime[])value).Select(FormatObservedDateTime)),
_ => throw new InvalidOperationException($"Unsupported value kind {_kind}."),
};
}
private static byte[] EncodeBoolean(bool value)
{
return value
? [0xff, 0xff, 0xff, 0x00]
: [0x00, 0xff, 0xff, 0x00];
}
private static bool DecodeBoolean(ReadOnlySpan<byte> body)
{
return body[FixedValueOffset] == 0xff && body[FixedValueOffset + 1] == 0xff;
}
private static byte[] EncodeInt32(int value)
{
byte[] bytes = new byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return bytes;
}
private static byte[] EncodeBooleanArray(bool[] values)
{
byte[] bytes = new byte[values.Length * sizeof(short)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt16LittleEndian(bytes.AsSpan(i * sizeof(short), sizeof(short)), values[i] ? (short)-1 : (short)0);
}
return bytes;
}
private static bool[] DecodeBooleanArray(ReadOnlySpan<byte> body)
{
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
bool[] values = new bool[count];
for (int i = 0; i < values.Length; i++)
{
values[i] = BinaryPrimitives.ReadInt16LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(short), sizeof(short))) != 0;
}
return values;
}
private static byte[] EncodeInt32Array(int[] values)
{
byte[] bytes = new byte[values.Length * sizeof(int)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(int), sizeof(int)), values[i]);
}
return bytes;
}
private static int[] DecodeInt32Array(ReadOnlySpan<byte> body)
{
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
int[] values = new int[count];
for (int i = 0; i < values.Length; i++)
{
values[i] = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(int), sizeof(int)));
}
return values;
}
private static byte[] EncodeFloat32(float value)
{
byte[] bytes = new byte[sizeof(float)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, BitConverter.SingleToInt32Bits(value));
return bytes;
}
private static byte[] EncodeFloat32Array(float[] values)
{
byte[] bytes = new byte[values.Length * sizeof(float)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(float), sizeof(float)), BitConverter.SingleToInt32Bits(values[i]));
}
return bytes;
}
private static float[] DecodeFloat32Array(ReadOnlySpan<byte> body)
{
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
float[] values = new float[count];
for (int i = 0; i < values.Length; i++)
{
values[i] = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(float), sizeof(float))));
}
return values;
}
private static byte[] EncodeFloat64(double value)
{
byte[] bytes = new byte[sizeof(double)];
BinaryPrimitives.WriteInt64LittleEndian(bytes, BitConverter.DoubleToInt64Bits(value));
return bytes;
}
private static byte[] EncodeFloat64Array(double[] values)
{
byte[] bytes = new byte[values.Length * sizeof(double)];
for (int i = 0; i < values.Length; i++)
{
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(i * sizeof(double), sizeof(double)), BitConverter.DoubleToInt64Bits(values[i]));
}
return bytes;
}
private static double[] DecodeFloat64Array(ReadOnlySpan<byte> body)
{
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
double[] values = new double[count];
for (int i = 0; i < values.Length; i++)
{
values[i] = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(body.Slice(ArrayValueOffset + i * sizeof(double), sizeof(double))));
}
return values;
}
private static byte[] EncodeUtf16String(string value)
{
byte[] textBytes = Encoding.Unicode.GetBytes(value);
byte[] bytes = new byte[textBytes.Length + 2];
textBytes.CopyTo(bytes, 0);
return bytes;
}
private static string DecodeUtf16String(ReadOnlySpan<byte> body)
{
int valueByteLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(22, 4));
ReadOnlySpan<byte> raw = body.Slice(VariableValueOffset, valueByteLength);
if (raw.Length >= 2 && raw[^1] == 0 && raw[^2] == 0)
{
raw = raw[..^2];
}
return Encoding.Unicode.GetString(raw);
}
private static DateTime DecodeDateTime(ReadOnlySpan<byte> body)
{
string text = DecodeUtf16String(body);
return DateTime.ParseExact(text, "M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None);
}
private static byte[] EncodeVariableArray(IEnumerable<string> values)
{
using var stream = new MemoryStream();
foreach (var value in values)
{
byte[] textBytes = EncodeUtf16String(value);
byte[] header = new byte[13];
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(0, 4), 1 + sizeof(int) + sizeof(int) + textBytes.Length);
header[4] = 0x05;
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(5, 4), textBytes.Length + sizeof(int));
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(9, 4), textBytes.Length);
stream.Write(header);
stream.Write(textBytes, 0, textBytes.Length);
}
return stream.ToArray();
}
private static T[] DecodeVariableArray<T>(ReadOnlySpan<byte> body, Func<ReadOnlySpan<byte>, T> decode)
{
int count = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(22, 2));
T[] values = new T[count];
int offset = ArrayValueOffset;
for (int i = 0; i < values.Length; i++)
{
int recordLength = BinaryPrimitives.ReadInt32LittleEndian(body.Slice(offset, 4));
ReadOnlySpan<byte> record = body.Slice(offset + 4, recordLength);
values[i] = decode(record);
offset += sizeof(int) + recordLength;
}
return values;
}
private static string DecodeVariableArrayString(ReadOnlySpan<byte> record)
{
if (record[0] != 0x05)
{
throw new ArgumentException("Unexpected variable array element type.", nameof(record));
}
int valueByteLength = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(5, 4));
ReadOnlySpan<byte> raw = record.Slice(9, valueByteLength);
if (raw.Length >= 2 && raw[^1] == 0 && raw[^2] == 0)
{
raw = raw[..^2];
}
return Encoding.Unicode.GetString(raw);
}
private static DateTime DecodeVariableArrayDateTime(ReadOnlySpan<byte> record)
{
return DateTime.ParseExact(DecodeVariableArrayString(record), "M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None);
}
private static string FormatObservedDateTime(DateTime value)
{
return value.ToString("M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture);
}
private void PatchVariableLengths(byte[] body, int valueByteLength)
{
if (_kind is not (MxValueKind.String or MxValueKind.DateTime))
{
return;
}
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, 4), valueByteLength + 4);
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, 4), valueByteLength);
return;
}
private void PatchArrayDescriptor(byte[] body, int valueByteLength)
{
if (_kind is not (MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array or MxValueKind.StringArray or MxValueKind.DateTimeArray))
{
return;
}
if (_kind is MxValueKind.StringArray or MxValueKind.DateTimeArray)
{
return;
}
int count = _kind switch
{
MxValueKind.BooleanArray => valueByteLength / sizeof(short),
MxValueKind.Int32Array => valueByteLength / sizeof(int),
MxValueKind.Float32Array => valueByteLength / sizeof(float),
MxValueKind.Float64Array => valueByteLength / sizeof(double),
_ => 0,
};
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, 2), checked((ushort)count));
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<Reference Include="ArchestrA.MxAccess">
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
</Project>
+364
View File
@@ -0,0 +1,364 @@
using System;
using System.Globalization;
using System.Linq;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using ComTypes = System.Runtime.InteropServices.ComTypes;
namespace NmxComHarness;
internal static class Program
{
[STAThread]
private static int Main(string[] args)
{
var engineId = GetInt(args, "--engine-id") ?? 0x7100;
var engineName = GetString(args, "--engine-name") ?? $"NmxComHarness.{Process.GetCurrentProcess().Id}";
var version = GetInt(args, "--version") ?? 6;
var holdSeconds = GetInt(args, "--hold-seconds") ?? 1;
var useNullCallback = HasFlag(args, "--null-callback");
var dumpCallbackObjRef = HasFlag(args, "--dump-callback-objref");
var selfTransfer = HasFlag(args, "--self-transfer");
var allowEmptyTransfer = HasFlag(args, "--allow-empty-transfer");
Console.WriteLine($"process=x64:{Environment.Is64BitProcess}");
Console.WriteLine($"engine_id={engineId}");
Console.WriteLine($"engine_name={engineName}");
Console.WriteLine($"version={version}");
Console.WriteLine($"null_callback={useNullCallback}");
object? instance = null;
try
{
var type = Type.GetTypeFromProgID("NmxSvc.NmxService", throwOnError: true)
?? throw new InvalidOperationException("ProgID NmxSvc.NmxService was not resolved.");
instance = Activator.CreateInstance(type)
?? throw new InvalidOperationException("ProgID NmxSvc.NmxService activation returned null.");
var service = (INmxService2)instance;
INmxSvcCallback? callback = useNullCallback ? null : new CallbackSink();
if (callback is not null && dumpCallbackObjRef)
{
byte[] objref = ComObjRefProvider.MarshalInterfaceObjRef(
callback,
new Guid("B49F92F7-C748-4169-8ECA-A0670B012746"),
ComObjRefProvider.MarshalContextDifferentMachine);
Console.WriteLine($"callback_objref_size={objref.Length}");
Console.WriteLine($"callback_objref_hex={BitConverter.ToString(objref).Replace("-", string.Empty)}");
}
Console.WriteLine("register.begin");
service.RegisterEngine2(engineId, engineName, version, callback);
Console.WriteLine("register.ok");
if (selfTransfer)
{
if (HasFlag(args, "--connect-self"))
{
service.Connect(engineId, 1, 1, engineId);
Console.WriteLine("connect.self.ok");
}
if (HasFlag(args, "--add-self-subscriber"))
{
service.AddSubscriberEngine(engineId, 1, 1, engineId);
Console.WriteLine("add-subscriber.self.ok");
}
string bodyHex = GetString(args, "--transfer-body-hex")
?? throw new InvalidOperationException(
"--self-transfer requires --transfer-body-hex with a complete 46-byte NMX envelope plus inner body. " +
"Use --allow-empty-transfer only for controlled size-zero probes.");
byte[] body = ParseHex(bodyHex);
ValidateTransferDataBody(body, allowEmptyTransfer);
if (body.Length == 0)
{
byte empty = 0;
service.TransferData(1, 1, engineId, 0, ref empty);
}
else
{
service.TransferData(1, 1, engineId, body.Length, ref body[0]);
}
Console.WriteLine("transfer.self.ok");
}
try
{
service.GetPartnerVersion(1, 1, 0x7ffd, out var partnerVersion);
Console.WriteLine($"partner_version={partnerVersion}");
}
catch (COMException ex)
{
Console.WriteLine($"partner_version_error=0x{ex.HResult:X8} {ex.Message}");
}
Thread.Sleep(TimeSpan.FromSeconds(Math.Max(0, holdSeconds)));
Console.WriteLine("unregister.begin");
service.UnRegisterEngine(engineId);
Console.WriteLine("unregister.ok");
return 0;
}
catch (COMException ex)
{
Console.Error.WriteLine($"com_error=0x{ex.HResult:X8} {ex.Message}");
return 2;
}
catch (Exception ex)
{
Console.Error.WriteLine($"{ex.GetType().FullName}: {ex.Message}");
return 1;
}
finally
{
if (instance is not null && Marshal.IsComObject(instance))
{
while (Marshal.ReleaseComObject(instance) > 0)
{
}
}
}
}
private static string? GetString(string[] args, string name)
{
var prefix = name + "=";
var match = args.FirstOrDefault(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
return match is null ? null : match.Substring(prefix.Length);
}
private static int? GetInt(string[] args, string name)
{
var raw = GetString(args, name);
if (string.IsNullOrWhiteSpace(raw))
{
return null;
}
return raw!.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? int.Parse(raw.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture)
: int.Parse(raw, CultureInfo.InvariantCulture);
}
private static bool HasFlag(string[] args, string name)
{
return args.Any(arg => arg.Equals(name, StringComparison.OrdinalIgnoreCase));
}
private static void ValidateTransferDataBody(byte[] body, bool allowEmptyTransfer)
{
const int headerLength = 46;
const int innerLengthOffset = 2;
if (body.Length == 0)
{
if (allowEmptyTransfer)
{
return;
}
throw new ArgumentException("TransferData body cannot be empty without --allow-empty-transfer.");
}
if (body.Length < headerLength)
{
throw new ArgumentException($"TransferData body must be at least {headerLength} bytes.");
}
int innerLength = BitConverter.ToInt32(body, innerLengthOffset);
int expectedInnerLength = body.Length - headerLength;
if (innerLength != expectedInnerLength)
{
throw new ArgumentException(
$"TransferData envelope declares inner length {innerLength}, but body length {body.Length} requires {expectedInnerLength}.");
}
if (innerLength == 0 && !allowEmptyTransfer)
{
throw new ArgumentException("TransferData body must include an inner message after the 46-byte envelope.");
}
}
private static byte[] ParseHex(string hex)
{
string compact = new string(hex.Where(static ch => !char.IsWhiteSpace(ch)).ToArray());
if ((compact.Length % 2) != 0)
{
throw new FormatException("Hex string must contain an even number of digits.");
}
byte[] bytes = new byte[compact.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = byte.Parse(compact.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
return bytes;
}
}
internal static class ComObjRefProvider
{
public const uint MarshalContextDifferentMachine = 2;
public static byte[] MarshalInterfaceObjRef(object comObject, Guid iid, uint destinationContext)
{
IntPtr unknown = IntPtr.Zero;
ComTypes.IStream? stream = null;
try
{
unknown = Marshal.GetIUnknownForObject(comObject);
Marshal.ThrowExceptionForHR(CreateStreamOnHGlobal(IntPtr.Zero, true, out stream));
Marshal.ThrowExceptionForHR(CoMarshalInterface(stream, ref iid, unknown, destinationContext, IntPtr.Zero, 0));
Marshal.ThrowExceptionForHR(GetHGlobalFromStream(stream, out var hglobal));
uint size = GlobalSize(hglobal);
IntPtr pointer = GlobalLock(hglobal);
if (pointer == IntPtr.Zero)
{
throw new InvalidOperationException("GlobalLock failed.");
}
try
{
byte[] buffer = new byte[(int)size];
Marshal.Copy(pointer, buffer, 0, buffer.Length);
return buffer;
}
finally
{
GlobalUnlock(hglobal);
}
}
finally
{
if (unknown != IntPtr.Zero)
{
Marshal.Release(unknown);
}
if (stream is not null)
{
Marshal.ReleaseComObject(stream);
}
}
}
[DllImport("ole32.dll")]
private static extern int CreateStreamOnHGlobal(IntPtr hGlobal, [MarshalAs(UnmanagedType.Bool)] bool deleteOnRelease, out ComTypes.IStream stream);
[DllImport("ole32.dll")]
private static extern int CoMarshalInterface(ComTypes.IStream stream, ref Guid iid, IntPtr unknown, uint destinationContext, IntPtr destinationContextPointer, uint marshalFlags);
[DllImport("ole32.dll")]
private static extern int GetHGlobalFromStream(ComTypes.IStream stream, out IntPtr hGlobal);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GlobalSize(IntPtr hMem);
}
[ComImport]
[Guid("AE24BD51-2E80-44CC-905B-E5446C942BEB")]
[ClassInterface(ClassInterfaceType.None)]
internal sealed class NmxServiceClass
{
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("575008DB-845D-46C6-A906-F6F8CA86F315")]
internal interface INmxService
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RegisterEngine(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback? callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void UnRegisterEngine(int engineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("2630A513-A974-4B1A-8025-457A9A7C56B8")]
internal interface INmxService2 : INmxService
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void RegisterEngine(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback? callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void UnRegisterEngine(int engineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RegisterEngine2(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, int version, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback? callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
}
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("B49F92F7-C748-4169-8ECA-A0670B012746")]
public interface INmxSvcCallback
{
[PreserveSig]
int DataReceived(int bufferSize, ref sbyte dataBuffer);
[PreserveSig]
int StatusReceived(int bufferSize, ref sbyte statusBuffer);
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public sealed class CallbackSink : INmxSvcCallback
{
public int DataReceived(int bufferSize, ref sbyte dataBuffer)
{
Console.WriteLine($"callback.data size={bufferSize}");
return 0;
}
public int StatusReceived(int bufferSize, ref sbyte statusBuffer)
{
Console.WriteLine($"callback.status size={bufferSize}");
return 0;
}
}