#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck source=scripts/local-debug/env.sh source "$ROOT_DIR/scripts/local-debug/env.sh" mkdir -p "$LOCAL_DEBUG_LOG_DIR" "$LOCAL_DEBUG_PID_DIR" "$PLATFORM_DATA_DIR" "$PLATFORM_LOG_DIR" "$RUN_WORKSPACE_ROOT" "$RUN_SPOOL_ROOT" "$RUN_BUILD_BUCKET_ROOT" mkdir -p "$GOCACHE" local_debug_prepare_run_lifecycle_templates local_debug_prepare_distribution_builder managed_pid_running() { local pid_file="$1" if [[ ! -f "$pid_file" ]]; then return 1 fi local pid pid="$(cat "$pid_file")" [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null } pid_is_descendant_of() { local pid="$1" local ancestor="$2" local parent while [[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 1 ]]; do if [[ "$pid" == "$ancestor" ]]; then return 0 fi parent="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d '[:space:]')" [[ -n "$parent" && "$parent" != "$pid" ]] || return 1 pid="$parent" done return 1 } port_listener_pid() { local port="$1" lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n 1 || true } listener_owned_by_local_debug() { local pid="$1" local details details="$(lsof -nP -p "$pid" 2>/dev/null || true)" [[ "$details" == *"$ROOT_DIR"* || "$details" == *"$LOCAL_DEBUG_ROOT"* ]] } wait_for_pid_exit() { local pid="$1" for _ in $(seq 1 20); do if ! kill -0 "$pid" 2>/dev/null; then return 0 fi sleep 0.25 done return 1 } descendant_pids() { local pid="$1" local child while IFS= read -r child; do [[ -n "$child" ]] || continue printf '%s\n' "$child" descendant_pids "$child" done < <(pgrep -P "$pid" 2>/dev/null || true) } stop_pid_tree() { local name="$1" local root_pid="$2" local pids local pid local forced=0 [[ "$root_pid" =~ ^[0-9]+$ ]] || return 0 pids="$(descendant_pids "$root_pid"; printf '%s\n' "$root_pid")" for pid in $pids; do kill "$pid" 2>/dev/null || true done for pid in $pids; do if ! wait_for_pid_exit "$pid"; then forced=1 fi done if [[ "$forced" -eq 1 ]]; then printf '%s process tree rooted at pid %s did not stop after SIGTERM; sending SIGKILL\n' "$name" "$root_pid" for pid in $pids; do kill -9 "$pid" 2>/dev/null || true done for pid in $pids; do wait_for_pid_exit "$pid" || true done fi } stop_orphaned_run_bootstrap() { local pid_file="$LOCAL_DEBUG_PID_DIR/run.pid" local pid if managed_pid_running "$pid_file"; then return 0 fi while IFS= read -r pid; do [[ "$pid" =~ ^[0-9]+$ ]] || continue [[ "$pid" != "$$" ]] || continue if kill -0 "$pid" 2>/dev/null; then printf 'stopping stale run bootstrap pid %s before start\n' "$pid" stop_pid_tree "run bootstrap" "$pid" fi done < <(pgrep -f "$RUN_BOOTSTRAP_BIN" 2>/dev/null || true) } ensure_port_available() { local name="$1" local port="$2" local pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid" local listener_pid listener_pid="$(port_listener_pid "$port")" if [[ -z "$listener_pid" ]]; then return 0 fi if managed_pid_running "$pid_file"; then local managed_pid managed_pid="$(cat "$pid_file")" if pid_is_descendant_of "$listener_pid" "$managed_pid"; then return 0 fi fi if listener_owned_by_local_debug "$listener_pid"; then printf 'stopping stale %s listener pid %s on port %s\n' "$name" "$listener_pid" "$port" kill "$listener_pid" 2>/dev/null || true if ! wait_for_pid_exit "$listener_pid"; then printf 'stale %s listener pid %s did not stop after SIGTERM; sending SIGKILL\n' "$name" "$listener_pid" kill -9 "$listener_pid" 2>/dev/null || true wait_for_pid_exit "$listener_pid" || true fi rm -f "$pid_file" return 0 fi printf '%s port %s is already used by pid %s; choose another LOCAL_DEBUG_*_PORT or stop the existing listener\n' "$name" "$port" "$listener_pid" >&2 return 1 } start_service() { local name="$1" local service_dir="$2" local pid_file="$LOCAL_DEBUG_PID_DIR/$name.pid" shift 2 if [[ -f "$pid_file" ]]; then local existing_pid existing_pid="$(cat "$pid_file")" if kill -0 "$existing_pid" 2>/dev/null; then printf '%s already running with pid %s\n' "$name" "$existing_pid" return 0 fi rm -f "$pid_file" fi printf 'starting %s\n' "$name" ( cd "$service_dir" exec nohup "$@" >"$LOCAL_DEBUG_LOG_DIR/$name.log" 2>&1 ) >/dev/null 2>&1 & printf '%s' "$!" >"$pid_file" printf '%s pid %s log %s\n' "$name" "$(cat "$pid_file")" "$LOCAL_DEBUG_LOG_DIR/$name.log" } wait_for_url() { local name="$1" local url="$2" local attempts="${3:-30}" printf 'waiting for %s at %s\n' "$name" "$url" for _ in $(seq 1 "$attempts"); do if curl -fsS "$url" >/dev/null 2>&1; then printf '%s is ready\n' "$name" return 0 fi sleep 1 done printf '%s did not become ready at %s\n' "$name" "$url" >&2 return 1 } wait_for_run_registration() { local attempts="${1:-45}" local run_pid_file="$LOCAL_DEBUG_PID_DIR/run.pid" printf 'waiting for run endpoint %s registration\n' "$RUN_ENDPOINT_ID" for _ in $(seq 1 "$attempts"); do if ! managed_pid_running "$run_pid_file"; then printf 'run exited before endpoint registration; see %s\n' "$LOCAL_DEBUG_LOG_DIR/run.log" >&2 return 1 fi if run_endpoint_has_recent_heartbeat; then printf 'run endpoint %s is registered\n' "$RUN_ENDPOINT_ID" return 0 fi sleep 1 done printf 'run endpoint %s did not register before timeout\n' "$RUN_ENDPOINT_ID" >&2 return 1 } run_endpoint_has_recent_heartbeat() { [[ -f "$PLATFORM_METADATA_PATH" ]] || return 1 node -e ' const fs = require("fs"); const metadata = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const endpointID = process.argv[2]; function findEndpoint(value) { if (!value || typeof value !== "object") return null; if ((value.ID || value.id) === endpointID && (value.LastHeartbeatAt || value.lastHeartbeatAt)) return value; if (Array.isArray(value)) { for (const item of value) { const found = findEndpoint(item); if (found) return found; } return null; } for (const item of Object.values(value)) { const found = findEndpoint(item); if (found) return found; } return null; } const endpoint = findEndpoint(metadata); const heartbeat = endpoint && (endpoint.LastHeartbeatAt || endpoint.lastHeartbeatAt); const ageMilliseconds = heartbeat ? Date.now() - Date.parse(heartbeat) : Number.POSITIVE_INFINITY; process.exit(Number.isFinite(ageMilliseconds) && ageMilliseconds >= 0 && ageMilliseconds < 30000 ? 0 : 1); ' "$PLATFORM_METADATA_PATH" "$RUN_ENDPOINT_ID" } printf 'local debug root: %s\n' "$LOCAL_DEBUG_ROOT" printf 'platform: %s\n' "$(local_debug_platform_url)" printf 'platform_web: %s\n' "$(local_debug_web_url)" printf 'run source: %s\n' "$RUN_SOURCE_DIR" printf 'run build bucket: %s\n' "$RUN_BUILD_BUCKET_ROOT" printf 'run build source snapshot: %s\n' "$RUN_BUILD_SOURCE_ROOT" printf 'run bootstrap binary: %s\n' "$RUN_BOOTSTRAP_BIN" printf 'platform builder image: %s\n' "$PLATFORM_BUILDER_IMAGE" printf 'platform builder workspace: %s\n' "$PLATFORM_BUILDER_WORKSPACE_DIR" printf 'platform builder cache: %s\n' "$PLATFORM_BUILDER_CACHE_DIR" printf 'platform log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform.log" printf 'run log: %s\n' "$LOCAL_DEBUG_LOG_DIR/run.log" printf 'platform_web log: %s\n' "$LOCAL_DEBUG_LOG_DIR/platform_web.log" ensure_port_available platform "$LOCAL_DEBUG_PLATFORM_PORT" ensure_port_available platform_web "$LOCAL_DEBUG_WEB_PORT" start_service platform "$ROOT_DIR/platform" env \ GOCACHE="$GOCACHE" \ PLATFORM_ADDR="$PLATFORM_ADDR" \ PLATFORM_STORAGE_BACKEND="$PLATFORM_STORAGE_BACKEND" \ PLATFORM_DATA_DIR="$PLATFORM_DATA_DIR" \ PLATFORM_METADATA_PATH="$PLATFORM_METADATA_PATH" \ PLATFORM_LOG_BODY_BACKEND="$PLATFORM_LOG_BODY_BACKEND" \ PLATFORM_LOG_DIR="$PLATFORM_LOG_DIR" \ PLATFORM_ARTIFACT_DIR="$PLATFORM_ARTIFACT_DIR" \ PLATFORM_BOOTSTRAP_ADMIN_EMAIL="$PLATFORM_BOOTSTRAP_ADMIN_EMAIL" \ PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="$PLATFORM_BOOTSTRAP_ADMIN_PASSWORD" \ PLATFORM_SECRET_ENVELOPE_KEY="$PLATFORM_SECRET_ENVELOPE_KEY" \ PLATFORM_RUN_RELEASE_URL="$PLATFORM_RUN_RELEASE_URL" \ PLATFORM_BUILDER_DOCKER_BINARY="$PLATFORM_BUILDER_DOCKER_BINARY" \ PLATFORM_BUILDER_IMAGE="$PLATFORM_BUILDER_IMAGE" \ PLATFORM_BUILDER_SOURCE_DIR="$PLATFORM_BUILDER_SOURCE_DIR" \ PLATFORM_BUILDER_WORKSPACE_DIR="$PLATFORM_BUILDER_WORKSPACE_DIR" \ PLATFORM_BUILDER_CACHE_DIR="$PLATFORM_BUILDER_CACHE_DIR" \ PLATFORM_BUILDER_TIMEOUT_SECONDS="$PLATFORM_BUILDER_TIMEOUT_SECONDS" \ go run ./cmd/platform wait_for_url platform "$(local_debug_platform_url)/healthz" start_service platform_web "$ROOT_DIR" env \ PLATFORM_API_PROXY="$PLATFORM_API_PROXY" \ VITE_PLATFORM_API_BASE_URL="$VITE_PLATFORM_API_BASE_URL" \ VITE_ENABLE_LOCAL_AUTH_FALLBACK="$VITE_ENABLE_LOCAL_AUTH_FALLBACK" \ npm --prefix platform_web run dev -- --host "0.0.0.0" --port "$LOCAL_DEBUG_WEB_PORT" wait_for_url platform_web "$(local_debug_web_url)" stop_orphaned_run_bootstrap local_debug_build_bootstrap_run start_service run "$(dirname "$RUN_BOOTSTRAP_BIN")" env \ GOCACHE="$GOCACHE" \ RUN_MODE="$RUN_MODE" \ RUN_PLATFORM_URL="$RUN_PLATFORM_URL" \ RUN_ENDPOINT_ID="$RUN_ENDPOINT_ID" \ RUN_DISPLAY_NAME="$RUN_DISPLAY_NAME" \ RUN_VERSION="$RUN_VERSION" \ RUN_REGISTRATION_TOKEN="$RUN_REGISTRATION_TOKEN" \ RUN_WORKSPACE_ROOT="$RUN_WORKSPACE_ROOT" \ RUN_BUILD_SOURCE_ROOT="$RUN_BUILD_SOURCE_ROOT" \ RUN_SPOOL_ROOT="$RUN_SPOOL_ROOT" \ RUN_MAX_JOBS="$RUN_MAX_JOBS" \ RUN_HEARTBEAT_INTERVAL_MS="$RUN_HEARTBEAT_INTERVAL_MS" \ RUN_POLL_INTERVAL_MS="$RUN_POLL_INTERVAL_MS" \ RUN_RETRY_BACKOFF_MS="$RUN_RETRY_BACKOFF_MS" \ "$RUN_BOOTSTRAP_BIN" wait_for_run_registration cat <