Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/AR.Iec61850/Mms/MmsClientSession.ReceivePumpLifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace AR.Iec61850.Mms;

public sealed partial class MmsClientSession
{
/// <summary>
/// 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.
/// </summary>
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();
}
}
49 changes: 49 additions & 0 deletions tests/AR.Iec61850.Tests/Mms/MmsReceivePumpLifetimePolicyTests.cs
Original file line number Diff line number Diff line change
@@ -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<byte[]>();
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);
}
}
Loading