public OpcPublisherFixture() { // init publisher logging //LogLevel = "debug"; LogLevel = "info"; if (Logger == null) { InitLogging(); } // init publisher application configuration AutoAcceptCerts = true; // mitigation for bug in .NET Core 2.1 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { OpcOwnCertStoreType = CertificateStoreType.X509Store; OpcOwnCertStorePath = OpcOwnCertX509StorePathDefault; } if (_opcApplicationConfiguration == null) { _opcApplicationConfiguration = new OpcApplicationConfiguration(); _opcApplicationConfiguration.ConfigureAsync().Wait(); } // configure hub communication HubCommunicationBase.DefaultSendIntervalSeconds = 0; HubCommunicationBase.HubMessageSize = 0; }
/// <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"); } } }, { "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: {SessionConnectWaitSec}", (int i) => { if (i > 10) { SessionConnectWaitSec = 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 }, { "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 IoT Edge module\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: {ServerPort}", (ushort p) => ServerPort = p }, { "pa|path=", $"the enpoint URL path part of the publisher OPC server endpoint.\nDefault: '{ServerPath}'", (string a) => ServerPath = 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"); } } }, { "aa|autoaccept", $"the publisher trusts all servers it is establishing a connection to.\nDefault: {AutoAcceptCerts}", b => AutoAcceptCerts = 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 }, // 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 }, { "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault: '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s }, { "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault '{OpcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s }, { "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault '{OpcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s }, { "csr", $"show data to create a certificate signing request\nDefault '{ShowCreateSigningRequestInfo}'", c => ShowCreateSigningRequestInfo = c != null }, { "ab|applicationcertbase64=", $"update/set this applications certificate with the certificate passed in as bas64 string", (string s) => { NewCertificateBase64String = s; } }, { "af|applicationcertfile=", $"update/set this applications certificate with the certificate file specified", (string s) => { if (File.Exists(s)) { NewCertificateFileName = s; } else { throw new OptionException("The file '{s}' does not exist.", "applicationcertfile"); } } }, { "pb|privatekeybase64=", $"initial provisioning of the application certificate (with a PEM or PFX fomat) requires a private key passed in as base64 string", (string s) => { PrivateKeyBase64String = s; } }, { "pk|privatekeyfile=", $"initial provisioning of the application certificate (with a PEM or PFX fomat) requires a private key passed in as file", (string s) => { if (File.Exists(s)) { PrivateKeyFileName = s; } else { throw new OptionException("The file '{s}' does not exist.", "privatekeyfile"); } } }, { "cp|certpassword="******"the optional password for the PEM or PFX or the installed application certificate", (string s) => { CertificatePassword = s; } }, { "tb|addtrustedcertbase64=", $"adds the certificate to the applications trusted cert store passed in as base64 string (multiple strings supported)", (string s) => { TrustedCertificateBase64Strings = ParseListOfStrings(s); } }, { "tf|addtrustedcertfile=", $"adds the certificate file(s) to the applications trusted cert store passed in as base64 string (multiple filenames supported)", (string s) => { TrustedCertificateFileNames = ParseListOfFileNames(s, "addtrustedcertfile"); } }, { "ib|addissuercertbase64=", $"adds the specified issuer certificate to the applications trusted issuer cert store passed in as base64 string (multiple strings supported)", (string s) => { IssuerCertificateBase64Strings = ParseListOfStrings(s); } }, { "if|addissuercertfile=", $"adds the specified issuer certificate file(s) to the applications trusted issuer cert store (multiple filenames supported)", (string s) => { IssuerCertificateFileNames = ParseListOfFileNames(s, "addissuercertfile"); } }, { "rb|updatecrlbase64=", $"update the CRL passed in as base64 string to the corresponding cert store (trusted or trusted issuer)", (string s) => { CrlBase64String = s; } }, { "uc|updatecrlfile=", $"update the CRL passed in as file to the corresponding cert store (trusted or trusted issuer)", (string s) => { if (File.Exists(s)) { CrlFileName = s; } else { throw new OptionException("The file '{s}' does not exist.", "updatecrlfile"); } } }, { "rc|removecert=", $"remove cert(s) with the given thumbprint(s) (multiple thumbprints supported)", (string s) => { ThumbprintsToRemove = ParseListOfStrings(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 }, // all the following are only supported to not break existing command lines, but some of them are just ignored { "st|opcstacktracemask=", $"ignored, only supported for backward comaptibility.", i => {} }, { "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"); } } }, { "vc|verboseconsole=", $"ignored, only supported for backward comaptibility.", b => {} }, { "as|autotrustservercerts=", $"same as autoaccept, only supported for backward cmpatibility.\nDefault: {AutoAcceptCerts}", (bool b) => AutoAcceptCerts = b }, { "tt|trustedcertstoretype=", $"ignored, only supported for backward compatibility. the trusted cert store will always reside in a directory.", s => { } }, { "rt|rejectedcertstoretype=", $"ignored, only supported for backward compatibility. the rejected cert store will always reside in a directory.", s => { } }, { "it|issuercertstoretype=", $"ignored, only supported for backward compatibility. the trusted issuer cert store will always reside in a directory.", s => { } }, }; 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 OpcApplicationConfiguration opcApplicationConfiguration = new OpcApplicationConfiguration(); await opcApplicationConfiguration.ConfigureAsync(); // log shopfloor site setting if (string.IsNullOrEmpty(PublisherSite)) { Logger.Information("There is no site configured."); } else { Logger.Information($"Publisher is in site '{PublisherSite}'."); } // start our server interface try { Logger.Information($"Starting server on endpoint {OpcApplicationConfiguration.ApplicationConfiguration.ServerConfiguration.BaseAddresses[0].ToString()} ..."); _publisherServer = new PublisherServer(); _publisherServer.Start(OpcApplicationConfiguration.ApplicationConfiguration); 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 OPC session creation and node monitoring await SessionStartAsync(); // 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..."); // stop the server _publisherServer.Stop(); // shutdown all OPC 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... "); } }