mirror of https://github.com/garrytan/gstack.git
fix(setup): bound Playwright bootstrap
This commit is contained in:
parent
7c9df1c568
commit
5f9d4d8761
102
setup
102
setup
|
|
@ -30,6 +30,82 @@ case "$(uname -s)" in
|
|||
MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
|
||||
esac
|
||||
|
||||
# Only one setup may mutate an install/HOME at a time. An atomic directory is
|
||||
# portable to macOS, Linux, and Git Bash (unlike flock), and the pid lets a
|
||||
# later run recover after a crash.
|
||||
SETUP_LOCK_DIR="${GSTACK_HOME:-$HOME/.gstack}/.setup.lock.d"
|
||||
mkdir -p "$(dirname "$SETUP_LOCK_DIR")"
|
||||
SETUP_LOCK_ACQUIRED=0
|
||||
if mkdir "$SETUP_LOCK_DIR" 2>/dev/null; then
|
||||
SETUP_LOCK_ACQUIRED=1
|
||||
else
|
||||
_setup_lock_pid="$(cat "$SETUP_LOCK_DIR/pid" 2>/dev/null || true)"
|
||||
if [ -n "$_setup_lock_pid" ] && ! kill -0 "$_setup_lock_pid" 2>/dev/null; then
|
||||
rm -rf "$SETUP_LOCK_DIR"
|
||||
if mkdir "$SETUP_LOCK_DIR" 2>/dev/null; then
|
||||
SETUP_LOCK_ACQUIRED=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$SETUP_LOCK_ACQUIRED" -ne 1 ]; then
|
||||
echo "gstack setup is already running for this install; waiting is not necessary." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$$" > "$SETUP_LOCK_DIR/pid"
|
||||
|
||||
cleanup_setup() {
|
||||
if command -v cleanup_copied_bun >/dev/null 2>&1; then
|
||||
cleanup_copied_bun
|
||||
fi
|
||||
if [ "$(cat "$SETUP_LOCK_DIR/pid" 2>/dev/null || true)" = "$$" ]; then
|
||||
rm -rf "$SETUP_LOCK_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup_setup EXIT
|
||||
|
||||
# Kill a command and its descendants after a deadline. `timeout` is absent on
|
||||
# stock macOS, so setup cannot rely on coreutils being installed yet.
|
||||
terminate_process_tree() {
|
||||
local parent="$1" child
|
||||
if [ "$IS_WINDOWS" -eq 1 ] && command -v taskkill.exe >/dev/null 2>&1; then
|
||||
taskkill.exe //PID "$parent" //T //F >/dev/null 2>&1 || true
|
||||
return
|
||||
fi
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
for child in $(pgrep -P "$parent" 2>/dev/null || true); do
|
||||
terminate_process_tree "$child"
|
||||
done
|
||||
fi
|
||||
kill -TERM "$parent" 2>/dev/null || true
|
||||
sleep 0.1
|
||||
kill -KILL "$parent" 2>/dev/null || true
|
||||
}
|
||||
|
||||
run_with_deadline() {
|
||||
local seconds="$1" marker pid watcher status
|
||||
shift
|
||||
marker="${TMPDIR:-/tmp}/gstack-setup-timeout.$$.$RANDOM"
|
||||
rm -f "$marker"
|
||||
"$@" &
|
||||
pid=$!
|
||||
(
|
||||
sleep "$seconds"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
terminate_process_tree "$pid"
|
||||
: > "$marker"
|
||||
fi
|
||||
) &
|
||||
watcher=$!
|
||||
if wait "$pid"; then status=0; else status=$?; fi
|
||||
kill "$watcher" 2>/dev/null || true
|
||||
wait "$watcher" 2>/dev/null || true
|
||||
if [ -f "$marker" ]; then
|
||||
rm -f "$marker"
|
||||
return 124
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# ─── Symlink-or-copy helper ───────────────────────────────────
|
||||
# On macOS/Linux: create a symlink (existing behavior).
|
||||
# On Windows without Developer Mode (MSYS2/Git Bash): plain ln -snf silently
|
||||
|
|
@ -250,17 +326,19 @@ if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|||
fi
|
||||
|
||||
ensure_playwright_browser() {
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
# On Windows, Bun can't launch Chromium due to broken pipe handling
|
||||
# (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
|
||||
local probe_timeout="${GSTACK_PLAYWRIGHT_PROBE_TIMEOUT_SECONDS:-15}"
|
||||
[ -n "${GSTACK_UNDER_TEST:-}" ] && probe_timeout="${GSTACK_PLAYWRIGHT_PROBE_TIMEOUT_SECONDS:-2}"
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ "$(uname -s)" = "Darwin" ]; then
|
||||
# Bun can hang while launching Chromium because of pipe handling on Windows
|
||||
# and macOS. Node is also what the Playwright CLI itself uses.
|
||||
(
|
||||
cd "$SOURCE_GSTACK_DIR"
|
||||
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
|
||||
run_with_deadline "$probe_timeout" node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
|
||||
)
|
||||
else
|
||||
(
|
||||
cd "$SOURCE_GSTACK_DIR"
|
||||
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
|
||||
run_with_deadline "$probe_timeout" bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
|
||||
) >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
|
@ -372,7 +450,6 @@ cleanup_copied_bun() {
|
|||
}
|
||||
|
||||
prepare_bun_for_windows_compile
|
||||
trap cleanup_copied_bun EXIT
|
||||
|
||||
# 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock)
|
||||
NEEDS_BUILD=0
|
||||
|
|
@ -478,10 +555,15 @@ fi
|
|||
# 2. Ensure Playwright's Chromium is available
|
||||
if ! ensure_playwright_browser; then
|
||||
echo "Installing Playwright Chromium..."
|
||||
(
|
||||
_playwright_install_timeout="${GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT_SECONDS:-300}"
|
||||
[ -n "${GSTACK_UNDER_TEST:-}" ] && _playwright_install_timeout="${GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT_SECONDS:-2}"
|
||||
if ! (
|
||||
cd "$SOURCE_GSTACK_DIR"
|
||||
bunx playwright install chromium
|
||||
)
|
||||
run_with_deadline "$_playwright_install_timeout" bunx playwright install chromium
|
||||
); then
|
||||
echo " warning: Chromium installation failed or timed out after ${_playwright_install_timeout}s." >&2
|
||||
echo " Skills will still be registered; rerun ./setup to retry browser installation." >&2
|
||||
fi
|
||||
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
|
||||
|
|
@ -512,7 +594,7 @@ if ! ensure_playwright_browser; then
|
|||
else
|
||||
echo "gstack setup failed: Playwright Chromium could not be launched" >&2
|
||||
fi
|
||||
exit 1
|
||||
echo " Browser-backed skills will be unavailable until Chromium setup succeeds." >&2
|
||||
fi
|
||||
|
||||
# 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import { describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const SETUP = readFileSync(join(import.meta.dir, '..', 'setup'), 'utf8');
|
||||
|
||||
describe('setup Playwright failure containment', () => {
|
||||
test('serializes setup with a portable stale-pid lock', () => {
|
||||
expect(SETUP).toContain('SETUP_LOCK_DIR="${GSTACK_HOME:-$HOME/.gstack}/.setup.lock.d"');
|
||||
expect(SETUP).toContain('if mkdir "$SETUP_LOCK_DIR" 2>/dev/null; then');
|
||||
expect(SETUP).toContain('kill -0 "$_setup_lock_pid"');
|
||||
expect(SETUP).toContain('gstack setup is already running for this install');
|
||||
expect(SETUP).toContain('trap cleanup_setup EXIT');
|
||||
});
|
||||
|
||||
test('bounds browser launch and install and kills descendants', () => {
|
||||
expect(SETUP).toContain('run_with_deadline "$probe_timeout" node -e');
|
||||
expect(SETUP).toContain('run_with_deadline "$probe_timeout" bun --eval');
|
||||
expect(SETUP).toContain('run_with_deadline "$_playwright_install_timeout" bunx playwright install chromium');
|
||||
expect(SETUP).toContain('terminate_process_tree "$child"');
|
||||
expect(SETUP).toContain('GSTACK_PLAYWRIGHT_PROBE_TIMEOUT_SECONDS');
|
||||
expect(SETUP).toContain('GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT_SECONDS');
|
||||
});
|
||||
|
||||
test('uses Node for the macOS launch probe', () => {
|
||||
expect(SETUP).toContain('[ "$IS_WINDOWS" -eq 1 ] || [ "$(uname -s)" = "Darwin" ]');
|
||||
});
|
||||
|
||||
test('continues to skill registration after Chromium failure', () => {
|
||||
const failure = SETUP.indexOf('Browser-backed skills will be unavailable until Chromium setup succeeds.');
|
||||
const registration = SETUP.indexOf('link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"');
|
||||
expect(failure).toBeGreaterThan(-1);
|
||||
expect(registration).toBeGreaterThan(failure);
|
||||
expect(SETUP.slice(failure, registration)).not.toMatch(/^\s*exit 1\s*$/m);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue