Conversation
…-PlcAdsConnectionService fix: fix potential deadlock in PlcAdsConnectionService
|
@copilot review the code |
There was a problem hiding this comment.
Pull request overview
This PR introduces support for writing structs with CommandInputBuilder and PrimitiveCommandArgumentHandler, fixes a double-write bug, adds auto-reconnect functionality to PlcAdsConnectionService, improves PlcAdsStateReader robustness, upgrades dependencies, and migrates all test projects from FluentAssertions to AwesomeAssertions.
Key Changes:
- Added recursive struct writing support in
PrimitiveCommandArgumentHandlerfor PLC commands - Fixed double write issue in
WriteInputDatamethod - Implemented auto-reconnect feature with configurable timeout in
PlcAdsConnectionService - Enhanced
PlcAdsStateReader.StopSampling()with safer notification cleanup usingTryDeleteDeviceNotification - Upgraded Beckhoff.TwinCAT.Ads from 6.1.332 to 6.2.521 and bumped package versions to 5.1.0.0
- Migrated all test projects to AwesomeAssertions 9.* and updated assertion syntax
- Updated auto-generated resource files to English and newer tool versions
Reviewed changes
Copilot reviewed 47 out of 49 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| NET/Mbc.Pcs.Net.Command/PrimitiveCommandArgumentHandler.cs | Added struct support to WriteInputData, removed duplicate write call, enabled recursive struct handling |
| NET/Mbc.Pcs.Net/State/PlcAdsStateReader.cs | Enhanced StopSampling() with safer notification cleanup and handle validation |
| NET/Mbc.Pcs.Net/Connection/PlcAdsConnectionService.cs | Implemented auto-reconnect feature with cancellation token and configurable retry interval |
| NET/Mbc.Pcs.Net/Connection/PlcAdsConnectionProvider.cs | Updated to use ILoggerFactory instead of ILogger for proper logger instantiation |
| NET/Mbc.Pcs.Net/Connection/ConnectionResources.Designer.cs | Auto-generated file updated from German to English comments |
| NET/Mbc.Pcs.Net.Command/CommandResources.Designer.cs | Auto-generated file updated to version 17.0.0.0 |
| NET/Mbc.Pcs.Net/Mbc.Pcs.Net.csproj | Version bumped to 5.1.0.0 |
| NET/Mbc.Pcs.Net.Alarm/Mbc.Pcs.Net.Alarm.csproj | Version bumped to 5.1.0.0, Beckhoff.TwinCAT.Ads upgraded to 6.2.521 |
| NET/Mbc.Ads.Utils/Mbc.Ads.Utils.csproj | Version bumped to 5.1.0.0, Beckhoff.TwinCAT.Ads upgraded to 6.2.521 |
| NET/Mbc.Pcs.Net.Test/Mbc.Pcs.Net.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Pcs.Net.DataRecorder.Test/Mbc.Pcs.Net.DataRecorder.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Pcs.Net.Command.Test/Mbc.Pcs.Net.Command.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Pcs.Net.Alarm.Test/Mbc.Pcs.Net.Alarm.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Pcs.Net.Alarm.Service.Test/Mbc.Pcs.Net.Alarm.Service.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Ads.Utils.Test/Mbc.Ads.Utils.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Ads.Mapper.Test/Mbc.Ads.Mapper.Test.csproj | Replaced FluentAssertions with AwesomeAssertions 9.* |
| NET/Mbc.Pcs.Net.Command.Test/PlcCommandAsyncTests.cs | Updated assertion method from BeLessOrEqualTo to BeLessThanOrEqualTo for AwesomeAssertions compatibility |
| NET/Mbc.Ads.Mapper.Test/AdsMapperTestFakePlcData.cs | Updated StringMarshaler.UTF16 to StringMarshaler.UTF16.Encoding for clarity |
| NET/Changelog.md | Added version 5.0.1 entry documenting struct support and double-write bugfix |
| Various test files | Updated imports from FluentAssertions to AwesomeAssertions |
Files not reviewed (2)
- NET/Mbc.Pcs.Net.Command/CommandResources.Designer.cs: Language not supported
- NET/Mbc.Pcs.Net/Connection/ConnectionResources.Designer.cs: Language not supported
Comments suppressed due to low confidence (1)
NET/Mbc.Ads.Mapper.Test/AdsMapperTestFakePlcData.cs:261
- 'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
'MockMemberCollection' exposes the internal representation stored in field '_members'. The value may be modified through the variable fakeMembers.
public MockMemberCollection(List<IMember> members)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private void StopReconnectTask() | ||
| { | ||
| _reconnectCancellationTokenSource?.Cancel(); | ||
| _reconnectCancellationTokenSource?.Dispose(); | ||
| _reconnectCancellationTokenSource = null; | ||
| _reconnectTask = null; |
There was a problem hiding this comment.
Potential resource leak: _reconnectTask is set to null after cancellation but is never awaited. If the task is still running when StopReconnectTask() is called, it could continue executing in the background. Consider awaiting the task (with a timeout) before nullifying it to ensure proper cleanup:
_reconnectTask?.Wait(TimeSpan.FromSeconds(5));or using async disposal pattern.
| { | ||
| _connected = e.Connected; | ||
| _connection = e.Connection; | ||
| ConnectionStateChangedInternal?.Invoke(this, e); | ||
|
|
||
| // Execute auto-reconnect if enabled | ||
| if (_autoReconnectEnabled && _serviceStarted && !_connected) | ||
| { | ||
| StartReconnectTask(); | ||
| } | ||
| else if (_connected) | ||
| { | ||
| StopReconnectTask(); | ||
| } | ||
| } | ||
|
|
||
| ConnectionStateChangedInternal?.Invoke(this, e); | ||
| } | ||
|
|
||
| private void StartReconnectTask() | ||
| { | ||
|
|
||
| // Stop existing reconnect task if running | ||
| StopReconnectTask(); | ||
|
|
||
| _reconnectCancellationTokenSource = new CancellationTokenSource(); | ||
| var cancelationToken = _reconnectCancellationTokenSource.Token; | ||
|
|
||
| _reconnectTask = Task.Run(async () => | ||
| { | ||
| while (!cancelationToken.IsCancellationRequested) | ||
| { | ||
| try | ||
| { | ||
| _logger?.LogInformation("Wait {time} to try a reconnection to PLC.", ReconnectionTime); | ||
| await Task.Delay(ReconnectionTime, cancelationToken); | ||
|
|
||
| if (!cancelationToken.IsCancellationRequested && !_connected) | ||
| { | ||
| _logger?.LogInformation("Try a reconnection to PLC."); | ||
| _plcConnection.Connect(); | ||
| } | ||
| } | ||
| catch (TaskCanceledException) | ||
| { | ||
| // Expected when cancellation is requested | ||
| break; | ||
| } | ||
| catch (Exception) | ||
| { | ||
| // Continue reconnection attempts even if one fails | ||
| } | ||
| } | ||
| }, cancelationToken); | ||
| } | ||
|
|
||
| private void StopReconnectTask() | ||
| { | ||
| _reconnectCancellationTokenSource?.Cancel(); | ||
| _reconnectCancellationTokenSource?.Dispose(); | ||
| _reconnectCancellationTokenSource = null; | ||
| _reconnectTask = null; |
There was a problem hiding this comment.
Potential race condition: The fields _connected, _serviceStarted, _reconnectCancellationTokenSource, and _reconnectTask are accessed from multiple threads (the reconnect task thread and the main thread) without synchronization. This can lead to race conditions. Consider using locks or making these fields volatile, or better yet, use thread-safe patterns for managing the reconnect task lifecycle.
| ICommandInput inputStructValues = CommandInputBuilder.FromDictionary(new Dictionary<string, object>() | ||
| { | ||
| ["StructValue1"] = 123, | ||
| ["StructValue1"] = 3333, |
There was a problem hiding this comment.
Duplicate key in dictionary example. "StructValue1" appears twice in the dictionary with different values (123 and 3333). This will cause a runtime error. The second occurrence should likely be "StructValue2".
| ["StructValue1"] = 3333, | |
| ["StructValue2"] = 3333, |
| { | ||
| ["value1"] = (byte)123, | ||
| ["stAbc"] = inputStructValues, | ||
| ["alue2"] = (byte)222, |
There was a problem hiding this comment.
Typo in dictionary key. The key is "alue2" but should likely be "value2" (missing leading 'v').
| ["alue2"] = (byte)222, | |
| ["value2"] = (byte)222, |
|
|
||
| if (result.Failed()) | ||
| { | ||
| _logger.LogWarning("Could not deregister Device Notification for '{variable_path}' because no connection is available.", _config.VariablePath); |
There was a problem hiding this comment.
The error message suggests "no connection is available" but this is misleading. The code is called when _adsConnectionService.IsConnected is true, so a connection exists. The error occurred during TryDeleteDeviceNotification, which can fail for other reasons (e.g., invalid handle, communication error). Consider a more accurate message like "Could not deregister Device Notification for '{variable_path}' (Error code: {result})."
| private bool _connected; | ||
| private IAdsConnection _connection; | ||
| private bool _autoReconnectEnabled; | ||
| private ILogger _logger = null; |
There was a problem hiding this comment.
[nitpick] The field _logger is initialized to null by default on line 24, but this explicit initialization is unnecessary in C# as reference types are null by default. Consider removing the = null for cleaner code.
| marshaledValues.Add(marshaledValueBuffer.Slice(0, size).ToArray()); | ||
| if (!converter.TryMarshal(item.DataType, encoding, value, marshaledValueBuffer.Span, out int size)) | ||
| { | ||
| throw new PlcCommandException(string.Format("Input variable {0} has invalid PLC data type {1} to serialize with PrimitiveTypeMarshaler.", item.InstanceName, item.DataType.ToString())); |
There was a problem hiding this comment.
Redundant call to 'ToString' on a String object.
| private readonly PlcAdsConnectionProvider _plcConnection; | ||
| private bool _connected; | ||
| private IAdsConnection _connection; | ||
| private bool _autoReconnectEnabled; |
There was a problem hiding this comment.
Field '_autoReconnectEnabled' can be 'readonly'.
| private bool _connected; | ||
| private IAdsConnection _connection; | ||
| private bool _autoReconnectEnabled; | ||
| private ILogger _logger = null; |
There was a problem hiding this comment.
Field '_logger' can be 'readonly'.
- Removed Support for .NET Framework 4.7.1 (But still .NET Standard 2.0.) - Removed Dependency to Mbc.Hdf5Utils Nuget package. The code is now part of Mbc.Pcs.Net.DataRecorder.
Support for .NET 10.0.
This pull request introduces a new feature for writing structs with
CommandInputBuilderandPrimitiveCommandArgumentHandler, fixes a bug with double writes, and upgrades several dependencies. Additionally, all test projects now useAwesomeAssertionsinstead ofFluentAssertions, and the Beckhoff TwinCAT.Ads package is updated across projects. Below are the most important changes grouped by theme.Features and Bugfixes
CommandInputBuilderandPrimitiveCommandArgumentHandler. Struct fields must be flagged with{attribute 'PlcCommandInput'}and be of typeCommandInputBuilderin C#.PrimitiveCommandArgumentHandlerwould perform a double write inWriteInputData.Dependency and Version Upgrades
Beckhoff.TwinCAT.Adspackage from version6.1.332to6.2.521in bothMbc.Ads.UtilsandMbc.Pcs.Net.Alarmprojects. [1] [2]Mbc.Ads.UtilsandMbc.Pcs.Net.Alarmprojects from5.0.0.0to5.1.0.0. [1] [2]Test Suite Updates
FluentAssertionswithAwesomeAssertions(version9.*) across all test projects and updated their package references accordingly. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16]Minor Code Improvements
StringMarshaler.UTF16toStringMarshaler.UTF16.Encodingin test helpers for improved clarity and correctness. [1] [2]