Skip to content

Commit 0cc78b1

Browse files
authored
Merge pull request #1379 from microsoft/copilot/fix-unregister-event-handlers
Unregister event handlers in RpcTargetInfo.DisposeAsync
1 parent bf92fcd commit 0cc78b1

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

src/StreamJsonRpc/Reflection/RpcTargetInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ internal RpcTargetInfo(JsonRpc jsonRpc)
4242

4343
public async ValueTask DisposeAsync()
4444
{
45+
// Unregister event handlers first to prevent any events from being raised during disposal.
46+
this.UnregisterEventHandlersFromTargetObjects();
47+
4548
List<object>? objectsToDispose;
4649
lock (this.SyncObject)
4750
{

test/StreamJsonRpc.Tests/TargetObjectEventsTests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,80 @@ public void GenericServerEventSubscriptionLifetime()
150150
Assert.Null(this.server.ServerEventWithCustomArgsAccessor);
151151
}
152152

153+
/// <summary>
154+
/// Verifies that event handlers are unregistered when multiple connections are established and disposed.
155+
/// This tests for a memory leak where event handlers would accumulate if not properly unregistered on disposal.
156+
/// </summary>
157+
[Fact]
158+
public void EventHandlersUnregisteredOnMultipleConnectionDisposals()
159+
{
160+
// Use a shared server object across multiple connections to simulate the real-world scenario
161+
var sharedServer = new Server();
162+
163+
// Verify no handlers initially
164+
Assert.Null(sharedServer.ServerEventAccessor);
165+
Assert.Null(sharedServer.ServerEventWithCustomArgsAccessor);
166+
167+
// Create and dispose multiple connections
168+
for (int i = 0; i < 3; i++)
169+
{
170+
var streams = FullDuplexStream.CreateStreams();
171+
var rpc = this.CreateJsonRpcWithTargetObject(streams.Item1, sharedServer);
172+
rpc.StartListening();
173+
174+
// Verify handler is registered
175+
Assert.NotNull(sharedServer.ServerEventAccessor);
176+
Assert.NotNull(sharedServer.ServerEventWithCustomArgsAccessor);
177+
178+
// Count the number of handlers attached
179+
int serverEventHandlerCount = sharedServer.ServerEventAccessor?.GetInvocationList().Length ?? 0;
180+
int customArgsEventHandlerCount = sharedServer.ServerEventWithCustomArgsAccessor?.GetInvocationList().Length ?? 0;
181+
182+
// Should only have one handler per event, not accumulating
183+
Assert.Equal(1, serverEventHandlerCount);
184+
Assert.Equal(1, customArgsEventHandlerCount);
185+
186+
// Dispose the connection
187+
rpc.Dispose();
188+
189+
// Verify handlers are unregistered after disposal
190+
Assert.Null(sharedServer.ServerEventAccessor);
191+
Assert.Null(sharedServer.ServerEventWithCustomArgsAccessor);
192+
}
193+
}
194+
195+
/// <summary>
196+
/// Verifies that event handlers are unregistered when the stream is closed without explicit disposal.
197+
/// This simulates the scenario where a websocket connection drops unexpectedly.
198+
/// </summary>
199+
[Fact]
200+
public async Task EventHandlersUnregisteredWhenStreamClosesUnexpectedly()
201+
{
202+
var sharedServer = new Server();
203+
204+
// Verify no handlers initially
205+
Assert.Null(sharedServer.ServerEventAccessor);
206+
207+
var streams = FullDuplexStream.CreateStreams();
208+
var serverRpc = this.CreateJsonRpcWithTargetObject(streams.Item1, sharedServer);
209+
var clientRpc = new JsonRpc(streams.Item2);
210+
211+
serverRpc.StartListening();
212+
clientRpc.StartListening();
213+
214+
// Verify handler is registered
215+
Assert.NotNull(sharedServer.ServerEventAccessor);
216+
217+
// Simulate connection drop by closing the stream without disposing JsonRpc
218+
streams.Item2.Dispose();
219+
220+
// Wait for the disconnection to be detected
221+
await serverRpc.Completion.WithCancellation(this.TimeoutToken);
222+
223+
// Verify handlers are unregistered after stream closure
224+
Assert.Null(sharedServer.ServerEventAccessor);
225+
}
226+
153227
/// <summary>Ensures that JsonRpc only adds one event handler to target objects where events are declared multiple times in an type hierarchy.</summary>
154228
/// <remarks>This is a regression test for <see href="https://github.com/microsoft/vs-streamjsonrpc/issues/481">this bug</see>.</remarks>
155229
[Fact]

0 commit comments

Comments
 (0)