Files
mxaccess/src/MxAsbClient.Probe/Program.cs
T
Joseph Doherty 104efc4e9b [M5] mxaccess-asb: F28 wire-format fixes — AuthenticateMe accepted live
Three wire-level bugs surfaced by side-by-side relay capture against
the .NET probe routed via the new --via flag:

1. **Dynamic-dictionary id drift**. Our `encode_envelope` hardcoded
   action_dict_id=1 / to_dict_id=3, which is correct for the FIRST
   message in a session but wrong for every subsequent one. The
   per-session dynamic dict accumulates across messages: Connect's
   binary header pre-pops [action,to] at ids 1,3; AuthenticateMe must
   reference the new action at id 5 (continuing the odd sequence) and
   the To URL at id 3 (still in the dict from Connect). Fix uses
   `DynamicDictionary::position_of` + `intern` to compute the right
   wire id, only pre-popping strings that are NEW to the session.
   Captured against .NET probe via asb-relay: AuthenticateMe binary
   header has only one string (action) at offset 0x260 (`06 de 08 2f
   2e ...`), and `<a:Action>` value `ab 05` references the new id 5.

2. **ConnectionValidator wire format depends on operation**. .NET's
   `IAsbDataV2` declares `[XmlSerializerFormat]` on AuthenticateMe,
   Disconnect, KeepAlive (one-way ops) — those use XmlSerializer for
   the ENTIRE message including the [MessageHeader] ConnectionValid-
   ator. Other ops use the default DataContractSerializer. The wire
   shapes differ:
     XmlSerializer: `<ConnectionId xmlns="http://asb.contracts.data/
       20111111">guid</ConnectionId>` (PascalCase property name in
       data namespace)
     DataContract: `<connectionIdField xmlns="http://schemas.data
       contract.org/2004/07/ArchestrAServices.ASBContract">guid</…>`
       (private "fooField" name in datacontract namespace)
   New `ValidatorWireFormat::for_action` picks the right shape per
   action; `encode_validator` now branches on it. New helpers
   `push_xml_text_field` / `push_xml_byte_array_field` for the
   XmlSerializer form. The DataContract form is preserved verbatim
   for Register/Read/Write/etc.

3. **Decoder missing 0x0A** (`ShortDictionaryXmlnsAttribute`). The
   server's RegisterItemsResponse uses `0x0A {dict-id}` to set the
   default namespace from the static dict; our decoder bailed out
   with `UnknownRecord(10)`. New decode arm produces a
   `DefaultNamespace` token with `DictionaryStatic` value.

**.NET probe gains a `--via` flag** (`AsbConnectionOptions.Via` →
`ChannelFactory.CreateChannel(addr, viaUri)`) so the probe can be
routed through asb-relay for byte-level capture without triggering
an `AddressFilterMismatch` fault. CoreWCF / .NET 10 dropped
`ClientViaBehavior`; the `CreateChannel(addr, via)` overload is the
modern equivalent.

Live status (this commit): Connect handshake works, AuthenticateMe
no longer faults (canonical XML + crypto + wire-format all match
.NET now), RegisterItemsResponse comes back from the server (a real
response, not a dispatcher fault). One remaining issue: our response
decoder hits `MissingField { field: "Status" }` — the server's
RegisterItemsResponse uses a slightly different element naming or
encoding than `collect_asbidata_payloads` expects. Next iteration
hunts that.

Workspace: 710 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:48 -04:00

1093 lines
48 KiB
C#

