/// <summary> /// Asynchronous part of the main method of the app. /// </summary> public async static Task MainAsync(string[] args) { try { var shouldShowHelp = false; // Shutdown token sources. ShutdownTokenSource = new CancellationTokenSource(); // detect the runtime environment. either we run standalone (native or containerized) or as IoT Edge module (containerized) // check if we have an environment variable containing an IoT Edge connectionstring, we run as IoT Edge module if (IsIotEdgeModule) { WriteLine("IoTEdge detected."); } // command line options Mono.Options.OptionSet options = new Mono.Options.OptionSet { // Publisher configuration options { "pf|publishfile=", $"the filename to configure the nodes to publish.\nDefault: '{PublisherNodeConfigurationFilename}'", (string p) => PublisherNodeConfigurationFilename = p }, { "tc|telemetryconfigfile=", $"the filename to configure the ingested telemetry\nDefault: '{PublisherTelemetryConfigurationFilename}'", (string p) => PublisherTelemetryConfigurationFilename = p }, { "s|site=", $"the site OPC Publisher is working in. if specified this domain is appended (delimited by a ':' to the 'ApplicationURI' property when telemetry is sent to IoTHub.\n" + "The value must follow the syntactical rules of a DNS hostname.\nDefault: not set", (string s) => { Regex siteNameRegex = new Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"); if (siteNameRegex.IsMatch(s)) { PublisherSite = s; } else { throw new OptionException("The shopfloor site is not a valid DNS hostname.", "site"); } } }, { "sd|shopfloordomain=", $"same as site option, only there for backward compatibility\n" + "The value must follow the syntactical rules of a DNS hostname.\nDefault: not set", (string s) => { Regex siteNameRegex = new Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"); if (siteNameRegex.IsMatch(s)) { PublisherSite = s; } else { throw new OptionException("The shopfloor domain is not a valid DNS hostname.", "shopfloordomain"); } } }, { "ic|iotcentral", $"publisher will send OPC UA data in IoTCentral compatible format (DisplayName of a node is used as key, this key is the Field name in IoTCentral). you need to ensure that all DisplayName's are unique. (Auto enables fetch display name)\nDefault: {IotCentralMode}", b => IotCentralMode = FetchOpcNodeDisplayName = b != null }, { "sw|sessionconnectwait=", $"specify the wait time in seconds publisher is trying to connect to disconnected endpoints and starts monitoring unmonitored items\nMin: 10\nDefault: {_publisherSessionConnectWaitSec}", (int i) => { if (i > 10) { _publisherSessionConnectWaitSec = i; } else { throw new OptionException("The sessionconnectwait must be greater than 10 sec", "sessionconnectwait"); } } }, { "mq|monitoreditemqueuecapacity=", $"specify how many notifications of monitored items can be stored in the internal queue, if the data can not be sent quick enough to IoTHub\nMin: 1024\nDefault: {MonitoredItemsQueueCapacity}", (int i) => { if (i >= 1024) { MonitoredItemsQueueCapacity = i; } else { throw new OptionException("The monitoreditemqueueitems must be greater than 1024", "monitoreditemqueueitems"); } } }, { "di|diagnosticsinterval=", $"shows publisher diagnostic info at the specified interval in seconds (need log level info). 0 disables diagnostic output.\nDefault: {DiagnosticsInterval}", (uint u) => DiagnosticsInterval = u }, { "vc|verboseconsole=", $"ignored, only supported for backward comaptibility.", b => {} }, { "ns|noshutdown=", $"same as runforever.\nDefault: {_noShutdown}", (bool b) => _noShutdown = b }, { "rf|runforever", $"publisher can not be stopped by pressing a key on the console, but will run forever.\nDefault: {_noShutdown}", b => _noShutdown = b != null }, { "lf|logfile=", $"the filename of the logfile to use.\nDefault: './{_logFileName}'", (string l) => _logFileName = l }, { "lt|logflushtimespan=", $"the timespan in seconds when the logfile should be flushed.\nDefault: {_logFileFlushTimeSpanSec} sec", (int s) => { if (s > 0) { _logFileFlushTimeSpanSec = TimeSpan.FromSeconds(s); } else { throw new Mono.Options.OptionException("The logflushtimespan must be a positive number.", "logflushtimespan"); } } }, { "ll|loglevel=", $"the loglevel to use (allowed: fatal, error, warn, info, debug, verbose).\nDefault: info", (string l) => { List <string> logLevels = new List <string> { "fatal", "error", "warn", "info", "debug", "verbose" }; if (logLevels.Contains(l.ToLowerInvariant())) { _logLevel = l.ToLowerInvariant(); } else { throw new Mono.Options.OptionException("The loglevel must be one of: fatal, error, warn, info, debug, verbose", "loglevel"); } } }, // IoTHub specific options { "ih|iothubprotocol=", $"{(IsIotEdgeModule ? "not supported when running as IoTEdge module (Mqtt_Tcp_Only is enforced)\n" : $"the protocol to use for communication with Azure IoTHub (allowed values: {string.Join(", ", Enum.GetNames(IotHubProtocol.GetType()))}).\nDefault: {Enum.GetName(IotHubProtocol.GetType(), IotHubProtocol)}")}", (Microsoft.Azure.Devices.Client.TransportType p) => { if (IsIotEdgeModule) { if (p != Microsoft.Azure.Devices.Client.TransportType.Mqtt_Tcp_Only) { WriteLine("When running as IoTEdge module Mqtt_Tcp_Only is enforced."); IotHubProtocol = Microsoft.Azure.Devices.Client.TransportType.Mqtt_Tcp_Only; } } else { IotHubProtocol = p; } } }, { "ms|iothubmessagesize=", $"the max size of a message which can be send to IoTHub. when telemetry of this size is available it will be sent.\n0 will enforce immediate send when telemetry is available\nMin: 0\nMax: {HubMessageSizeMax}\nDefault: {HubMessageSize}", (uint u) => { if (u >= 0 && u <= HubMessageSizeMax) { HubMessageSize = u; } else { throw new OptionException("The iothubmessagesize must be in the range between 1 and 256*1024.", "iothubmessagesize"); } } }, { "si|iothubsendinterval=", $"the interval in seconds when telemetry should be send to IoTHub. If 0, then only the iothubmessagesize parameter controls when telemetry is sent.\nDefault: '{DefaultSendIntervalSeconds}'", (int i) => { if (i >= 0) { DefaultSendIntervalSeconds = i; } else { throw new OptionException("The iothubsendinterval must be larger or equal 0.", "iothubsendinterval"); } } }, { "dc|deviceconnectionstring=", $"{(IsIotEdgeModule ? "not supported when running as IoTEdge module\n" : $"if publisher is not able to register itself with IoTHub, you can create a device with name <applicationname> manually and pass in the connectionstring of this device.\nDefault: none")}", (string dc) => DeviceConnectionString = (IsIotEdgeModule ? null : dc) }, { "c|connectionstring=", $"the IoTHub owner connectionstring.\nDefault: none", (string cs) => IotHubOwnerConnectionString = cs }, // opc server configuration options { "pn|portnum=", $"the server port of the publisher OPC server endpoint.\nDefault: {PublisherServerPort}", (ushort p) => PublisherServerPort = p }, { "pa|path=", $"the enpoint URL path part of the publisher OPC server endpoint.\nDefault: '{PublisherServerPath}'", (string a) => PublisherServerPath = a }, { "lr|ldsreginterval=", $"the LDS(-ME) registration interval in ms. If 0, then the registration is disabled.\nDefault: {LdsRegistrationInterval}", (int i) => { if (i >= 0) { LdsRegistrationInterval = i; } else { throw new OptionException("The ldsreginterval must be larger or equal 0.", "ldsreginterval"); } } }, { "ol|opcmaxstringlen=", $"the max length of a string opc can transmit/receive.\nDefault: {OpcMaxStringLength}", (int i) => { if (i > 0) { OpcMaxStringLength = i; } else { throw new OptionException("The max opc string length must be larger than 0.", "opcmaxstringlen"); } } }, { "ot|operationtimeout=", $"the operation timeout of the publisher OPC UA client in ms.\nDefault: {OpcOperationTimeout}", (int i) => { if (i >= 0) { OpcOperationTimeout = i; } else { throw new OptionException("The operation timeout must be larger or equal 0.", "operationtimeout"); } } }, { "oi|opcsamplinginterval=", "the publisher is using this as default value in milliseconds to request the servers to sample the nodes with this interval\n" + "this value might be revised by the OPC UA servers to a supported sampling interval.\n" + "please check the OPC UA specification for details how this is handled by the OPC UA stack.\n" + "a negative value will set the sampling interval to the publishing interval of the subscription this node is on.\n" + $"0 will configure the OPC UA server to sample in the highest possible resolution and should be taken with care.\nDefault: {OpcSamplingInterval}", (int i) => OpcSamplingInterval = i }, { "op|opcpublishinginterval=", "the publisher is using this as default value in milliseconds for the publishing interval setting of the subscriptions established to the OPC UA servers.\n" + "please check the OPC UA specification for details how this is handled by the OPC UA stack.\n" + $"a value less than or equal zero will let the server revise the publishing interval.\nDefault: {OpcPublishingInterval}", (int i) => { if (i > 0 && i >= OpcSamplingInterval) { OpcPublishingInterval = i; } else { if (i <= 0) { OpcPublishingInterval = 0; } else { throw new OptionException($"The opcpublishinterval ({i}) must be larger than the opcsamplinginterval ({OpcSamplingInterval}).", "opcpublishinterval"); } } } }, { "ct|createsessiontimeout=", $"specify the timeout in seconds used when creating a session to an endpoint. On unsuccessful connection attemps a backoff up to {OpcSessionCreationBackoffMax} times the specified timeout value is used.\nMin: 1\nDefault: {OpcSessionCreationTimeout}", (uint u) => { if (u > 1) { OpcSessionCreationTimeout = u; } else { throw new OptionException("The createsessiontimeout must be greater than 1 sec", "createsessiontimeout"); } } }, { "ki|keepaliveinterval=", $"specify the interval in seconds the publisher is sending keep alive messages to the OPC servers on the endpoints it is connected to.\nMin: 2\nDefault: {OpcKeepAliveIntervalInSec}", (int i) => { if (i >= 2) { OpcKeepAliveIntervalInSec = i; } else { throw new OptionException("The keepaliveinterval must be greater or equal 2", "keepalivethreshold"); } } }, { "kt|keepalivethreshold=", $"specify the number of keep alive packets a server can miss, before the session is disconneced\nMin: 1\nDefault: {OpcKeepAliveDisconnectThreshold}", (uint u) => { if (u > 1) { OpcKeepAliveDisconnectThreshold = u; } else { throw new OptionException("The keepalivethreshold must be greater than 1", "keepalivethreshold"); } } }, { "st|opcstacktracemask=", $"ignored, only supported for backward comaptibility.", i => {} }, { "as|autotrustservercerts=", $"same as autoaccept, only supported for backward cmpatibility.\nDefault: {OpcPublisherAutoTrustServerCerts}", (bool b) => OpcPublisherAutoTrustServerCerts = b }, { "aa|autoaccept", $"the publisher trusts all servers it is establishing a connection to.\nDefault: {OpcPublisherAutoTrustServerCerts}", b => OpcPublisherAutoTrustServerCerts = b != null }, // trust own public cert option { "tm|trustmyself=", $"same as trustowncert.\nDefault: {TrustMyself}", (bool b) => TrustMyself = b }, { "to|trustowncert", $"the publisher certificate is put into the trusted certificate store automatically.\nDefault: {TrustMyself}", t => TrustMyself = t != null }, // read the display name of the nodes to publish from the server and publish them instead of the node id { "fd|fetchdisplayname=", $"same as fetchname.\nDefault: {FetchOpcNodeDisplayName}", (bool b) => FetchOpcNodeDisplayName = IotCentralMode ? true : b }, { "fn|fetchname", $"enable to read the display name of a published node from the server. this will increase the runtime.\nDefault: {FetchOpcNodeDisplayName}", b => FetchOpcNodeDisplayName = IotCentralMode ? true : b != null }, // own cert store options { "at|appcertstoretype=", $"the own application cert store type. \n(allowed values: Directory, X509Store)\nDefault: '{OpcOwnCertStoreType}'", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase)) { OpcOwnCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory; OpcOwnCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcOwnCertX509StorePathDefault : OpcOwnCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "ap|appcertstorepath=", $"the path where the own application cert should be stored\nDefault (depends on store type):\n" + $"X509Store: '{OpcOwnCertX509StorePathDefault}'\n" + $"Directory: '{OpcOwnCertDirectoryStorePathDefault}'", (string s) => OpcOwnCertStorePath = s }, // trusted cert store options { "tt|trustedcertstoretype=", $"the trusted cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcTrustedCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase)) { OpcTrustedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory; OpcTrustedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcTrustedCertX509StorePathDefault : OpcTrustedCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcTrustedCertX509StorePathDefault}'\n" + $"Directory: '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s }, // rejected cert store options { "rt|rejectedcertstoretype=", $"the rejected cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcRejectedCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase)) { OpcRejectedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory; OpcRejectedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcRejectedCertX509StorePathDefault : OpcRejectedCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcRejectedCertX509StorePathDefault}'\n" + $"Directory: '{OpcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s }, // issuer cert store options { "it|issuercertstoretype=", $"the trusted issuer cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcIssuerCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase)) { OpcIssuerCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory; OpcIssuerCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcIssuerCertX509StorePathDefault : OpcIssuerCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcIssuerCertX509StorePathDefault}'\n" + $"Directory: '{OpcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s }, // device connection string cert store options { "dt|devicecertstoretype=", $"the iothub device cert store type. \n(allowed values: Directory, X509Store)\nDefault: {IotDeviceCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase)) { IotDeviceCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory; IotDeviceCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? IotDeviceCertX509StorePathDefault : IotDeviceCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "dp|devicecertstorepath=", $"the path of the iot device cert store\nDefault Default (depends on store type):\n" + $"X509Store: '{IotDeviceCertX509StorePathDefault}'\n" + $"Directory: '{IotDeviceCertDirectoryStorePathDefault}'", (string s) => IotDeviceCertStorePath = s }, // misc { "i|install", $"register OPC Publisher with IoTHub and then exits.\nDefault: {_installOnly}", i => _installOnly = i != null }, { "h|help", "show this message and exit", h => shouldShowHelp = h != null }, }; List <string> extraArgs = new List <string>(); try { // parse the command line extraArgs = options.Parse(args); } catch (OptionException e) { // initialize logging InitLogging(); // show message Logger.Error(e, "Error in command line options"); Logger.Error($"Command line arguments: {String.Join(" ", args)}"); // show usage Usage(options); return; } // initialize logging InitLogging(); // show usage if requested if (shouldShowHelp) { Usage(options); return; } // Validate and parse extra arguments. const int APP_NAME_INDEX = 0; const int CS_INDEX = 1; switch (extraArgs.Count) { case 0: { ApplicationName = Utils.GetHostName(); break; } case 1: { ApplicationName = extraArgs[APP_NAME_INDEX]; break; } case 2: { ApplicationName = extraArgs[APP_NAME_INDEX]; if (IsIotEdgeModule) { WriteLine($"Warning: connection string parameter is not supported in IoTEdge context, given parameter is ignored"); } else { IotHubOwnerConnectionString = extraArgs[CS_INDEX]; } break; } default: { Logger.Error("Error in command line options"); Logger.Error($"Command line arguments: {String.Join(" ", args)}"); Usage(options); return; } } // install only if requested if (_installOnly) { // initialize and start IoTHub communication IotHubCommunication = new IotHubCommunication(ShutdownTokenSource.Token); if (!await IotHubCommunication.InitAsync()) { return; } Logger.Information("Installation completed. Exiting..."); return; } // start operation Logger.Information("Publisher is starting up..."); // allow canceling the application var quitEvent = new ManualResetEvent(false); try { Console.CancelKeyPress += (sender, eArgs) => { quitEvent.Set(); eArgs.Cancel = true; ShutdownTokenSource.Cancel(); }; } catch { } // init OPC configuration and tracing OpcStackConfiguration opcStackConfiguration = new OpcStackConfiguration(); await opcStackConfiguration.ConfigureAsync(); // log shopfloor site setting if (string.IsNullOrEmpty(PublisherSite)) { Logger.Information("There is no site configured."); } else { Logger.Information($"Publisher is in site '{PublisherSite}'."); } // Set certificate validator. if (OpcPublisherAutoTrustServerCerts) { Logger.Information("Publisher configured to auto trust server certificates of the servers it is connecting to."); PublisherOpcApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_AutoTrustServerCerts); } else { Logger.Information("Publisher configured to not auto trust server certificates. When connecting to servers, you need to manually copy the rejected server certs to the trusted store to trust them."); PublisherOpcApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_Default); } // start our server interface try { Logger.Information($"Starting server on endpoint {PublisherOpcApplicationConfiguration.ServerConfiguration.BaseAddresses[0].ToString()} ..."); _publisherServer = new PublisherServer(); _publisherServer.Start(PublisherOpcApplicationConfiguration); Logger.Information("Server started."); } catch (Exception e) { Logger.Fatal(e, "Failed to start Publisher OPC UA server."); Logger.Fatal("exiting..."); return; } // read telemetry configuration file PublisherTelemetryConfiguration.Init(ShutdownTokenSource.Token); if (!await PublisherTelemetryConfiguration.ReadConfigAsync()) { return; } // read node configuration file PublisherNodeConfiguration.Init(); if (!await PublisherNodeConfiguration.ReadConfigAsync()) { return; } // initialize hub communication if (IsIotEdgeModule) { // initialize and start EdgeHub communication IotEdgeHubCommunication = new IotEdgeHubCommunication(ShutdownTokenSource.Token); if (!await IotEdgeHubCommunication.InitAsync()) { return; } } else { // initialize and start IoTHub communication IotHubCommunication = new IotHubCommunication(ShutdownTokenSource.Token); if (!await IotHubCommunication.InitAsync()) { return; } } if (!await CreateOpcPublishingDataAsync()) { return; } // kick off the task to maintain all sessions Task sessionConnectorAsync = Task.Run(async() => await SessionConnectorAsync(ShutdownTokenSource.Token)); // Show notification on session events _publisherServer.CurrentInstance.SessionManager.SessionActivated += ServerEventStatus; _publisherServer.CurrentInstance.SessionManager.SessionClosing += ServerEventStatus; _publisherServer.CurrentInstance.SessionManager.SessionCreated += ServerEventStatus; // initialize publisher diagnostics Diagnostics.Init(); // stop on user request Logger.Information(""); Logger.Information(""); if (_noShutdown) { // wait forever if asked to do so Logger.Information("Publisher is running infinite..."); await Task.Delay(Timeout.Infinite); } else { Logger.Information("Publisher is running. Press CTRL-C to quit."); // wait for Ctrl-C quitEvent.WaitOne(Timeout.Infinite); } Logger.Information(""); Logger.Information(""); ShutdownTokenSource.Cancel(); Logger.Information("Publisher is shutting down..."); // Wait for session connector completion await sessionConnectorAsync; // stop the server _publisherServer.Stop(); // Clean up Publisher sessions await SessionShutdownAsync(); // shutdown the IoTHub messaging await IotHubCommunication.ShutdownAsync(); IotHubCommunication = null; // shutdown diagnostics await ShutdownAsync(); // free resources PublisherTelemetryConfiguration.Deinit(); PublisherNodeConfiguration.Deinit(); ShutdownTokenSource = null; } catch (Exception e) { Logger.Fatal(e, e.StackTrace); e = e.InnerException ?? null; while (e != null) { Logger.Fatal(e, e.StackTrace); e = e.InnerException ?? null; } Logger.Fatal("Publisher exiting... "); } }
/// <summary> /// Asynchronous part of the main method of the app. /// </summary> public async static Task MainAsync(string[] args) { try { var shouldShowHelp = false; // command line options configuration Mono.Options.OptionSet options = new Mono.Options.OptionSet { // Publishing configuration options { "pf|publishfile=", $"the filename to configure the nodes to publish.\nDefault: '{PublisherNodeConfigurationFilename}'", (string p) => PublisherNodeConfigurationFilename = p }, { "tc|telemetryconfigfile=", $"the filename to configure the ingested telemetry\nDefault: '{PublisherTelemetryConfigurationFilename}'", (string p) => PublisherTelemetryConfigurationFilename = p }, { "sd|shopfloordomain=", $"the domain of the shopfloor. if specified this domain is appended (delimited by a ':' to the 'ApplicationURI' property when telemetry is sent to IoTHub.\n" + "The value must follow the syntactical rules of a DNS hostname.\nDefault: not set", (string s) => { Regex domainNameRegex = new Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"); if (domainNameRegex.IsMatch(s)) { ShopfloorDomain = s; } else { throw new OptionException("The shopfloor domain is not a valid DNS hostname.", "shopfloordomain"); } } }, { "sw|sessionconnectwait=", $"specify the wait time in seconds publisher is trying to connect to disconnected endpoints and starts monitoring unmonitored items\nMin: 10\nDefault: {_publisherSessionConnectWaitSec}", (int i) => { if (i > 10) { _publisherSessionConnectWaitSec = i; } else { throw new OptionException("The sessionconnectwait must be greater than 10 sec", "sessionconnectwait"); } } }, { "mq|monitoreditemqueuecapacity=", $"specify how many notifications of monitored items can be stored in the internal queue, if the data can not be sent quick enough to IoTHub\nMin: 1024\nDefault: {MonitoredItemsQueueCapacity}", (int i) => { if (i >= 1024) { MonitoredItemsQueueCapacity = i; } else { throw new OptionException("The monitoreditemqueueitems must be greater than 1024", "monitoreditemqueueitems"); } } }, { "di|diagnosticsinterval=", $"shows publisher diagnostic info at the specified interval in seconds. 0 disables diagnostic output.\nDefault: {DiagnosticsInterval}", (uint u) => DiagnosticsInterval = u }, { "vc|verboseconsole=", $"the output of publisher is shown on the console.\nDefault: {VerboseConsole}", (bool b) => VerboseConsole = b }, { "ns|noshutdown=", $"publisher can not be stopped by pressing a key on the console, but will run forever.\nDefault: {_noShutdown}", (bool b) => _noShutdown = b }, // IoTHub specific options { "ih|iothubprotocol=", $"the protocol to use for communication with Azure IoTHub (allowed values: {string.Join(", ", Enum.GetNames(IotHubProtocol.GetType()))}).\nDefault: {Enum.GetName(IotHubProtocol.GetType(), IotHubProtocol)}", (Microsoft.Azure.Devices.Client.TransportType p) => IotHubProtocol = p }, { "ms|iothubmessagesize=", $"the max size of a message which can be send to IoTHub. when telemetry of this size is available it will be sent.\n0 will enforce immediate send when telemetry is available\nMin: 0\nMax: {IotHubMessageSizeMax}\nDefault: {IotHubMessageSize}", (uint u) => { if (u >= 0 && u <= IotHubMessageSizeMax) { IotHubMessageSize = u; } else { throw new OptionException("The iothubmessagesize must be in the range between 1 and 256*1024.", "iothubmessagesize"); } } }, { "si|iothubsendinterval=", $"the interval in seconds when telemetry should be send to IoTHub. If 0, then only the iothubmessagesize parameter controls when telemetry is sent.\nDefault: '{DefaultSendIntervalSeconds}'", (int i) => { if (i >= 0) { DefaultSendIntervalSeconds = i; } else { throw new OptionException("The iothubsendinterval must be larger or equal 0.", "iothubsendinterval"); } } }, { "dc|deviceconnectionstring=", $"if publisher is not able to register itself with IoTHub, you can create a device with name <applicationname> manually and pass in the connectionstring of this device.\nDefault: none", (string dc) => DeviceConnectionString = dc }, // opc server configuration options { "lf|logfile=", $"the filename of the logfile to use.\nDefault: './Logs/<applicationname>.log.txt'", (string l) => LogFileName = l }, { "pn|portnum=", $"the server port of the publisher OPC server endpoint.\nDefault: {PublisherServerPort}", (ushort p) => PublisherServerPort = p }, { "pa|path=", $"the enpoint URL path part of the publisher OPC server endpoint.\nDefault: '{PublisherServerPath}'", (string a) => PublisherServerPath = a }, { "lr|ldsreginterval=", $"the LDS(-ME) registration interval in ms. If 0, then the registration is disabled.\nDefault: {LdsRegistrationInterval}", (int i) => { if (i >= 0) { LdsRegistrationInterval = i; } else { throw new OptionException("The ldsreginterval must be larger or equal 0.", "ldsreginterval"); } } }, { "ot|operationtimeout=", $"the operation timeout of the publisher OPC UA client in ms.\nDefault: {OpcOperationTimeout}", (int i) => { if (i >= 0) { OpcOperationTimeout = i; } else { throw new OptionException("The operation timeout must be larger or equal 0.", "operationtimeout"); } } }, { "oi|opcsamplinginterval=", "the publisher is using this as default value in milliseconds to request the servers to sample the nodes with this interval\n" + "this value might be revised by the OPC UA servers to a supported sampling interval.\n" + "please check the OPC UA specification for details how this is handled by the OPC UA stack.\n" + "a negative value will set the sampling interval to the publishing interval of the subscription this node is on.\n" + $"0 will configure the OPC UA server to sample in the highest possible resolution and should be taken with care.\nDefault: {OpcSamplingInterval}", (int i) => OpcSamplingInterval = i }, { "op|opcpublishinginterval=", "the publisher is using this as default value in milliseconds for the publishing interval setting of the subscriptions established to the OPC UA servers.\n" + "please check the OPC UA specification for details how this is handled by the OPC UA stack.\n" + $"a value less than or equal zero will let the server revise the publishing interval.\nDefault: {OpcPublishingInterval}", (int i) => { if (i > 0 && i >= OpcSamplingInterval) { OpcPublishingInterval = i; } else { if (i <= 0) { OpcPublishingInterval = 0; } else { throw new OptionException($"The opcpublishinterval ({i}) must be larger than the opcsamplinginterval ({OpcSamplingInterval}).", "opcpublishinterval"); } } } }, { "ct|createsessiontimeout=", $"specify the timeout in seconds used when creating a session to an endpoint. On unsuccessful connection attemps a backoff up to {OpcSessionCreationBackoffMax} times the specified timeout value is used.\nMin: 1\nDefault: {OpcSessionCreationTimeout}", (uint u) => { if (u > 1) { OpcSessionCreationTimeout = u; } else { throw new OptionException("The createsessiontimeout must be greater than 1 sec", "createsessiontimeout"); } } }, { "ki|keepaliveinterval=", $"specify the interval in seconds the publisher is sending keep alive messages to the OPC servers on the endpoints it is connected to.\nMin: 2\nDefault: {OpcKeepAliveIntervalInSec}", (int i) => { if (i >= 2) { OpcKeepAliveIntervalInSec = i; } else { throw new OptionException("The keepaliveinterval must be greater or equal 2", "keepalivethreshold"); } } }, { "kt|keepalivethreshold=", $"specify the number of keep alive packets a server can miss, before the session is disconneced\nMin: 1\nDefault: {OpcKeepAliveDisconnectThreshold}", (uint u) => { if (u > 1) { OpcKeepAliveDisconnectThreshold = u; } else { throw new OptionException("The keepalivethreshold must be greater than 1", "keepalivethreshold"); } } }, { "st|opcstacktracemask=", $"the trace mask for the OPC stack. See github OPC .NET stack for definitions.\nTo enable IoTHub telemetry tracing set it to 711.\nDefault: {OpcStackTraceMask:X} ({OpcStackTraceMask})", (int i) => { if (i >= 0) { OpcStackTraceMask = i; } else { throw new OptionException("The OPC stack trace mask must be larger or equal 0.", "opcstacktracemask"); } } }, { "as|autotrustservercerts=", $"the publisher trusts all servers it is establishing a connection to.\nDefault: {OpcPublisherAutoTrustServerCerts}", (bool b) => OpcPublisherAutoTrustServerCerts = b }, // trust own public cert option { "tm|trustmyself=", $"the publisher certificate is put into the trusted certificate store automatically.\nDefault: {TrustMyself}", (bool b) => TrustMyself = b }, // read the display name of the nodes to publish from the server and publish them instead of the node id { "fd|fetchdisplayname=", $"enable to read the display name of a published node from the server. this will increase the runtime.\nDefault: {FetchOpcNodeDisplayName}", (bool b) => FetchOpcNodeDisplayName = b }, // own cert store options { "at|appcertstoretype=", $"the own application cert store type. \n(allowed values: Directory, X509Store)\nDefault: '{OpcOwnCertStoreType}'", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcOwnCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcOwnCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcOwnCertX509StorePathDefault : OpcOwnCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "ap|appcertstorepath=", $"the path where the own application cert should be stored\nDefault (depends on store type):\n" + $"X509Store: '{OpcOwnCertX509StorePathDefault}'\n" + $"Directory: '{OpcOwnCertDirectoryStorePathDefault}'", (string s) => OpcOwnCertStorePath = s }, // trusted cert store options { "tt|trustedcertstoretype=", $"the trusted cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcTrustedCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcTrustedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcTrustedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcTrustedCertX509StorePathDefault : OpcTrustedCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcTrustedCertX509StorePathDefault}'\n" + $"Directory: '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s }, // rejected cert store options { "rt|rejectedcertstoretype=", $"the rejected cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcRejectedCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcRejectedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcRejectedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcRejectedCertX509StorePathDefault : OpcRejectedCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcRejectedCertX509StorePathDefault}'\n" + $"Directory: '{OpcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s }, // issuer cert store options { "it|issuercertstoretype=", $"the trusted issuer cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcIssuerCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { OpcIssuerCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; OpcIssuerCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcIssuerCertX509StorePathDefault : OpcIssuerCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault (depends on store type):\n" + $"X509Store: '{OpcIssuerCertX509StorePathDefault}'\n" + $"Directory: '{OpcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s }, // device connection string cert store options { "dt|devicecertstoretype=", $"the iothub device cert store type. \n(allowed values: Directory, X509Store)\nDefault: {IotDeviceCertStoreType}", (string s) => { if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) { IotDeviceCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; IotDeviceCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? IotDeviceCertX509StorePathDefault : IotDeviceCertDirectoryStorePathDefault; } else { throw new OptionException(); } } }, { "dp|devicecertstorepath=", $"the path of the iot device cert store\nDefault Default (depends on store type):\n" + $"X509Store: '{IotDeviceCertX509StorePathDefault}'\n" + $"Directory: '{IotDeviceCertDirectoryStorePathDefault}'", (string s) => IotDeviceCertStorePath = s }, // misc { "h|help", "show this message and exit", h => shouldShowHelp = h != null }, }; List<string> arguments = new List<string>(); int maxAllowedArguments = 2; int applicationNameIndex = 0; int connectionStringIndex = 1; if (IsIotEdgeModule) { maxAllowedArguments = 3; applicationNameIndex = 1; connectionStringIndex = 2; } try { // parse the command line arguments = options.Parse(args); } catch (OptionException e) { // show message WriteLine($"Error: {e.Message}"); // show usage Usage(options); return; } // Validate and parse arguments. if (arguments.Count > maxAllowedArguments || shouldShowHelp) { Usage(options); return; } else if (arguments.Count == maxAllowedArguments) { ApplicationName = arguments[applicationNameIndex]; IotHubOwnerConnectionString = arguments[connectionStringIndex]; } else if (arguments.Count == maxAllowedArguments - 1) { ApplicationName = arguments[applicationNameIndex]; } else { ApplicationName = Utils.GetHostName(); } WriteLine("Publisher is starting up..."); // Shutdown token sources. ShutdownTokenSource = new CancellationTokenSource(); // init OPC configuration and tracing OpcStackConfiguration opcStackConfiguration = new OpcStackConfiguration(); await opcStackConfiguration.ConfigureAsync(); _opcTraceInitialized = true; // log shopfloor domain setting if (string.IsNullOrEmpty(ShopfloorDomain)) { Trace("There is no shopfloor domain configured."); } else { Trace($"Publisher is in shopfloor domain '{ShopfloorDomain}'."); } // Set certificate validator. if (OpcPublisherAutoTrustServerCerts) { Trace("Publisher configured to auto trust server certificates of the servers it is connecting to."); PublisherOpcApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_AutoTrustServerCerts); } else { Trace("Publisher configured to not auto trust server certificates. When connecting to servers, you need to manually copy the rejected server certs to the trusted store to trust them."); PublisherOpcApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_Default); } // start our server interface try { Trace($"Starting server on endpoint {PublisherOpcApplicationConfiguration.ServerConfiguration.BaseAddresses[0].ToString()} ..."); _publisherServer = new PublisherServer(); _publisherServer.Start(PublisherOpcApplicationConfiguration); Trace("Server started."); } catch (Exception e) { Trace(e, "Failed to start Publisher OPC UA server."); Trace("exiting..."); return; } // read telemetry configuration file PublisherTelemetryConfiguration.Init(); if (!await PublisherTelemetryConfiguration.ReadConfigAsync()) { return; } // read node configuration file PublisherNodeConfiguration.Init(); if (!await PublisherNodeConfiguration.ReadConfigAsync()) { return; } // initialize and start IoTHub messaging IotHubCommunication = new IotHubMessaging(); if (!await IotHubCommunication.InitAsync()) { return; } if (!await PublisherNodeConfiguration.CreateOpcPublishingDataAsync()) { return; } // kick off the task to maintain all sessions Task sessionConnectorAsync = Task.Run(async () => await SessionConnectorAsync(ShutdownTokenSource.Token)); // Show notification on session events _publisherServer.CurrentInstance.SessionManager.SessionActivated += ServerEventStatus; _publisherServer.CurrentInstance.SessionManager.SessionClosing += ServerEventStatus; _publisherServer.CurrentInstance.SessionManager.SessionCreated += ServerEventStatus; // initialize publisher diagnostics Diagnostics.Init(); // stop on user request WriteLine(""); WriteLine(""); if (_noShutdown) { // wait forever if asked to do so WriteLine("Publisher is running infinite..."); await Task.Delay(Timeout.Infinite); } else { WriteLine("Publisher is running. Press any key to quit."); try { ReadKey(true); } catch { // wait forever if there is no console WriteLine("There is no console. Publisher is running infinite..."); await Task.Delay(Timeout.Infinite); } } WriteLine(""); WriteLine(""); ShutdownTokenSource.Cancel(); WriteLine("Publisher is shutting down..."); // Wait for session connector completion await sessionConnectorAsync; // stop the server _publisherServer.Stop(); // Clean up Publisher sessions await SessionShutdownAsync(); // shutdown the IoTHub messaging await IotHubCommunication.ShutdownAsync(); IotHubCommunication = null; // shutdown diagnostics await ShutdownAsync(); // free resources PublisherTelemetryConfiguration.Deinit(); PublisherNodeConfiguration.Deinit(); ShutdownTokenSource = null; } catch (Exception e) { if (_opcTraceInitialized) { Trace(e, e.StackTrace); e = e.InnerException ?? null; while (e != null) { Trace(e, e.StackTrace); e = e.InnerException ?? null; } Trace("Publisher exiting... "); } else { WriteLine($"{DateTime.Now.ToString()}: {e.Message.ToString()}"); WriteLine($"{DateTime.Now.ToString()}: {e.StackTrace}"); e = e.InnerException ?? null; while (e != null) { WriteLine($"{DateTime.Now.ToString()}: {e.Message.ToString()}"); WriteLine($"{DateTime.Now.ToString()}: {e.StackTrace}"); e = e.InnerException ?? null; } WriteLine($"{DateTime.Now.ToString()}: Publisher exiting..."); } } }