diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 87776fa6480d..9a200dc984cc 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,9 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "e6e37cda216c0292ae68c30c84a9dc8601d0fba5"; +// Preview of oven-sh/WebKit#385: guard InspectorDebuggerAgent::disable() so a redundant +// Debugger.disable does not trip ASSERT(!!m_client != !!client) in JSC::Debugger::setClient. +export const WEBKIT_VERSION = "autobuild-preview-pr-385-10e1ab9b"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/cli/inspect/inspect-debugger-disable.test.ts b/test/cli/inspect/inspect-debugger-disable.test.ts new file mode 100644 index 000000000000..5543d90b0d9d --- /dev/null +++ b/test/cli/inspect/inspect-debugger-disable.test.ts @@ -0,0 +1,92 @@ +import { spawn } from "bun"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, isASAN, isDebug } from "harness"; + +// InspectorDebuggerAgent::disable() used to call internalDisable() unconditionally, so a +// Debugger.disable on an already-disabled agent (or before any enable) re-ran +// m_debugger.setClient(nullptr) and tripped ASSERT(!!m_client != !!client) in +// JSC::Debugger::setClient. Frontends send Debugger.disable liberally on teardown, so this is +// an ordinary sequence. The assert is ASSERT_ENABLED-only, hence the skipIf. +test.skipIf(!isDebug && !isASAN)("Debugger.disable is idempotent and does not abort on asserts builds", async () => { + await using child = spawn({ + cmd: [bunExe(), "--inspect=127.0.0.1:0", "-e", "setInterval(()=>{},1000)"], + env: bunEnv, + stdout: "ignore", + stderr: "pipe", + }); + + let stderr = ""; + const decoder = new TextDecoder(); + const { promise: urlPromise, resolve: resolveUrl, reject: rejectUrl } = Promise.withResolvers(); + const stderrDrained = (async () => { + for await (const chunk of child.stderr) { + stderr += decoder.decode(chunk); + const m = stderr.match(/ws:\/\/[^\s]+/); + if (m) resolveUrl(new URL(m[0])); + } + rejectUrl(new Error("inspectee exited before printing inspector URL:\n" + stderr)); + })(); + + const url = await urlPromise; + const ws = new WebSocket(url); + const replies: Record = {}; + try { + let nextId = 1; + let failed: unknown; + const pending = new Map void>(); + const send = (method: string, params: object = {}) => + new Promise(resolve => { + if (failed) return resolve(failed); + const id = nextId++; + pending.set(id, resolve); + ws.send(JSON.stringify({ id, method, params })); + }); + const fail = (r: unknown) => { + failed ??= r; + for (const p of pending.values()) p(r); + pending.clear(); + }; + ws.addEventListener("message", ev => { + const msg = JSON.parse(String(ev.data)); + if (typeof msg.id === "number" && pending.has(msg.id)) { + replies[msg.id] = msg; + pending.get(msg.id)!(msg); + pending.delete(msg.id); + } + }); + ws.addEventListener("close", ({ code, reason }) => fail({ closed: { code, reason } })); + ws.addEventListener("error", cause => fail({ error: String(cause) })); + await new Promise((resolve, reject) => { + ws.addEventListener("open", () => resolve()); + ws.addEventListener("error", cause => reject(new Error("WebSocket error", { cause }))); + }); + + // Disable before any enable: m_enabled is false at startup, so the agent must early-return. + await send("Debugger.disable"); + // Enable, disable, disable: the second disable must be a no-op, not a second setClient(nullptr). + await send("Debugger.enable"); + await send("Debugger.disable"); + await send("Debugger.disable"); + // The agent must still be usable afterwards. + await send("Debugger.enable"); + await send("Debugger.disable"); + } finally { + ws.close(); + child.kill(); + } + + await Promise.all([child.exited, stderrDrained.catch(() => {})]); + if (child.signalCode === "SIGABRT") { + throw new Error( + `inspectee aborted on Debugger.disable (replies=${JSON.stringify(replies)}):\n${stderr.slice(-2000)}`, + ); + } + expect(replies).toEqual({ + 1: { id: 1, result: {} }, + 2: { id: 2, result: {} }, + 3: { id: 3, result: {} }, + 4: { id: 4, result: {} }, + 5: { id: 5, result: {} }, + 6: { id: 6, result: {} }, + }); +});