mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-23 02:00:00 +08:00
a81d981755
# Description
Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).
Six commits, applied bottom-up in safety order so each batch leaves the
build green:
1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
197 hits across ~30 files; pure mechanical cleanup.
2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.
3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
Roslynator formatting (RCS0xxx) blank-line and punctuation rules.
4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.
5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.
6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
- Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.
### What this PR does **not** touch
- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.
## Related Issues
_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
---------
Co-authored-by: agent <agent@local>
851 lines
32 KiB
C#
851 lines
32 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Opc.Ua.Client.Subscriptions.MonitoredItems
|
|
{
|
|
/// <summary>
|
|
/// A monitored item that can be extended to add extra
|
|
/// information as context in the subscription.
|
|
/// </summary>
|
|
internal abstract class MonitoredItem : IMonitoredItem, IAsyncDisposable
|
|
{
|
|
/// <inheritdoc/>
|
|
public string Name { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public uint Order => m_currentOptions?.Order ?? 0u;
|
|
|
|
/// <inheritdoc/>
|
|
public uint ServerId { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public bool Created => ServerId != 0;
|
|
|
|
/// <inheritdoc/>
|
|
public ServiceResult Error { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public MonitoringFilterResult? FilterResult { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public MonitoringMode CurrentMonitoringMode { get; internal set; }
|
|
|
|
/// <inheritdoc/>
|
|
public TimeSpan CurrentSamplingInterval { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public uint CurrentQueueSize { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public uint ClientHandle { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Client handle of the item that triggers this item, or
|
|
/// <c>0</c> if no triggering relationship has been set. Used
|
|
/// by the snapshot path and by the <see cref="TriggeringItem"/>
|
|
/// public projection which resolves the handle to the live
|
|
/// sibling via <see cref="IMonitoredItemContext"/>.
|
|
/// </summary>
|
|
internal uint TriggeringItemClientHandle { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
public IMonitoredItem? TriggeringItem
|
|
{
|
|
get
|
|
{
|
|
uint handle = TriggeringItemClientHandle;
|
|
if (handle == 0)
|
|
{
|
|
return null;
|
|
}
|
|
return Context.TryGetMonitoredItemByClientHandle(
|
|
handle, out IMonitoredItem? item) ? item : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply server-side identifiers from a saved snapshot to this
|
|
/// freshly-created monitored item. Used by
|
|
/// <see cref="ISubscriptionManager.LoadAsync"/> when the caller
|
|
/// requests <c>transferSubscriptions</c> so the loaded item can
|
|
/// be matched to its server-side state via
|
|
/// <c>TransferSubscriptions</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Re-assigning <see cref="ClientHandle"/> after construction
|
|
/// matters because the V2 publish loop dispatches notifications
|
|
/// by client handle. The snapshot's client handle is the one the
|
|
/// server still uses; the post-construction one is freshly
|
|
/// generated and would never match an incoming notification.
|
|
/// </remarks>
|
|
internal void ApplyTransferState(uint clientHandle, uint serverId)
|
|
{
|
|
ClientHandle = clientHandle;
|
|
ServerId = serverId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Install fully-loaded state from a snapshot during V2
|
|
/// transfer-on-load (<see cref="ISubscriptionManager.LoadAsync"/>
|
|
/// with <c>transferSubscriptions: true</c>). Unlike
|
|
/// <see cref="ApplyTransferState"/>, this also:
|
|
/// <list type="bullet">
|
|
/// <item><description>Abandons the create-or-modify
|
|
/// <see cref="Change"/> the ctor's <see cref="QueuePendingChanges"/>
|
|
/// just queued, so the V2 state machine does not issue a
|
|
/// redundant <c>CreateMonitoredItems</c> request whose
|
|
/// <c>ClientHandle</c> would mismatch the snapshot's
|
|
/// (the source of the <c>Debug.Assert</c> failure that
|
|
/// motivated this work).</description></item>
|
|
/// <item><description>Re-installs the saved triggering links
|
|
/// so a subsequent <c>SetTriggering</c> replay reflects the
|
|
/// snapshot.</description></item>
|
|
/// </list>
|
|
/// The caller is responsible for binding the (possibly
|
|
/// freshly-revised) server-side state via the
|
|
/// <c>TransferSubscriptions</c> / <c>GetMonitoredItems</c> path
|
|
/// that runs in <see cref="MonitoredItemManager.TrySynchronizeHandlesAsync"/>.
|
|
/// </summary>
|
|
internal void ApplyLoadState(MonitoredItemLoadState state)
|
|
{
|
|
// Abandon any pending changes queued during the ctor.
|
|
while (TryGetPendingChange(out Change? change))
|
|
{
|
|
change.Abandon();
|
|
}
|
|
// Snapshot the current options so the V2 state machine
|
|
// treats the item as "configured to the loaded values" and
|
|
// skips the create path until a real change arrives.
|
|
m_currentOptions = m_options.CurrentValue;
|
|
ClientHandle = state.ClientHandle;
|
|
ServerId = state.ServerId;
|
|
TriggeringItemClientHandle = state.TriggeringItemClientHandle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The subscription that owns the monitored item.
|
|
/// </summary>
|
|
protected IMonitoredItemContext Context { get; }
|
|
|
|
/// <summary>
|
|
/// Current monitored item options
|
|
/// </summary>
|
|
internal IOptionsMonitor<MonitoredItemOptions> Options
|
|
{
|
|
get => m_options;
|
|
set
|
|
{
|
|
if (m_options != value)
|
|
{
|
|
m_options = value;
|
|
QueuePendingChanges(m_options.CurrentValue, m_currentOptions);
|
|
m_changeTracking?.Dispose();
|
|
m_changeTracking = m_options.OnChange(
|
|
(o, _) => OnOptionsChanged(o));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create monitored item
|
|
/// </summary>
|
|
/// <param name="context"></param>
|
|
/// <param name="name"></param>
|
|
/// <param name="options"></param>
|
|
/// <param name="logger"></param>
|
|
protected MonitoredItem(IMonitoredItemContext context, string name,
|
|
IOptionsMonitor<MonitoredItemOptions> options, ILogger logger)
|
|
{
|
|
Context = context;
|
|
Name = name;
|
|
Error = ServiceResult.Good;
|
|
ClientHandle = Utils.IncrementIdentifier(ref GlobalClientHandleUint);
|
|
|
|
m_logger = logger;
|
|
m_options = Options = options;
|
|
m_logger.LogDebug("{Item} CREATED.", this);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
return DisposeAsync(disposing: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Capture an immutable snapshot of this item's configuration
|
|
/// + identifiers + triggering state.
|
|
/// </summary>
|
|
public MonitoredItemStateSnapshot Snapshot()
|
|
{
|
|
return MonitoredItemStateSnapshot.AsOptions(
|
|
Name,
|
|
m_options.CurrentValue,
|
|
ClientHandle,
|
|
ServerId,
|
|
TriggeringItemClientHandle);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask ConditionRefreshAsync(CancellationToken ct = default)
|
|
{
|
|
if (!Created)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadMonitoredItemIdInvalid,
|
|
"Monitored item has not been created on the server.");
|
|
}
|
|
return Context.ConditionRefreshAsync(ServerId, ct);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override string? ToString()
|
|
{
|
|
StringBuilder sb = new StringBuilder()
|
|
.Append(Context)
|
|
.Append('#')
|
|
.Append(ClientHandle)
|
|
.Append('|')
|
|
.Append(ServerId)
|
|
.Append(" (")
|
|
.Append(Name)
|
|
.Append(')');
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose monitored item
|
|
/// </summary>
|
|
/// <param name="disposing"></param>
|
|
protected virtual ValueTask DisposeAsync(bool disposing)
|
|
{
|
|
if (disposing && !m_disposedValue)
|
|
{
|
|
while (TryGetPendingChange(out Change? change))
|
|
{
|
|
change.Abandon();
|
|
}
|
|
|
|
Context.NotifyItemChange(this, true);
|
|
m_logger.LogDebug("{Item} REMOVED.", this);
|
|
|
|
ServerId = 0;
|
|
m_changeTracking?.Dispose();
|
|
m_disposedValue = true;
|
|
}
|
|
return default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the subscription state changed
|
|
/// </summary>
|
|
/// <param name="state"></param>
|
|
/// <param name="publishingInterval"></param>
|
|
protected internal virtual void OnSubscriptionStateChange(
|
|
SubscriptionState state, TimeSpan publishingInterval)
|
|
{
|
|
MonitoredItemOptions? options = m_currentOptions;
|
|
if (options == null ||
|
|
(state != SubscriptionState.Created &&
|
|
state != SubscriptionState.Modified))
|
|
{
|
|
return;
|
|
}
|
|
uint queueSize = options.QueueSize;
|
|
if (!options.AutoSetQueueSize)
|
|
{
|
|
return;
|
|
}
|
|
if (publishingInterval == TimeSpan.Zero)
|
|
{
|
|
return;
|
|
}
|
|
TimeSpan samplingInterval = CurrentSamplingInterval;
|
|
if (samplingInterval == TimeSpan.Zero)
|
|
{
|
|
samplingInterval = options.SamplingInterval;
|
|
}
|
|
if (samplingInterval <= TimeSpan.Zero)
|
|
{
|
|
return;
|
|
}
|
|
queueSize = Math.Max(queueSize, (uint)Math.Ceiling(
|
|
publishingInterval.TotalMilliseconds / samplingInterval.TotalMilliseconds)) +
|
|
1;
|
|
if (queueSize == options.QueueSize)
|
|
{
|
|
return;
|
|
}
|
|
OnOptionsChanged(options with { QueueSize = queueSize });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the options change
|
|
/// </summary>
|
|
/// <param name="options"></param>
|
|
protected virtual void OnOptionsChanged(MonitoredItemOptions options)
|
|
{
|
|
QueuePendingChanges(options, m_currentOptions);
|
|
Context.NotifyItemChange(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify subscription that the subscription manager has paused or
|
|
/// resumed operations.
|
|
/// </summary>
|
|
/// <param name="paused"></param>
|
|
protected internal virtual void NotifySubscriptionManagerPaused(bool paused)
|
|
{
|
|
// empty
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current pending change in the change list. The change list
|
|
/// collects the changes to be made to the monitored item while the
|
|
/// subscription is applying state changes.
|
|
/// </summary>
|
|
/// <param name="change"></param>
|
|
/// <returns></returns>
|
|
internal bool TryGetPendingChange([NotNullWhen(true)] out Change? change)
|
|
{
|
|
return m_pendingChanges.TryPeek(out change);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the object with the results of a transfer subscription request.
|
|
/// </summary>
|
|
/// <param name="clientHandle"></param>
|
|
/// <param name="serverHandle"></param>
|
|
/// <exception cref="ObjectDisposedException"></exception>
|
|
internal void SetTransferResult(uint clientHandle, uint serverHandle)
|
|
{
|
|
if (m_disposedValue)
|
|
{
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
}
|
|
|
|
// ensure the global counter is not duplicating future handle ids
|
|
if (clientHandle != ClientHandle)
|
|
{
|
|
m_logger.LogInformation("{Item}: UPDATE CLIENT ID from {Old} to {New}.",
|
|
this, ClientHandle, clientHandle);
|
|
|
|
ClientHandle = clientHandle;
|
|
|
|
Utils.SetIdentifierToAtLeast(ref GlobalClientHandleUint, clientHandle);
|
|
}
|
|
if (serverHandle != ServerId)
|
|
{
|
|
m_logger.LogInformation("{Item}: UPDATE SERVER ID from {Old} to {New}.",
|
|
this, ServerId, serverHandle);
|
|
|
|
ServerId = serverHandle;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset the monitored item to its initial state for recreation
|
|
/// on server side.
|
|
/// </summary>
|
|
/// <exception cref="ObjectDisposedException"></exception>
|
|
internal void Reset()
|
|
{
|
|
if (m_disposedValue)
|
|
{
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
}
|
|
|
|
m_logger.LogDebug("{Item}: RESET.", this);
|
|
ServerId = 0;
|
|
|
|
MonitoredItemOptions? options = m_currentOptions;
|
|
while (TryGetPendingChange(out Change? change))
|
|
{
|
|
change.Abandon();
|
|
options = change.Options;
|
|
}
|
|
m_currentOptions = null;
|
|
if (options == null)
|
|
{
|
|
return;
|
|
}
|
|
QueuePendingChanges(options, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Queues changes into the change queue for the item
|
|
/// </summary>
|
|
/// <param name="options"></param>
|
|
/// <param name="currentOptions"></param>
|
|
private void QueuePendingChanges(MonitoredItemOptions options,
|
|
MonitoredItemOptions? currentOptions)
|
|
{
|
|
if (currentOptions != null && currentOptions == options)
|
|
{
|
|
// No changes
|
|
Context.NotifyItemChangeResult(this, 0, options, new ServiceResult(
|
|
StatusCodes.BadNothingToDo), true, null);
|
|
return;
|
|
}
|
|
|
|
if (options.StartNodeId.IsNull)
|
|
{
|
|
// Not valid
|
|
Context.NotifyItemChangeResult(this, 0, options, new ServiceResult(
|
|
StatusCodes.BadNodeIdInvalid), true, null);
|
|
return;
|
|
}
|
|
m_currentOptions = options;
|
|
m_pendingChanges.Enqueue(new Change(this, options, currentOptions));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Complete a change
|
|
/// </summary>
|
|
/// <param name="change"></param>
|
|
/// <returns></returns>
|
|
internal bool CompleteChange(Change change)
|
|
{
|
|
return m_pendingChanges.TryDequeue(out Change? completed) &&
|
|
change == completed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change steps to apply to the monitored items in the
|
|
/// subscription context. The change list is a queue of
|
|
/// steps to perform inside the subscription.
|
|
/// see Subscription.ApplyMonitoredItemChangesAsync for
|
|
/// more information
|
|
/// </summary>
|
|
internal sealed class Change
|
|
{
|
|
/// <summary>
|
|
/// The item on which the changes are applied
|
|
/// </summary>
|
|
public MonitoredItem Item { get; }
|
|
|
|
/// <summary>
|
|
/// Timestamps to return
|
|
/// </summary>
|
|
public TimestampsToReturn Timestamps => Options.TimestampsToReturn;
|
|
|
|
/// <summary>
|
|
/// Create request if the item is not yet created
|
|
/// </summary>
|
|
public MonitoredItemCreateRequest? Create { get; }
|
|
|
|
/// <summary>
|
|
/// Modification request if the item is already created
|
|
/// </summary>
|
|
public MonitoredItemModifyRequest? Modify { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Monitoring mode change pending
|
|
/// </summary>
|
|
public MonitoringMode? MonitoringModeChange { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Force recreating the item due to changes in the
|
|
/// read item information.
|
|
/// </summary>
|
|
public bool ForceRecreate { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Current retry count of this change
|
|
/// </summary>
|
|
public int RetryCount { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Options that are the source of the change
|
|
/// </summary>
|
|
public MonitoredItemOptions Options { get; }
|
|
|
|
/// <summary>
|
|
/// Create change
|
|
/// </summary>
|
|
/// <param name="item"></param>
|
|
/// <param name="options"></param>
|
|
/// <param name="currentOptions"></param>
|
|
public Change(MonitoredItem item, MonitoredItemOptions options,
|
|
MonitoredItemOptions? currentOptions)
|
|
{
|
|
Debug.Assert(!options.StartNodeId.IsNull);
|
|
Options = options;
|
|
Item = item;
|
|
|
|
var parameters = new MonitoringParameters
|
|
{
|
|
ClientHandle = item.ClientHandle,
|
|
SamplingInterval = (int)options.SamplingInterval.TotalMilliseconds,
|
|
QueueSize = options.QueueSize,
|
|
DiscardOldest = options.DiscardOldest,
|
|
Filter = options.Filter != null ?
|
|
new ExtensionObject(options.Filter) : default
|
|
};
|
|
|
|
Create = new MonitoredItemCreateRequest
|
|
{
|
|
ItemToMonitor = new ReadValueId
|
|
{
|
|
NodeId = options.StartNodeId,
|
|
AttributeId = options.AttributeId,
|
|
IndexRange = options.IndexRange,
|
|
DataEncoding = options.Encoding ?? QualifiedName.Null
|
|
},
|
|
MonitoringMode = options.MonitoringMode,
|
|
RequestedParameters = parameters
|
|
};
|
|
|
|
if (currentOptions == null ||
|
|
currentOptions.StartNodeId != options.StartNodeId ||
|
|
currentOptions.AttributeId != options.AttributeId ||
|
|
currentOptions.IndexRange != options.IndexRange ||
|
|
currentOptions.Encoding != options.Encoding)
|
|
{
|
|
Modify = null;
|
|
MonitoringModeChange = null;
|
|
ForceRecreate = true;
|
|
}
|
|
else
|
|
{
|
|
Modify = new MonitoredItemModifyRequest
|
|
{
|
|
MonitoredItemId = item.ServerId,
|
|
RequestedParameters = parameters
|
|
};
|
|
|
|
if (currentOptions.MonitoringMode != options.MonitoringMode)
|
|
{
|
|
MonitoringModeChange = options.MonitoringMode;
|
|
|
|
// If only monitoring mode changed, no need to modify
|
|
if (currentOptions with
|
|
{
|
|
MonitoringMode = MonitoringModeChange.Value
|
|
} == options)
|
|
{
|
|
Modify = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MonitoringModeChange = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the object with the results of a create monitored item request.
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="result"></param>
|
|
/// <param name="index"></param>
|
|
/// <param name="diagnosticInfos"></param>
|
|
/// <param name="responseHeader"></param>
|
|
internal void SetCreateResult(MonitoredItemCreateRequest request,
|
|
MonitoredItemCreateResult result, int index,
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos, ResponseHeader responseHeader)
|
|
{
|
|
Debug.Assert(request.RequestedParameters.ClientHandle == Item.ClientHandle);
|
|
ServiceResult error = ServiceResult.Good;
|
|
|
|
if (StatusCode.IsBad(result.StatusCode))
|
|
{
|
|
error = ClientBase.GetResult(result.StatusCode, index,
|
|
diagnosticInfos, responseHeader);
|
|
}
|
|
|
|
Item.CurrentMonitoringMode = request.MonitoringMode;
|
|
Item.CurrentSamplingInterval = TimeSpan.FromMilliseconds(
|
|
request.RequestedParameters.SamplingInterval);
|
|
Item.CurrentQueueSize = request.RequestedParameters.QueueSize;
|
|
|
|
if (ServiceResult.IsGood(error))
|
|
{
|
|
Item.ServerId = result.MonitoredItemId;
|
|
Item.CurrentSamplingInterval =
|
|
TimeSpan.FromMilliseconds(result.RevisedSamplingInterval);
|
|
Item.CurrentQueueSize = result.RevisedQueueSize;
|
|
|
|
Item.LogRevisedSamplingRateAndQueueSize(Options, true);
|
|
Notify(error, true, result.FilterResult);
|
|
return;
|
|
}
|
|
|
|
// Declare final if not communication error which includes success
|
|
RetryCount++;
|
|
Notify(error, false, result.FilterResult);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the object with the results of a modify monitored item request.
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="result"></param>
|
|
/// <param name="index"></param>
|
|
/// <param name="diagnosticInfos"></param>
|
|
/// <param name="responseHeader"></param>
|
|
internal void SetModifyResult(MonitoredItemModifyRequest request,
|
|
MonitoredItemModifyResult result, int index,
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos, ResponseHeader responseHeader)
|
|
{
|
|
Debug.Assert(request.RequestedParameters.ClientHandle == Item.ClientHandle);
|
|
ServiceResult error = ServiceResult.Good;
|
|
if (StatusCode.IsBad(result.StatusCode))
|
|
{
|
|
error = ClientBase.GetResult(result.StatusCode, index,
|
|
diagnosticInfos, responseHeader);
|
|
}
|
|
|
|
if (ServiceResult.IsGood(error))
|
|
{
|
|
Item.CurrentSamplingInterval = TimeSpan.FromMilliseconds(
|
|
request.RequestedParameters.SamplingInterval);
|
|
Item.CurrentQueueSize = request.RequestedParameters.QueueSize;
|
|
|
|
Item.CurrentSamplingInterval = TimeSpan.FromMilliseconds(
|
|
result.RevisedSamplingInterval);
|
|
Item.CurrentQueueSize = result.RevisedQueueSize;
|
|
|
|
if (MonitoringModeChange == null)
|
|
{
|
|
Item.LogRevisedSamplingRateAndQueueSize(Options, false);
|
|
}
|
|
|
|
// Declare final
|
|
Notify(error, MonitoringModeChange == null, result.FilterResult);
|
|
return;
|
|
}
|
|
|
|
if (!IsCommunicationError(error))
|
|
{
|
|
// Do not apply the mode change but force a recreate
|
|
MonitoringModeChange = null;
|
|
Modify = null;
|
|
ForceRecreate = true;
|
|
}
|
|
|
|
if (MonitoringModeChange == null)
|
|
{
|
|
// Retry the modification request
|
|
RetryCount++;
|
|
}
|
|
Notify(error, false, result.FilterResult);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set monitoring mode result
|
|
/// </summary>
|
|
/// <param name="monitoringMode"></param>
|
|
/// <param name="statusCode"></param>
|
|
/// <param name="index"></param>
|
|
/// <param name="diagnosticInfos"></param>
|
|
/// <param name="responseHeader"></param>
|
|
/// <exception cref="NotImplementedException"></exception>
|
|
internal void SetMonitoringModeResult(MonitoringMode monitoringMode,
|
|
StatusCode statusCode, int index, ArrayOf<DiagnosticInfo> diagnosticInfos,
|
|
ResponseHeader responseHeader)
|
|
{
|
|
ServiceResult error = ServiceResult.Good;
|
|
if (StatusCode.IsBad(statusCode))
|
|
{
|
|
error = ClientBase.GetResult(statusCode, index, diagnosticInfos,
|
|
responseHeader);
|
|
}
|
|
|
|
if (ServiceResult.IsGood(error))
|
|
{
|
|
Item.CurrentMonitoringMode = monitoringMode;
|
|
Item.LogRevisedSamplingRateAndQueueSize(Options, false);
|
|
|
|
Notify(error, true);
|
|
return;
|
|
}
|
|
|
|
if (!IsCommunicationError(error))
|
|
{
|
|
// Reapply the mode change
|
|
Modify = null;
|
|
ForceRecreate = true;
|
|
}
|
|
// Retry
|
|
RetryCount++;
|
|
Notify(error, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set result of delete
|
|
/// </summary>
|
|
/// <param name="statusCode"></param>
|
|
/// <param name="index"></param>
|
|
/// <param name="diagnosticInfos"></param>
|
|
/// <param name="responseHeader"></param>
|
|
internal void SetDeleteResult(StatusCode statusCode, int index,
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos, ResponseHeader responseHeader)
|
|
{
|
|
ServiceResult error = ServiceResult.Good;
|
|
if (StatusCode.IsBad(statusCode))
|
|
{
|
|
error = ClientBase.GetResult(statusCode, index, diagnosticInfos,
|
|
responseHeader);
|
|
}
|
|
|
|
bool final = Create == null && Modify == null;
|
|
if (ServiceResult.IsGood(error) ||
|
|
error.StatusCode == StatusCodes.BadMonitoredItemIdInvalid ||
|
|
final)
|
|
{
|
|
Item.ServerId = 0;
|
|
ForceRecreate = false;
|
|
// Now state is !Created so continue here to recreate
|
|
}
|
|
else
|
|
{
|
|
// Retry
|
|
RetryCount++;
|
|
}
|
|
Notify(error, final);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Abandon the change
|
|
/// </summary>
|
|
internal void Abandon()
|
|
{
|
|
Notify(new ServiceResult(StatusCodes.BadOperationAbandoned), true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify and handle retries
|
|
/// </summary>
|
|
/// <param name="error"></param>
|
|
/// <param name="final"></param>
|
|
/// <param name="filterResultExtensionObject"></param>
|
|
private void Notify(ServiceResult error, bool final,
|
|
ExtensionObject? filterResultExtensionObject = null)
|
|
{
|
|
MonitoringFilterResult? filterResult = null;
|
|
if (filterResultExtensionObject.HasValue &&
|
|
filterResultExtensionObject.Value.TryGetValue(out MonitoringFilterResult? fr))
|
|
{
|
|
filterResult = fr;
|
|
}
|
|
bool stop = Item.Context.NotifyItemChangeResult(
|
|
Item, RetryCount, Options, error, final, filterResult);
|
|
if (final || stop)
|
|
{
|
|
Item.CompleteChange(this);
|
|
}
|
|
Item.Error = error;
|
|
Item.FilterResult = filterResult == null ? Item.FilterResult :
|
|
(MonitoringFilterResult)filterResult.Clone();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if communication issue and not an error
|
|
/// in subscription or even success or uncertain states
|
|
/// </summary>
|
|
/// <param name="error"></param>
|
|
/// <returns></returns>
|
|
private static bool IsCommunicationError(ServiceResult error)
|
|
{
|
|
StatusCode code = error.StatusCode;
|
|
return code == StatusCodes.BadCommunicationError ||
|
|
code == StatusCodes.BadNotConnected ||
|
|
code == StatusCodes.BadSecureChannelClosed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Log revised sampling rate and queue size
|
|
/// </summary>
|
|
/// <param name="options"></param>
|
|
/// <param name="created"></param>
|
|
public void LogRevisedSamplingRateAndQueueSize(MonitoredItemOptions options,
|
|
bool created)
|
|
{
|
|
if (options.SamplingInterval != CurrentSamplingInterval &&
|
|
options.QueueSize != CurrentQueueSize &&
|
|
CurrentQueueSize != 0)
|
|
{
|
|
m_logger.LogInformation(
|
|
"{Item}: {Action} SamplingInterval was " +
|
|
"revised from {SamplingInterval} to {CurrentSamplingInterval} " +
|
|
"and QueueSize from {QueueSize} to {CurrentQueueSize}.",
|
|
this, created ? "CREATED" : "UPDATED",
|
|
options.SamplingInterval, CurrentSamplingInterval,
|
|
options.QueueSize, CurrentQueueSize);
|
|
}
|
|
else if (options.SamplingInterval != CurrentSamplingInterval)
|
|
{
|
|
m_logger.LogInformation(
|
|
"{Item}: {Action} SamplingInterval was " +
|
|
"revised from {SamplingInterval} to {CurrentSamplingInterval}.",
|
|
this, created ? "CREATED" : "UPDATED",
|
|
options.SamplingInterval, CurrentSamplingInterval);
|
|
}
|
|
else if (options.QueueSize != CurrentQueueSize && CurrentQueueSize != 0)
|
|
{
|
|
m_logger.LogInformation(
|
|
"{Item}: {Action} QueueSize was " +
|
|
"revised from {QueueSize} to {CurrentQueueSize}.",
|
|
this, created ? "CREATED" : "UPDATED",
|
|
options.QueueSize, CurrentQueueSize);
|
|
}
|
|
else
|
|
{
|
|
m_logger.LogDebug(
|
|
"{Item}: {Action} with desired configuration.",
|
|
this, created ? "CREATED" : "UPDATED");
|
|
}
|
|
}
|
|
|
|
private bool m_disposedValue;
|
|
private MonitoredItemOptions? m_currentOptions;
|
|
#pragma warning disable CA2213 // Disposed in DisposeAsync(bool)
|
|
private IDisposable? m_changeTracking;
|
|
#pragma warning restore CA2213
|
|
private readonly ConcurrentQueue<Change> m_pendingChanges = new();
|
|
private readonly ILogger m_logger;
|
|
internal static uint GlobalClientHandleUint;
|
|
private IOptionsMonitor<MonitoredItemOptions> m_options;
|
|
}
|
|
}
|