|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Owns one pinned WebKit server; start prints its loopback endpoint, stop preserves its logs. |
| 3 | +set -euo pipefail |
| 4 | +umask 077 |
| 5 | + |
| 6 | +action=${1:-} |
| 7 | +state_argument=${2:-} |
| 8 | +if [ "$#" -ne 2 ] || { [ "$action" != start ] && [ "$action" != stop ]; }; then |
| 9 | + echo 'Usage: playwright-server.sh <start|stop> <state-directory-under-RUNNER_TEMP>' >&2 |
| 10 | + exit 2 |
| 11 | +fi |
| 12 | +: "${RUNNER_TEMP:?RUNNER_TEMP must be set}" |
| 13 | +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" |
| 14 | +: "${GITHUB_RUN_ID:?GITHUB_RUN_ID must be set}" |
| 15 | +: "${GITHUB_RUN_ATTEMPT:?GITHUB_RUN_ATTEMPT must be set}" |
| 16 | +: "${GITHUB_JOB:?GITHUB_JOB must be set}" |
| 17 | +if ! [[ "$GITHUB_RUN_ID" =~ ^[1-9][0-9]*$ && "$GITHUB_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ && "$GITHUB_JOB" =~ ^[a-zA-Z0-9_-]+$ ]]; then |
| 18 | + echo 'Invalid GitHub job ownership metadata.' >&2 |
| 19 | + exit 2 |
| 20 | +fi |
| 21 | +state=$(node - "$RUNNER_TEMP" "$state_argument" <<'JS' |
| 22 | +const fs = require('node:fs') |
| 23 | +const path = require('node:path') |
| 24 | +const root = fs.realpathSync(process.argv[2]) |
| 25 | +const state = path.resolve(process.argv[3]) |
| 26 | +if (path.dirname(state) !== root || (fs.existsSync(state) && fs.realpathSync(state) !== state)) |
| 27 | + throw new Error('State must be a direct, non-symlink child of RUNNER_TEMP') |
| 28 | +process.stdout.write(state) |
| 29 | +JS |
| 30 | +) |
| 31 | +state_key=$(node -e 'process.stdout.write(require("node:crypto").createHash("sha256").update(process.argv[1]).digest("hex"))' "$state") |
| 32 | +image='mcr.microsoft.com/playwright@sha256:2f05f8a734509a7b3f26a912639488ee08f58a7b292d14d62271bcff9038768f' |
| 33 | +starting=false |
| 34 | + |
| 35 | +stop_container() { |
| 36 | + local cid inspection remaining status=0 |
| 37 | + [ -f "$state/cid" ] || return 0 |
| 38 | + cid=$(< "$state/cid") |
| 39 | + if ! [[ "$cid" =~ ^[0-9a-f]{64}$ ]]; then |
| 40 | + echo 'Refusing to stop an invalid container ID.' >&2 |
| 41 | + return 1 |
| 42 | + fi |
| 43 | + if ! inspection=$(docker container inspect "$cid" 2> "$state/inspect-error.log"); then |
| 44 | + remaining=$(docker container ls --all --no-trunc --filter "id=$cid" --format '{{.ID}}') || return 1 |
| 45 | + [ -z "$remaining" ] && return 0 |
| 46 | + cat "$state/inspect-error.log" >&2 |
| 47 | + return 1 |
| 48 | + fi |
| 49 | + if ! jq -e --arg repository "$GITHUB_REPOSITORY" --arg run "$GITHUB_RUN_ID" \ |
| 50 | + --arg attempt "$GITHUB_RUN_ATTEMPT" --arg job "$GITHUB_JOB" --arg state "$state_key" \ |
| 51 | + '.[0].Config.Labels | .["com.sim.ci.playwright-server"] == "true" |
| 52 | + and .["com.sim.ci.repository"] == $repository and .["com.sim.ci.run"] == $run |
| 53 | + and .["com.sim.ci.attempt"] == $attempt and .["com.sim.ci.job"] == $job |
| 54 | + and .["com.sim.ci.state"] == $state' <<< "$inspection" > /dev/null; then |
| 55 | + echo 'Refusing to stop a container owned by another job or state directory.' >&2 |
| 56 | + return 1 |
| 57 | + fi |
| 58 | + docker stop --timeout 10 "$cid" > "$state/stop.log" 2>&1 || status=1 |
| 59 | + docker logs "$cid" > "$state/server.log" 2>&1 || status=1 |
| 60 | + docker rm "$cid" > "$state/remove.log" 2>&1 || status=1 |
| 61 | + return "$status" |
| 62 | +} |
| 63 | + |
| 64 | +trap ' |
| 65 | + status=$? |
| 66 | + trap - EXIT |
| 67 | + if [ "$status" -ne 0 ] && [ "$starting" = true ]; then |
| 68 | + stop_container || echo "Owned Playwright server cleanup failed; preserve its state and logs." >&2 |
| 69 | + fi |
| 70 | + exit "$status" |
| 71 | +' EXIT |
| 72 | +trap 'exit 130' INT |
| 73 | +trap 'exit 143' TERM |
| 74 | + |
| 75 | +if [ "$action" = stop ]; then |
| 76 | + stop_container |
| 77 | + exit 0 |
| 78 | +fi |
| 79 | +if [ -e "$state/cid" ]; then |
| 80 | + echo 'Refusing to reuse an existing Playwright server state.' >&2 |
| 81 | + exit 1 |
| 82 | +fi |
| 83 | +mkdir -p -- "$state" |
| 84 | +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) |
| 85 | +driver=$(node - "$repo_root/apps/sim" <<'JS' |
| 86 | +const fs = require('node:fs') |
| 87 | +const path = require('node:path') |
| 88 | +const packagePath = require.resolve('playwright-core/package.json', { paths: [process.argv[2]] }) |
| 89 | +const root = fs.realpathSync(path.dirname(packagePath)) |
| 90 | +const manifest = JSON.parse(fs.readFileSync(path.join(root, 'browsers.json'), 'utf8')) |
| 91 | +if (JSON.parse(fs.readFileSync(packagePath, 'utf8')).version !== '1.64.1' || |
| 92 | + manifest.browsers.find(browser => browser.name === 'webkit')?.revision !== '2370') |
| 93 | + throw new Error('The pinned WebKit image requires Playwright 1.64.1, revision 2370') |
| 94 | +process.stdout.write(root) |
| 95 | +JS |
| 96 | +) |
| 97 | +docker pull "$image" > "$state/pull.log" 2>&1 |
| 98 | +starting=true |
| 99 | +docker run --detach --init --cidfile "$state/cid" \ |
| 100 | + --name "sim-ci-playwright-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$GITHUB_JOB-$$" \ |
| 101 | + --label com.sim.ci.playwright-server=true --label "com.sim.ci.repository=$GITHUB_REPOSITORY" \ |
| 102 | + --label "com.sim.ci.run=$GITHUB_RUN_ID" --label "com.sim.ci.attempt=$GITHUB_RUN_ATTEMPT" \ |
| 103 | + --label "com.sim.ci.job=$GITHUB_JOB" --label "com.sim.ci.state=$state_key" \ |
| 104 | + --network host --cpus 8 --memory 4g --memory-swap 4g --pids-limit 512 --shm-size 256m \ |
| 105 | + --mount "type=bind,src=$driver,dst=/opt/playwright-core,readonly" \ |
| 106 | + "$image" node /opt/playwright-core/cli.js run-server --host 127.0.0.1 --port 0 \ |
| 107 | + > "$state/start.log" 2>&1 |
| 108 | +cid=$(< "$state/cid") |
| 109 | +if ! [[ "$cid" =~ ^[0-9a-f]{64}$ ]]; then |
| 110 | + echo 'Docker did not return a complete container ID.' >&2 |
| 111 | + exit 1 |
| 112 | +fi |
| 113 | +docker container inspect "$cid" > "$state/metadata.json" |
| 114 | +deadline=$((SECONDS + 30)) |
| 115 | +while [ "$SECONDS" -lt "$deadline" ]; do |
| 116 | + live=$(docker container inspect --format '{{.State.Running}} {{.State.Pid}}' "$cid") |
| 117 | + if ! [[ "$live" =~ ^true\ [1-9][0-9]*$ ]]; then |
| 118 | + docker logs "$cid" > "$state/server.log" 2>&1 |
| 119 | + echo 'Playwright server exited before readiness.' >&2 |
| 120 | + exit 1 |
| 121 | + fi |
| 122 | + docker logs "$cid" > "$state/server.log" 2>&1 |
| 123 | + endpoint=$(sed -n 's@^Listening on \(ws://127\.0\.0\.1:[1-9][0-9]*/\)$@\1@p' "$state/server.log") |
| 124 | + if [ -n "$endpoint" ]; then |
| 125 | + node - "$endpoint" <<'JS' |
| 126 | +const endpoint = process.argv[2] |
| 127 | +if (!/^ws:\/\/127\.0\.0\.1:[1-9]\d*\/$/.test(endpoint)) throw new Error('Invalid server endpoint') |
| 128 | +new URL(endpoint) |
| 129 | +process.stdout.write(`${endpoint}\n`) |
| 130 | +JS |
| 131 | + exit 0 |
| 132 | + fi |
| 133 | + sleep 0.1 |
| 134 | +done |
| 135 | +echo 'Playwright server did not report a live loopback endpoint within 30 seconds.' >&2 |
| 136 | +exit 1 |
0 commit comments