#!/bin/sh
# starts the localinfra stack the way e2e needs it, with the web as a production build.
# --check says whether it is up and its web build is current: 0 yes, 1 not up, 2 web out of date
# a live pc stack wins over compose, so this check has to come first
set -eu
here="$(cd "$(dirname "$0")" && pwd)"
cd "$here/.."
E2E_WEB_SRC="$(node "$here/harness/web-hash.ts")"
export E2E_WEB_SRC
pcdir="$PWD/localinfra/pc"
state="$(here="$pcdir" && . "$pcdir/bin/env.sh" && echo "$TNGL_STATE")"
sock="$state/run/pc.sock"
pc() { curl -sf --unix-socket "$sock" "$@"; }
pc_running() { pc http://localhost/live >/dev/null 2>&1; }
stack="${E2E_STACK:-}"
if [ -z "$stack" ]; then
if pc_running; then
stack=pc
elif command -v podman >/dev/null 2>&1 || command -v docker >/dev/null 2>&1; then
stack=compose
else
stack=pc
fi
fi
if [ "${1:-}" = "--stack" ]; then
echo "$stack"
exit
fi
# only the browser knows these names, so curl needs --resolve to reach caddy
alice_resolves() {
curl -sf --cacert localinfra/certs/root.crt --resolve pds.tngl.boltless.dev:443:127.0.0.1 \
"https://pds.tngl.boltless.dev/xrpc/com.atproto.identity.resolveHandle?handle=alice.pds.tngl.boltless.dev" >/dev/null
}
if [ "$stack" = pc ]; then
log="$state/up.log"
launcher=0
# a live stack only rebuilds the web. its other services have to come from this tree
backend="$(git log -1 --format=%H -- . ':(exclude)web' ':(exclude)e2e')"
# setup writes the source hash first, then the env checksum
built="$(cut -d ' ' -f 1 "$state/data/web/built" 2>/dev/null || true)"
if [ "${1:-}" = "--check" ]; then
pc_running || exit 1
[ "$built" = "$E2E_WEB_SRC" ] || exit 2
exit
fi
if pc_running; then
if [ "$(cat "$state/backend" 2>/dev/null)" != "$backend" ]; then
echo "the running stack's services are not from this tree, stop it with: curl --unix-socket $sock -X POST http://localhost/project/stop" >&2
exit 1
fi
if [ "$built" != "$E2E_WEB_SRC" ]; then
(here="$pcdir" && . "$pcdir/bin/env.sh" && sh "$pcdir/services/web/setup")
pc -X POST http://localhost/process/restart/web >/dev/null
# a restart keeps reporting the old exit code until the new process is up, so poll for ready
tries=0
until pc http://localhost/process/web | grep -q '"is_ready":"Ready"'; do
tries=$((tries + 1))
[ "$tries" -lt 300 ] || { echo "the web did not come back after its restart" >&2; exit 1; }
sleep 1
done
fi
else
mkdir -p "$state"
echo "$backend" >"$state/backend"
# pipeline specs need a spindle, and compose always runs one. the sites specs need the worker and sitesd
for profile in spindle sites; do
case ",${TNGL_PROFILES:-}," in
*,"$profile",*) ;;
*) TNGL_PROFILES=${TNGL_PROFILES:+$TNGL_PROFILES,}$profile ;;
esac
done
export TNGL_PROFILES
# not -D, linux port forwarders and slirp die with the foreground process
# own session, so a caller killing its process group doesn't take the stack down
own_session() {
if command -v setsid >/dev/null 2>&1; then exec setsid "$@"; fi
exec perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' "$@"
}
if command -v nix >/dev/null 2>&1; then
own_session nix run .#localinfra -- -t=false >"$log" 2>&1 &
else
own_session "$pcdir/bin/up" -t=false >"$log" 2>&1 &
fi
launcher=$!
fi
node -e '
const http = require("node:http");
const list = () =>
new Promise((resolve, reject) =>
http
.get({ socketPath: process.argv[1], path: "/processes" }, (res) => {
let body = "";
res.on("data", (chunk) => (body += chunk));
res.on("end", () => resolve(JSON.parse(body).data));
})
.on("error", reject)
);
const failed = (p) => p.status === "Error" || (p.status === "Completed" && p.exit_code !== 0);
const launcher = Number(process.argv[2]);
const deadline = Date.now() + Number(process.argv[3]) * 1000;
const launcherGone = () => {
if (!launcher) return false;
try {
process.kill(launcher, 0);
return false;
} catch {
return true;
}
};
(async () => {
for (;;) {
const all = await list().catch(() => []);
const bad = all.find(failed);
if (bad) {
console.error(`${bad.name} failed, its log is in the stack`);
process.exit(1);
}
if (all.length && all.every((p) => p.status === "Completed" || p.is_ready === "Ready")) return;
if (launcherGone() || Date.now() > deadline) {
console.error("the stack did not come up");
process.exit(1);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
})();
' "$sock" "$launcher" "${E2E_UP_TIMEOUT:-3600}" || {
[ "$launcher" = 0 ] || tail -n 30 "$log" >&2
exit 1
}
if ! alice_resolves; then
echo "alice doesn't resolve, the stack's init-data hasn't run" >&2
exit 1
fi
exit
fi
# the web is a production build here, so no page waits on a module compiling when it is first asked for
web="-f localinfra/docker-compose.web-build.yml"
if command -v podman >/dev/null 2>&1; then
engine=podman
compose="podman compose -f docker-compose.yml $web"
else
# docker doesn't publish ports from an internal network, which caddy sits on
engine=docker
compose="docker compose -f docker-compose.yml -f localinfra/docker-compose.docker.yml $web"
fi
if [ "${1:-}" = "--check" ]; then
id=$($compose ps --status running -q web 2>/dev/null || true)
[ -n "$id" ] || exit 1
# the dev server answers too, but every first page waits on it compiling
$engine inspect --format '{{join .Config.Cmd " "}}' "$id" | grep -q "vite build" || exit 1
built="$($engine inspect --format '{{index .Config.Labels "e2e.web-src"}}' "$id")"
[ "$built" = "$E2E_WEB_SRC" ] || exit 2
exit
fi
if [ ! -f localinfra/certs/root.crt ]; then
echo "no dev ca yet, run ./localinfra/scripts/make-certs.sh and trust localinfra/certs/root.crt" >&2
exit 1
fi
# compose only recreates the web when this label value changes
$compose up -d --wait
if ! alice_resolves; then
echo "alice doesn't resolve, the stack's init-data hasn't run" >&2
exit 1
fi