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 `` // 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 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 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 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 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()), 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(string[] args, string name, TryParse 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(string value, out T result);