/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.IO; using Opc.Ua; namespace GmsSample.Client { /// /// Bounds passive reads and requires both an exact target and consent for result acknowledgement. /// internal sealed record GmsClientArguments { /// /// Gets the discovery URL. /// public string Endpoint { get; private init; } = "opc.tcp://127.0.0.1:62558/GmsServer"; /// /// Gets an optional exact system browse name, required before acknowledgement. /// public string? SystemName { get; private init; } /// /// Gets the maximum window per observation; zero skips subscriptions. /// public int Seconds { get; private init; } = 5; /// /// Gets the notification bound; result queries additionally cap their page at sixteen identifiers. /// public int Count { get; private init; } = 8; /// /// Gets the per-file byte bound. /// public int MaximumFileBytes { get; private init; } = 32 * 1024; /// /// Gets whether optional files should be read into a bounded in-memory ByteString. /// public bool Download { get; private init; } /// /// Gets whether to observe events, retained environmental conditions and intermediate characteristics. /// public bool Events { get; private init; } /// /// Gets the explicit characteristic placeholder name in the selected instance's namespace. /// public string? Characteristic { get; private init; } /// /// Gets the exact result identifier to acknowledge; null leaves all result state untouched. /// public string? AcknowledgeResult { get; private init; } /// /// Gets explicit consent to change the targeted result's acknowledgement state. /// public bool ConfirmAcknowledgement { get; private init; } /// /// Gets whether bounded local-demo error and cancellation cleanup checks are requested. /// public bool VerifyCleanup { get; private init; } /// /// Gets explicit loopback-only consent to accept an unknown server certificate. /// public bool DemoTrust { get; private init; } /// /// Gets the retained certificate-store directory. /// public string PkiRoot { get; private init; } = Path.Combine("artifacts", "gms-pki", "client"); /// /// Rejects unsafe trust exceptions, unbounded observations and ambiguous state-changing targets. /// /// An option, endpoint or acknowledgement consent is invalid. internal static GmsClientArguments Parse(ArrayOf arguments) { var options = new GmsClientArguments(); for (int i = 0; i < arguments.Count; i++) { options = arguments[i] switch { "--endpoint" => options with { Endpoint = SampleArguments.ReadValue(arguments, ref i) }, "--system" => options with { SystemName = SampleArguments.ReadValue(arguments, ref i) }, "--seconds" => options with { Seconds = SampleArguments.ReadInteger(arguments, ref i, 0, 15) }, "--count" => options with { Count = SampleArguments.ReadInteger(arguments, ref i, 1, 64) }, "--max-file-bytes" => options with { MaximumFileBytes = SampleArguments.ReadInteger(arguments, ref i, 1, 1024 * 1024) }, "--pki" => options with { PkiRoot = SampleArguments.ReadValue(arguments, ref i) }, "--characteristic" => options with { Characteristic = SampleArguments.ReadValue(arguments, ref i) }, "--acknowledge" => options with { AcknowledgeResult = SampleArguments.ReadValue(arguments, ref i) }, "--confirm-acknowledgement" => options with { ConfirmAcknowledgement = true }, "--events" => options with { Events = true }, "--download" => options with { Download = true }, "--verify-cleanup" => options with { VerifyCleanup = true }, "--demo-trust" => options with { DemoTrust = true }, _ => throw new ArgumentException($"Unknown option '{arguments[i]}'.", nameof(arguments)) }; } if (!Uri.TryCreate(options.Endpoint, UriKind.Absolute, out Uri? endpoint) || endpoint.Scheme != "opc.tcp" || endpoint.Port < 1 || !string.IsNullOrEmpty(endpoint.UserInfo)) { throw new ArgumentException("--endpoint requires an opc.tcp URL with a port and no credentials.", nameof(arguments)); } if (options.DemoTrust && !endpoint.IsLoopback) { throw new ArgumentException("--demo-trust is restricted to loopback endpoints.", nameof(arguments)); } if (options.AcknowledgeResult != null && (options.SystemName == null || !options.ConfirmAcknowledgement)) { throw new ArgumentException( "--acknowledge requires an explicit --system and --confirm-acknowledgement.", nameof(arguments)); } if (options.ConfirmAcknowledgement && options.AcknowledgeResult == null) { throw new ArgumentException("--confirm-acknowledgement requires --acknowledge .", nameof(arguments)); } if (options.Characteristic != null && (!options.Events || options.Seconds == 0)) { throw new ArgumentException("--characteristic requires --events and a nonzero --seconds.", nameof(arguments)); } if (options.VerifyCleanup && (!options.DemoTrust || !options.Download || options.SystemName == null || options.Seconds == 0)) { throw new ArgumentException( "--verify-cleanup requires --demo-trust, --download, --system and nonzero --seconds.", nameof(arguments)); } return options; } /// /// Prints all implemented flags without connecting or changing trust. /// internal static void PrintHelp() { Console.WriteLine(""" GmsClient [--endpoint ] [--system ] [--pki ] [--seconds <0..15>] [--count <1..64>] [--events] [--characteristic ] [--download] [--max-file-bytes <1..1048576>] [--demo-trust] [--acknowledge --confirm-acknowledgement] [--verify-cleanup] [--help] Defaults: 127.0.0.1:62558/GmsServer, all systems, 5 seconds, 8 records, 32768 file bytes. Passive by default. Acknowledgement requires an explicit system, result and confirmation. No measurement/job/calibration commands, correction writes or implicit condition acknowledgement. --events reads retained sensor warnings and live events; --characteristic Bore selects the sample's native intermediate-result placeholder in the system's application namespace. --download skips absent file transfer explicitly; failures are never reported as empty successes. --verify-cleanup demonstrates missing-result/file-limit refusal and cancellation, then reuses the sample's single metadata/file-handle capacity. It requires a loopback demo and a system. SignAndEncrypt and domain validation remain enabled with --demo-trust; remote use rejects it. Two selected systems maximum, 128 KiB metadata/read bounds, 1 MiB data/event collection bounds. Query pages contain at most 16 IDs. Use --count 32 to observe beyond the initial advanced inventory. All work has a 120-second deadline. Ctrl+C cancels and awaits subscription/session/host cleanup. """); } } }