using MxAsbClient;
using System.Globalization;
string endpoint = GetArg(args, "--endpoint")
?? "net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2";
// `--via` overrides the TCP destination without changing the `<a:To>`
// SOAP header (so the registered service URL still matches inside
// SMSvcHost). Use to route the probe through `asb-relay` for wire
// byte capture, e.g. `--via net.tcp://127.0.0.1:8088/...`.
string? clientVia = GetArg(args, "--via");
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;
}
// `--dump-signed-xml` produces deterministic .NET `XmlSerializer` output
// for each ConnectedRequest type that goes through `AsbSystemAuthenticator
// .Sign` (`AsbSystemAuthenticator.cs:79`). The output is exactly what
// the .NET HMAC computation runs over, so the Rust port's canonical-XML
// emitter (F28) needs to produce byte-identical bytes for every type
// listed here. Connection IDs, MACs, IVs, and message numbers are pinned
// to deterministic values so the dump is reproducible.
if (args.Any(arg => arg.Equals("--dump-signed-xml", StringComparison.OrdinalIgnoreCase)))
{
Guid connectionId = Guid.Parse("8cba964a-74c1-ef74-f6aa-761b3540191b");
byte[] mac = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw==");
byte[] sigIv = Convert.FromBase64String("EBESExQVFhcYGRobHB0eHw==");
void Dump(string label, object request)
{
string xml = AsbSerialization.ToXml(request);
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(xml);
Console.WriteLine($"--- {label} ({xmlBytes.Length} UTF-8 bytes) ---");
Console.WriteLine(xml);
Console.WriteLine($"--- {label} (base64) ---");
Console.WriteLine(Convert.ToBase64String(xmlBytes));
}
ConnectionValidator validator = new()
{
ConnectionId = connectionId,
MessageNumber = 42,
MessageAuthenticationCode = mac,
SignatureInitializationVector = sigIv,
};
// The actual signing flow uses an EMPTY MessageAuthenticationCode +
// SignatureInitializationVector at the time of HMAC computation
// (`AsbSystemAuthenticator.Sign:79` calls request.ToXml() while the
// validator's MAC/IV are still `[]`; the encrypt-and-fill happens
// immediately after). The Rust port has to know what XmlSerializer
// emits for `byte[] = []` to produce HMAC-matching XML — capture
// the variant with empty MAC + IV so we can pin both shapes.
ConnectionValidator emptyValidator = new()
{
ConnectionId = connectionId,
MessageNumber = 42,
MessageAuthenticationCode = [],
SignatureInitializationVector = [],
};
AuthenticateMe authMeEmpty = new()
{
ConnectionValidator = emptyValidator,
ConsumerAuthenticationData = new AuthenticationData
{
Data = Convert.FromBase64String("ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz"),
InitializationVector = Convert.FromBase64String("MDEyMzQ1Njc4OWFiY2RlZg=="),
},
};
Dump("AuthenticateMe-empty-mac-iv", authMeEmpty);
AuthenticateMe authMe = new()
{
ConnectionValidator = validator,
ConsumerAuthenticationData = new AuthenticationData
{
Data = Convert.FromBase64String("ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz"),
InitializationVector = Convert.FromBase64String("MDEyMzQ1Njc4OWFiY2RlZg=="),
},
};
Dump("AuthenticateMe", authMe);
Disconnect disconnect = new()
{
ConnectionValidator = validator,
ConsumerAuthenticationData = new AuthenticationData
{
Data = Convert.FromBase64String("ZGlzY29ubmVjdC1jaXBoZXJ0ZXh0"),
InitializationVector = Convert.FromBase64String("MDEyMzQ1Njc4OWFiY2RlZg=="),
},
};
Dump("Disconnect", disconnect);
KeepAlive keepAlive = new() { ConnectionValidator = validator };
Dump("KeepAlive", keepAlive);
RegisterItemsRequest registerDump = new()
{
ConnectionValidator = validator,
Items = [new ItemIdentity
{
Type = (ushort)ItemIdentityType.Name,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Name = "TestChildObject.TestInt",
ContextName = string.Empty,
}],
RequireId = true,
RegisterOnly = false,
};
Dump("RegisterItemsRequest", registerDump);
UnregisterItemsRequest unregisterDump = new()
{
ConnectionValidator = validator,
Items = [new ItemIdentity
{
Type = (ushort)ItemIdentityType.Id,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Id = 0xCAFE_BABE_DEAD_BEEFul,
IdSpecified = true,
}],
};
Dump("UnregisterItemsRequest", unregisterDump);
return;
}
// `--dump-deterministic-hmac` runs the AuthenticateMe sign path with
// FIXED inputs end-to-end (no randomness): pinned passphrase, prime,
// generator, private-key bytes, remote-pub bytes, connection ID,
// message number, AES IV, and consumer-data/IV bytes. Output is the
// resulting crypto_key, AES key, canonical XML, HMAC-SHA1, and
// AES-CBC-encrypted MAC. The Rust port uses these as a fixture for a
// byte-equality unit test that localises any HMAC/AES/derivation
// divergence vs the .NET reference without depending on session
// randomness. Mirrors the per-step decomposition of `AsbSystemAuthent
// icator.Sign` (`AsbSystemAuthenticator.cs:62-82`) but inlines the
// math so we control every byte of input.
if (args.Any(arg => arg.Equals("--dump-deterministic-hmac", StringComparison.OrdinalIgnoreCase)))
{
System.Numerics.BigInteger prime = System.Numerics.BigInteger.Parse(AsbSolutionCryptoParameters.DefaultPrimeText);
System.Numerics.BigInteger generator = 22;
// 33 bytes: 0x01..0x20 with trailing 0x00 sign byte. Mirrors the
// shape `AsbSystemAuthenticator.CreatePrivateKey` produces.
byte[] privateKeyBytes = new byte[33];
for (int i = 0; i < 32; i++) { privateKeyBytes[i] = (byte)(i + 1); }
privateKeyBytes[32] = 0x00;
// Remote public key — 128 bytes (1024-bit), high bit clear so
// .NET's BigInteger LE-two's-complement reads it as positive
// without a sign-byte fix-up.
byte[] remotePub = new byte[128];
for (int i = 0; i < 127; i++) { remotePub[i] = (byte)((i * 7 + 13) & 0xFF); }
remotePub[127] = 0x7F;
string passphrase = "deterministic-hmac-fixture-passphrase-rust-vs-dotnet";
Guid connectionId = Guid.Parse("8cba964a-74c1-ef74-f6aa-761b3540191b");
ulong messageNumber = 42;
// ConsumerAuthenticationData payload. Encrypted bytes are opaque
// to the HMAC test (they get base64-embedded in the XML and
// signed); use deterministic bytes 0x80..0xFF + 0x00..0x4F (208
// bytes — same as a real AuthenticateMe under a 768-bit prime).
byte[] consumerData = new byte[208];
for (int i = 0; i < 208; i++) { consumerData[i] = (byte)((i * 3 + 7) & 0xFF); }
byte[] consumerIv = new byte[16];
for (int i = 0; i < 16; i++) { consumerIv[i] = (byte)((i * 11 + 5) & 0xFF); }
// Deterministic AES IV for encrypting the HMAC. We pick all-zeros
// so the Rust test can reproduce without a random-IV injection
// hack. (The real wire path uses a random IV per call; here we
// bypass that to make the test reproducible.)
byte[] aesIv = new byte[16];
// ---- crypto_key = shared_secret || passphrase_utf8 ----------
System.Numerics.BigInteger sharedValue = System.Numerics.BigInteger.ModPow(
new System.Numerics.BigInteger(remotePub),
new System.Numerics.BigInteger(privateKeyBytes),
prime);
byte[] shared = sharedValue.ToByteArray();
byte[] cryptoKey = [.. shared, .. System.Text.Encoding.UTF8.GetBytes(passphrase)];
// ---- canonical XML (empty MAC + IV) -------------------------
AuthenticateMe req = new()
{
ConnectionValidator = new()
{
ConnectionId = connectionId,
MessageNumber = messageNumber,
MessageAuthenticationCode = [],
SignatureInitializationVector = [],
},
ConsumerAuthenticationData = new AuthenticationData
{
Data = consumerData,
InitializationVector = consumerIv,
},
};
string xmlText = req.ToXml();
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(xmlText);
// ---- HMAC-SHA1(crypto_key, xml_utf8) ------------------------
using System.Security.Cryptography.HMACSHA1 hmac = new(cryptoKey);
byte[] hash = hmac.ComputeHash(xmlBytes);
// ---- AES key = PBKDF2-SHA1(base64(crypto_key), salt, 1000) --
byte[] salt = System.Text.Encoding.ASCII.GetBytes("ArchestrAService");
byte[] aesKey = System.Security.Cryptography.Rfc2898DeriveBytes.Pbkdf2(
Convert.ToBase64String(cryptoKey),
salt,
iterations: 1000,
System.Security.Cryptography.HashAlgorithmName.SHA1,
outputLength: 16);
// ---- AES-CBC encrypt(hash) with fixed IV --------------------
byte[] encryptedMac;
using (System.Security.Cryptography.Aes aes = System.Security.Cryptography.Aes.Create())
{
aes.Key = aesKey;
aes.IV = aesIv;
// CBC mode, PKCS7 padding (defaults).
using System.IO.MemoryStream ms = new();
using (System.Security.Cryptography.CryptoStream cs = new(
ms,
aes.CreateEncryptor(),
System.Security.Cryptography.CryptoStreamMode.Write))
{
cs.Write(hash, 0, hash.Length);
}
encryptedMac = ms.ToArray();
}
Console.WriteLine("# deterministic-hmac fixture (.NET reference output)");
Console.WriteLine($"prime_decimal={prime}");
Console.WriteLine($"generator={generator}");
Console.WriteLine($"private_key_hex={Convert.ToHexString(privateKeyBytes)}");
Console.WriteLine($"remote_pub_hex={Convert.ToHexString(remotePub)}");
Console.WriteLine($"passphrase={passphrase}");
Console.WriteLine($"connection_id={connectionId:D}");
Console.WriteLine($"message_number={messageNumber}");
Console.WriteLine($"consumer_data_hex={Convert.ToHexString(consumerData)}");
Console.WriteLine($"consumer_iv_hex={Convert.ToHexString(consumerIv)}");
Console.WriteLine($"aes_iv_hex={Convert.ToHexString(aesIv)}");
Console.WriteLine($"shared_secret_hex={Convert.ToHexString(shared)}");
Console.WriteLine($"shared_secret_len={shared.Length}");
Console.WriteLine($"crypto_key_hex={Convert.ToHexString(cryptoKey)}");
Console.WriteLine($"crypto_key_len={cryptoKey.Length}");
Console.WriteLine($"xml_utf8_len={xmlBytes.Length}");
Console.WriteLine($"xml_utf8_b64={Convert.ToBase64String(xmlBytes)}");
Console.WriteLine($"hmac_sha1_hex={Convert.ToHexString(hash)}");
Console.WriteLine($"aes_key_hex={Convert.ToHexString(aesKey)}");
Console.WriteLine($"encrypted_mac_hex={Convert.ToHexString(encryptedMac)}");
Console.WriteLine($"encrypted_mac_len={encryptedMac.Length}");
return;
}
if (probeConnectFailure)
{
try
{
using MxAsbDataClient connectFailureClient = MxAsbDataClient.Connect(new AsbConnectionOptions
{
Endpoint = endpoint,
SolutionName = solution,
Trace = Console.WriteLine,
DumpMessages = dumpMessages,
Via = clientVia,
});
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(new AsbConnectionOptions
{
Endpoint = endpoint,
SolutionName = solution,
Trace = Console.WriteLine,
DumpMessages = dumpMessages,
Via = clientVia,
});
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);