Files
5chan/scripts/pw-session.sh
T
Tommaso Casaburi 1a33f7dc88 chore(agents): add a machine-wide Playwright browser resource budget
Playwright disables normal background throttling, so a hidden 5chan page keeps
doing P2P and rendering work after a check finishes. Agents verifying in
parallel across worktrees stacked whole browser engines on one machine.

Add scripts/pw-session.sh, a wrapper that permits one active Playwright browser
at a time and records who holds it:

- The lock is machine-wide, not per-repository, because the contended resource
  is RAM and CPU. Every worktree and checkout shares one slot.
- Acquisition is an atomic mkdir. Stale locks clear themselves: `open` reclaims
  any slot whose recorded browser is no longer `status: open` in
  `playwright-cli list --all`, so an interrupted workflow cannot strand the
  budget. When that list cannot be read the lock is left alone, so a broken CLI
  never silently disables the budget.
- `open` exits 75 when the slot is busy; `--wait[=SECONDS]` blocks instead.
- `close` always stops the browser, even when the lock was already lost, and
  never releases a slot held by a different session.
- `status` reports the holder and whether its browser is still alive.

Agent policy now runs browser engines and profiler batches sequentially, uses
Chrome/Blink during iteration and the full engine matrix only for final
verification, and never uses `close-all` or `kill-all` while other agents may
own sessions.

Covered by scripts/pw-session.test.js.
2026-08-01 19:21:01 +02:00

