Files
UA-.NETStandard/Applications/ConsoleReferenceClient/Program.cs
T
Martin Regen ebf8f6265d Fix reopen secure channel without activate (#2577)
Fix that if a socket is closed, new service calls are queued and a new channel is established without reconnect / activate.
Add some logic which ensures that a BeginConnect operation is only started if a valid service call is queued first.
The activate is prepared by the session reconnect handler which may take some time to respond. Until then, all other service fail immediately with BadNotConnected.
To detect socket failures earlier, the PublishRequest and KeepAlive send are also hooked up to the KeepAliveError to trigger a reconnect. To disable the keep alive timing calcualtion, an additional variable tracks the statuscode of the last error to set KeepAliveStopped to true.
2024-06-02 19:05:17 +02:00

377 lines
19 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2021 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.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
namespace Quickstarts.ConsoleReferenceClient
{
/// <summary>
/// The program.
/// </summary>
public static class Program
{
/// <summary>
/// Main entry point.
/// </summary>
public static async Task Main(string[] args)
{
TextWriter output = Console.Out;
output.WriteLine("OPC UA Console Reference Client");
output.WriteLine("OPC UA library: {0} @ {1} -- {2}",
Utils.GetAssemblyBuildNumber(),
Utils.GetAssemblyTimestamp().ToString("G", CultureInfo.InvariantCulture),
Utils.GetAssemblySoftwareVersion());
// The application name and config file names
var applicationName = "ConsoleReferenceClient";
var configSectionName = "Quickstarts.ReferenceClient";
var usage = $"Usage: dotnet {applicationName}.dll [OPTIONS]";
// command line options
bool showHelp = false;
bool autoAccept = false;
string username = null;
string userpassword = null;
bool logConsole = false;
bool appLog = false;
bool renewCertificate = false;
bool loadTypes = false;
bool browseall = false;
bool fetchall = false;
bool jsonvalues = false;
bool verbose = false;
bool subscribe = false;
bool noSecurity = false;
string password = null;
int timeout = Timeout.Infinite;
string logFile = null;
string reverseConnectUrlString = null;
bool leakChannels = false;
bool forever = false;
Mono.Options.OptionSet options = new Mono.Options.OptionSet {
usage,
{ "h|help", "show this message and exit", h => showHelp = h != null },
{ "a|autoaccept", "auto accept certificates (for testing only)", a => autoAccept = a != null },
{ "nsec|nosecurity", "select endpoint with security NONE, least secure if unavailable", s => noSecurity = s != null },
{ "un|username=", "the name of the user identity for the connection", (string u) => username = u },
{ "up|userpassword=", "the password of the user identity for the connection", (string u) => userpassword = u },
{ "c|console", "log to console", c => logConsole = c != null },
{ "l|log", "log app output", c => appLog = c != null },
{ "p|password=", "optional password for private key", (string p) => password = p },
{ "r|renew", "renew application certificate", r => renewCertificate = r != null },
{ "t|timeout=", "timeout in seconds to exit application", (int t) => timeout = t * 1000 },
{ "logfile=", "custom file name for log output", l => { if (l != null) { logFile = l; } } },
{ "lt|loadtypes", "Load custom types", lt => { if (lt != null) loadTypes = true; } },
{ "b|browseall", "Browse all references", b => { if (b != null) browseall = true; } },
{ "f|fetchall", "Fetch all nodes", f => { if (f != null) fetchall = true; } },
{ "j|json", "Output all Values as JSON", j => { if (j != null) jsonvalues = true; } },
{ "v|verbose", "Verbose output", v => { if (v != null) verbose = true; } },
{ "s|subscribe", "Subscribe", s => { if (s != null) subscribe = true; } },
{ "rc|reverseconnect=", "Connect using the reverse connect endpoint. (e.g. rc=opc.tcp://localhost:65300)", (string url) => reverseConnectUrlString = url},
{ "forever", "run inner connect/disconnect loop forever", f => { if (f != null) forever = true; } },
{ "leakchannels", "Leave a channel leak open when disconnecting a session.", l => { if (l != null) leakChannels = true; } },
};
ReverseConnectManager reverseConnectManager = null;
try
{
// parse command line and set options
var extraArg = ConsoleUtils.ProcessCommandLine(output, args, options, ref showHelp, "REFCLIENT", false);
// connect Url?
Uri serverUrl = new Uri("opc.tcp://localhost:62541/Quickstarts/ReferenceServer");
if (!string.IsNullOrEmpty(extraArg))
{
serverUrl = new Uri(extraArg);
}
// log console output to logger
if (logConsole && appLog)
{
output = new LogWriter();
}
// Define the UA Client application
ApplicationInstance.MessageDlg = new ApplicationMessageDlg(output);
CertificatePasswordProvider PasswordProvider = new CertificatePasswordProvider(password);
ApplicationInstance application = new ApplicationInstance {
ApplicationName = applicationName,
ApplicationType = ApplicationType.Client,
ConfigSectionName = configSectionName,
CertificatePasswordProvider = PasswordProvider
};
// load the application configuration.
var config = await application.LoadApplicationConfiguration(silent: false).ConfigureAwait(false);
// override logfile
if (logFile != null)
{
var logFilePath = config.TraceConfiguration.OutputFilePath;
var filename = Path.GetFileNameWithoutExtension(logFilePath);
config.TraceConfiguration.OutputFilePath = logFilePath.Replace(filename, logFile);
config.TraceConfiguration.DeleteOnLoad = true;
config.TraceConfiguration.ApplySettings();
}
// setup the logging
ConsoleUtils.ConfigureLogging(config, applicationName, logConsole, LogLevel.Information);
// delete old certificate
if (renewCertificate)
{
await application.DeleteApplicationInstanceCertificate().ConfigureAwait(false);
}
// check the application certificate.
bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, minimumKeySize: 0).ConfigureAwait(false);
if (!haveAppCertificate)
{
throw new ErrorExitException("Application instance certificate invalid!", ExitCode.ErrorCertificate);
}
if (reverseConnectUrlString != null)
{
// start the reverse connection manager
output.WriteLine("Create reverse connection endpoint at {0}.", reverseConnectUrlString);
reverseConnectManager = new ReverseConnectManager();
reverseConnectManager.AddEndpoint(new Uri(reverseConnectUrlString));
reverseConnectManager.StartService(config);
}
// wait for timeout or Ctrl-C
var quitCTS = new CancellationTokenSource();
var quitEvent = ConsoleUtils.CtrlCHandler(quitCTS);
// 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;
}
else
{
waitTime = 0;
}
}
if (forever)
{
start = DateTime.UtcNow;
}
}
// create the UA Client object and connect to configured server.
using (UAClient uaClient = new UAClient(application.ApplicationConfiguration, reverseConnectManager, output, ClientBase.ValidateResponse) {
AutoAccept = autoAccept,
SessionLifeTime = 60_000,
})
{
// set user identity
if (!String.IsNullOrEmpty(username))
{
uaClient.UserIdentity = new UserIdentity(username, userpassword ?? string.Empty);
}
bool connected = await uaClient.ConnectAsync(serverUrl.ToString(), !noSecurity, quitCTS.Token).ConfigureAwait(false);
if (connected)
{
output.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(output, ClientBase.ValidateResponse, quitEvent, verbose);
if (loadTypes)
{
var complexTypeSystem = await samples.LoadTypeSystemAsync(uaClient.Session).ConfigureAwait(false);
}
if (browseall || fetchall || jsonvalues)
{
NodeIdCollection variableIds = null;
ReferenceDescriptionCollection referenceDescriptions = null;
if (browseall)
{
referenceDescriptions =
await samples.BrowseFullAddressSpaceAsync(uaClient, Objects.RootFolder).ConfigureAwait(false);
variableIds = new NodeIdCollection(referenceDescriptions
.Where(r => r.NodeClass == NodeClass.Variable && r.TypeDefinition.NamespaceIndex != 0)
.Select(r => ExpandedNodeId.ToNodeId(r.NodeId, uaClient.Session.NamespaceUris)));
}
IList<INode> allNodes = null;
if (fetchall)
{
allNodes = await samples.FetchAllNodesNodeCacheAsync(uaClient, Objects.RootFolder, true, true, false).ConfigureAwait(false);
variableIds = new NodeIdCollection(allNodes
.Where(r => r.NodeClass == NodeClass.Variable && r is VariableNode && ((VariableNode)r).DataType.NamespaceIndex != 0)
.Select(r => ExpandedNodeId.ToNodeId(r.NodeId, uaClient.Session.NamespaceUris)));
}
if (jsonvalues && variableIds != null)
{
var (allValues, results) = await samples.ReadAllValuesAsync(uaClient, variableIds).ConfigureAwait(false);
}
if (subscribe && (browseall || fetchall))
{
// subscribe to 1000 random variables
const int MaxVariables = 1000;
NodeCollection variables = new NodeCollection();
Random random = new Random(62541);
if (fetchall)
{
variables.AddRange(allNodes
.Where(r => r.NodeClass == NodeClass.Variable && r.NodeId.NamespaceIndex > 1)
.Select(r => ((VariableNode)r))
.OrderBy(o => random.Next())
.Take(MaxVariables));
}
else if (browseall)
{
var variableReferences = referenceDescriptions
.Where(r => r.NodeClass == NodeClass.Variable && r.NodeId.NamespaceIndex > 1)
.Select(r => r.NodeId)
.OrderBy(o => random.Next())
.Take(MaxVariables)
.ToList();
variables.AddRange(uaClient.Session.NodeCache.Find(variableReferences).Cast<Node>());
}
await samples.SubscribeAllValuesAsync(uaClient,
variableIds: new NodeCollection(variables),
samplingInterval: 100,
publishingInterval: 1000,
queueSize: 10,
lifetimeCount: 60,
keepAliveCount: 2).ConfigureAwait(false);
// Wait for DataChange notifications from MonitoredItems
output.WriteLine("Subscribed to {0} variables. Press Ctrl-C to exit.", MaxVariables);
// 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;
var variableIterator = variables.GetEnumerator();
while (!quit && endTime > DateTime.UtcNow)
{
if (variableIterator.MoveNext())
{
try
{
var value = await uaClient.Session.ReadValueAsync(variableIterator.Current.NodeId).ConfigureAwait(false);
output.WriteLine("Value of {0} is {1}", variableIterator.Current.NodeId, value);
}
catch (Exception ex)
{
output.WriteLine("Error reading value of {0}: {1}", variableIterator.Current.NodeId, ex.Message);
}
}
else
{
variableIterator = variables.GetEnumerator();
}
quit = quitEvent.WaitOne(500);
}
}
else
{
quit = true;
}
}
else
{
// Run tests for available methods on reference server.
samples.ReadNodes(uaClient.Session);
samples.WriteNodes(uaClient.Session);
samples.Browse(uaClient.Session);
samples.CallMethod(uaClient.Session);
samples.SubscribeToDataChanges(uaClient.Session, 120_000);
output.WriteLine("Waiting...");
// Wait for some DataChange notifications from MonitoredItems
quit = quitEvent.WaitOne(timeout > 0 ? waitTime : 30_000);
}
output.WriteLine("Client disconnected.");
uaClient.Disconnect(leakChannels);
}
else
{
output.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);
output.WriteLine("Client stopped.");
}
catch (Exception ex)
{
output.WriteLine(ex.Message);
}
finally
{
Utils.SilentDispose(reverseConnectManager);
output.Close();
}
}
}
}