Skip to content

Commit 22b9bc7

Browse files
authored
fix(transport): dispose StdIOTransport and break event cycles on shutdown (#3307)
1 parent 285a824 commit 22b9bc7

2 files changed

Lines changed: 39 additions & 13 deletions

File tree

src/Playwright/Playwright.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,41 @@ public static async Task<IPlaywright> CreateAsync()
4141
{
4242
var transport = new StdIOTransport();
4343
var connection = new Connection();
44-
transport.MessageReceived += (_, message) =>
44+
45+
EventHandler<byte[]> onMessageReceived = (_, message) =>
4546
{
4647
Connection.TraceMessage("pw:channel:recv", message);
4748
connection.Dispatch(JsonSerializer.Deserialize<PlaywrightServerMessage>(message, JsonExtensions.DefaultJsonSerializerOptions)!);
4849
};
49-
transport.LogReceived += (_, log) =>
50+
EventHandler<string> onLogReceived = (_, log) =>
5051
{
5152
// workaround for https://github.com/nunit/nunit/issues/4144
5253
var writer = Environment.GetEnvironmentVariable("PWAPI_TO_STDOUT") != null ? Console.Out : Console.Error;
5354
writer.WriteLine(log);
5455
};
55-
transport.TransportClosed += (_, reason) => connection.DoClose(reason);
56+
EventHandler<Exception> onTransportClosed = (_, reason) => connection.DoClose(reason);
57+
58+
transport.MessageReceived += onMessageReceived;
59+
transport.LogReceived += onLogReceived;
60+
transport.TransportClosed += onTransportClosed;
5661
connection.OnMessage = (message, keepNulls) =>
5762
{
5863
var rawMessage = JsonSerializer.SerializeToUtf8Bytes(message, keepNulls ? connection.DefaultJsonSerializerOptionsKeepNulls : connection.DefaultJsonSerializerOptions);
5964
Connection.TraceMessage("pw:channel:send", rawMessage);
6065
return transport.SendAsync(rawMessage);
6166
};
62-
connection.Close += (_, reason) => transport.Close(reason);
67+
connection.Close += (_, reason) =>
68+
{
69+
// Break Connection<->Transport closure cycles and release the underlying
70+
// process / token / reader task, so that callers who call `Dispose` on a
71+
// short-lived Playwright don't accumulate orphaned graphs.
72+
transport.MessageReceived -= onMessageReceived;
73+
transport.LogReceived -= onLogReceived;
74+
transport.TransportClosed -= onTransportClosed;
75+
connection.OnMessage = null!;
76+
transport.Close(reason);
77+
transport.Dispose();
78+
};
6379
return await connection.InitializePlaywrightAsync().ConfigureAwait(false);
6480
}
6581
}

src/Playwright/Transport/StdIOTransport.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,8 @@ internal StdIOTransport()
4444
{
4545
_process = GetProcess("run-driver");
4646
StartProcessWithUTF8IOEncoding(_process);
47-
_process.Exited += (_, _) => Close(new TargetClosedException("Process exited"));
48-
_process.ErrorDataReceived += (_, error) =>
49-
{
50-
if (error.Data != null)
51-
{
52-
LogReceived?.Invoke(this, error.Data);
53-
}
54-
};
47+
_process.Exited += OnProcessExited;
48+
_process.ErrorDataReceived += OnProcessErrorDataReceived;
5549
_process.BeginErrorReadLine();
5650

5751
_getResponseTask = ScheduleTransportTaskAsync(GetResponseAsync, _readerCancellationSource.Token);
@@ -195,11 +189,27 @@ private void Dispose(bool disposing)
195189
return;
196190
}
197191

192+
if (_process != null)
193+
{
194+
_process.Exited -= OnProcessExited;
195+
_process.ErrorDataReceived -= OnProcessErrorDataReceived;
196+
_process.Dispose();
197+
}
198198
_readerCancellationSource?.Dispose();
199-
_process?.Dispose();
200199
_getResponseTask?.Dispose();
201200
}
202201

202+
private void OnProcessExited(object? sender, EventArgs e)
203+
=> Close(new TargetClosedException("Process exited"));
204+
205+
private void OnProcessErrorDataReceived(object? sender, DataReceivedEventArgs e)
206+
{
207+
if (e.Data != null)
208+
{
209+
LogReceived?.Invoke(this, e.Data);
210+
}
211+
}
212+
203213
private async Task GetResponseAsync(CancellationToken token)
204214
{
205215
try

0 commit comments

Comments
 (0)