diff --git a/src/Playwright/Playwright.cs b/src/Playwright/Playwright.cs index c81d2f243..5119762ce 100644 --- a/src/Playwright/Playwright.cs +++ b/src/Playwright/Playwright.cs @@ -41,25 +41,41 @@ public static async Task CreateAsync() { var transport = new StdIOTransport(); var connection = new Connection(); - transport.MessageReceived += (_, message) => + + EventHandler onMessageReceived = (_, message) => { Connection.TraceMessage("pw:channel:recv", message); connection.Dispatch(JsonSerializer.Deserialize(message, JsonExtensions.DefaultJsonSerializerOptions)!); }; - transport.LogReceived += (_, log) => + EventHandler onLogReceived = (_, log) => { // workaround for https://github.com/nunit/nunit/issues/4144 var writer = Environment.GetEnvironmentVariable("PWAPI_TO_STDOUT") != null ? Console.Out : Console.Error; writer.WriteLine(log); }; - transport.TransportClosed += (_, reason) => connection.DoClose(reason); + EventHandler onTransportClosed = (_, reason) => connection.DoClose(reason); + + transport.MessageReceived += onMessageReceived; + transport.LogReceived += onLogReceived; + transport.TransportClosed += onTransportClosed; connection.OnMessage = (message, keepNulls) => { var rawMessage = JsonSerializer.SerializeToUtf8Bytes(message, keepNulls ? connection.DefaultJsonSerializerOptionsKeepNulls : connection.DefaultJsonSerializerOptions); Connection.TraceMessage("pw:channel:send", rawMessage); return transport.SendAsync(rawMessage); }; - connection.Close += (_, reason) => transport.Close(reason); + connection.Close += (_, reason) => + { + // Break Connection<->Transport closure cycles and release the underlying + // process / token / reader task, so that callers who call `Dispose` on a + // short-lived Playwright don't accumulate orphaned graphs. + transport.MessageReceived -= onMessageReceived; + transport.LogReceived -= onLogReceived; + transport.TransportClosed -= onTransportClosed; + connection.OnMessage = null!; + transport.Close(reason); + transport.Dispose(); + }; return await connection.InitializePlaywrightAsync().ConfigureAwait(false); } } diff --git a/src/Playwright/Transport/StdIOTransport.cs b/src/Playwright/Transport/StdIOTransport.cs index c7a8184b5..ea3d15736 100644 --- a/src/Playwright/Transport/StdIOTransport.cs +++ b/src/Playwright/Transport/StdIOTransport.cs @@ -44,14 +44,8 @@ internal StdIOTransport() { _process = GetProcess("run-driver"); StartProcessWithUTF8IOEncoding(_process); - _process.Exited += (_, _) => Close(new TargetClosedException("Process exited")); - _process.ErrorDataReceived += (_, error) => - { - if (error.Data != null) - { - LogReceived?.Invoke(this, error.Data); - } - }; + _process.Exited += OnProcessExited; + _process.ErrorDataReceived += OnProcessErrorDataReceived; _process.BeginErrorReadLine(); _getResponseTask = ScheduleTransportTaskAsync(GetResponseAsync, _readerCancellationSource.Token); @@ -195,11 +189,27 @@ private void Dispose(bool disposing) return; } + if (_process != null) + { + _process.Exited -= OnProcessExited; + _process.ErrorDataReceived -= OnProcessErrorDataReceived; + _process.Dispose(); + } _readerCancellationSource?.Dispose(); - _process?.Dispose(); _getResponseTask?.Dispose(); } + private void OnProcessExited(object? sender, EventArgs e) + => Close(new TargetClosedException("Process exited")); + + private void OnProcessErrorDataReceived(object? sender, DataReceivedEventArgs e) + { + if (e.Data != null) + { + LogReceived?.Invoke(this, e.Data); + } + } + private async Task GetResponseAsync(CancellationToken token) { try