From f9db5cd2722f88d11719e9da91ebba80c38fb55f Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:48:07 +0700 Subject: [PATCH 1/2] Add association-owned receive pump lifetime rebinding --- .../MmsClientSession.ReceivePumpLifetime.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/AR.Iec61850/Mms/MmsClientSession.ReceivePumpLifetime.cs diff --git a/src/AR.Iec61850/Mms/MmsClientSession.ReceivePumpLifetime.cs b/src/AR.Iec61850/Mms/MmsClientSession.ReceivePumpLifetime.cs new file mode 100644 index 0000000..148a97a --- /dev/null +++ b/src/AR.Iec61850/Mms/MmsClientSession.ReceivePumpLifetime.cs @@ -0,0 +1,50 @@ +namespace AR.Iec61850.Mms; + +public sealed partial class MmsClientSession +{ + /// + /// Rebinds the confirmed-service receive pump to the MMS association lifetime instead + /// of the cancellation token that was used only to establish the connection. + /// Call this when a long-lived session will be reused across multiple UI operations. + /// + public async Task RebindReceivePumpToSessionLifetimeAsync( + CancellationToken cancellationToken = default) + { + EnsureMmsReady(); + cancellationToken.ThrowIfCancellationRequested(); + + if (_receivePump.PendingOperationCount != 0) + { + throw new InvalidOperationException( + "The MMS receive pump cannot be rebound while confirmed operations are pending."); + } + + await MmsReceivePumpLifetimePolicy.RebindAsync( + _receivePump, + cancellationToken).ConfigureAwait(false); + + if (!IsTransportConnected || !IsMmsInitiated || !_receivePump.IsRunning) + { + throw new InvalidOperationException( + "The MMS receive pump could not be rebound to the active association lifetime."); + } + + LastReceiveRoutingSummary = + "MMS receive pump rebound to association-owned lifetime for reusable confirmed services."; + } +} + +internal static class MmsReceivePumpLifetimePolicy +{ + public static async Task RebindAsync( + MmsReceivePump receivePump, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(receivePump); + cancellationToken.ThrowIfCancellationRequested(); + + await receivePump.StopAsync().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + receivePump.Start(); + } +} From 93170d9ea86d090ed469f0f179e2ed8f01fcce96 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:48:19 +0700 Subject: [PATCH 2/2] Test reusable MMS receive pump lifetime after connect cancellation --- .../Mms/MmsReceivePumpLifetimePolicyTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/AR.Iec61850.Tests/Mms/MmsReceivePumpLifetimePolicyTests.cs diff --git a/tests/AR.Iec61850.Tests/Mms/MmsReceivePumpLifetimePolicyTests.cs b/tests/AR.Iec61850.Tests/Mms/MmsReceivePumpLifetimePolicyTests.cs new file mode 100644 index 0000000..0f49423 --- /dev/null +++ b/tests/AR.Iec61850.Tests/Mms/MmsReceivePumpLifetimePolicyTests.cs @@ -0,0 +1,49 @@ +using System.Threading.Channels; +using AR.Iec61850.Asn1; +using AR.Iec61850.Mms; + +namespace AR.Iec61850.Tests.Mms; + +public sealed class MmsReceivePumpLifetimePolicyTests +{ + [Fact] + public async Task Rebind_Detaches_Pump_From_Completed_Connect_Operation_Token() + { + var channel = Channel.CreateUnbounded(); + var router = new MmsReceiveRouter(); + var pump = new MmsReceivePump( + router, + cancellationToken => channel.Reader.ReadAsync(cancellationToken).AsTask()); + using var connectOperation = new CancellationTokenSource(); + + pump.Start(connectOperation.Token); + await MmsReceivePumpLifetimePolicy.RebindAsync(pump); + connectOperation.Cancel(); + + using var pending = pump.RegisterConfirmedOperation(27); + await channel.Writer.WriteAsync(BuildConfirmedReadResponse(27)); + + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + var response = await pending.WaitAsync(timeout.Token); + + Assert.True(pump.IsRunning); + Assert.Equal(MmsPduKind.ConfirmedResponse, response.Kind); + Assert.Equal(27, response.InvokeId); + + await pump.StopAsync(); + } + + private static byte[] BuildConfirmedReadResponse(int invokeId) + { + var data = MmsDataCodec.Encode(MmsDataValue.Boolean(true)); + var listOfAccessResult = BerWriter.EncodeTlv(0xA1, data); + var readService = BerWriter.EncodeTlv(0xA4, listOfAccessResult); + var mms = BerWriter.EncodeTlv( + 0xA1, + BerWriter.EncodeTlv(0x02, BerWriter.EncodeSignedInteger(invokeId)) + .Concat(readService) + .ToArray()); + + return MmsPresentation.WrapIsoPresentationPData(mms); + } +}