From fdf7f94f4626de6a68f5827aa352b7c5f6915bd3 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 7 May 2026 11:05:32 +0000 Subject: [PATCH] ci(mac): single-process Chromium + JSON.parse try/catch in pipeTransport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 25491698868 / job 74801076186 hit the Playwright pipeTransport 'Unexpected end of JSON input' crash on ALL THREE retry attempts (at 11:00:52, 11:01:07, 11:01:21 — only ~15s apart). The retry-with- Studio-reset wrapper from d35bf6a couldn't recover because the crash hits 100% of attempts on this run, not as a rare race. Two complementary fixes: 1. tests/studio/playwright_chat_ui.py + playwright_extra_ui.py: pass --single-process / --no-sandbox / --disable-dev-shm-usage / --disable-gpu to chromium.launch. --single-process is the key one: it keeps the renderer in the browser process, eliminating the browser↔renderer IPC pipe that was the actual crash site (Chromium's renderer was dying mid-startup and corrupting the pipe stream the Node driver was parsing). 2. .github/workflows/studio-mac-ui-smoke.yml: backport upstream Playwright's try/catch around the two JSON.parse(message) sites in driver/.../pipeTransport.js so a malformed stdout chunk (e.g. empty buffer between two \0 delimiters) is dropped silently instead of throwing and killing the entire Node driver. Newer Playwright versions ship this guard upstream; we patch it in via a python script after `playwright install chromium` so the fix lives only in CI's Mac job. Idempotent: prints "no matches; skipping" if upstream changes the pattern. The retry loop from d35bf6a is kept as a third line of defense for any residual Chromium-died-and-stayed-dead scenarios. --- .github/workflows/studio-mac-ui-smoke.yml | 42 ++++++++++++++++++++--- tests/studio/playwright_chat_ui.py | 21 +++++++++++- tests/studio/playwright_extra_ui.py | 15 +++++++- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/workflows/studio-mac-ui-smoke.yml b/.github/workflows/studio-mac-ui-smoke.yml index 403706bb3b..b820c3f0b2 100644 --- a/.github/workflows/studio-mac-ui-smoke.yml +++ b/.github/workflows/studio-mac-ui-smoke.yml @@ -93,15 +93,47 @@ jobs: # needs already. # Pinned <1.58 because all 1.55-1.58 drivers ship Node 24 on # macos-14 and intermittently hit 'SyntaxError: Unexpected end - # of JSON input' in pipeTransport.js when the Chromium child - # transiently flushes an empty buffer. The crash is racy and - # only triggers on a fraction of runs; the retry wrapper in - # "Drive the chat UI with Playwright" is what actually keeps - # the job green when the race fires. + # of JSON input' in pipeTransport.js. Run 25491698868 showed + # the crash hitting 100% of three retry attempts -- not a + # rare race but a hard reproduction. Belt-and-suspenders fix: + # the test scripts pass --single-process to Chromium (see + # tests/studio/playwright_chat_ui.py) AND we patch + # pipeTransport.js below to swallow JSON parse errors instead + # of crashing the driver Node process. Both together let the + # in-script retry recover from any residual flakes. run: | pip install 'playwright>=1.55,<1.58' python -m playwright install chromium + - name: Patch Playwright pipeTransport.js to tolerate malformed JSON + # In Playwright 1.55-1.58, pipeTransport.js does + # `JSON.parse(message)` with no try/catch; when Chromium dies + # mid-write the partial buffer crashes the driver Node + # process and the test script exits with 'Connection closed + # while reading from the driver'. Newer Playwright versions + # added a try/catch upstream. Backport that here. + run: | + python - <<'PY' + import os, re, sys + import playwright + driver_dir = os.path.join(os.path.dirname(playwright.__file__), "driver", "package", "lib", "server") + path = os.path.join(driver_dir, "pipeTransport.js") + src = open(path).read() + # Wrap both `this.onmessage.call(null, JSON.parse(...))` sites in try/catch. + patched = re.sub( + r"this\.onmessage\.call\(null, JSON\.parse\((message2?)\)\);", + r"try { this.onmessage.call(null, JSON.parse(\1)); } " + r"catch (e) { /* swallow malformed JSON from a crashing browser */ }", + src, + ) + if patched == src: + # Already patched, or upstream changed -- either way, don't fail the build. + print(f"pipeTransport.js: no JSON.parse calls matched at {path}; skipping.") + else: + open(path, "w").write(patched) + print(f"pipeTransport.js: patched JSON.parse calls in {path}") + PY + - name: Reset auth + boot Studio run: | unsloth studio reset-password diff --git a/tests/studio/playwright_chat_ui.py b/tests/studio/playwright_chat_ui.py index e0316313bd..c7c9189eef 100644 --- a/tests/studio/playwright_chat_ui.py +++ b/tests/studio/playwright_chat_ui.py @@ -118,7 +118,26 @@ def parse_rgb(s): with sync_playwright() as p: - browser = p.chromium.launch(headless = True) + # Chromium stability args for macos-14 free runners. Without these + # Chromium browser process dies in the first few seconds (during + # change-password page load) and the driver Node process can't + # parse the truncated stdout JSON-RPC line, throwing + # 'SyntaxError: Unexpected end of JSON input' in pipeTransport.js + # — see runs 25491698868 / 25489049059. --disable-dev-shm-usage and + # --no-sandbox are the standard set; --disable-gpu forces software + # rendering on the headless runner; --single-process keeps the + # renderer in the same process as the browser, eliminating the + # browser↔renderer IPC pipe that was the actual crash site. + _CHROMIUM_STABILITY_ARGS = [ + "--disable-dev-shm-usage", + "--no-sandbox", + "--disable-gpu", + "--single-process", + ] + browser = p.chromium.launch( + headless = True, + args = _CHROMIUM_STABILITY_ARGS, + ) ctx = browser.new_context( viewport = {"width": 1280, "height": 900}, # Reduces motion so the theme toggle's view-transition diff --git a/tests/studio/playwright_extra_ui.py b/tests/studio/playwright_extra_ui.py index b71500a39d..72521b336b 100644 --- a/tests/studio/playwright_extra_ui.py +++ b/tests/studio/playwright_extra_ui.py @@ -81,7 +81,20 @@ def runtime_warn(m: str) -> None: with sync_playwright() as p: - browser = p.chromium.launch(headless = True) + # Chromium stability args -- same set as playwright_chat_ui.py. + # Without these Chromium dies in the first seconds on macos-14 + # free runners and pipeTransport.js throws + # 'SyntaxError: Unexpected end of JSON input'. + _CHROMIUM_STABILITY_ARGS = [ + "--disable-dev-shm-usage", + "--no-sandbox", + "--disable-gpu", + "--single-process", + ] + browser = p.chromium.launch( + headless = True, + args = _CHROMIUM_STABILITY_ARGS, + ) ctx = browser.new_context( viewport = {"width": 1280, "height": 900}, reduced_motion = "reduce",