Skip to content

Commit 6956969

Browse files
kblokclaude
andcommitted
fix: subscribe ExecutionContext to CDP destruction events directly
Each ExecutionContext now handles Runtime.executionContextDestroyed and Runtime.executionContextsCleared independently, matching upstream TypeScript. This fixes hangs in headless-shell and cdp-pipe where executionContextsCleared fires before frameNavigated, causing IsolatedWorld to lose track of the old context before it can be signalled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 877166f commit 6956969

1 file changed

Lines changed: 31 additions & 1 deletion

File tree

lib/PuppeteerSharp/ExecutionContext.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using PuppeteerSharp.Cdp;
99
using PuppeteerSharp.Cdp.Messaging;
1010
using PuppeteerSharp.Helpers;
11+
using PuppeteerSharp.Helpers.Json;
1112
using PuppeteerSharp.QueryHandlers;
1213

1314
namespace PuppeteerSharp
@@ -37,6 +38,11 @@ internal ExecutionContext(
3738
ContextId = contextPayload.Id;
3839
ContextName = contextPayload.Name;
3940
World = world;
41+
42+
// Match upstream: each context subscribes to its own destruction events so that
43+
// pending evaluations are cancelled even when IsolatedWorld state is out of sync
44+
// (e.g. headless-shell sends executionContextsCleared after executionContextCreated).
45+
Client.MessageReceived += OnCdpMessageReceived;
4046
}
4147

4248
/// <summary>
@@ -89,6 +95,7 @@ public Task<T> EvaluateFunctionAsync<T>(string script, params object[] args)
8995
/// <inheritdoc />
9096
public async ValueTask DisposeAsync()
9197
{
98+
Client.MessageReceived -= OnCdpMessageReceived;
9299
_contextDestroyedTcs.TrySetResult(true);
93100

94101
if (_puppeteerUtilQueue != null)
@@ -107,6 +114,7 @@ public async ValueTask DisposeAsync()
107114
/// <inheritdoc />
108115
public void Dispose()
109116
{
117+
Client.MessageReceived -= OnCdpMessageReceived;
110118
Dispose(true);
111119
GC.SuppressFinalize(this);
112120
}
@@ -153,7 +161,11 @@ internal IJSHandle CreateJSHandle(RemoteObject remoteObject)
153161
? new CdpElementHandle(World, remoteObject)
154162
: new CdpJSHandle(World, remoteObject);
155163

156-
internal void NotifyDestroyed() => _contextDestroyedTcs.TrySetResult(true);
164+
internal void NotifyDestroyed()
165+
{
166+
Client.MessageReceived -= OnCdpMessageReceived;
167+
_contextDestroyedTcs.TrySetResult(true);
168+
}
157169

158170
#if NET8_0_OR_GREATER
159171
[GeneratedRegex(@"^[\040\t]*\/\/[@#] sourceURL=\s*\S*?\s*$", RegexOptions.Multiline)]
@@ -208,6 +220,24 @@ private void Dispose(bool disposing)
208220
_ = DisposeAsync();
209221
}
210222

223+
private void OnCdpMessageReceived(object sender, MessageEventArgs e)
224+
{
225+
switch (e.MessageID)
226+
{
227+
case "Runtime.executionContextDestroyed":
228+
var destroyed = e.MessageData.ToObject<RuntimeExecutionContextDestroyedResponse>();
229+
if (destroyed.ExecutionContextId == ContextId)
230+
{
231+
NotifyDestroyed();
232+
}
233+
234+
break;
235+
case "Runtime.executionContextsCleared":
236+
NotifyDestroyed();
237+
break;
238+
}
239+
}
240+
211241
private async Task<T> RemoteObjectTaskToObject<T>(Task<RemoteObject> remote)
212242
{
213243
var response = await remote.ConfigureAwait(false);

0 commit comments

Comments
 (0)