mirror of https://github.com/garrytan/gstack.git
Merge 5f9d4d8761 into a3259400a3
This commit is contained in:
commit
58a796e2bb
102
setup
102
setup
|
|
@ -30,6 +30,82 @@ case "$(uname -s)" in
|
||||||
MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
|
MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
|
||||||
esac
|
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 ───────────────────────────────────
|
# ─── Symlink-or-copy helper ───────────────────────────────────
|
||||||
# On macOS/Linux: create a symlink (existing behavior).
|
# On macOS/Linux: create a symlink (existing behavior).
|
||||||
# On Windows without Developer Mode (MSYS2/Git Bash): plain ln -snf silently
|
# On Windows without Developer Mode (MSYS2/Git Bash): plain ln -snf silently
|
||||||
|
|
@ -250,17 +326,19 @@ if [ "$INSTALL_CODEX" -eq 1 ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ensure_playwright_browser() {
|
ensure_playwright_browser() {
|
||||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
local probe_timeout="${GSTACK_PLAYWRIGHT_PROBE_TIMEOUT_SECONDS:-15}"
|
||||||
# On Windows, Bun can't launch Chromium due to broken pipe handling
|
[ -n "${GSTACK_UNDER_TEST:-}" ] && probe_timeout="${GSTACK_PLAYWRIGHT_PROBE_TIMEOUT_SECONDS:-2}"
|
||||||
# (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
|
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"
|
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
|
else
|
||||||
(
|
(
|
||||||
cd "$SOURCE_GSTACK_DIR"
|
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
|
) >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +450,6 @@ cleanup_copied_bun() {
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_bun_for_windows_compile
|
prepare_bun_for_windows_compile
|
||||||
trap cleanup_copied_bun EXIT
|
|
||||||
|
|
||||||
# 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock)
|
# 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock)
|
||||||
NEEDS_BUILD=0
|
NEEDS_BUILD=0
|
||||||
|
|
@ -478,10 +555,15 @@ fi
|
||||||
# 2. Ensure Playwright's Chromium is available
|
# 2. Ensure Playwright's Chromium is available
|
||||||
if ! ensure_playwright_browser; then
|
if ! ensure_playwright_browser; then
|
||||||
echo "Installing Playwright Chromium..."
|
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"
|
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
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||||
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
|
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
|
||||||
|
|
@ -512,7 +594,7 @@ if ! ensure_playwright_browser; then
|
||||||
else
|
else
|
||||||
echo "gstack setup failed: Playwright Chromium could not be launched" >&2
|
echo "gstack setup failed: Playwright Chromium could not be launched" >&2
|
||||||
fi
|
fi
|
||||||
exit 1
|
echo " Browser-backed skills will be unavailable until Chromium setup succeeds." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux).
|
# 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