mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
912 lines
45 KiB
C#
912 lines
45 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 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.Collections.Generic;
|
|
using System.CommandLine;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using Opc.Ua.Client.ComplexTypes;
|
|
using Opc.Ua.Configuration;
|
|
using Opc.Ua.Identity;
|
|
using Opc.Ua.Security.Certificates;
|
|
|
|
namespace Quickstarts.ConsoleReferenceClient
|
|
{
|
|
/// <summary>
|
|
/// The program.
|
|
/// </summary>
|
|
public static class Program
|
|
{
|
|
/// <summary>
|
|
/// Main entry point.
|
|
/// </summary>
|
|
/// <exception cref="ErrorExitException"></exception>
|
|
public static Task<int> Main(string[] args)
|
|
{
|
|
Console.WriteLine("OPC UA Console Reference Client");
|
|
|
|
Console.WriteLine(
|
|
"OPC UA library: {0} @ {1} -- {2}",
|
|
Utils.GetAssemblyBuildNumber(),
|
|
Utils.GetAssemblyTimestamp().ToString("G", CultureInfo.InvariantCulture),
|
|
Utils.GetAssemblySoftwareVersion()
|
|
);
|
|
|
|
// The application name and config file names
|
|
const string applicationName = "ConsoleReferenceClient";
|
|
const string configSectionName = "Quickstarts.ReferenceClient";
|
|
|
|
// command line options
|
|
var autoAcceptOption = new Option<bool>("--autoaccept", "-a")
|
|
{
|
|
Description = "auto accept certificates (for testing only)"
|
|
};
|
|
var noSecurityOption = new Option<bool>("--nosecurity", "--nsec")
|
|
{
|
|
Description = "select endpoint with security NONE, least secure if unavailable"
|
|
};
|
|
var usernameOption = new Option<string>("--username", "--un")
|
|
{
|
|
Description = "the name of the user identity for the connection"
|
|
};
|
|
var userPasswordOption = new Option<string>("--userpassword", "--up")
|
|
{
|
|
Description = "the password of the user identity for the connection"
|
|
};
|
|
var userCertificateOption = new Option<string>("--usercertificate", "--uc")
|
|
{
|
|
Description = "the thumbprint of the user certificate for the user identity"
|
|
};
|
|
var userCertificatePasswordOption = new Option<string>("--usercertificatepassword", "--ucp")
|
|
{
|
|
Description = "the password of the user certificate for the user identity"
|
|
};
|
|
var consoleOption = new Option<bool>("--console", "-c") { Description = "log to console" };
|
|
var logOption = new Option<bool>("--log", "-l") { Description = "log app output" };
|
|
var fileOption = new Option<bool>("--file", "-f") { Description = "log to file" };
|
|
var passwordOption = new Option<string>("--password", "-p")
|
|
{
|
|
Description = "optional password for private key"
|
|
};
|
|
var renewOption = new Option<bool>("--renew", "-r")
|
|
{
|
|
Description = "renew application certificate"
|
|
};
|
|
var timeoutOption = new Option<int>("--timeout", "-t")
|
|
{
|
|
Description = "timeout in seconds to exit application",
|
|
DefaultValueFactory = _ => Timeout.Infinite
|
|
};
|
|
var logFileOption = new Option<string>("--logfile")
|
|
{
|
|
Description = "custom file name for log output"
|
|
};
|
|
var loadTypesOption = new Option<bool>("--loadtypes", "--lt")
|
|
{
|
|
Description = "Load custom types"
|
|
};
|
|
var managedBrowseAllOption = new Option<bool>("--managedbrowseall", "-m")
|
|
{
|
|
Description = "Browse all references using the MangedBrowseAsync method"
|
|
};
|
|
var browseAllOption = new Option<bool>("--browseall", "-b")
|
|
{
|
|
Description = "Browse all references"
|
|
};
|
|
var exportOption = new Option<bool>("--export", "-e")
|
|
{
|
|
Description = "Export all fetched nodes into Nodeset2 xml per default"
|
|
};
|
|
var exportNamespaceOption = new Option<string[]>("--namespace", "--ns")
|
|
{
|
|
Description =
|
|
"Restrict --export to nodes whose namespace URI is in the supplied list. " +
|
|
"Repeat the option to add more, e.g. --namespace http://opcfoundation.org/UA/Boiler/ " +
|
|
"--namespace urn:other. When omitted, all non-OPC-UA-base namespaces are exported."
|
|
};
|
|
var fetchAllOption = new Option<bool>("--fetchall", "--fa")
|
|
{
|
|
Description = "Fetch all nodes"
|
|
};
|
|
var jsonOption = new Option<bool>("--json", "-j")
|
|
{
|
|
Description = "Output all Values as JSON"
|
|
};
|
|
var verboseOption = new Option<bool>("--verbose", "-v") { Description = "Verbose output" };
|
|
var subscribeOption = new Option<bool>("--subscribe", "-s") { Description = "Subscribe" };
|
|
var testallEndpointsOption = new Option<bool>("--testall", "--ea") { Description = "Test All Endpoints" };
|
|
var reverseConnectOption = new Option<string>("--reverseconnect", "--rc")
|
|
{
|
|
Description = "Connect using the reverse connect endpoint. (e.g. --rc opc.tcp://localhost:65300)"
|
|
};
|
|
var foreverOption = new Option<bool>("--forever")
|
|
{
|
|
Description = "run inner connect/disconnect loop forever"
|
|
};
|
|
var leakChannelsOption = new Option<bool>("--leakchannels")
|
|
{
|
|
Description = "Leave a channel leak open when disconnecting a session."
|
|
};
|
|
var durableSubscriptionOption = new Option<bool>("--durablesubscription", "--ds")
|
|
{
|
|
Description = "SetDurableSubscription example"
|
|
};
|
|
var serverUrlArgument = new Argument<string>("serverUrl")
|
|
{
|
|
Description = "The OPC UA server URL to connect to",
|
|
DefaultValueFactory = _ => "opc.tcp://localhost:62541/Quickstarts/ReferenceServer"
|
|
};
|
|
|
|
var rootCommand = new RootCommand($"Usage: dotnet {applicationName}.dll [OPTIONS] [serverUrl]")
|
|
{
|
|
autoAcceptOption,
|
|
noSecurityOption,
|
|
usernameOption,
|
|
userPasswordOption,
|
|
userCertificateOption,
|
|
userCertificatePasswordOption,
|
|
consoleOption,
|
|
logOption,
|
|
fileOption,
|
|
passwordOption,
|
|
renewOption,
|
|
timeoutOption,
|
|
logFileOption,
|
|
loadTypesOption,
|
|
managedBrowseAllOption,
|
|
browseAllOption,
|
|
exportOption,
|
|
exportNamespaceOption,
|
|
fetchAllOption,
|
|
jsonOption,
|
|
verboseOption,
|
|
subscribeOption,
|
|
testallEndpointsOption,
|
|
reverseConnectOption,
|
|
foreverOption,
|
|
leakChannelsOption,
|
|
durableSubscriptionOption,
|
|
serverUrlArgument
|
|
};
|
|
|
|
rootCommand.SetAction(async (parseResult, cancellationToken) =>
|
|
{
|
|
bool autoAccept = parseResult.GetValue(autoAcceptOption);
|
|
bool noSecurity = parseResult.GetValue(noSecurityOption);
|
|
string? username = parseResult.GetValue(usernameOption);
|
|
byte[]? userpassword =
|
|
parseResult.GetValue(userPasswordOption) is string upStr ?
|
|
Encoding.UTF8.GetBytes(upStr) : null;
|
|
string? userCertificateThumbprint =
|
|
parseResult.GetValue(userCertificateOption);
|
|
byte[]? userCertificatePassword =
|
|
parseResult.GetValue(userCertificatePasswordOption) is string ucpStr ?
|
|
Encoding.UTF8.GetBytes(ucpStr) : null;
|
|
bool logConsole = parseResult.GetValue(consoleOption);
|
|
bool appLog = parseResult.GetValue(logOption);
|
|
bool fileLog = parseResult.GetValue(fileOption);
|
|
byte[]? pfxPassword =
|
|
parseResult.GetValue(passwordOption) is string pfxStr ?
|
|
Encoding.UTF8.GetBytes(pfxStr) : null;
|
|
bool renewCertificate = parseResult.GetValue(renewOption);
|
|
int timeout = parseResult.GetValue(timeoutOption);
|
|
if (timeout > 0)
|
|
{
|
|
timeout *= 1000;
|
|
}
|
|
string? logFile = parseResult.GetValue(logFileOption);
|
|
bool loadTypes = parseResult.GetValue(loadTypesOption);
|
|
bool managedbrowseall =
|
|
parseResult.GetValue(managedBrowseAllOption);
|
|
bool browseall = parseResult.GetValue(browseAllOption);
|
|
bool exportNodes = parseResult.GetValue(exportOption);
|
|
string[]? exportNamespacesRaw = parseResult.GetValue(exportNamespaceOption);
|
|
HashSet<string>? exportNamespaces = exportNamespacesRaw is { Length: > 0 }
|
|
? new HashSet<string>(exportNamespacesRaw, StringComparer.OrdinalIgnoreCase)
|
|
: null;
|
|
bool fetchall = parseResult.GetValue(fetchAllOption);
|
|
bool jsonvalues = parseResult.GetValue(jsonOption);
|
|
bool verbose = parseResult.GetValue(verboseOption);
|
|
bool subscribe = parseResult.GetValue(subscribeOption);
|
|
string? reverseConnectUrlString =
|
|
parseResult.GetValue(reverseConnectOption);
|
|
bool forever = parseResult.GetValue(foreverOption);
|
|
bool leakChannels = parseResult.GetValue(leakChannelsOption);
|
|
bool enableDurableSubscriptions =
|
|
parseResult.GetValue(durableSubscriptionOption);
|
|
// serverUrlArgument has DefaultValueFactory so GetValue returns non-null at runtime.
|
|
var serverUrl = new Uri(parseResult.GetValue(serverUrlArgument)!);
|
|
bool testallEndpoints = parseResult.GetValue(testallEndpointsOption);
|
|
|
|
ReverseConnectManager? reverseConnectManager = null;
|
|
using var telemetry = new ConsoleTelemetry();
|
|
ILogger logger = LoggerUtils.Null.Logger;
|
|
try
|
|
{
|
|
// log console output to logger
|
|
if (logConsole && appLog)
|
|
{
|
|
logger = telemetry.CreateLogger("Main");
|
|
}
|
|
|
|
// Define the UA Client application
|
|
ApplicationInstance.MessageDlg = new ApplicationMessageDlg();
|
|
// Lib param is annotated non-null but the impl tolerates null at runtime.
|
|
var passwordProvider = new CertificatePasswordProvider(pfxPassword!);
|
|
var application = new ApplicationInstance(telemetry)
|
|
{
|
|
ApplicationName = applicationName,
|
|
ApplicationType = ApplicationType.Client,
|
|
ConfigSectionName = configSectionName,
|
|
CertificatePasswordProvider = passwordProvider
|
|
};
|
|
|
|
// load the application configuration.
|
|
ApplicationConfiguration config = await application
|
|
.LoadApplicationConfigurationAsync(silent: false, ct: cancellationToken)
|
|
.ConfigureAwait(false);
|
|
// After LoadApplicationConfigurationAsync the property is set; cache the
|
|
// non-null reference so we don't repeat the null-forgiving operator.
|
|
ApplicationConfiguration appConfig = application.ApplicationConfiguration!;
|
|
|
|
// override logfile
|
|
if (logFile != null)
|
|
{
|
|
string? logFilePath = config.TraceConfiguration!.OutputFilePath;
|
|
string filename = Path.GetFileNameWithoutExtension(logFilePath)!;
|
|
config.TraceConfiguration.OutputFilePath = logFilePath!.Replace(
|
|
filename,
|
|
logFile,
|
|
StringComparison.Ordinal
|
|
);
|
|
config.TraceConfiguration.DeleteOnLoad = true;
|
|
// TODO: remove this in 2.0 migration; use telemetry.ConfigureLogging below.
|
|
#pragma warning disable CS0618 // Type or member is obsolete
|
|
config.TraceConfiguration.ApplySettings();
|
|
#pragma warning restore CS0618 // Type or member is obsolete
|
|
}
|
|
|
|
// setup the logging
|
|
telemetry.ConfigureLogging(
|
|
config,
|
|
applicationName,
|
|
logConsole,
|
|
fileLog,
|
|
appLog,
|
|
LogLevel.Warning);
|
|
|
|
// delete old certificate
|
|
if (renewCertificate)
|
|
{
|
|
await application.DeleteApplicationInstanceCertificateAsync(ct: cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
// If the stored application certificate was generated under a
|
|
// different hostname, its SAN URN no longer matches the runtime
|
|
// ApplicationUri (the stack substitutes "localhost" with the
|
|
// current hostname). LoadPrivateKeyAsync then filters the cert
|
|
// out and CheckApplicationInstanceCertificatesAsync throws
|
|
// "Cannot access private key" instead of regenerating. Pre-delete
|
|
// stale certs so the regeneration path runs transparently.
|
|
ArrayOf<CertificateIdentifier> applicationCerts
|
|
= appConfig.SecurityConfiguration.ApplicationCertificates;
|
|
for (int idx = 0; idx < applicationCerts.Count; idx++)
|
|
{
|
|
CertificateIdentifier certId = applicationCerts[idx];
|
|
Certificate? existing = await CertificateIdentifierResolver
|
|
.ResolveAsync(
|
|
certId,
|
|
registry: null,
|
|
needPrivateKey: false,
|
|
applicationUri: null,
|
|
telemetry,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
if (existing == null)
|
|
{
|
|
continue;
|
|
}
|
|
bool uriMatches;
|
|
using (existing)
|
|
{
|
|
uriMatches = X509Utils.CompareApplicationUriWithCertificate(
|
|
existing, appConfig.ApplicationUri!);
|
|
}
|
|
if (!uriMatches)
|
|
{
|
|
logger?.LogInformation(
|
|
"Stored application certificate's SAN URN does not match the configured ApplicationUri ({Uri}); regenerating.",
|
|
appConfig.ApplicationUri);
|
|
await application
|
|
.DeleteApplicationInstanceCertificateAsync(ct: cancellationToken)
|
|
.ConfigureAwait(false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check the application certificate.
|
|
bool haveAppCertificate = await application
|
|
.CheckApplicationInstanceCertificatesAsync(false, ct: cancellationToken)
|
|
.ConfigureAwait(false);
|
|
if (!haveAppCertificate)
|
|
{
|
|
throw new ErrorExitException(
|
|
"Application instance certificate invalid!",
|
|
ExitCode.ErrorCertificate
|
|
);
|
|
}
|
|
|
|
if (reverseConnectUrlString != null)
|
|
{
|
|
// start the reverse connection manager
|
|
Console.WriteLine($"Create reverse connection endpoint at {reverseConnectUrlString}.");
|
|
reverseConnectManager = new ReverseConnectManager(telemetry);
|
|
reverseConnectManager.AddEndpoint(new Uri(reverseConnectUrlString));
|
|
reverseConnectManager.StartService(config);
|
|
}
|
|
|
|
// wait for timeout or Ctrl-C
|
|
var quitCTS = new CancellationTokenSource();
|
|
CancellationToken ct = quitCTS.Token;
|
|
ManualResetEvent quitEvent = ConsoleUtils.CtrlCHandler(quitCTS);
|
|
|
|
// handle connect all endpoints test.
|
|
if (testallEndpoints)
|
|
{
|
|
var tester = new ConnectTester(telemetry, quitEvent);
|
|
await using (tester.ConfigureAwait(false))
|
|
{
|
|
await tester.RunAsync(ct).ConfigureAwait(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var identityProviders = new List<IClientIdentityProvider>();
|
|
|
|
// set user identity of type username/pw
|
|
if (!string.IsNullOrEmpty(username))
|
|
{
|
|
if (userpassword == null)
|
|
{
|
|
Console.WriteLine($"No password provided for user {username}, using empty password.");
|
|
}
|
|
|
|
var passwordStore = new InMemorySecretStore();
|
|
var passwordId = new SecretIdentifier(
|
|
"console-reference-client-password",
|
|
passwordStore.StoreType);
|
|
await passwordStore
|
|
.SetAsync(passwordId, userpassword ?? [], ct)
|
|
.ConfigureAwait(false);
|
|
identityProviders.Add(new UserNamePasswordIdentityProvider(
|
|
username!,
|
|
new SecretRegistry(passwordStore),
|
|
passwordId));
|
|
Console.WriteLine($"Connect with user identity for user {username}");
|
|
}
|
|
|
|
// set user identity of type certificate
|
|
if (!string.IsNullOrEmpty(userCertificateThumbprint))
|
|
{
|
|
CertificateIdentifier? userCertificateIdentifier =
|
|
await FindUserCertificateIdentifierAsync(
|
|
userCertificateThumbprint!,
|
|
appConfig.SecurityConfiguration
|
|
.TrustedUserCertificates!,
|
|
telemetry,
|
|
ct).ConfigureAwait(true);
|
|
|
|
if (userCertificateIdentifier != null)
|
|
{
|
|
identityProviders.Insert(0, new X509ClientIdentityProvider(
|
|
userCertificateIdentifier,
|
|
new CertificatePasswordProvider(userCertificatePassword!),
|
|
application.ApplicationConfiguration!.CertificateManager!.CertificateProvider));
|
|
|
|
Console.WriteLine($"Connect with user certificate with Thumbprint {userCertificateThumbprint}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Failed to load user certificate with Thumbprint {userCertificateThumbprint}");
|
|
}
|
|
}
|
|
|
|
identityProviders.Add(new AnonymousIdentityProvider());
|
|
var identityProvider = new CompositeClientIdentityProvider(identityProviders);
|
|
|
|
// connect to a server until application stops
|
|
bool quit = false;
|
|
DateTime start = DateTime.UtcNow;
|
|
int waitTime = int.MaxValue;
|
|
do
|
|
{
|
|
if (timeout > 0)
|
|
{
|
|
waitTime = timeout - (int)DateTime.UtcNow.Subtract(start).TotalMilliseconds;
|
|
if (waitTime <= 0)
|
|
{
|
|
if (!forever)
|
|
{
|
|
break;
|
|
}
|
|
|
|
waitTime = 0;
|
|
}
|
|
|
|
if (forever)
|
|
{
|
|
start = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
// create the UA Client object and connect to configured server.
|
|
using var uaClient = new UAClient(
|
|
appConfig,
|
|
reverseConnectManager,
|
|
telemetry,
|
|
null
|
|
)
|
|
{
|
|
AutoAccept = autoAccept,
|
|
SessionLifeTime = 60_000,
|
|
IdentityProvider = identityProvider
|
|
};
|
|
|
|
if (enableDurableSubscriptions)
|
|
{
|
|
uaClient.ReconnectPeriodExponentialBackoff = 60000;
|
|
}
|
|
|
|
bool connected = await uaClient
|
|
.ConnectAsync(serverUrl.ToString(), !noSecurity, ct)
|
|
.ConfigureAwait(false);
|
|
if (connected)
|
|
{
|
|
Console.WriteLine("Connected! Ctrl-C to quit.");
|
|
|
|
// enable subscription transfer
|
|
uaClient.ReconnectPeriod = 1000;
|
|
uaClient.ReconnectPeriodExponentialBackoff = 10000;
|
|
uaClient.Session.MinPublishRequestCount = 3;
|
|
uaClient.Session.TransferSubscriptionsOnReconnect = true;
|
|
var samples = new ClientSamples(
|
|
telemetry,
|
|
null,
|
|
quitEvent,
|
|
verbose);
|
|
if (loadTypes)
|
|
{
|
|
var complexTypeSystem = new ComplexTypeSystem(uaClient.Session, telemetry);
|
|
await samples.LoadTypeSystemAsync(complexTypeSystem, ct).ConfigureAwait(false);
|
|
}
|
|
|
|
if (browseall || fetchall || jsonvalues || managedbrowseall)
|
|
{
|
|
List<NodeId>? variableIds = null;
|
|
List<NodeId>? variableIdsManagedBrowse = null;
|
|
ArrayOf<ReferenceDescription> referenceDescriptions = default;
|
|
ArrayOf<ReferenceDescription> referenceDescriptionsFromManagedBrowse = default;
|
|
|
|
if (browseall)
|
|
{
|
|
Console.WriteLine("Browse the full address space.");
|
|
referenceDescriptions = await samples
|
|
.BrowseFullAddressSpaceAsync(uaClient, ObjectIds.RootFolder, ct: ct)
|
|
.ConfigureAwait(false);
|
|
variableIds =
|
|
[
|
|
.. referenceDescriptions
|
|
.Filter(r =>
|
|
r.NodeClass == NodeClass.Variable &&
|
|
r.TypeDefinition.NamespaceIndex != 0
|
|
)
|
|
.ConvertAll(r => ExpandedNodeId.ToNodeId(
|
|
r.NodeId,
|
|
uaClient.Session.NamespaceUris))
|
|
];
|
|
}
|
|
|
|
if (managedbrowseall)
|
|
{
|
|
Console.WriteLine("ManagedBrowse the full address space.");
|
|
referenceDescriptionsFromManagedBrowse = await samples
|
|
.ManagedBrowseFullAddressSpaceAsync(
|
|
uaClient,
|
|
ObjectIds.RootFolder,
|
|
ct: ct)
|
|
.ConfigureAwait(false);
|
|
variableIdsManagedBrowse =
|
|
[
|
|
.. referenceDescriptionsFromManagedBrowse
|
|
.Filter(r =>
|
|
r.NodeClass == NodeClass.Variable &&
|
|
r.TypeDefinition.NamespaceIndex != 0
|
|
)
|
|
.ConvertAll(r => ExpandedNodeId.ToNodeId(
|
|
r.NodeId,
|
|
uaClient.Session.NamespaceUris))
|
|
];
|
|
}
|
|
|
|
// treat managedBrowseall result like browseall results if the latter is missing
|
|
if (!browseall && managedbrowseall)
|
|
{
|
|
referenceDescriptions = referenceDescriptionsFromManagedBrowse;
|
|
browseall = managedbrowseall;
|
|
}
|
|
|
|
IList<INode>? allNodes = null;
|
|
if (fetchall)
|
|
{
|
|
allNodes = await samples
|
|
.FetchAllNodesNodeCacheAsync(
|
|
uaClient,
|
|
ObjectIds.RootFolder,
|
|
true,
|
|
true,
|
|
false,
|
|
ct: ct)
|
|
.ConfigureAwait(false);
|
|
variableIds =
|
|
[
|
|
.. allNodes
|
|
.Where(r =>
|
|
r.NodeClass == NodeClass.Variable &&
|
|
r is VariableNode v &&
|
|
v.DataType.NamespaceIndex != 0
|
|
)
|
|
.Select(r => ExpandedNodeId.ToNodeId(
|
|
r.NodeId,
|
|
uaClient.Session.NamespaceUris))
|
|
];
|
|
|
|
if (exportNodes)
|
|
{
|
|
await samples.ExportNodesToNodeSet2PerNamespaceAsync(
|
|
uaClient.Session,
|
|
allNodes,
|
|
Environment.CurrentDirectory,
|
|
exportNamespaces,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
if (jsonvalues && variableIds != null)
|
|
{
|
|
(
|
|
ArrayOf<DataValue> allValues,
|
|
ArrayOf<ServiceResult> results
|
|
) = await samples
|
|
.ReadAllValuesAsync(uaClient, variableIds.ToArrayOf(), ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
if (subscribe && (browseall || fetchall))
|
|
{
|
|
// subscribe to 1000 random variables
|
|
const int maxVariables = 1000;
|
|
var variables = new List<Node>();
|
|
|
|
if (fetchall)
|
|
{
|
|
variables.AddRange(
|
|
allNodes!
|
|
.Where(r =>
|
|
r.NodeClass == NodeClass.Variable &&
|
|
r.NodeId.NamespaceIndex > 1
|
|
)
|
|
.Cast<VariableNode>()
|
|
.OrderBy(o => UnsecureRandom.Shared.Next())
|
|
.Take(maxVariables)
|
|
);
|
|
}
|
|
else if (browseall)
|
|
{
|
|
var variableReferences = referenceDescriptions
|
|
.ToList()
|
|
.Where(r => r.NodeClass == NodeClass.Variable &&
|
|
r.NodeId.NamespaceIndex > 1)
|
|
.Select(r => r.NodeId)
|
|
.OrderBy(o => UnsecureRandom.Shared.Next())
|
|
.Take(maxVariables)
|
|
.ToList();
|
|
variables.AddRange(
|
|
(await uaClient.Session.NodeCache.FindAsync(variableReferences, ct)
|
|
.ConfigureAwait(false))
|
|
.ToList()
|
|
.Cast<Node>()
|
|
);
|
|
}
|
|
|
|
await samples
|
|
.SubscribeAllValuesAsync(
|
|
uaClient,
|
|
variableIds: [.. variables],
|
|
samplingInterval: 100,
|
|
publishingInterval: 1000,
|
|
queueSize: 10,
|
|
lifetimeCount: 60,
|
|
keepAliveCount: 2,
|
|
ct: ct
|
|
)
|
|
.ConfigureAwait(false);
|
|
|
|
// Wait for DataChange notifications from MonitoredItems
|
|
Console.WriteLine($"Subscribed to {maxVariables} variables. Press Ctrl-C to exit.");
|
|
|
|
// free unused memory
|
|
uaClient.Session.NodeCache.Clear();
|
|
|
|
waitTime = timeout -
|
|
(int)DateTime.UtcNow.Subtract(start).TotalMilliseconds;
|
|
DateTime endTime =
|
|
waitTime > 0
|
|
? DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(waitTime))
|
|
: DateTime.MaxValue;
|
|
List<Node>.Enumerator variableIterator = variables.GetEnumerator();
|
|
while (!quit && endTime > DateTime.UtcNow)
|
|
{
|
|
if (variableIterator.MoveNext())
|
|
{
|
|
try
|
|
{
|
|
DataValue value = await uaClient
|
|
.Session.ReadValueAsync(
|
|
variableIterator.Current.NodeId, ct)
|
|
.ConfigureAwait(false);
|
|
Console.WriteLine($"Value of {variableIterator.Current.NodeId} is {value}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error reading value of {variableIterator.Current.NodeId}: {ex.Message}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
variableIterator = variables.GetEnumerator();
|
|
}
|
|
quit = quitEvent.WaitOne(500);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
quit = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int quitTimeout = 65_000;
|
|
if (enableDurableSubscriptions)
|
|
{
|
|
quitTimeout = 150_000;
|
|
uaClient.ReconnectPeriod = 500_000;
|
|
}
|
|
|
|
NodeId sessionNodeId = uaClient.Session.SessionId;
|
|
// Run tests for available methods on reference server.
|
|
await samples.ReadNodesAsync(
|
|
uaClient.Session,
|
|
ct).ConfigureAwait(false);
|
|
await samples.WriteNodesAsync(
|
|
uaClient.Session,
|
|
ct).ConfigureAwait(false);
|
|
await samples.BrowseAsync(
|
|
uaClient.Session,
|
|
ct).ConfigureAwait(false);
|
|
await samples.CallMethodAsync(
|
|
uaClient.Session,
|
|
ct).ConfigureAwait(false);
|
|
await samples.EnableEventsAsync(
|
|
uaClient.Session,
|
|
(uint)quitTimeout,
|
|
ct).ConfigureAwait(false);
|
|
await samples.SubscribeToDataChangesAsync(
|
|
uaClient.Session,
|
|
60_000,
|
|
enableDurableSubscriptions,
|
|
ct).ConfigureAwait(false);
|
|
|
|
Console.WriteLine("Waiting...");
|
|
|
|
// Wait for some DataChange notifications from MonitoredItems
|
|
int waitCounters = 0;
|
|
const int checkForWaitTime = 1000;
|
|
const int closeSessionTime = checkForWaitTime * 15;
|
|
const int restartSessionTime = checkForWaitTime * 45;
|
|
const bool stopNotQuit = false;
|
|
int stopCount = 0;
|
|
while (!quit && !stopNotQuit && waitCounters < quitTimeout)
|
|
{
|
|
quit = quitEvent.WaitOne(checkForWaitTime);
|
|
waitCounters += checkForWaitTime;
|
|
if (enableDurableSubscriptions)
|
|
{
|
|
if (waitCounters == closeSessionTime &&
|
|
uaClient.Session.SubscriptionCount == 1)
|
|
{
|
|
Console.WriteLine($"Closing Session (CurrentTime: {DateTime.Now:T})");
|
|
await uaClient.Session.CloseAsync(closeChannel: false, ct: ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
if (waitCounters == restartSessionTime)
|
|
{
|
|
Console.WriteLine($"Restarting Session (CurrentTime: {DateTime.Now:T})");
|
|
await uaClient
|
|
.DurableSubscriptionTransferAsync(
|
|
serverUrl.ToString(),
|
|
useSecurity: !noSecurity,
|
|
ct
|
|
)
|
|
.ConfigureAwait(true);
|
|
}
|
|
|
|
if (waitCounters is > closeSessionTime and < restartSessionTime)
|
|
{
|
|
Console.WriteLine(
|
|
"No Communication Interval " +
|
|
stopCount.ToString(CultureInfo.InvariantCulture)
|
|
);
|
|
stopCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Console.WriteLine("Client disconnected.");
|
|
|
|
await uaClient.DisconnectAsync(leakChannels, ct).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Could not connect to server! Retry in 10 seconds or Ctrl-C to quit.");
|
|
quit = quitEvent.WaitOne(Math.Min(10_000, waitTime));
|
|
}
|
|
} while (!quit);
|
|
|
|
Console.WriteLine("Client stopped.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"{ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
reverseConnectManager?.Dispose();
|
|
}
|
|
});
|
|
|
|
args = ConsoleUtils.MergeEnvironmentArgs(args, "REFCLIENT", rootCommand);
|
|
ParseResult cmdParseResult = rootCommand.Parse(args);
|
|
return cmdParseResult
|
|
.InvokeAsync(new InvocationConfiguration(), CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// returns a CertificateIdentifier of the Certificate with the specified thumbprint
|
|
/// if it is found in the trustedUserCertificates TrustList
|
|
/// </summary>
|
|
/// <param name="thumbprint">the thumbprint of the certificate to select</param>
|
|
/// <param name="trustedUserCertificates">the trustlist of the user certificates</param>
|
|
/// <param name="ct">the cancellation token</param>
|
|
/// <returns>Certificate Identifier</returns>
|
|
private static async Task<CertificateIdentifier?> FindUserCertificateIdentifierAsync(
|
|
string thumbprint,
|
|
CertificateTrustList trustedUserCertificates,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct = default)
|
|
{
|
|
CertificateIdentifier? userCertificateIdentifier = null;
|
|
|
|
using CertificateCollection certificates =
|
|
await trustedUserCertificates.GetCertificatesAsync(telemetry, ct).ConfigureAwait(false);
|
|
// get user certificate with matching thumbprint
|
|
using CertificateCollection userCertificatesWithMatchingThumbprint =
|
|
certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
|
|
|
|
// create Certificate Identifier
|
|
if (userCertificatesWithMatchingThumbprint.Count == 1)
|
|
{
|
|
Certificate userCert = userCertificatesWithMatchingThumbprint[0];
|
|
userCertificateIdentifier = new CertificateIdentifier
|
|
{
|
|
Thumbprint = userCert.Thumbprint,
|
|
SubjectName = userCert.Subject,
|
|
StorePath = trustedUserCertificates.StorePath,
|
|
StoreType = trustedUserCertificates.StoreType
|
|
};
|
|
}
|
|
|
|
return userCertificateIdentifier;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A dialog which asks for user input.
|
|
/// </summary>
|
|
public class ApplicationMessageDlg : IApplicationMessageDlg
|
|
{
|
|
private readonly TextWriter m_output;
|
|
private string m_message = string.Empty;
|
|
private bool m_ask;
|
|
|
|
public ApplicationMessageDlg(TextWriter? output = null)
|
|
{
|
|
m_output = output ?? Console.Out;
|
|
}
|
|
|
|
public override void Message(string text, bool ask)
|
|
{
|
|
m_message = text;
|
|
m_ask = ask;
|
|
}
|
|
|
|
public override async Task<bool> ShowAsync()
|
|
{
|
|
if (m_ask)
|
|
{
|
|
var message = new StringBuilder(m_message);
|
|
message.Append(" (y/n, default y): ");
|
|
m_output.Write(message.ToString());
|
|
|
|
try
|
|
{
|
|
ConsoleKeyInfo result = Console.ReadKey();
|
|
m_output.WriteLine();
|
|
return await Task.FromResult(result.KeyChar is 'y' or 'Y' or '\r')
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// intentionally fall through
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_output.WriteLine(m_message);
|
|
}
|
|
|
|
return await Task.FromResult(true).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|