ci(mac): single-process Chromium + JSON.parse try/catch in pipeTransport

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.
This commit is contained in:
Daniel Han 2026-05-07 11:05:32 +00:00
parent a810e8fbbb
commit fdf7f94f46
3 changed files with 71 additions and 7 deletions

View file

@ -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

View file

@ -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

View file

@ -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",