diff --git a/deploy/k3s/docker-compose.yaml b/deploy/k3s/docker-compose.yaml new file mode 100644 index 0000000000..ccd8c88691 --- /dev/null +++ b/deploy/k3s/docker-compose.yaml @@ -0,0 +1,19 @@ +services: + nginx-proxy: + image: nginxproxy/nginx-proxy + container_name: nginx-proxy + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + autok3s: + image: cnrancher/autok3s:v0.9.1 + init: true + ports: + - 8080 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - $HOME/.autok3s/:$HOME/.autok3s/ + environment: + - AUTOK3S_CONFIG=$HOME/.autok3s/ + - VIRTUAL_HOST=autok3s.vcap.me \ No newline at end of file diff --git a/docs/opc-publisher/commandline.md b/docs/opc-publisher/commandline.md index 023f36014c..cc1f6ceb0d 100644 --- a/docs/opc-publisher/commandline.md +++ b/docs/opc-publisher/commandline.md @@ -18,7 +18,8 @@ When both environment variable and CLI argument are provided, the command line o ██║ ██║██╔═══╝ ██║ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██╔══██║██╔══╝ ██╔══██╗ ╚██████╔╝██║ ╚██████╗ ██║ ╚██████╔╝██████╔╝███████╗██║███████║██║ ██║███████╗██║ ██║ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ - 2.9.4 + + 2.9.4 (.NET 8.0.0/win-x64/OPC Stack 1.4.372.106) General ------- @@ -250,6 +251,7 @@ Messaging configuration `Dapr` `Http` `FileSystem` + `Null` Default: `IoTHub` or the first configured transport of the allowed value list. @@ -522,6 +524,7 @@ Subscription settings `PeriodicLKV` `PeriodicLKG` `WatchdogLKVWithUpdatedTimestamps` + `WatchdogLKVDiagnosticsOnly` Default: `WatchdogLKV` (Sending LKV in a watchdog fashion). --hb, --heartbeatinterval, --DefaultHeartbeatInterval=VALUE @@ -611,6 +614,14 @@ OPC UA Client configuration The port to use when accepting inbound reverse connect requests from servers. Default: `4840`. + --mpr, --minpublishrequests, --MinPublishRequests=VALUE + Minimum number of publish requests to queue once + subscriptions are created in the session. + Default: `3`. + --ppr, --percentpublishrequests, --PublishRequestsPerSubscriptionPercent=VALUE + Percentage ratio of publish requests per + subscriptions in the session in percent. + Default: `100`% (1 request per subscription). --smi, --subscriptionmanagementinterval, --SubscriptionManagementInterval=VALUE The interval in seconds after which the publisher re-applies the desired state of the subscription @@ -845,6 +856,15 @@ Diagnostic options metrics directly on the standard path. Default: `disabled` if Otlp collector is configured, otherwise `enabled`. + --cap, --capturedevice, --CaptureDevice=VALUE + The capture device to use to capture network + traffic. + Network capture is not supported on this system. + --cpf, --capturefile, --CaptureFileName=VALUE + The file name to capture traffic to. + A device must be selected using `--cd` if + capture capability is supported on this system. + Default: `opcua.pcap`. ``` Currently supported combinations of `--mm` snd `--me` can be found [here](./messageformats.md). diff --git a/docs/opc-publisher/observability.md b/docs/opc-publisher/observability.md index 1b653705e4..f1c0c6e037 100644 --- a/docs/opc-publisher/observability.md +++ b/docs/opc-publisher/observability.md @@ -40,32 +40,41 @@ The following table describes the instruments that are collected per writer grou | Log line item name | Diagnostic info property name | Description | |-----------------------------------------|-------------------------------------|-------------| +| # OPC Publisher Version (Runtime) | n/a | The full version and runtime used by the publisher | +| # Time | timestamp | The timestamp of the diagnostics information | | # Ingestion duration | ingestionDuration | How long the data flow inside the publisher has been executing after it was created (either from file or API) | -| # Ingress DataChanges (from OPC) | ingressDataChanges | The number of OPC UA subscription notification messages with data value changes that have been received by publisher inside this data flow | -| # Ingress ValueChanges (from OPC) | ingressValueChanges | The number of value changes inside the OPC UA subscription notifications processed by the data flow. | -| # of which are Heartbeats | ingressHeartbeats | The number of heartbeats inside the published value changes. | -| # of which are Cyclic reads | ingressCyclicReads | The number of cyclic reads of the total number of value changes. | -| # Ingress EventData (from OPC) | ingressEventNotifications | The number of OPC UA subscription notification messages with events that have been received by publisher so far inside this data flow | -| # Ingress Events (from OPC) | ingressEvents | The number of events that were part of these OPC UA subscription notifications that were so far processed by the data flow. | -| # Ingress BatchBlock buffer size | ingressBatchBlockBufferSize | The number of messages awaiting encoding and sending tot he telemetry message destination inside the data flow pipeline. | -| # Encoding Block input / output size | encodingBlockInputSize | The number of messages awaiting encoding into the output format. | -| # Encoding Block input / output size | encodingBlockOutputSize | The number of messages already encoded and waiting to be sent to the telemetry message destination. | -| # Encoder Notifications processed | encoderNotificationsProcessed | The total number of subscription notifications processed by the encoder stage of the data flow pipeline since the pipeline started. | -| # Encoder Notifications dropped | encoderNotificationsDropped | The total number of subscription notifications that were dropped because they could not be encoded, e.g., due to their size being to large to fit into the message. | -| # Encoder IoT Messages processed | encoderIoTMessagesProcessed | The total number of encoded messages produced by the encoder since the start of the pipeline. | -| # Encoder avg Notifications/Message | encoderAvgNotificationsMessage | The average number of subscription notifications that were pressed into a message. | -| # Encoder avg IoT Message body size | encoderAvgIoTMessageBodySize | The average size of the message body produced over the course of the pipeline run. | -| # Encoder avg IoT Chunk (4 Kb) usage | encoderAvgIoTChunkUsage | The average use of IoT Hub chunks (4k). | -| # Estimated IoT Chunks (4 KB) per day | estimatedIoTChunksPerDay | An estimate of how many chunks are used per day by publisher which enables correct sizing of the IoT Hub to avoid data loss due to throttling. | -| # Outgress Batch Block buffer size | outgressBatchBlockBufferSize | The number of messages that are waiting to be sent to all configured telemetry message destination via the message sink. | -| # Outgress input bufffer count | outgressInputBufferCount | The aggregated number of messages waiting in the input buffer of the configured telemetry message destination sinks. | -| # Outgress input buffer dropped | outgressInputBufferDropped | The aggregated number of messages that were dropped in any of the configured telemetry message destination sinks. | -| # Outgress IoT message count | outgressIoTMessageCount | The aggregated number of messages that were sent by all configured telemetry message destination sinks. | -| | sentMessagesPerSec | Publisher throughput meaning the number of messages sent to the telemetry message destination (e.g., IoT Hub / Edge Hub) per second | -| # Connection retries | connectionRetries | How many times connections to the OPC UA server broke and needed to be reconnected as it pertains to the data flow. | | # Opc endpoint connected? | opcEndpointConnected | Whether the pipeline is currently connected to the OPC UA server endpoint or in a reconnect attempt. | +| # Connection retries | connectionRetries | How many times connections to the OPC UA server broke and needed to be reconnected as it pertains to the data flow. | | # Monitored Opc nodes succeeded count | monitoredOpcNodesSucceededCount | How many of the configured monitored items have been established successfully inside the data flow's OPC UA subscription and should be producing data. | | # Monitored Opc nodes failed count | monitoredOpcNodesFailedCount | How many of the configured monitored items inside the data flow failed to be created in the subscription (the logs will provide more information). | +| # Subscriptions count | subscriptionsCount (*) | How many subscriptions were created that contain above monitored items. | +| # Queued/Minimum request count | publishRequestsRatio (*) | The ratio of currently queued requests to the server as a percentage of the subscription count measured here vs. the overall number of subscriptions in the underlying session (e.g., if the session is shared). | +| | minPublishRequestsRatio (*) | The ratio of minimum number of publish requests that should always be queued to the server. | +| # Good/Bad Publish request count | goodPublishRequestsRatio (*) | The ratio of currently queued publish requests that are in progress in the server and awaiting a response. | +| | badPublishRequestsRatio (*) | The ratio of defunct publish requests which have not been resulting in a publish response from the server. | +| # Ingress value changes | ingressValueChanges | The number of value changes inside the OPC UA subscription notifications processed by the data flow. | +| # Ingress Events | ingressEvents | The number of events that were part of these OPC UA subscription notifications that were so far processed by the data flow. | +| # Received Data Change Notifications | ingressDataChanges | The number of OPC UA subscription notification messages with data value changes that have been received by publisher inside this data flow | +| # Received Event Notifications | ingressEventNotifications | The number of OPC UA subscription notification messages with events that have been received by publisher so far inside this data flow | +| # Received Keep Alive Notifications | ingressEventNotifications | The number of received OPC UA subscription notification messages that were keep alive messages | +| # Generated Cyclic read Notifications | ingressCyclicReads | The number of cyclic read notifications generated from sampling nodes on the client side. Each notification contains the changed value. | +| # Generated Heartbeats Notifications | ingressHeartbeats | The number of notifications that contain heartbeats. Each notification contains the heartbeat value. | +| # Notification batch buffer size | ingressBatchBlockBufferSize | The number of messages awaiting encoding and sending tot he telemetry message destination inside the data flow pipeline. | +| # Encoder input / output size | encodingBlockInputSize | The number of messages awaiting encoding into the output format. | +| | encodingBlockOutputSize | The number of messages already encoded and waiting to be sent to the telemetry message destination. | +| # Encoder Notif. processed/dropped | encoderNotificationsProcessed | The total number of subscription notifications processed by the encoder stage of the data flow pipeline since the pipeline started. | +| | encoderNotificationsDropped | The total number of subscription notifications that were dropped because they could not be encoded, e.g., due to their size being to large to fit into the message. | +| # Encoder Network Messages produced | encoderIoTMessagesProcessed | The total number of encoded messages produced by the encoder since the start of the pipeline. | +| # Encoder avg Notifications/Message | encoderAvgNotificationsMessage | The average number of subscription notifications that were pressed into a message. | +| # Encoder avg Message body size | encoderAvgIoTMessageBodySize | The average size of the message body produced over the course of the pipeline run. | +| # Encoder avg Chunk (4 Kb) usage | encoderAvgIoTChunkUsage | The average use of IoT Hub chunks (4k). | +| # Estimated Chunks (4 KB) per day | estimatedIoTChunksPerDay | An estimate of how many chunks are used per day by publisher which enables correct sizing of the IoT Hub to avoid data loss due to throttling. | +| # Egress Messages queued/dropped | outgressInputBufferCount | The aggregated number of messages waiting in the input buffer of the configured telemetry message destination sinks. | +| | outgressInputBufferDropped | The aggregated number of messages that were dropped in any of the configured telemetry message destination sinks. | +| # Egress Messages successfully sent | outgressIoTMessageCount | The aggregated number of messages that were sent by all configured telemetry message destination sinks. | +| | sentMessagesPerSec | Publisher throughput meaning the number of messages sent to the telemetry message destination (e.g., IoT Hub / Edge Hub) per second | + +(*) Not exposed through the API ## Available metrics @@ -308,7 +317,7 @@ In this tutorial two pre-configured docker images (for Prometheus and Grafana) m - host IP or name}:3000 - When prompted for a user name and password enter the values entered in the environment variables - **Note**: When using a VM, make sure to add an inbound rule for port 3000 - + - Prometheus has already been configured as a data source and can now be directly accessed. Prometheus is scraping EdgeHub and OPC Publisher metrics. - Select the dashboards option to view the available dashboards and select “Publisher” to view the pre-configured dashboard as shown below. diff --git a/docs/opc-publisher/readme.md b/docs/opc-publisher/readme.md index 950ac83248..b78ed0903d 100644 --- a/docs/opc-publisher/readme.md +++ b/docs/opc-publisher/readme.md @@ -610,6 +610,8 @@ The behavior of heartbeat can be fine tuned using the `--hbb, --heartbeatbehavio Option of the node entry. The behavior can be set to watch dog behavior with Last Known Value (`WatchdogLKV`, which is the default) or Last Known Good (`WatchdogLKG`) semantics. A last known good value has either a status code of `Good` or a valid value (!= Null) and not a bad status code (which covers other Good or Uncertain status codes). Bad values are not causing heartbeat messages in LKG mode. A continuous periodic sending of the last known value (`PeriodicLKV`) or last good value (`PeriodicLKG`) can also be selected. +The hearbeat behavior `WatchdogLKVDiagnosticsOnly` is special, it allows you to log heartbeat in the diagnostics output without sending heartbeats as part of the outgoing messages. + ##### Timestamps The OPC UA data value contains a source and server timestamp. These are reported by the server and are based on the OPC UA server clock. The server is free to send whatever timestamp it wants, including none even though the OPC Publisher is setting up all monitored items to report both timestamps. diff --git a/docs/release-announcement.md b/docs/release-announcement.md index 9a439b49f9..f1e2aafc9d 100644 --- a/docs/release-announcement.md +++ b/docs/release-announcement.md @@ -3,6 +3,7 @@ ## Table Of Contents - [Azure Industrial IoT OPC Publisher 2.9.4](#azure-industrial-iot-opc-publisher-294) + - [Breaking changes in 2.9.4](#breaking-changes-in-294) - [Changes in 2.9.4](#changes-in-294) - [Azure Industrial IoT OPC Publisher 2.9.3](#azure-industrial-iot-opc-publisher-293) - [Breaking changes in 2.9.3](#breaking-changes-in-293) @@ -42,7 +43,14 @@ ## Azure Industrial IoT OPC Publisher 2.9.4 -We are pleased to announce the release of version 2.9.4 of OPC Publisher and the companion web api. This release comes with several bug and security fixes and is the latest supported release. +We are pleased to announce the release of version 2.9.4 of OPC Publisher and the companion web api service. This release comes with several bug and security fixes and is the latest supported release. + +### Breaking changes in 2.9.4 + +> IMPORTANT. Please read when updating from previous versions of OPC Publisher + +- Arm64 and AMD64 container images are published now with Mariner (Azure) Linux (distroless) as base images instead of Alpine. +- Arm32 (v7) images of OPC Publisher continue to use Alpine as base image. Support transitions to the same model as for "preview" features. Security updates are released as a result of updates to the AMD64 and ARM64 version of OPC Publisher. ### Changes in 2.9.4 @@ -64,7 +72,8 @@ We are pleased to announce the release of version 2.9.3 of OPC Publisher and the > IMPORTANT. Please read when updating from previous versions of OPC Publisher -- All container images published now use Mariner Linux (distroless) base images instead of Alpine. +- Arm64 and AMD64 container images are published now with (Azure) Mariner Linux (distroless) as base images instead of Alpine. +- Arm32 (v7) images of OPC Publisher continue to use Alpine as base image. Support transitions to the same model as for "preview" features. Security updates are released as a result of updates to the AMD64 and ARM64 version of OPC Publisher. - Metadata collection has shown to be very taxing on OPC UA servers. When 2.9 was dropped in to replace 2.8 in production, memory consumption was too large and connections would drop. OPC Publisher now defaults to `--dm=true` in 2.9.3 to disable metadata messages to be compatible with 2.8 when `--strict` / `-c` is not specified. If you need meta data messages but do not use strict mode (not recommended) you must explicitly enable it using `--dm=false`. ### New features in 2.9.3 diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj index b020413188..8dbf86f458 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj @@ -8,6 +8,6 @@ enable - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/HeartbeatBehavior.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/HeartbeatBehavior.cs index 190bf5e7a2..09f8e0935a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/HeartbeatBehavior.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/HeartbeatBehavior.cs @@ -43,8 +43,17 @@ public enum HeartbeatBehavior /// Update value timestamps to be different /// [EnumMember(Value = "WatchdogLKVWithUpdatedTimestamps")] - WatchdogLKVWithUpdatedTimestamps = 0x4 + WatchdogLKVWithUpdatedTimestamps = 0x4, // Others can be combining Cont, LKG with 0x4 + + /// + /// Does not send heartbeat but counts it in + /// diagnostics + /// + [EnumMember(Value = "WatchdogLKVDiagnosticsOnly")] + WatchdogLKVDiagnosticsOnly = 0x8, + + // Others can be combining Cont, LKG with 0x8 } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/RuntimeStateEventModel.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/RuntimeStateEventModel.cs index 9723f4dbf1..f47d54724a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/RuntimeStateEventModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/RuntimeStateEventModel.cs @@ -36,7 +36,7 @@ public class RuntimeStateEventModel public DateTime TimestampUtc { get; set; } /// - /// The Publisher semver version + /// The Publisher version /// [DataMember(Name = "Version", Order = 3, EmitDefaultValue = true)] @@ -71,10 +71,10 @@ public class RuntimeStateEventModel public string? ModuleId { get; set; } /// - /// The Publisher full version string + /// The Publisher semantic version string /// - [DataMember(Name = "FullVersion", Order = 8, + [DataMember(Name = "SemVer", Order = 8, EmitDefaultValue = true)] - public string? FullVersion { get; set; } + public string? SemVer { get; set; } } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs index 47b6a3f15b..594082053b 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs @@ -224,19 +224,47 @@ public record class WriterGroupDiagnosticModel EmitDefaultValue = true)] public double EncoderMaxMessageSplitRatio { get; set; } + /// + /// Number of incoming keep alive notifications + /// + [DataMember(Name = "IngressKeepAliveNotifications", Order = 30, + EmitDefaultValue = true)] + public long IngressKeepAliveNotifications { get; set; } + /// /// Number Of Subscriptions in the writer group /// - [DataMember(Name = "NumberOfSubscriptions", Order = 30, + [DataMember(Name = "NumberOfSubscriptions", Order = 31, EmitDefaultValue = true)] public long NumberOfSubscriptions { get; set; } /// - /// Number of incoming keep alive notifications + /// Publish requests ratio per group /// - [DataMember(Name = "IngressKeepAliveNotifications", Order = 31, + [DataMember(Name = "PublishRequestsRatio", Order = 32, EmitDefaultValue = true)] - public long IngressKeepAliveNotifications { get; set; } + public double PublishRequestsRatio { get; set; } + + /// + /// Good publish requests ratio per group + /// + [DataMember(Name = "GoodPublishRequestsRatio", Order = 33, + EmitDefaultValue = true)] + public double GoodPublishRequestsRatio { get; set; } + + /// + /// Bad publish requests ratio per group + /// + [DataMember(Name = "BadPublishRequestsRatio", Order = 34, + EmitDefaultValue = true)] + public double BadPublishRequestsRatio { get; set; } + + /// + /// Min publish requests assigned to the group + /// + [DataMember(Name = "MinPublishRequestsRatio", Order = 35, + EmitDefaultValue = true)] + public double MinPublishRequestsRatio { get; set; } /// /// Publisher version diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupTransport.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupTransport.cs index b7519f0d45..120aed1814 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupTransport.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupTransport.cs @@ -41,6 +41,12 @@ public enum WriterGroupTransport /// File system /// [EnumMember(Value = "FileSystem")] - FileSystem + FileSystem, + + /// + /// Null + /// + [EnumMember(Value = "Null")] + Null } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj index d0526107a7..376a8985e9 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj @@ -15,9 +15,9 @@ all runtime; build; native; contentfiles; analyzers - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/GlobalSuppressions.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/GlobalSuppressions.cs index 78e6ca3f5b..0adb68dd00 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/GlobalSuppressions.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/GlobalSuppressions.cs @@ -6,3 +6,4 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "xunit")] +[assembly: SuppressMessage("Usage", "xUnit1042:The member referenced by the MemberData attribute returns untyped data rows", Justification = "xunit")] diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj index 97437dc14c..6e5996a27f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj @@ -4,8 +4,10 @@ net8.0 - - + + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs index 7a7ccd93c0..60f13872d3 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs @@ -5,9 +5,9 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime { + using Azure.IIoT.OpcUa.Publisher.Services; using Azure.IIoT.OpcUa.Publisher.Stack.Sample; using Azure.IIoT.OpcUa.Publisher.Stack.Services; - using Azure.IIoT.OpcUa.Publisher.Services; using Autofac; using Furly.Azure; using Furly.Azure.IoT; @@ -47,18 +47,11 @@ public static void Main(string[] args) var logger = loggerFactory.CreateLogger(); logger.LogInformation("Publisher module command line interface."); - var configuration = new ConfigurationBuilder() - .AddFromDotEnvFile() - .AddEnvironmentVariables() - .AddFromKeyVault(ConfigurationProviderPriority.Lowest) - .Build(); - var cs = configuration.GetValue("PCS_IOTHUB_CONNSTRING", null); - if (string.IsNullOrEmpty(cs)) - { - cs = configuration.GetValue("_HUB_CS", null); - } var instances = 1; + string connectionString = null; string publishProfile = null; + string publishedNodesFilePath = null; + var useNullTransport = false; var unknownArgs = new List(); try { @@ -71,7 +64,7 @@ public static void Main(string[] args) i++; if (i < args.Length) { - cs = args[i]; + connectionString = args[i]; break; } throw new ArgumentException( @@ -104,6 +97,10 @@ public static void Main(string[] args) case "--only-trusted": checkTrust = true; break; + case "-X": + case "--out-null": + useNullTransport = true; + break; case "-S": case "--with-server": withServer = true; @@ -119,18 +116,48 @@ public static void Main(string[] args) } throw new ArgumentException( "Missing argument for --publish-profile"); + case "--pnjson": + i++; + if (i < args.Length) + { + publishedNodesFilePath = args[i]; + break; + } + throw new ArgumentException( + "Missing argument for --pnjson"); + case "--": + break; default: unknownArgs.Add(args[i]); break; } } - if (string.IsNullOrEmpty(cs)) - { - throw new ArgumentException("Missing connection string."); - } - if (!ConnectionString.TryParse(cs, out var connectionString)) + + if (string.IsNullOrEmpty(connectionString) && !useNullTransport) { - throw new ArgumentException("Bad connection string."); + try + { + var configuration = new ConfigurationBuilder() + .AddFromDotEnvFile() + .AddEnvironmentVariables() + .AddFromKeyVault(ConfigurationProviderPriority.Lowest) + .Build(); + connectionString = configuration.GetValue("PCS_IOTHUB_CONNSTRING", null); + if (string.IsNullOrEmpty(connectionString)) + { + connectionString = configuration.GetValue("_HUB_CS", null); + } + if (!string.IsNullOrEmpty(connectionString) && + !ConnectionString.TryParse(connectionString, out _)) + { + throw new ArgumentException("Bad connection string configured."); + } + } + catch (Exception e) + { + logger.LogInformation("Error {Error}: Missing connection string - continue...", + e.Message); + } } deviceId = Utils.GetHostName(); @@ -170,12 +197,13 @@ public static void Main(string[] args) { if (!withServer) { - hostingTask = HostAsync(cs, loggerFactory, - deviceId, moduleId, args, reverseConnectPort, !checkTrust, cts.Token); + hostingTask = HostAsync(connectionString, loggerFactory, + deviceId, moduleId, args, reverseConnectPort, !checkTrust, + publishedNodesFilePath, cts.Token); } else { - hostingTask = WithServerAsync(cs, loggerFactory, deviceId, moduleId, args, + hostingTask = WithServerAsync(connectionString, loggerFactory, deviceId, moduleId, args, publishProfile, !checkTrust, reverseConnectPort, cts.Token); } @@ -215,7 +243,7 @@ public static void Main(string[] args) private static readonly AsyncAutoResetEvent _restartPublisher = new(false); /// - /// Host the module giving it its connection string. + /// Host the module with connection string loaded from iot hub /// /// /// @@ -224,31 +252,35 @@ public static void Main(string[] args) /// /// /// + /// /// private static async Task HostAsync(string connectionString, ILoggerFactory loggerFactory, string deviceId, string moduleId, string[] args, int? reverseConnectPort, - bool acceptAll, CancellationToken ct) + bool acceptAll, string publishedNodesFilePath = null, CancellationToken ct = default) { var logger = loggerFactory.CreateLogger(); logger.LogInformation("Create or retrieve connection string for {DeviceId} {ModuleId}...", deviceId, moduleId); - ConnectionString cs; - while (true) + ConnectionString cs = null; + if (connectionString != null) { - try + while (true) { - cs = await AddOrGetAsync(connectionString, deviceId, moduleId, - logger).ConfigureAwait(false); + try + { + cs = await AddOrGetAsync(connectionString, deviceId, moduleId, + logger).ConfigureAwait(false); - logger.LogInformation("Retrieved connection string for {DeviceId} {ModuleId}.", - deviceId, moduleId); - break; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to get connection string for {DeviceId} {ModuleId}...", - deviceId, moduleId); + logger.LogInformation("Retrieved connection string for {DeviceId} {ModuleId}.", + deviceId, moduleId); + break; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get connection string for {DeviceId} {ModuleId}...", + deviceId, moduleId); + } } } @@ -257,7 +289,7 @@ private static async Task HostAsync(string connectionString, ILoggerFactory logg using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); var running = RunAsync(logger, deviceId, moduleId, args, acceptAll, cs, - reverseConnectPort, cts.Token); + reverseConnectPort, publishedNodesFilePath, cts.Token); Console.WriteLine("Publisher running (Press P to restart)..."); await _restartPublisher.WaitAsync(ct).ConfigureAwait(false); @@ -270,12 +302,25 @@ private static async Task HostAsync(string connectionString, ILoggerFactory logg } static async Task RunAsync(ILogger logger, string deviceId, string moduleId, string[] args, - bool acceptAll, ConnectionString cs, int? reverseConnectPort, CancellationToken ct) + bool acceptAll, ConnectionString cs, int? reverseConnectPort, string publishedNodesFilePath, + CancellationToken ct) { logger.LogInformation("Starting publisher module {DeviceId} {ModuleId}...", deviceId, moduleId); var arguments = args.ToList(); - arguments.Add($"--ec={cs}"); + + if (publishedNodesFilePath != null) + { + arguments.Add($"--pf={publishedNodesFilePath}"); + } + if (cs != null) + { + arguments.Add($"--ec={cs}"); + } + else + { + arguments.Add("-t=Null"); + } arguments.Add("--cl=5"); // enable 5 second client linger if (acceptAll) { @@ -315,33 +360,39 @@ private static async Task WithServerAsync(string connectionString, ILoggerFactor // Start test server using (var server = new ServerWrapper(loggerFactory, reverseConnectPort)) { - if (publishProfile != null) - { - var publishedNodesFile = $"./Profiles/{publishProfile}.json"; - if (File.Exists(publishedNodesFile)) - { - var publishedNodesFilePath = Path.GetTempFileName(); - - await File.WriteAllTextAsync(publishedNodesFilePath, - (await File.ReadAllTextAsync(publishedNodesFile, ct).ConfigureAwait(false)) - .Replace("{{EndpointUrl}}", $"opc.tcp://localhost:{server.Port}/UA/SampleServer", - StringComparison.Ordinal), ct).ConfigureAwait(false); - - args = args.Concat(new[] - { - $"--pf={publishedNodesFilePath}" - }).ToArray(); - } - } + var publishedNodesFilePath = await LoadPnJson(publishProfile, + $"opc.tcp://localhost:{server.Port}/UA/SampleServer", ct).ConfigureAwait(false); // Start publisher module await HostAsync(connectionString, loggerFactory, deviceId, moduleId, - args, reverseConnectPort, acceptAll, ct).ConfigureAwait(false); + args, reverseConnectPort, acceptAll, publishedNodesFilePath, ct).ConfigureAwait(false); } } catch (OperationCanceledException) { } } + private static async Task LoadPnJson(string publishProfile, + string endpointUrl, CancellationToken ct) + { + string publishedNodesFile = null; + if (publishProfile != null) + { + publishedNodesFile = $"./Profiles/{publishProfile}.json"; + } + if (publishedNodesFile != null && File.Exists(publishedNodesFile)) + { + var publishedNodesFilePath = Path.GetTempFileName(); + + await File.WriteAllTextAsync(publishedNodesFilePath, + (await File.ReadAllTextAsync(publishedNodesFile, ct).ConfigureAwait(false)) + .Replace("{{EndpointUrl}}", endpointUrl, + StringComparison.Ordinal), ct).ConfigureAwait(false); + + return publishedNodesFilePath; + } + return null; + } + /// /// Add or get module identity /// @@ -358,44 +409,46 @@ private static async Task AddOrGetAsync(string connectionStrin options => options.ConnectionString = connectionString); builder.AddDefaultJsonSerializer(); builder.AddLogging(); - using var container = builder.Build(); - - var registry = container.Resolve(); - - // Create iot edge gateway - try + var container = builder.Build(); + await using (container.ConfigureAwait(false)) { - await registry.CreateOrUpdateAsync(new DeviceTwinModel + var registry = container.Resolve(); + + // Create iot edge gateway + try { - Id = deviceId, - Tags = new Dictionary + await registry.CreateOrUpdateAsync(new DeviceTwinModel { - [Constants.TwinPropertyTypeKey] = Constants.EntityTypeGateway - }, - IotEdge = true - }, false).ConfigureAwait(false); - } - catch (ResourceConflictException) - { - logger.LogInformation("IoT Edge device {DeviceId} already exists.", deviceId); - } + Id = deviceId, + Tags = new Dictionary + { + [Constants.TwinPropertyTypeKey] = Constants.EntityTypeGateway + }, + IotEdge = true + }, false).ConfigureAwait(false); + } + catch (ResourceConflictException) + { + logger.LogInformation("IoT Edge device {DeviceId} already exists.", deviceId); + } - // Create publisher module - try - { - await registry.CreateOrUpdateAsync(new DeviceTwinModel + // Create publisher module + try { - Id = deviceId, - ModuleId = moduleId - }, false, default).ConfigureAwait(false); - } - catch (ResourceConflictException) - { - logger.LogInformation("Publisher {ModuleId} already exists...", moduleId); + await registry.CreateOrUpdateAsync(new DeviceTwinModel + { + Id = deviceId, + ModuleId = moduleId + }, false, default).ConfigureAwait(false); + } + catch (ResourceConflictException) + { + logger.LogInformation("Publisher {ModuleId} already exists...", moduleId); + } + var module = await registry.GetRegistrationAsync(deviceId, moduleId).ConfigureAwait(false); + return ConnectionString.CreateModuleConnectionString(registry.HostName, + deviceId, moduleId, module.PrimaryKey); } - var module = await registry.GetRegistrationAsync(deviceId, moduleId).ConfigureAwait(false); - return ConnectionString.CreateModuleConnectionString(registry.HostName, - deviceId, moduleId, module.PrimaryKey); } /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj index ab8ede6202..51de271332 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj @@ -33,11 +33,11 @@ - - - - - + + + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/CertificatesController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/CertificatesController.cs index a4a0c9ad46..688adecb34 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/CertificatesController.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/CertificatesController.cs @@ -12,9 +12,9 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Controllers using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; - using System.ComponentModel.DataAnnotations; /// /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/ConfigurationController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/ConfigurationController.cs index b64fb348e3..a779c708bc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/ConfigurationController.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/ConfigurationController.cs @@ -11,10 +11,10 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Controllers using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; - using System.ComponentModel.DataAnnotations; /// /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/DiscoveryController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/DiscoveryController.cs index 5009619e61..964f11ad63 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/DiscoveryController.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/DiscoveryController.cs @@ -10,9 +10,9 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Controllers using Furly.Tunnel.Router; using Microsoft.AspNetCore.Mvc; using System; + using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; - using System.ComponentModel.DataAnnotations; /// /// OPC UA and network discovery related API. diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/GeneralController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/GeneralController.cs index 9f4949dfc7..406190d956 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/GeneralController.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/GeneralController.cs @@ -12,9 +12,9 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Controllers using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; - using System.ComponentModel.DataAnnotations; /// /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/HistoryController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/HistoryController.cs index b9574f066e..6169af8f50 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/HistoryController.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/HistoryController.cs @@ -11,9 +11,9 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Controllers using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; - using System.ComponentModel.DataAnnotations; /// /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs index 2b6685b7b7..8ade062980 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs @@ -16,6 +16,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module using System.Linq; using System.Threading; using System.Threading.Tasks; + using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; /// /// Module @@ -34,7 +35,7 @@ private static void LogLogo() ██║ ██║██╔═══╝ ██║ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██╔══██║██╔══╝ ██╔══██╗ ╚██████╔╝██║ ╚██████╗ ██║ ╚██████╔╝██████╔╝███████╗██║███████║██║ ██║███████╗██║ ██║ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ -{PublisherConfig.Version.PadLeft(96)}) +{PublisherConfig.Version.PadLeft(97)} "); } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs index 3bee70697e..40bbdb9f8d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs @@ -7,6 +7,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime { using Azure.IIoT.OpcUa.Publisher.Models; using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; + using Azure.IIoT.OpcUa.Publisher.Stack.Services; using Furly.Azure.IoT.Edge; using Furly.Extensions.Messaging; using Microsoft.Extensions.Configuration; @@ -333,6 +334,13 @@ public CommandLine(string[] args, CommandLineLogger? logger = null) $"The port to use when accepting inbound reverse connect requests from servers.\nDefault: `{OpcUaClientConfig.ReverseConnectPortDefault}`.\n", (ushort u) => this[OpcUaClientConfig.ReverseConnectPortKey] = u.ToString(CultureInfo.CurrentCulture) }, + { $"mpr|minpublishrequests=|{OpcUaClientConfig.MinPublishRequestsKey}=", + $"Minimum number of publish requests to queue once subscriptions are created in the session.\nDefault: `{OpcUaClientConfig.MinPublishRequestsDefault}`.\n", + (int u) => this[OpcUaClientConfig.MinPublishRequestsKey] = u.ToString(CultureInfo.CurrentCulture) }, + { $"ppr|percentpublishrequests=|{OpcUaClientConfig.PublishRequestsPerSubscriptionPercentKey}=", + $"Percentage ratio of publish requests per subscriptions in the session in percent.\nDefault: `{OpcUaClientConfig.PublishRequestsPerSubscriptionPercentDefault}`% (1 request per subscription).\n", + (int u) => this[OpcUaClientConfig.PublishRequestsPerSubscriptionPercentKey] = u.ToString(CultureInfo.CurrentCulture) }, + { $"smi|subscriptionmanagementinterval=|{OpcUaClientConfig.SubscriptionManagementIntervalKey}=", "The interval in seconds after which the publisher re-applies the desired state of the subscription to a session.\nDefault: `never` (only on configuration change).\n", (int i) => this[OpcUaClientConfig.SubscriptionManagementIntervalKey] = TimeSpan.FromSeconds(i).ToString() }, @@ -467,6 +475,12 @@ public CommandLine(string[] args, CommandLineLogger? logger = null) { $"em|enableprometheusendpoint=|{Configuration.Otlp.EnableMetricsKey}=", "Explicitly enable or disable exporting prometheus metrics directly on the standard path.\nDefault: `disabled` if Otlp collector is configured, otherwise `enabled`.\n", (bool? b) => this[Configuration.Otlp.EnableMetricsKey] = b?.ToString() ?? "True" }, + { $"cap|capturedevice=|{OpcUaClientConfig.CaptureDeviceKey}=", + $"The capture device to use to capture network traffic.\n{SupportsCapture(OpcUaClientCapture.AvailableDevices)}\n", + (string s) => this[OpcUaClientConfig.CaptureDeviceKey] = s }, + { $"cpf|capturefile=|{OpcUaClientConfig.CaptureFileNameKey}=", + $"The file name to capture traffic to.\nA device must be selected using `--cd` if capture capability is supported on this system.\nDefault: `{OpcUaClientConfig.CaptureFileNameDefault}`.\n", + (string s) => this[OpcUaClientConfig.CaptureFileNameKey] = s }, // testing purposes @@ -603,6 +617,16 @@ void SetStoreType(string s, string storeTypeKey, string optionName) throw new OptionException("Bad store type", optionName); } } + + private static string SupportsCapture(IReadOnlyList devices) + { + if (devices.Count == 0) + { + return "Network capture is not supported on this system."; + } + return $"Available devices on your system:\n `{string.Join("`\n `", devices)}`\nDefault: `null` (disabled)."; + } + private readonly CommandLineLogger _logger; } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs index c2ab8b55a7..689976f0ec 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs @@ -6,6 +6,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime { using Azure.IIoT.OpcUa.Publisher.Module.Controllers; + using Autofac; using Furly.Azure.IoT.Edge; using Furly.Extensions.AspNetCore.OpenApi; using Furly.Extensions.Configuration; @@ -22,7 +23,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; - using Autofac; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs index 911ae999f0..8192a857cd 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Controller/DmApiPublisherControllerTests.cs @@ -115,7 +115,7 @@ private PublisherConfigurationService InitPublisherConfigService() public async Task DmApiPublishUnpublishNodesTestAsync(string publishedNodesFile) { CopyContent("Resources/empty_pn.json", _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var methodsController = new ConfigurationController(configService); @@ -175,7 +175,7 @@ await FluentActions public async Task DmApiPublishUnpublishAllNodesTestAsync(string publishedNodesFile) { CopyContent("Resources/empty_pn.json", _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var methodsController = new ConfigurationController(configService); using var publishPayloads = new StreamReader(publishedNodesFile); @@ -235,7 +235,7 @@ await FluentActions public async Task DmApiPublishNodesToJobTestAsync(string publishedNodesFile) { CopyContent("Resources/empty_pn.json", _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var methodsController = new ConfigurationController(configService); @@ -527,7 +527,7 @@ await FluentActions public async Task DmApiGetConfiguredEndpointsTestAsync(string publishedNodesFile) { CopyContent("Resources/empty_pn.json", _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var methodsController = new ConfigurationController(configService); using var publishPayloads = new StreamReader(publishedNodesFile); @@ -579,7 +579,7 @@ await FluentActions public async Task DmApiGetDiagnosticInfoTestAsync() { CopyContent("Resources/empty_pn.json", _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var methodsController = new ConfigurationController(configService); var response = await FluentActions diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs index 9ca4f1c710..762fb1e5dc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs @@ -16,6 +16,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Tests.Fixtures using Divergic.Logging.Xunit; using Furly.Azure; using Furly.Azure.IoT; + using Furly.Azure.IoT.Edge; using Furly.Azure.IoT.Mock; using Furly.Azure.IoT.Mock.Services; using Furly.Azure.IoT.Models; @@ -44,7 +45,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Tests.Fixtures using System.Threading.Channels; using System.Threading.Tasks; using Xunit.Abstractions; - using Furly.Azure.IoT.Edge; /// /// Publisher telemetry diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttConfigurationIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttConfigurationIntegrationTests.cs index 1e6ac93e42..56df73e62f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttConfigurationIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttConfigurationIntegrationTests.cs @@ -12,11 +12,11 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Tests.Mqtt.ReferenceServer using Json.More; using System; using System.Collections.Generic; + using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; - using System.Linq; public class MqttConfigurationIntegrationTests : PublisherIntegrationTestBase { diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs index c82485df0c..4cd896ad29 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs @@ -241,7 +241,7 @@ public async Task CanEncodeWithReversibleEncodingSamplesTest(string publishedNod var serviceMessageContext = new ServiceMessageContext(); serviceMessageContext.Factory.AddEncodeableType(typeof(EncodeableDictionary)); - using (var stream = new MemoryStream(buffer)) + await using (var stream = new MemoryStream(buffer)) { using var decoder = new JsonDecoderEx(stream, serviceMessageContext); var actual = new EncodeableDictionary(); diff --git a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj index e0c1a82fea..b1f08eb6bc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj @@ -8,9 +8,9 @@ enable - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj index 8626147389..f0053ed5ee 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Program.cs b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Program.cs index 82777cfb63..ec4f7d2f91 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Program.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Program.cs @@ -908,30 +908,32 @@ private async Task QueryPublishersAsync(CliOptions options) private async Task GetConfiguredEndpointsAsync(CliOptions options) { var file = options.GetValueOrNull("f", "--file"); - using var stream = file == null ? Console.Out : File.CreateText(file); - - await stream.WriteLineAsync("[").ConfigureAwait(false); - var empty = true; + var stream = file == null ? Console.Out : File.CreateText(file); + await using (stream.ConfigureAwait(false)) + { + await stream.WriteLineAsync("[").ConfigureAwait(false); + var empty = true; - await foreach (var endpoint in _client.Registry.GetConfiguredEndpointsAsync( - GetPublisherId(options), new GetConfiguredEndpointsRequestModel + await foreach (var endpoint in _client.Registry.GetConfiguredEndpointsAsync( + GetPublisherId(options), new GetConfiguredEndpointsRequestModel + { + IncludeNodes = options.IsProvidedOrNull("-n", "--nodes") + })) { - IncludeNodes = options.IsProvidedOrNull("-n", "--nodes") - })) - { + if (!empty) + { + await stream.WriteLineAsync(",").ConfigureAwait(false); + } + empty = false; + await stream.WriteAsync(_client.Serializer.SerializeToString(endpoint, + options.GetValueOrDefault(SerializeOption.Indented, "-F", "--format"))).ConfigureAwait(false); + } if (!empty) { - await stream.WriteLineAsync(",").ConfigureAwait(false); + await stream.WriteLineAsync().ConfigureAwait(false); } - empty = false; - await stream.WriteAsync(_client.Serializer.SerializeToString(endpoint, - options.GetValueOrDefault(SerializeOption.Indented, "-F", "--format"))).ConfigureAwait(false); - } - if (!empty) - { - await stream.WriteLineAsync().ConfigureAwait(false); + await stream.WriteLineAsync("]").ConfigureAwait(false); } - await stream.WriteLineAsync("]").ConfigureAwait(false); } /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj index 5140a69f51..28f3cd4b16 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj @@ -11,10 +11,10 @@ - - - - + + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj index 043acda46e..8ed473f699 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj @@ -28,9 +28,9 @@ - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Fixtures/PublisherModule.cs b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Fixtures/PublisherModule.cs index 9bd376016b..4de29ee94e 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Fixtures/PublisherModule.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Fixtures/PublisherModule.cs @@ -11,6 +11,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests using Autofac.Extensions.DependencyInjection; using Furly.Azure; using Furly.Azure.IoT; + using Furly.Azure.IoT.Edge; using Furly.Azure.IoT.Mock; using Furly.Azure.IoT.Models; using Furly.Extensions.Utils; @@ -23,7 +24,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests using System.Collections.Generic; using System.IO; using System.Linq; - using Furly.Azure.IoT.Edge; /// /// Opc Publisher module fixture diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/PublisherServiceEventsTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/PublisherServiceEventsTests.cs index c1e6d4a486..d24b559841 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/PublisherServiceEventsTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/PublisherServiceEventsTests.cs @@ -39,7 +39,7 @@ public void Dispose() [Fact] public async Task TestPublishVariantTelemetryEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -93,7 +93,7 @@ await channel.Writer.WriteAsync(ev))) [InlineData(262345)] public async Task TestPublishPublisherEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/RegistryServiceEventsTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/RegistryServiceEventsTests.cs index 39c62f10d5..e6235f6878 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/RegistryServiceEventsTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Sdk/RegistryServiceEventsTests.cs @@ -36,7 +36,7 @@ public void Dispose() [Fact] public async Task TestPublishPublisherEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -69,7 +69,7 @@ public async Task TestPublishPublisherEventAndReceiveAsync() [InlineData(678)] public async Task TestPublishPublisherEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -104,7 +104,7 @@ public async Task TestPublishPublisherEventAndReceiveMultipleAsync(int total) [Fact] public async Task TestPublishDiscovererEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -145,7 +145,7 @@ public async Task TestPublishDiscovererEventAndReceiveAsync() [InlineData(375)] public async Task TestPublishDiscovererEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -179,7 +179,7 @@ public async Task TestPublishDiscovererEventAndReceiveMultipleAsync(int total) [Fact] public async Task TestPublishSupervisorEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -213,7 +213,7 @@ public async Task TestPublishSupervisorEventAndReceiveAsync() [InlineData(4634)] public async Task TestPublishSupervisorEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -247,7 +247,7 @@ public async Task TestPublishSupervisorEventAndReceiveMultipleAsync(int total) [Fact] public async Task TestPublishApplicationEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -286,7 +286,7 @@ public async Task TestPublishApplicationEventAndReceiveAsync() [InlineData(4634)] public async Task TestPublishApplicationEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -323,7 +323,7 @@ public async Task TestPublishApplicationEventAndReceiveMultipleAsync(int total) [Fact] public async Task TestPublishEndpointEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -361,7 +361,7 @@ public async Task TestPublishEndpointEventAndReceiveAsync() [InlineData(7384)] public async Task TestPublishEndpointEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -400,7 +400,7 @@ public async Task TestPublishEndpointEventAndReceiveMultipleAsync(int total) [Fact] public async Task TestPublishGatewayEventAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -432,7 +432,7 @@ public async Task TestPublishGatewayEventAndReceiveAsync() [InlineData(100)] public async Task TestPublishGatewayEventAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -466,7 +466,7 @@ public async Task TestPublishGatewayEventAndReceiveMultipleAsync(int total) [Fact] public async Task TestPublishDiscoveryProgressWithDiscovererIdAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -505,7 +505,7 @@ public async Task TestPublishDiscoveryProgressWithDiscovererIdAndReceiveAsync() [Fact] public async Task TestPublishDiscoveryProgressWithRequestIdAndReceiveAsync() { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); @@ -555,7 +555,7 @@ public async Task TestPublishDiscoveryProgressWithRequestIdAndReceiveAsync() [InlineData(678)] public async Task TestPublishDiscoveryProgressAndReceiveMultipleAsync(int total) { - using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); + await using var scope = _factory.CreateClientScope(_output, TestSerializerType.NewtonsoftJson); var bus = _factory.Resolve(); var client = scope.Resolve(); diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj index 3a16240251..355c8c26d7 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj @@ -6,7 +6,7 @@ enable - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj index 676c366751..18b0582dfe 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj @@ -55,7 +55,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj index 82f178972a..149511ba29 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj @@ -5,9 +5,9 @@ enable - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj index 7b325f4138..0d37aac3da 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj @@ -4,10 +4,16 @@ true enable + + SHARPPCAP + + + + - + diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Discovery/NetworkDiscovery.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Discovery/NetworkDiscovery.cs index e21e92015a..24cf3408eb 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Discovery/NetworkDiscovery.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Discovery/NetworkDiscovery.cs @@ -362,10 +362,11 @@ private async Task> DiscoverServersAsync( request.Token)) { // Log progress - using (var progress = new Timer(_ => ProgressTimer( + var progress = new Timer(_ => ProgressTimer( () => _progress.OnNetScanProgress(request.Request, netscanner.ActiveProbes, netscanner.ScanCount, request.TotalAddresses, addresses.Count)), - null, kProgressInterval, kProgressInterval)) + null, kProgressInterval, kProgressInterval); + await using (progress.ConfigureAwait(false)) { await netscanner.WaitToCompleteAsync().ConfigureAwait(false); } @@ -401,10 +402,11 @@ private async Task> DiscoverServersAsync( request.Configuration.MinPortProbesPercent, request.Configuration.PortProbeTimeout, request.Token)) { - using (var progress = new Timer(_ => ProgressTimer( + var progress = new Timer(_ => ProgressTimer( () => _progress.OnPortScanProgress(request.Request, portscan.ActiveProbes, portscan.ScanCount, totalPorts, ports.Count)), - null, kProgressInterval, kProgressInterval)) + null, kProgressInterval, kProgressInterval); + await using (progress.ConfigureAwait(false)) { await portscan.WaitToCompleteAsync().ConfigureAwait(false); } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Parser/RelativePathParser.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Parser/RelativePathParser.cs index 048fd40c14..b640e74adc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Parser/RelativePathParser.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Parser/RelativePathParser.cs @@ -8,9 +8,9 @@ namespace Azure.IIoT.OpcUa.Publisher.Parser using Azure.IIoT.OpcUa.Publisher.Models; using Azure.IIoT.OpcUa.Encoders.Utils; using System; + using System.Buffers; using System.Collections.Generic; using System.Linq; - using System.Buffers; using System.Text; /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs index 4221a05705..f08190fef3 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs @@ -6,10 +6,12 @@ namespace Azure.IIoT.OpcUa.Publisher { using Azure.IIoT.OpcUa.Publisher.Models; + using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; using Furly.Extensions.Configuration; using Furly.Extensions.Hosting; using Furly.Extensions.Messaging; using Microsoft.Extensions.Configuration; + using Opc.Ua; using System; using System.Configuration; using System.Linq; @@ -405,6 +407,8 @@ public PublisherConfig(IConfiguration configuration, IProcessIdentity? identity .Append('/') .Append(AppContext.GetData("RUNTIME_IDENTIFIER") as string ?? RuntimeInformation.ProcessArchitecture.ToString()) + .Append("/OPC Stack ") + .Append(typeof(SessionChannel).Assembly.GetReleaseVersion().ToString()) .Append(')') .ToString(); diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs index f7bafb9269..49252263eb 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs @@ -138,8 +138,8 @@ public NetworkMessageSink(WriterGroupModel writerGroup, InitializeMetrics(); _logger.LogInformation("Writer group {WriterGroup} set up to publish notifications " + - "{Interval} {Batching} with {MaxSize} to {Transport} with {HeaderLayout} " + - "{MessageType} encoding (queuing at most {MaxQueueSize} notifications)...", + "{Interval} {Batching} with {MaxSize} to {Transport} with {HeaderLayout} layout and " + + "{MessageType} encoding (queuing at most {MaxQueueSize} subscription notifications)...", writerGroup.Name ?? Constants.DefaultWriterGroupId, _batchTriggerInterval == TimeSpan.Zero ? "as soon as they arrive" : $"every {_batchTriggerInterval} (hh:mm:ss)", @@ -147,8 +147,8 @@ public NetworkMessageSink(WriterGroupModel writerGroup, "and individually" : $"or when a batch of {_maxNotificationsPerMessage} notifications is ready", _maxNetworkMessageSize == int.MaxValue ? - "unlimited" : $"at most {_maxNetworkMessageSize / 1024} kb", - _eventClient.Name, writerGroup.HeaderLayoutUri, + "unlimited size" : $"at most {_maxNetworkMessageSize / 1024} kb", + _eventClient.Name, writerGroup.HeaderLayoutUri ?? "unknown", writerGroup.MessageType ?? MessageEncoding.Json, _maxPublishQueueSize); } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs index 984649a999..ed1cac4f2d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs @@ -198,7 +198,15 @@ internal WriterGroupDiagnosticModel AggregateModel ConnectionRetries = writers.Count == 0 ? 0 : (int) writers.Average(w => w.ConnectionRetries), OpcEndpointConnected = OpcEndpointConnected || - writers.Any(w => w.OpcEndpointConnected) + writers.Any(w => w.OpcEndpointConnected), + PublishRequestsRatio = PublishRequestsRatio + + writers.Sum(w => w.PublishRequestsRatio), + BadPublishRequestsRatio = BadPublishRequestsRatio + + writers.Sum(w => w.BadPublishRequestsRatio), + GoodPublishRequestsRatio = GoodPublishRequestsRatio + + writers.Sum(w => w.GoodPublishRequestsRatio), + MinPublishRequestsRatio = MinPublishRequestsRatio + + writers.Sum(w => w.MinPublishRequestsRatio), }; } } @@ -283,7 +291,15 @@ internal WriterGroupDiagnosticModel AggregateModel ["iiot_edge_publisher_connection_retries"] = (d, i) => d.ConnectionRetries = (long)i, ["iiot_edge_publisher_subscriptions"] = - (d, i) => d.NumberOfSubscriptions = (long)i + (d, i) => d.NumberOfSubscriptions = (long)i, + ["iiot_edge_publisher_publish_requests_per_subscription"] = + (d, i) => d.PublishRequestsRatio = (double)i, + ["iiot_edge_publisher_good_publish_requests_per_subscription"] = + (d, i) => d.GoodPublishRequestsRatio = (double)i, + ["iiot_edge_publisher_bad_publish_requests_per_subscription"] = + (d, i) => d.BadPublishRequestsRatio = (double)i, + ["iiot_edge_publisher_min_publish_requests_per_subscription"] = + (d, i) => d.MinPublishRequestsRatio = (double)i // ... Add here more items if needed }; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs index 05a6105fa3..5def8b3dce 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs @@ -16,19 +16,20 @@ namespace Azure.IIoT.OpcUa.Publisher.Services using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; - using System.Collections.Generic; using System.Collections.Concurrent; + using System.Collections.Generic; using System.Diagnostics; + using System.Diagnostics.Metrics; + using System.Globalization; using System.Linq; using System.Net; + using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; - using System.Diagnostics.Metrics; - using System.Runtime.InteropServices; - using System.Globalization; + using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; /// /// This class manages reporting of runtime state. @@ -146,8 +147,8 @@ public async ValueTask SendRestartAnnouncementAsync(CancellationToken ct) MessageVersion = 1, MessageType = RuntimeStateEventType.RestartAnnouncement, PublisherId = _options.Value.PublisherId, - FullVersion = PublisherConfig.Version, - Version = GetType().Assembly.GetReleaseVersion().ToString(), + SemVer = GetType().Assembly.GetReleaseVersion().ToString(), + Version = PublisherConfig.Version, Site = _options.Value.Site, DeviceId = _identity?.DeviceId, ModuleId = _identity?.ModuleId @@ -517,6 +518,14 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .Append(" # Subscriptions count : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0}", info.NumberOfSubscriptions) .AppendLine() + .Append(" # Queued/Minimum request count : ") + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0.##}", info.PublishRequestsRatio).Append(" | ") + .AppendFormat(CultureInfo.CurrentCulture, "{0:0.##}", info.MinPublishRequestsRatio) + .AppendLine() + .Append(" # Good/Bad Publish request count : ") + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0.##}", info.GoodPublishRequestsRatio).Append(" | ") + .AppendFormat(CultureInfo.CurrentCulture, "{0:0.##}", info.BadPublishRequestsRatio) + .AppendLine() .Append(" # Ingress value changes : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressValueChanges).Append(' ') .AppendLine(valueChangesPerSecFormatted) @@ -526,16 +535,16 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .Append(" # Received Data Change Notifications : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressDataChanges) .Append(' ').AppendLine(dataChangesPerSecFormatted) - .Append(" # Received Event List notifications : ") + .Append(" # Received Event Notifications : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressEventNotifications) .AppendLine() - .Append(" # Received Keep Alive notifications : ") + .Append(" # Received Keep Alive Notifications : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressKeepAliveNotifications) .AppendLine() - .Append(" # Generated Cyclic read notifications: ") + .Append(" # Generated Cyclic read Notifications: ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressCyclicReads) .AppendLine() - .Append(" # Generated Heartbeat notifications : ") + .Append(" # Generated Heartbeat Notifications : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressHeartbeats) .AppendLine() .Append(" # Notification batch buffer size : ") @@ -543,12 +552,11 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .AppendLine() .Append(" # Encoder input/output buffer size : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0}", info.EncodingBlockInputSize).Append(" | ") - .AppendFormat(CultureInfo.CurrentCulture, "{0:0}", info.EncodingBlockOutputSize).AppendLine() - .Append(" # Encoder Notifications processed : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.EncoderNotificationsProcessed) + .AppendFormat(CultureInfo.CurrentCulture, "{0:0}", info.EncodingBlockOutputSize) .AppendLine() - .Append(" # Encoder Notifications dropped : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.EncoderNotificationsDropped) + .Append(" # Encoder Notif. processed/dropped : ") + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.EncoderNotificationsProcessed).Append(" | ") + .AppendFormat(CultureInfo.CurrentCulture, "{0:0}", info.EncoderNotificationsDropped) .AppendLine() .Append(" # Encoder Network Messages produced : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.EncoderIoTMessagesProcessed) @@ -557,7 +565,7 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0}", info.EncoderAvgNotificationsMessage) .AppendLine() .Append(" # Encoder worst Message split ratio : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0.#}", info.EncoderMaxMessageSplitRatio) + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0.##}", info.EncoderMaxMessageSplitRatio) .AppendLine() .Append(" # Encoder avg Message body size : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.EncoderAvgIoTMessageBodySize) @@ -568,15 +576,14 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .Append(" # Estimated Chunks (4 KB) per day : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.EstimatedIoTChunksPerDay) .AppendLine() - .Append(" # Egress messages ready to send : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.OutgressInputBufferCount) - .AppendLine() - .Append(" # Egress messages dropped : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.OutgressInputBufferDropped) + .Append(" # Egress Messages queued/dropped : ") + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.OutgressInputBufferCount).Append(" | ") + .AppendFormat(CultureInfo.CurrentCulture, "{0:0}", info.OutgressInputBufferDropped) .AppendLine() - .Append(" # Egress messages successfully sent : ") + .Append(" # Egress Messages successfully sent : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.OutgressIoTMessageCount) - .Append(' ').AppendLine(sentMessagesPerSecFormatted) + .Append(' ') + .AppendLine(sentMessagesPerSecFormatted) ; } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs index 35cb538785..d094586a87 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs @@ -581,26 +581,26 @@ private void OnSubscriptionDataDiagnosticsChanged(object? sender, (bool, int, in { lock (_lock) { - if (_outer.DataChangesCount >= kNumberOfInvokedMessagesResetThreshold || - _outer.ValueChangesCount >= kNumberOfInvokedMessagesResetThreshold) - { - // reset both - _outer._logger.LogDebug("Notifications counter in subscription {Id} has been reset to prevent" + - " overflow. So far, {DataChangesCount} data changes and {ValueChangesCount} " + - "value changes were invoked by message source.", - Id, _outer.DataChangesCount, _outer.ValueChangesCount); - _outer.DataChangesCount = 0; - _outer.ValueChangesCount = 0; - _outer._heartbeatsCount = 0; - _outer._cyclicReadsCount = 0; - _outer.OnCounterReset?.Invoke(this, EventArgs.Empty); - } - - _outer.ValueChangesCount += notificationCounts.Item2; _outer._heartbeatsCount += notificationCounts.Item3; _outer._cyclicReadsCount += notificationCounts.Item4; if (notificationCounts.Item1) { + if (_outer.DataChangesCount >= kNumberOfInvokedMessagesResetThreshold || + _outer.ValueChangesCount >= kNumberOfInvokedMessagesResetThreshold) + { + // reset both + _outer._logger.LogDebug("Notifications counter in subscription {Id} has been reset to prevent" + + " overflow. So far, {DataChangesCount} data changes and {ValueChangesCount} " + + "value changes were invoked by message source.", + Id, _outer.DataChangesCount, _outer.ValueChangesCount); + _outer.DataChangesCount = 0; + _outer.ValueChangesCount = 0; + _outer._heartbeatsCount = 0; + _outer._cyclicReadsCount = 0; + _outer.OnCounterReset?.Invoke(this, EventArgs.Empty); + } + + _outer.ValueChangesCount += notificationCounts.Item2; _outer.DataChangesCount++; } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs index d9d83718e2..06e4f28953 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs @@ -31,6 +31,8 @@ public static void AddOpcUaStack(this ContainerBuilder builder) .AsImplementedInterfaces().SingleInstance(); builder.RegisterType() .AsImplementedInterfaces().SingleInstance(); + builder.RegisterType() + .AsImplementedInterfaces().SingleInstance(); builder.RegisterType() .AsImplementedInterfaces(); diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientState.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientState.cs new file mode 100644 index 0000000000..e970ab72ac --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientState.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher.Stack +{ + /// + /// Safely access client state for diagnostics + /// + internal interface IOpcUaClientState + { + /// + /// Bad publish requests tracked by this client + /// + int BadPublishRequestCount { get; } + + /// + /// Good publish requests tracked by this client + /// + int GoodPublishRequestCount { get; } + + /// + /// Outstanding requests + /// + int OutstandingRequestCount { get; } + + /// + /// Number of subscriptions tracked by client + /// + int SubscriptionCount { get; } + + /// + /// Is connected + /// + bool IsConnected { get; } + + /// + /// Connection attempts + /// + int ReconnectCount { get; } + + /// + /// Current min publish request count + /// + int MinPublishRequestCount { get; } + } +} diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs index 95ca7935aa..acdd6e2d58 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs @@ -106,7 +106,7 @@ bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, Action> cb); + IEnumerable, bool> cb); /// /// Get any changes in the monitoring mode to apply if any. diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs index c3d1817024..8eafb7e66c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs @@ -34,9 +34,9 @@ ValueTask SyncWithSessionAsync(IOpcUaSession session, /// or when the subscription was disconnected. /// /// - /// + /// void OnSubscriptionStateChanged(bool online, - int connectionAttempts); + IOpcUaClientState state); /// /// Try get the current position in the out stream. diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs index a6dd8ce6d6..3722d68cc6 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs @@ -11,6 +11,8 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Runtime using System; using System.Globalization; using System.IO; + using System.Reflection; + using System.Runtime.InteropServices; using System.Text; /// @@ -70,6 +72,10 @@ public sealed class OpcUaClientConfig : PostConfigureOptionBase @@ -83,8 +89,8 @@ public sealed class OpcUaClientConfig : PostConfigureOptionBase @@ -237,6 +246,30 @@ public override void PostConfigure(string? name, OpcUaClientOptions options) } } + if (options.MinPublishRequests == null) + { + options.MinPublishRequests = GetIntOrDefault(MinPublishRequestsKey, + MinPublishRequestsDefault); + } + + if (options.PublishRequestsPerSubscriptionPercent == null) + { + options.PublishRequestsPerSubscriptionPercent = GetIntOrNull( + PublishRequestsPerSubscriptionPercentKey, + PublishRequestsPerSubscriptionPercentDefault); + } + + if (string.IsNullOrEmpty(options.CaptureDevice)) + { + options.CaptureDevice = GetStringOrDefault(CaptureDeviceKey); + } + + if (string.IsNullOrEmpty(options.CaptureFileName)) + { + options.CaptureFileName = GetStringOrDefault(CaptureFileNameKey, + CaptureFileNameDefault); + } + if (options.Security.MinimumCertificateKeySize == 0) { options.Security.MinimumCertificateKeySize = (ushort)GetIntOrDefault( diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientOptions.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientOptions.cs index cab7098301..40b8c90ad8 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientOptions.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientOptions.cs @@ -113,5 +113,28 @@ public sealed class OpcUaClientOptions /// Enable traces in the stack beyond errors /// public bool? EnableOpcUaStackLogging { get; set; } + + /// + /// Minimum number of publish requests to queue + /// at all times. Default is 3. + /// + public int? MinPublishRequests { get; set; } + + /// + /// The publish requests per subscription factor in + /// percent, e.g., 120% means 1.2 requests per + /// subscription. Use this to control network latency + /// + public int? PublishRequestsPerSubscriptionPercent { get; set; } + + /// + /// Use the specific device to capture traffice. + /// + public string? CaptureDevice { get; set; } + + /// + /// Use the specified capture file + /// + public string? CaptureFileName { get; set; } } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs index f38a5d5b84..3a69d742ec 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs @@ -30,8 +30,8 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services /// /// OPC UA Client based on official ua client reference sample. /// - internal sealed class OpcUaClient : IAsyncDisposable, IOpcUaClient, - ISessionAccessor + internal sealed class OpcUaClient : IOpcUaClient, ISessionAccessor, + IOpcUaClientState { /// /// The session keepalive interval to be used in ms. @@ -59,20 +59,58 @@ internal sealed class OpcUaClient : IAsyncDisposable, IOpcUaClient, public TimeSpan? OperationTimeout { get; set; } /// - /// The linger timeout. + /// Minimum number of publish requests to queue /// - public TimeSpan? LingerTimeout { get; set; } + public int? MinPublishRequests { get; set; } /// - /// Is reconnecting + /// Percentage ratio of publish requests per subscription /// - internal bool IsConnected => _session?.Session.Connected ?? false; + public int? PublishRequestsPerSubscriptionPercent { get; set; } + + /// + /// The linger timeout. + /// + public TimeSpan? LingerTimeout { get; set; } /// /// Disable complex type preloading. /// public bool? DisableComplexTypePreloading { get; set; } + /// + public bool IsConnected + => _session?.Session.Connected ?? false; + + /// + public int BadPublishRequestCount + => _session?.Session?.DefunctRequestCount ?? 0; + + /// + public int GoodPublishRequestCount + => _session?.Session?.GoodPublishRequestCount ?? 0; + + /// + public int OutstandingRequestCount + => _session?.Session?.OutstandingRequestCount ?? 0; + + /// + public int SubscriptionCount + => _session?.Session?.Subscriptions.Count(s => s.Created) ?? 0; + + /// + public int MinPublishRequestCount + => _session?.Session?.MinPublishRequestCount ?? 0; + + /// + public int ReconnectCount => _numberOfConnectRetries; + + /// + /// Disconnected state + /// + internal static IOpcUaClientState Disconnected { get; } + = new DisconnectState(); + /// /// Create client /// @@ -226,8 +264,11 @@ public void UnregisterSubscription(ISubscriptionHandle subscription) } } - /// - public async ValueTask DisposeAsync() + /// + /// Close client + /// + /// + internal async ValueTask CloseAsync() { ObjectDisposedException.ThrowIf(_disposed, this); try @@ -810,8 +851,7 @@ await subscription.SyncWithSessionAsync(session, newSession, } if (online != null) { - subscription.OnSubscriptionStateChanged(online.Value, - _numberOfConnectRetries); + subscription.OnSubscriptionStateChanged(online.Value, this); } } catch (OperationCanceledException) { } @@ -822,6 +862,8 @@ await subscription.SyncWithSessionAsync(session, newSession, } })).ConfigureAwait(false); + EnsureMinimumNumberOfPublishRequestsQueued(); + if (subscriptions.Count > 1) { // Clear the node cache - TODO: we should have a real node cache here @@ -847,6 +889,47 @@ int GetMinReconnectPeriod() } } + private const int kMinPublishRequestCount = 3; + + /// + /// Ensure min publish requests are queued + /// + private void EnsureMinimumNumberOfPublishRequestsQueued() + { + var session = _session?.Session; + if (session == null) + { + return; + } + var created = SubscriptionCount; + if (created == 0) + { + return; + } + + var percentage = PublishRequestsPerSubscriptionPercent ?? 100; + var desiredRequests = Math.Max( + MinPublishRequests ?? kMinPublishRequestCount, + percentage == 100 || percentage < 0 ? created : + (int)Math.Ceiling(created * (percentage / 100.0))); + if (desiredRequests < 0) + { + _logger.LogDebug("Negative number of publish requests configured."); + desiredRequests = kMinPublishRequestCount; + } + if (_maxPublishRequests.HasValue && desiredRequests > _maxPublishRequests) + { + desiredRequests = _maxPublishRequests.Value; + } + session.MinPublishRequestCount = desiredRequests; + + // Queue requests + for (var i = GoodPublishRequestCount; i < desiredRequests; i++) + { + session.BeginPublish(session.OperationTimeout); + } + } + /// /// Connect client /// @@ -947,8 +1030,6 @@ private async ValueTask TryConnectAsync(CancellationToken ct) "New Session {Name} created with endpoint {EndpointUrl} ({Original}).", _sessionName, endpointUrl, _connection.Endpoint.Url); - _numberOfConnectRetries++; - _logger.LogInformation("Client {Client} CONNECTED to {EndpointUrl}!", this, endpointUrl); return true; @@ -974,6 +1055,11 @@ private void Session_HandlePublishError(ISession session, PublishErrorEventArgs { switch (e.Status.Code) { + case StatusCodes.BadTooManyPublishRequests: + _maxPublishRequests = GoodPublishRequestCount; + _logger.LogDebug("Limit publish requests to {Limit}...", + _maxPublishRequests); + break; case StatusCodes.BadSessionIdInvalid: case StatusCodes.BadSecureChannelClosed: case StatusCodes.BadSessionClosed: @@ -1122,7 +1208,8 @@ private async ValueTask UpdateSessionAsync(ISession session) KeepAliveInterval ?? TimeSpan.FromSeconds(30), OperationTimeout ?? TimeSpan.FromMinutes(1), _serializer, _loggerFactory.CreateLogger(), - Session_HandlePublishError, Session_PublishSequenceNumbersToAcknowledge, + Session_HandlePublishError, + Session_PublishSequenceNumbersToAcknowledge, DisableComplexTypePreloading != true); NotifyConnectivityStateChange(EndpointConnectivityState.Ready); @@ -1588,6 +1675,27 @@ static void NotifyAll(uint seq, ReadValueIdCollection nodesToRead, uint statusCo private readonly PeriodicTimer _timer; } + /// + /// Disconnected state + /// + private sealed class DisconnectState : IOpcUaClientState + { + /// + public int BadPublishRequestCount => 0; + /// + public int GoodPublishRequestCount => 0; + /// + public int OutstandingRequestCount => 0; + /// + public int SubscriptionCount => 0; + /// + public bool IsConnected => false; + /// + public int ReconnectCount => 0; + /// + public int MinPublishRequestCount => 0; + } + /// /// Create observable metrics /// @@ -1605,15 +1713,27 @@ private void InitializeMetrics() _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_ref_count", () => new Measurement(_refCount, _metrics.TagList), "References", "Number of references to this client."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_good_publish_requests_count", + () => new Measurement(GoodPublishRequestCount, + _metrics.TagList), "Requests", "Number of good publish requests."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_bad_publish_requests_count", + () => new Measurement(BadPublishRequestCount, + _metrics.TagList), "Requests", "Number of bad publish requests."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_min_publish_requests_count", + () => new Measurement(MinPublishRequestCount, + _metrics.TagList), "Requests", "Number of min publish requests that should be queued."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_outstanding_requests_count", + () => new Measurement(OutstandingRequestCount, + _metrics.TagList), "Requests", "Number of outstanding requests."); } private static readonly UpDownCounter kSessions = Diagnostics.Meter.CreateUpDownCounter( "iiot_edge_publisher_session_count", "Number of active sessions."); - private OpcUaSession? _session; private OpcUaSession? _reconnectingSession; private int _reconnectRequired; #pragma warning disable CA2213 // Disposable fields should be disposed + private OpcUaSession? _session; private IDisposable? _disconnectLock; #pragma warning restore CA2213 // Disposable fields should be disposed private EndpointConnectivityState _lastState; @@ -1621,6 +1741,7 @@ private void InitializeMetrics() private int _numberOfConnectRetries; private bool _disposed; private int _refCount; + private int? _maxPublishRequests; private readonly object _subscriptionsLock = new(); private readonly ReverseConnectManager? _reverseConnectManager; private readonly ISessionFactory _sessionFactory; @@ -1633,9 +1754,11 @@ private void InitializeMetrics() private readonly ConnectionModel _connection; private readonly IMetricsContext _metrics; private readonly ILogger _logger; +#pragma warning disable CA2213 // Disposable fields should be disposed private readonly SessionReconnectHandler _reconnectHandler; - private readonly TimeSpan _maxReconnectPeriod; private readonly CancellationTokenSource _cts; +#pragma warning restore CA2213 // Disposable fields should be disposed + private readonly TimeSpan _maxReconnectPeriod; private readonly Channel<(ConnectionEvent, object?)> _channel; private readonly EventHandler? _notifier; private readonly Dictionary _samplers = new(); diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs new file mode 100644 index 0000000000..bea516cc6c --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs @@ -0,0 +1,191 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher.Stack.Services +{ + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using System; +#if SHARPPCAP + using SharpPcap; + using SharpPcap.LibPcap; +#endif + using System.Collections.Generic; + using System.Linq; + using System.IO; + using Autofac; + + /// + /// Opc Ua traffic capture capabilities + /// + public sealed class OpcUaClientCapture : IStartable, IDisposable + { + /// + /// Get the devices that can be used to capture + /// + public static IReadOnlyList AvailableDevices + { + get + { +#if SHARPPCAP + try + { + return LibPcapLiveDeviceList.Instance + .Select(d => d.Interface.FriendlyName ?? d.Name) + .ToArray(); + } + catch +#endif + { + return Array.Empty(); + } + } + } + + /// + /// Create capture service + /// + /// + /// + public OpcUaClientCapture(IOptions options, + ILogger logger) + { + _options = options; + _logger = logger; + } + + /// + public void Start() + { + // Find device + var deviceName = _options.Value.CaptureDevice; + if (string.IsNullOrEmpty(deviceName)) + { + return; + } + +#if SHARPPCAP + _logger.LogInformation("Using SharpPcap {Version}", Pcap.SharpPcapVersion); + var device = FindDeviceByName(deviceName); + if (device == null) + { + _logger.LogError("Could not find a capture device with name {Name}! " + + "Not capturing traffic...", deviceName); + return; + } + + _device?.Dispose(); + _device = new CaptureDevice(this, device, _options.Value.CaptureFileName); +#else + _logger.LogWarning("SharpPcap is not included in this build."); +#endif + } + + /// + public void Dispose() + { +#if SHARPPCAP + _device?.Dispose(); + _device = null; +#endif + } + +#if SHARPPCAP + /// + /// Find device by name + /// + /// + /// + private static LibPcapLiveDevice? FindDeviceByName(string deviceName) + { + var device = LibPcapLiveDeviceList.Instance + .FirstOrDefault(d => d.Interface.FriendlyName == deviceName); + if (device == null) + { + device = LibPcapLiveDeviceList.Instance + .FirstOrDefault(d => d.Name == deviceName); + if (device == null && deviceName == "loopback") + { + device = LibPcapLiveDeviceList.Instance + .FirstOrDefault(d => d.Loopback); + } + } + return device; + } + + /// + /// Capture device + /// + private sealed class CaptureDevice : IDisposable + { + /// + /// Create capture device + /// + /// + /// + /// + public CaptureDevice(OpcUaClientCapture outer, + LibPcapLiveDevice device, string? fileName = null) + { + _outer = outer; + _device = device; + + if (string.IsNullOrEmpty(fileName)) + { + fileName = "opcua.pcap"; + } + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + _outer._logger.LogInformation( + "Start capturing {Device} ({Description}) to {FileName}.", + device.Name, device.Description, fileName); + + // Open the device for capturing + _device.Open(mode: DeviceModes.NoCaptureLocal, 1000); + // _device.Filter = "ip and tcp and not port 80 and not port 25"; + + _writer = new CaptureFileWriterDevice(fileName); + _writer.Open(_device); + _device.OnPacketArrival += (_, e) => _writer.Write(e.GetPacket()); + + // Start the capturing process + _device.StartCapture(); + } + + /// + public void Dispose() + { + try + { + _device.StopCapture(); + + _outer._logger.LogInformation( + "Stopped capturing {Device} ({Description}) to file ({Statistics}).", + _device.Name, _device.Description, _device.Statistics.ToString()); + + _writer.Close(); + } + finally + { + _writer.Dispose(); + _device.Dispose(); + } + } + + private readonly LibPcapLiveDevice _device; + private readonly CaptureFileWriterDevice _writer; + private readonly OpcUaClientCapture _outer; + } + + private CaptureDevice? _device; +#endif + private readonly IOptions _options; + private readonly ILogger _logger; + } +} diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs index 110b6430ef..dbc80a528f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs @@ -319,7 +319,7 @@ public async ValueTask DisposeAsync() { try { - await client.Value.DisposeAsync().ConfigureAwait(false); + await client.Value.CloseAsync().ConfigureAwait(false); } catch (OperationCanceledException) { } catch (Exception ex) @@ -559,7 +559,11 @@ private OpcUaClient GetOrAddClient(ConnectionModel connection) CreateSessionTimeout = _options.Value.CreateSessionTimeout, KeepAliveInterval = _options.Value.KeepAliveInterval, SessionTimeout = _options.Value.DefaultSessionTimeout, - LingerTimeout = _options.Value.LingerTimeout + LingerTimeout = _options.Value.LingerTimeout, + + MinPublishRequests = _options.Value.MinPublishRequests, + PublishRequestsPerSubscriptionPercent = + _options.Value.PublishRequestsPerSubscriptionPercent }; _logger.LogInformation("New client {Client} created.", client); return client; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs index a0b475bf28..e937b0e990 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs @@ -212,7 +212,7 @@ public virtual bool RemoveFrom(Subscription subscription, /// public virtual bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action, bool> cb) { if (Item == null) { @@ -659,7 +659,7 @@ public override bool RemoveFrom(Subscription subscription, out bool metadataChan /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action, bool> cb) { return true; } @@ -1229,7 +1229,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action, bool> cb) { var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); @@ -1331,14 +1331,16 @@ private void SendHeartbeatNotifications() Flags = MonitoredItemSourceFlags.Heartbeat, SequenceNumber = 0 }; - callback(MessageType.DeltaFrame, null, heartbeat.YieldReturn()); + callback(MessageType.DeltaFrame, null, heartbeat.YieldReturn(), + (_heartbeatBehavior & HeartbeatBehavior.WatchdogLKVDiagnosticsOnly) + == HeartbeatBehavior.WatchdogLKVDiagnosticsOnly); } private readonly Timer _heartbeatTimer; private TimeSpan _timerInterval; private HeartbeatBehavior _heartbeatBehavior; private TimeSpan _heartbeatInterval; - private Action>? _callback; + private Action, bool>? _callback; private DateTime? _lastValueReceived; } @@ -1459,7 +1461,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action, bool> cb) { // Dont call base implementation as it is not what we want. if (Item == null) @@ -1521,7 +1523,7 @@ private void OnSampledDataValueReceived(uint sequenceNumber, DataValue value) Flags = MonitoredItemSourceFlags.CyclicRead, Value = value }; - callback(MessageType.DeltaFrame, null, notification.YieldReturn()); + callback(MessageType.DeltaFrame, null, notification.YieldReturn(), false); } /// @@ -1544,7 +1546,7 @@ internal DataValue LastSampledValue private readonly ConnectionIdentifier _connection; private readonly IClientSampler _sampler; - private Action>? _callback; + private Action, bool>? _callback; #pragma warning disable CA2213 // Disposable fields should be disposed private IAsyncDisposable? _sampling; #pragma warning restore CA2213 // Disposable fields should be disposed @@ -1706,7 +1708,7 @@ await AddVariableFieldAsync(fields, dataTypes, session, typeSystem, variable, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action, bool> cb) { if (!base.TryCompleteChanges(subscription, ref applyChanges, cb)) { @@ -2370,7 +2372,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action, bool> cb) { var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); if (!AttachedToSubscription || !result) @@ -2518,7 +2520,7 @@ private void SendPendingConditions() foreach (var conditionNotification in notifications) { - callback(MessageType.Condition, DataSetName, conditionNotification); + callback(MessageType.Condition, DataSetName, conditionNotification, false); } } @@ -2546,7 +2548,7 @@ private sealed class ConditionHandlingState = new Dictionary>(); } - private Action>? _callback; + private Action, bool>? _callback; private ConditionHandlingState _conditionHandlingState; private DateTime _lastSentPendingConditions = DateTime.UtcNow; private int _snapshotInterval; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs index 5b26cf2211..73d1916120 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs @@ -87,7 +87,6 @@ public OpcUaSession(ISession session, KeepAliveEventHandler keepAlive, // support transfer Session.DeleteSubscriptionsOnClose = false; Session.TransferSubscriptionsOnReconnect = true; - Session.MinPublishRequestCount = 3; Session.KeepAliveInterval = (int)keepAliveInterval.TotalMilliseconds; Session.OperationTimeout = (int)operationTimeout.TotalMilliseconds; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs index cc50e169ba..fc8c80af51 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs @@ -124,9 +124,9 @@ internal static async ValueTask CreateAsync( } /// - public void OnSubscriptionStateChanged(bool online, int connectionAttempts) + public void OnSubscriptionStateChanged(bool online, IOpcUaClientState state) { - _connectionAttempts = connectionAttempts; + _state = state; _online = online; foreach (var monitoredItem in _currentlyMonitored.Values) @@ -304,6 +304,7 @@ public async ValueTask CloseAsync() // Does not throw await CloseCurrentSubscriptionAsync().ConfigureAwait(false); client?.Dispose(); + _state = OpcUaClient.Disconnected; _lock.Release(); } } @@ -330,8 +331,9 @@ public void Dispose() /// /// /// + /// internal void SendNotification(MessageType messageType, string? dataSetName, - IEnumerable notifications) + IEnumerable notifications, bool diagnosticsOnly) { var subscription = _currentSubscription; if (subscription == null || subscription.Id == 0) @@ -349,10 +351,14 @@ internal void SendNotification(MessageType messageType, string? dataSetName, #pragma warning disable CA2000 // Dispose objects before losing scope var message = CreateMessage(notifications, messageType, dataSetName, subscription); #pragma warning restore CA2000 // Dispose objects before losing scope - onSubscriptionEventChange.Invoke(this, message); + if (!diagnosticsOnly) + { + onSubscriptionEventChange.Invoke(this, message); + } if (message.Notifications.Count > 0 && onSubscriptionEventDiagnosticsChange != null) { - onSubscriptionEventDiagnosticsChange.Invoke(this, (false, message.Notifications.Count)); + onSubscriptionEventDiagnosticsChange.Invoke(this, + (false, message.Notifications.Count)); } } else @@ -366,7 +372,10 @@ internal void SendNotification(MessageType messageType, string? dataSetName, #pragma warning disable CA2000 // Dispose objects before losing scope var message = CreateMessage(notifications, messageType, dataSetName, subscription); #pragma warning restore CA2000 // Dispose objects before losing scope - onSubscriptionDataChange.Invoke(this, message); + if (!diagnosticsOnly) + { + onSubscriptionDataChange.Invoke(this, message); + } if (message.Notifications.Count > 0 && onSubscriptionDataDiagnosticsChange != null) { onSubscriptionDataDiagnosticsChange.Invoke(this, @@ -1064,7 +1073,7 @@ await SynchronizeMonitoredItemsAsync(subscription, handle, var shouldEnable = _currentlyMonitored.Values .Any(m => m.Item != null && m.Item.MonitoringMode != Opc.Ua.MonitoringMode.Disabled); var isEnabled = subscription.PublishingEnabled; - if ((!isEnabled && shouldEnable) || (isEnabled && !shouldEnable)) + if (isEnabled ^ shouldEnable) { await subscription.SetPublishingModeAsync(shouldEnable, ct).ConfigureAwait(false); @@ -2137,23 +2146,38 @@ internal record MetaDataLoaderArguments(TaskCompletionSource? tcs, public void InitializeMetrics() { _meter.CreateObservableCounter("iiot_edge_publisher_missing_keep_alives", - () => new Measurement(NumberOfMissingKeepAlives, _metrics.TagList), - "Keep Alives", "Number of missing keep alives in subscription."); + () => new Measurement(NumberOfMissingKeepAlives, + _metrics.TagList), "Keep Alives", "Number of missing keep alives in subscription."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_good_nodes", - () => new Measurement(NumberOfCreatedItems, _metrics.TagList), - "Monitored items", "Monitored items successfully created."); + () => new Measurement(NumberOfCreatedItems, + _metrics.TagList), "Monitored items", "Monitored items successfully created."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_bad_nodes", - () => new Measurement(NumberOfNotCreatedItems, _metrics.TagList), - "Monitored items", "Monitored items with errors."); + () => new Measurement(NumberOfNotCreatedItems, + _metrics.TagList), "Monitored items", "Monitored items with errors."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_monitored_items", - () => new Measurement(_currentlyMonitored.Count, _metrics.TagList), - "Monitored items", "Monitored item count."); + () => new Measurement(_currentlyMonitored.Count, + _metrics.TagList), "Monitored items", "Monitored item count."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_connection_retries", - () => new Measurement(_connectionAttempts - 1, _metrics.TagList), - "Attempts", "OPC UA connect retries."); + () => new Measurement(_state.ReconnectCount, + _metrics.TagList), "Attempts", "OPC UA connect retries."); _meter.CreateObservableGauge("iiot_edge_publisher_is_connection_ok", - () => new Measurement(_online && !_closed ? 1 : 0, _metrics.TagList), - "", "OPC UA connection success flag."); + () => new Measurement(_online && !_closed ? 1 : 0, + _metrics.TagList), "Online", "OPC UA connection success flag."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_publish_requests_per_subscription", + () => new Measurement(Ratio(_state.OutstandingRequestCount, _state.SubscriptionCount), + _metrics.TagList), "Requests per Subscription", "Good publish requests per subsciption."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_good_publish_requests_per_subscription", + () => new Measurement(Ratio(_state.GoodPublishRequestCount, _state.SubscriptionCount), + _metrics.TagList), "Requests per Subscription", "Good publish requests per subsciption."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_bad_publish_requests_per_subscription", + () => new Measurement(Ratio(_state.BadPublishRequestCount, _state.SubscriptionCount), + _metrics.TagList), "Requests per Subscription", "Bad publish requests per subsciption."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_min_publish_requests_per_subscription", + () => new Measurement(Ratio(_state.MinPublishRequestCount, _state.SubscriptionCount), + _metrics.TagList), "Requests per Subscription", "Min publish requests queued per subsciption."); + + static double Ratio(int value, int count) => count == 0 ? 0.0 : (double)value / count; } private static readonly TimeSpan kDefaultErrorRetryDelay = TimeSpan.FromSeconds(2); @@ -2162,8 +2186,8 @@ public void InitializeMetrics() #pragma warning disable CA2213 // Disposable fields should be disposed private Subscription? _currentSubscription; #pragma warning restore CA2213 // Disposable fields should be disposed + private IOpcUaClientState _state = OpcUaClient.Disconnected; private bool _online; - private long _connectionAttempts; private uint _previousSequenceNumber; private bool _useDeferredAcknoledge; private uint _sequenceNumber; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs index 65ca0938f0..0f110b27eb 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesProvider.cs @@ -106,7 +106,7 @@ public string ReadContent() } catch (Exception e) { - _logger.LogError(e, "Failed to read content of published nodes file from \"{Path}\"", + _logger.LogDebug(e, "Failed to read content of published nodes file from \"{Path}\"", _fileName); throw; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublisherConfigServicesTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublisherConfigServicesTests.cs index b4f7365124..f525e30cba 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublisherConfigServicesTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/PublisherConfigServicesTests.cs @@ -112,7 +112,7 @@ private PublisherConfigurationService InitPublisherConfigService() public async Task Legacy25PublishedNodesFile(string publishedNodesFile) { Utils.CopyContent(publishedNodesFile, _tempFile); - using (var configService = InitPublisherConfigService()) + await using (var configService = InitPublisherConfigService()) { var endpoints = await configService.GetConfiguredEndpointsAsync(); Assert.Single(endpoints); @@ -150,7 +150,7 @@ public async Task Legacy25PublishedNodesFile(string publishedNodesFile) Assert.Equal("nsu=http://microsoft.com/Opc/OpcPlc/;s=FastUInt2", nodes[1].Id); } // Simulate restart. - using (var configService = InitPublisherConfigService()) + await using (var configService = InitPublisherConfigService()) { // We should get the same endpoint and nodes after restart. var endpoints = await configService.GetConfiguredEndpointsAsync(); @@ -177,7 +177,7 @@ public async Task Legacy25PublishedNodesFile(string publishedNodesFile) public async Task Legacy25PublishedNodesFileError(string publishedNodesFile) { Utils.CopyContent(publishedNodesFile, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); // Transformation of published nodes entries should throw a serialization error since // Engine/pn_2.5_legacy_error.json contains both NodeId and OpcNodes. @@ -199,7 +199,7 @@ public void TestPnJsonWithMultipleJobsExpectDifferentJobIds(string publishedNode [Fact] public async Task TestSerializableExceptionResponse() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var exceptionResponse = "Response 400 null request is provided"; @@ -261,7 +261,7 @@ await FluentActions [Fact] public async Task TestPublishNodesNullOrEmpty() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); // Check null request. await FluentActions @@ -304,7 +304,7 @@ await FluentActions [Fact] public async Task TestUnpublishNodesNullRequest() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); // Check null request. await FluentActions @@ -326,7 +326,7 @@ public async Task TestUnpublishNodesNullOrEmptyOpcNodes( bool useEmptyOpcNodes, bool customEndpoint) { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); const int numberOfEndpoints = 3; var opcNodes = Enumerable.Range(0, numberOfEndpoints) @@ -370,7 +370,7 @@ await FluentActions [Fact] public async Task TestGetConfiguredNodesOnEndpointNullRequest() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); // Check call with null. await FluentActions @@ -386,7 +386,7 @@ await FluentActions [Fact] public async Task TestAddOrUpdateEndpointsNullRequest() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); // Check call with null. await FluentActions @@ -402,7 +402,7 @@ await FluentActions [Fact] public async Task TestAddOrUpdateEndpointsMultipleEndpointEntries() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); const int numberOfEndpoints = 3; var opcNodes = Enumerable.Range(0, numberOfEndpoints) @@ -435,7 +435,7 @@ await FluentActions [Fact] public async Task TestAddOrUpdateEndpointsMultipleEndpointEntriesTimesapn() { - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); const int numberOfEndpoints = 3; var opcNodes = Enumerable.Range(0, numberOfEndpoints) @@ -478,7 +478,7 @@ bool useDataSetSpecificEndpoints { _options.Value.MaxNodesPerDataSet = 2; - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); Assert.Empty(_publisher.WriterGroups); @@ -528,7 +528,7 @@ bool useDataSetSpecificEndpoints public async Task TestAddOrUpdateEndpointsRemoveEndpoints(string publishedNodesFile) { Utils.CopyContent(publishedNodesFile, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = Utils.GetFileContent(publishedNodesFile); var payloadRequests = _newtonSoftJsonSerializer.Deserialize>(payload); @@ -578,7 +578,7 @@ public async Task TestAddOrUpdateEndpointsAddAndRemove() { _options.Value.MaxNodesPerDataSet = 2; - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); Assert.Empty(_publisher.WriterGroups); @@ -688,7 +688,7 @@ await AssertGetConfiguredNodesOnEndpointThrows(configService, endpoints[3]) public async Task TestInitStandaloneJobOrchestratorFromEmptyOpcNodes(string publishedNodesFile) { Utils.CopyContent(publishedNodesFile, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); // Engine/empty_opc_nodes.json contains entries with null or empty OpcNodes. // Those entries should not result in any endpoint entries in publisherConfigurationService. @@ -706,7 +706,7 @@ public async Task TestInitStandaloneJobOrchestratorFromEmptyOpcNodes(string publ public async Task OptionalFieldsPublishedNodesFile(string publishedNodesFile) { Utils.CopyContent(publishedNodesFile, _tempFile); - using (var configService = InitPublisherConfigService()) + await using (var configService = InitPublisherConfigService()) { var endpoints = await configService.GetConfiguredEndpointsAsync(); Assert.Equal(2, endpoints.Count); @@ -749,7 +749,7 @@ public async Task OptionalFieldsPublishedNodesFile(string publishedNodesFile) Assert.Equal("nsu=http://microsoft.com/Opc/OpcPlc/;s=FastUInt2", nodes[1].Id); } // Simulate restart. - using (var configService = InitPublisherConfigService()) + await using (var configService = InitPublisherConfigService()) { // We should get the same endpoint and nodes after restart. var endpoints = await configService.GetConfiguredEndpointsAsync(); @@ -779,7 +779,7 @@ public async Task OptionalFieldsPublishedNodesFile(string publishedNodesFile) public async Task PublishNodesOnEmptyConfiguration(string publishedNodesFile) { Utils.CopyContent("Publisher/empty_pn.json", _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = Utils.GetFileContent(publishedNodesFile); foreach (var request in _newtonSoftJsonSerializer.Deserialize>(payload)) @@ -805,7 +805,7 @@ await FluentActions public async Task PublishNodesOnExistingConfiguration(string existingConfig, string newConfig) { Utils.CopyContent(existingConfig, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = Utils.GetFileContent(newConfig); foreach (var request in _newtonSoftJsonSerializer.Deserialize>(payload)) @@ -830,7 +830,7 @@ await FluentActions public async Task PublishNodesOnNewConfiguration(string existingConfig, string newConfig) { Utils.CopyContent(existingConfig, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = Utils.GetFileContent(newConfig); foreach (var request in _newtonSoftJsonSerializer.Deserialize>(payload)) @@ -855,7 +855,7 @@ await FluentActions public async Task UnpublishNodesOnExistingConfiguration(string publishedNodesFile) { Utils.CopyContent(publishedNodesFile, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = Utils.GetFileContent(publishedNodesFile); foreach (var request in _newtonSoftJsonSerializer.Deserialize>(payload)) @@ -880,7 +880,7 @@ await FluentActions public async Task UnpublishNodesOnNonExistingConfiguration(string existingConfig, string newConfig) { Utils.CopyContent(existingConfig, _tempFile); - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = Utils.GetFileContent(newConfig); foreach (var request in _newtonSoftJsonSerializer.Deserialize>(payload)) @@ -903,11 +903,11 @@ await FluentActions // [InlineData(100, 1000)] public async Task PublishNodesStressTest(int numberOfEndpoints, int numberOfNodes) { - using (var fileStream = new FileStream(_tempFile, FileMode.Open, FileAccess.Write)) + await using (var fileStream = new FileStream(_tempFile, FileMode.Open, FileAccess.Write)) { fileStream.Write(Encoding.UTF8.GetBytes("[]")); } - using var configService = InitPublisherConfigService(); + await using var configService = InitPublisherConfigService(); var payload = new List(); for (var endpointIndex = 0; endpointIndex < numberOfEndpoints; ++endpointIndex) diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs index 084bd5acb5..21ae080dd7 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs @@ -23,11 +23,11 @@ public class OpcUaApplicationTests [Fact] public async Task GetApplicationCertificateTest1Async() { - using var bootstrap = Build(); + await using var bootstrap = Build(); var oldCerts = bootstrap.Resolve(); await CleanAsync(oldCerts, CertificateStoreName.Application); - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.Application, true); var own = Assert.Single(certificates); @@ -37,7 +37,7 @@ public async Task GetApplicationCertificateTest1Async() [Fact] public async Task GetApplicationCertificateTest2Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Application); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.Application); @@ -56,7 +56,7 @@ await certs.AddCertificateAsync(CertificateStoreName.Application, [Fact] public async Task GetTrustedCertificatesTest1Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Trusted); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.Trusted); @@ -79,7 +79,7 @@ await certs.AddCertificateAsync(CertificateStoreName.Trusted, [Fact] public async Task GetTrustedCertificatesTest2Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Trusted); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.Trusted); @@ -100,7 +100,7 @@ public async Task GetTrustedCertificatesTest2Async() [Fact] public async Task GetTrustedCertificatesTest3Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Trusted); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.Trusted); @@ -127,7 +127,7 @@ await certs.AddCertificateAsync(CertificateStoreName.Trusted, [Fact] public async Task GetTrustedCertificatesTest4Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Trusted); await CleanAsync(certs, CertificateStoreName.Issuer); @@ -154,7 +154,7 @@ public async Task GetTrustedCertificatesTest4Async() [Fact] public async Task GetTrustedHttpsCertificatesTestAsync() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Https); await CleanAsync(certs, CertificateStoreName.HttpsIssuer); @@ -181,7 +181,7 @@ public async Task GetTrustedHttpsCertificatesTestAsync() [Fact] public async Task ApproveRejectedCertificateTestAsync() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.Trusted); await CleanAsync(certs, CertificateStoreName.Rejected); @@ -211,7 +211,7 @@ await certs.AddCertificateAsync(CertificateStoreName.Rejected, [Fact] public async Task ApproveRejectedCertificateNotFoundTestAsync() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); using var rejectedCert = CreateRSACertificate("test1"); await Assert.ThrowsAsync( @@ -221,7 +221,7 @@ await Assert.ThrowsAsync( [Fact] public async Task RemoveCertificateNotFoundTestAsync() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); using var rejectedCert = CreateRSACertificate("test1"); await Assert.ThrowsAsync( @@ -232,7 +232,7 @@ await Assert.ThrowsAsync( [Fact] public async Task GetUserCertificateTest1Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.User); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.User); @@ -278,7 +278,7 @@ await certs.AddCertificateAsync(CertificateStoreName.User, [Fact] public async Task GetUserCertificateTest2Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.User); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.User); @@ -323,7 +323,7 @@ await certs.AddCertificateAsync(CertificateStoreName.User, [Fact] public async Task GetUserCertificateTest3Async() { - using var container = Build(); + await using var container = Build(); var certs = container.Resolve(); await CleanAsync(certs, CertificateStoreName.User); var certificates = await certs.ListCertificatesAsync(CertificateStoreName.User); diff --git a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj index 8562b7a0aa..73bdc6bde3 100644 --- a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj +++ b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj index 3f8bbee17a..dbb074154f 100644 --- a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj +++ b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/tools/e2etesting/DeployPLCs.ps1 b/tools/e2etesting/DeployPLCs.ps1 index 54ec7d4703..40844a5a5b 100644 --- a/tools/e2etesting/DeployPLCs.ps1 +++ b/tools/e2etesting/DeployPLCs.ps1 @@ -33,7 +33,7 @@ Param( $ErrorActionPreference = "Stop" if (!$PLCImage) { - $PLCImage = "mcr.microsoft.com/iotedge/opc-plc:latest" + $PLCImage = "mcr.microsoft.com/iotedge/opc-plc:2.9.10" } if (!$ResourceGroupName) { diff --git a/tools/e2etesting/K8s-Standalone/opcplc/deployment.yaml b/tools/e2etesting/K8s-Standalone/opcplc/deployment.yaml index 7ebcc51c21..334f92bf79 100644 --- a/tools/e2etesting/K8s-Standalone/opcplc/deployment.yaml +++ b/tools/e2etesting/K8s-Standalone/opcplc/deployment.yaml @@ -14,7 +14,7 @@ spec: spec: containers: - name: opcplc - image: mcr.microsoft.com/iotedge/opc-plc:2.2.0 + image: mcr.microsoft.com/iotedge/opc-plc:2.9.10 ports: - containerPort: 50000 - containerPort: 8080 diff --git a/tools/e2etesting/NestedEdge/scripts/import_acr.sh b/tools/e2etesting/NestedEdge/scripts/import_acr.sh index 92e87cf3ff..e671534cd7 100644 --- a/tools/e2etesting/NestedEdge/scripts/import_acr.sh +++ b/tools/e2etesting/NestedEdge/scripts/import_acr.sh @@ -92,7 +92,7 @@ az acr import --name $acrName --force --source mcr.microsoft.com/azuremonitor/co echo "API proxy..." az acr import --name $acrName --force --source mcr.microsoft.com/azureiotedge-api-proxy:latest --image azureiotedge-api-proxy:latest echo "IIoT - OpcPlc server ..." -az acr import --name $acrName --force --source mcr.microsoft.com/iotedge/opc-plc:latest --image opc-plc:latest +az acr import --name $acrName --force --source mcr.microsoft.com/iotedge/opc-plc:latest --image opc-plc:2.9.10 echo "IIoT - OPC Publisher..." az acr import --name $acrName --force --source mcr.microsoft.com/iotedge/opc-publisher:2.8.4 --image opc-publisher:2.8.4 az acr import --name $acrName --force --source mcr.microsoft.com/iotedge/opc-publisher:2.8.4 --image iotedge/opc-publisher:2.8.4 diff --git a/tools/scripts/publish.ps1 b/tools/scripts/publish.ps1 index 52ee0e0a86..d86c744783 100644 --- a/tools/scripts/publish.ps1 +++ b/tools/scripts/publish.ps1 @@ -76,15 +76,14 @@ Get-ChildItem $Path -Filter *.csproj -Recurse | ForEach-Object { } $baseImage = $($properties.ContainerBaseImage -split "-")[0] + $runtimeId = "$($script:Os)-$($script:Arch)" # see architecture tags e.g., here https://hub.docker.com/_/microsoft-dotnet-aspnet if ($script:Arch -eq "x64") { $baseImage = "$($baseImage)-cbl-mariner-distroless-amd64" - $runtimeId = "$($script:Os)-$($script:Arch)" } if ($script:Arch -eq "arm64") { $baseImage = "$($baseImage)-cbl-mariner-distroless-arm64v8" - $runtimeId = "$($script:Os)-$($script:Arch)" } if ($script:Arch -eq "arm") { $baseImage = "$($baseImage)-alpine-arm32v7"