319 lines
9.5 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
umask 077
# pw-session.sh — shared resource lock for playwright-cli browser sessions.
#
# Playwright disables normal background throttling, so a hidden 5chan page keeps
# doing P2P and rendering work after a check finishes. Agents verifying in
# parallel therefore stack whole browser engines on one machine. This wrapper
# permits one active Playwright browser at a time and records who holds it.
#
# The lock is machine-wide, not per-repository: the contended resource is RAM and
# CPU, so every checkout that ships this script shares a single slot. Set
# PLAYWRIGHT_RESOURCE_LOCK_DIR to isolate a lock (tests, or a deliberate second
# slot on a machine with headroom).
#
# Liveness comes from `playwright-cli list --all`, which reports `status: open`
# for a running browser. A lock whose recorded session is no longer open is
# stale, and is reclaimed automatically rather than blocking every later
# workflow. When playwright-cli cannot be queried the lock is left alone, so a
# broken CLI never silently disables the budget.
#
# PW_SESSION_POLL_SECONDS overrides how often `--wait` re-checks the slot.
usage() {
cat <<'EOF'
Usage:
./scripts/pw-session.sh open [--wait[=SECONDS]] <session> [playwright-cli open arguments...]
./scripts/pw-session.sh close <session>
./scripts/pw-session.sh status
./scripts/pw-session.sh release <session>
One browser slot is shared by every worktree and repository on this machine.
open Acquire the slot, then start the browser. Exits 75 when the slot is
held by a live session; --wait polls until it frees (default 300s).
A slot whose browser is gone is reclaimed automatically.
close Stop the named browser, then release the slot. Always attempts the
browser close, even when the lock was already lost, and never
releases a slot held by a different session.
status Report the holder and whether its browser is still running.
release Drop a lock without closing a browser. Normal cleanup uses `close`.
EOF
}
playwright_cli="${PLAYWRIGHT_CLI_BIN:-playwright-cli}"
lock_root="${XDG_CACHE_HOME:-$HOME/.cache}/bitsocial"
lock_dir="${PLAYWRIGHT_RESOURCE_LOCK_DIR:-$lock_root/playwright-session.lock}"
owner_file="$lock_dir/owner"
started_file="$lock_dir/started-at"
workspace_file="$lock_dir/workspace"
default_wait_seconds=300
poll_seconds="${PW_SESSION_POLL_SECONDS:-5}"
# Recorded for diagnostics only: the lock is machine-wide, so a checkout outside
# a Git worktree is unusual but not an error.
workspace="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
validate_session() {
local session="$1"
if [[ ! "$session" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$ ]]; then
echo "pw-session: session must be 1-40 characters using letters, numbers, '.', '_', or '-'" >&2
exit 1
fi
}
current_owner() {
if [ -f "$owner_file" ]; then
sed -n '1p' "$owner_file"
fi
}
# Echoes `live`, `dead`, or `unknown` for a session name. `unknown` means the
# browser list could not be read, and callers must treat the lock as held.
session_state() {
local session="$1" listing line current=''
if ! listing="$("$playwright_cli" list --all 2>/dev/null)"; then
echo unknown
return 0
fi
# `playwright-cli list --all` prints a `- <session>:` header per browser,
# followed by indented fields including ` - status: open|closed`.
while IFS= read -r line; do
case "$line" in
'- '*':')
current="${line#- }"
current="${current%:}"
;;
' - status: open')
if [ "$current" = "$session" ]; then
echo live
return 0
fi
;;
esac
done <<<"$listing"
echo dead
}
write_lock_metadata() {
printf '%s\n' "$1" >"$owner_file"
date -u '+%Y-%m-%dT%H:%M:%SZ' >"$started_file"
printf '%s\n' "$workspace" >"$workspace_file"
}
print_status() {
local owner started held_workspace state
if [ ! -d "$lock_dir" ]; then
echo "pw-session: browser slot is available"
return 0
fi
owner="$(current_owner)"
started="$(sed -n '1p' "$started_file" 2>/dev/null || true)"
held_workspace="$(sed -n '1p' "$workspace_file" 2>/dev/null || true)"
state="$([ -n "$owner" ] && session_state "$owner" || echo unknown)"
case "$state" in
live) echo "pw-session: browser slot is held" ;;
dead) echo "pw-session: browser slot is held by a stale lock" ;;
*) echo "pw-session: browser slot is held (browser state unverifiable)" ;;
esac
echo "Session: ${owner:-unknown}"
echo "Started: ${started:-unknown}"
echo "Workspace: ${held_workspace:-unknown}"
echo "Lock: $lock_dir"
case "$state" in
live) echo "Browser: running" ;;
dead)
echo "Browser: not running — the next 'open' reclaims this slot automatically"
;;
*)
echo "Browser: unverifiable — '$playwright_cli list --all' failed, so the lock is left alone"
;;
esac
}
# Atomically drop a lock we have confirmed is stale. Renaming first means only
# one racing reclaimer can win, so a concurrent fresh lock is never deleted.
reclaim_stale_lock() {
local owner="$1" staged="${lock_dir}.stale.$$"
if mv "$lock_dir" "$staged" 2>/dev/null; then
rm -rf "$staged"
echo "pw-session: reclaimed stale slot from '$owner' (its browser is no longer running)" >&2
fi
}
acquire() {
local session="$1" wait_seconds="$2" owner state reclaims=0
validate_session "$session"
mkdir -p "$(dirname "$lock_dir")"
SECONDS=0
while true; do
if mkdir "$lock_dir" 2>/dev/null; then
write_lock_metadata "$session"
echo "pw-session: acquired browser slot for '$session'"
return 0
fi
owner="$(current_owner)"
state="$([ -n "$owner" ] && session_state "$owner" || echo dead)"
# Bounded so an unremovable lock directory fails loudly instead of spinning.
if [ "$state" = dead ] && [ "$reclaims" -lt 3 ]; then
reclaims=$((reclaims + 1))
reclaim_stale_lock "${owner:-unknown}"
continue
fi
if [ "$state" = dead ]; then
print_status >&2
echo "pw-session: could not reclaim the stale slot at $lock_dir; remove it by hand" >&2
return 75
fi
if [ "$wait_seconds" -gt 0 ] && [ "$SECONDS" -lt "$wait_seconds" ]; then
echo "pw-session: slot held by '$owner'; retrying in ${poll_seconds}s (waited ${SECONDS}s of ${wait_seconds}s)" >&2
sleep "$poll_seconds"
continue
fi
print_status >&2
if [ "$wait_seconds" -gt 0 ]; then
echo "pw-session: gave up after ${wait_seconds}s; do not bypass the lock" >&2
else
echo "pw-session: another browser workflow is active; do not bypass the lock" >&2
fi
return 75
done
}
release() {
local session="$1" owner
validate_session "$session"
if [ ! -d "$lock_dir" ]; then
echo "pw-session: browser slot is already available"
return 0
fi
owner="$(current_owner)"
if [ "$owner" != "$session" ]; then
echo "pw-session: '$session' cannot release the slot held by '${owner:-unknown}'" >&2
if [ -n "$owner" ] && [ "$(session_state "$owner")" = dead ]; then
echo "pw-session: that lock is stale; the next 'open' reclaims it automatically" >&2
fi
return 1
fi
rm -f "$owner_file" "$started_file" "$workspace_file"
rmdir "$lock_dir"
echo "pw-session: released browser slot for '$session'"
}
command="${1:-}"
case "$command" in
open)
shift
wait_seconds=0
session=''
open_args=()
# `--wait` is accepted anywhere so `open <session> --wait` is not a silent
# no-op. `playwright-cli open` has no --wait of its own, so nothing that
# belongs to it is swallowed here. The first bare argument is the session;
# the rest pass through untouched.
while [ "$#" -gt 0 ]; do
case "$1" in
--wait)
wait_seconds="$default_wait_seconds"
;;
--wait=*)
wait_seconds="${1#--wait=}"
if [[ ! "$wait_seconds" =~ ^[0-9]+$ ]]; then
echo "pw-session: --wait expects a whole number of seconds" >&2
exit 1
fi
;;
*)
if [ -z "$session" ]; then
session="$1"
else
open_args+=("$1")
fi
;;
esac
shift
done
if [ -z "$session" ]; then
usage >&2
exit 1
fi
acquire "$session" "$wait_seconds"
# Guarded expansion: Bash 3.2 (macOS /bin/bash) errors on an empty array
# under `set -u`.
if ! "$playwright_cli" -s="$session" open ${open_args[@]+"${open_args[@]}"}; then
release "$session"
exit 1
fi
;;
close)
session="${2:-}"
if [ -z "$session" ] || [ "$#" -ne 2 ]; then
usage >&2
exit 1
fi
validate_session "$session"
# Cleanup must always stop the browser, even when the lock was lost, so a
# failed workflow cannot strand a running engine.
owner="$(current_owner)"
close_status=0
"$playwright_cli" -s="$session" close || close_status=$?
if [ "$close_status" -ne 0 ]; then
echo "pw-session: warning: closing browser '$session' exited $close_status" >&2
fi
if [ -z "$owner" ]; then
echo "pw-session: browser slot was already free; closed '$session' anyway"
elif [ "$owner" = "$session" ]; then
release "$session"
else
echo "pw-session: closed '$session'; left the slot held by '$owner' untouched" >&2
fi
;;
status)
if [ "$#" -ne 1 ]; then
usage >&2
exit 1
fi
print_status
;;
release)
session="${2:-}"
if [ -z "$session" ] || [ "$#" -ne 2 ]; then
usage >&2
exit 1
fi
release "$session"
;;
*)
usage >&2
exit 1
;;
esac