267 lines
6.2 KiB
Bash
Executable File
267 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
TMP_DIR="$(cd /tmp && pwd -P)"
|
|
|
|
BACKEND_PORT="${BACKEND_PORT:-3000}"
|
|
FRONTEND_PORT="${FRONTEND_PORT:-5173}"
|
|
FRONTEND_HOST="${FRONTEND_HOST:-127.0.0.1}"
|
|
|
|
BACKEND_BIN="${TMP_DIR}/inbox-host"
|
|
BACKEND_BUILD_TARGET="./cmd/inbox"
|
|
BACKEND_LOG="${TMP_DIR}/ai-workflow-v2-inbox.log"
|
|
FRONTEND_LOG="${TMP_DIR}/ai-workflow-v2-dashboard.log"
|
|
FRONTEND_BIN="${REPO_ROOT}/dashboard/node_modules/.bin/vite"
|
|
GO_BUILD_CACHE="${TMP_DIR}/ai-workflow-v2-go-build"
|
|
WORKSPACES_DIR="${REPO_ROOT}/inbox-worktrees"
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "error: missing required command: $1" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
start_detached() {
|
|
local cwd="$1"
|
|
local log_file="$2"
|
|
shift 2
|
|
|
|
LAUNCH_CWD="$cwd" LAUNCH_LOG="$log_file" python3 - "$@" <<'PY'
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
cwd = os.environ["LAUNCH_CWD"]
|
|
log_path = os.environ["LAUNCH_LOG"]
|
|
argv = sys.argv[1:]
|
|
|
|
with open(log_path, "ab", buffering=0) as log:
|
|
proc = subprocess.Popen(
|
|
argv,
|
|
cwd=cwd,
|
|
env=os.environ.copy(),
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=log,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
print(proc.pid)
|
|
PY
|
|
}
|
|
|
|
listening_pid() {
|
|
local port="$1"
|
|
lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | head -n 1 || true
|
|
}
|
|
|
|
cwd_for_pid() {
|
|
local pid="$1"
|
|
lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1
|
|
}
|
|
|
|
txt_for_pid() {
|
|
local pid="$1"
|
|
lsof -a -p "$pid" -d txt -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1
|
|
}
|
|
|
|
wait_for_port_to_close() {
|
|
local port="$1"
|
|
for _ in $(seq 1 40); do
|
|
if [ -z "$(listening_pid "$port")" ]; then
|
|
return 0
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
return 1
|
|
}
|
|
|
|
process_exists() {
|
|
local pid="$1"
|
|
lsof -p "$pid" >/dev/null 2>&1
|
|
}
|
|
|
|
send_signal() {
|
|
local signal="$1"
|
|
local pid="$2"
|
|
local label="$3"
|
|
local port="$4"
|
|
local err=""
|
|
|
|
if ! err="$(kill "$signal" "$pid" 2>&1)"; then
|
|
if [ -z "$(listening_pid "$port")" ]; then
|
|
return 0
|
|
fi
|
|
echo "error: failed to send ${signal} to ${label} (pid ${pid}): ${err}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
stop_known_process() {
|
|
local pid="$1"
|
|
local label="$2"
|
|
local port="$3"
|
|
|
|
echo "stopping ${label} (pid ${pid})..."
|
|
send_signal "-TERM" "$pid" "$label" "$port"
|
|
if wait_for_port_to_close "$port"; then
|
|
return 0
|
|
fi
|
|
|
|
echo "${label} did not exit in time, forcing stop..."
|
|
send_signal "-KILL" "$pid" "$label" "$port"
|
|
if wait_for_port_to_close "$port"; then
|
|
return 0
|
|
fi
|
|
|
|
echo "error: ${label} is still listening on port ${port} after SIGKILL." >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_backend_port_ready() {
|
|
local pid
|
|
pid="$(listening_pid "$BACKEND_PORT")"
|
|
if [ -z "$pid" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local cwd
|
|
local txt
|
|
cwd="$(cwd_for_pid "$pid")"
|
|
txt="$(txt_for_pid "$pid")"
|
|
if [ "$cwd" = "$REPO_ROOT" ] && [ "$txt" = "$BACKEND_BIN" ]; then
|
|
stop_known_process "$pid" "backend" "$BACKEND_PORT"
|
|
return 0
|
|
fi
|
|
|
|
echo "error: port ${BACKEND_PORT} is occupied by another process." >&2
|
|
echo "pid: ${pid}" >&2
|
|
echo "cwd: ${cwd}" >&2
|
|
echo "exe: ${txt}" >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_frontend_port_ready() {
|
|
local pid
|
|
pid="$(listening_pid "$FRONTEND_PORT")"
|
|
if [ -z "$pid" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local cwd
|
|
cwd="$(cwd_for_pid "$pid")"
|
|
if [ "$cwd" = "${REPO_ROOT}/dashboard" ]; then
|
|
stop_known_process "$pid" "frontend" "$FRONTEND_PORT"
|
|
return 0
|
|
fi
|
|
|
|
local txt
|
|
txt="$(txt_for_pid "$pid")"
|
|
echo "error: port ${FRONTEND_PORT} is occupied by another process." >&2
|
|
echo "pid: ${pid}" >&2
|
|
echo "cwd: ${cwd}" >&2
|
|
echo "exe: ${txt}" >&2
|
|
exit 1
|
|
}
|
|
|
|
wait_for_http() {
|
|
local url="$1"
|
|
local label="$2"
|
|
local log_file="$3"
|
|
local pid="$4"
|
|
|
|
for _ in $(seq 1 60); do
|
|
if curl -fsS "$url" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if ! process_exists "$pid"; then
|
|
echo "error: ${label} exited before becoming ready (pid ${pid})." >&2
|
|
if [ -f "$log_file" ]; then
|
|
echo "--- ${label} log tail ---" >&2
|
|
tail -n 40 "$log_file" >&2 || true
|
|
fi
|
|
exit 1
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
echo "error: ${label} failed to become ready: ${url}" >&2
|
|
if [ -f "$log_file" ]; then
|
|
echo "--- ${label} log tail ---" >&2
|
|
tail -n 40 "$log_file" >&2 || true
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
start_backend() {
|
|
: > "$BACKEND_LOG"
|
|
start_detached "$REPO_ROOT" "$BACKEND_LOG" \
|
|
"$BACKEND_BIN" server \
|
|
--workspaces-dir "$WORKSPACES_DIR" \
|
|
--port "$BACKEND_PORT"
|
|
}
|
|
|
|
start_frontend() {
|
|
: > "$FRONTEND_LOG"
|
|
start_detached "${REPO_ROOT}/dashboard" "$FRONTEND_LOG" \
|
|
"$FRONTEND_BIN" --host "$FRONTEND_HOST" --port "$FRONTEND_PORT"
|
|
}
|
|
|
|
backend_pid_summary() {
|
|
listening_pid "$BACKEND_PORT"
|
|
}
|
|
|
|
frontend_pid_summary() {
|
|
listening_pid "$FRONTEND_PORT"
|
|
}
|
|
|
|
main() {
|
|
require_cmd go
|
|
require_cmd lsof
|
|
require_cmd curl
|
|
require_cmd python3
|
|
if [ ! -x "$FRONTEND_BIN" ]; then
|
|
echo "error: missing dashboard dependencies at ${FRONTEND_BIN}" >&2
|
|
echo "run: cd ${REPO_ROOT}/dashboard && npm install" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "building backend binary..."
|
|
mkdir -p "$GO_BUILD_CACHE"
|
|
mkdir -p "$WORKSPACES_DIR"
|
|
(
|
|
cd "${REPO_ROOT}/inbox"
|
|
GOCACHE="$GO_BUILD_CACHE" go build -o "$BACKEND_BIN" "$BACKEND_BUILD_TARGET"
|
|
)
|
|
if [ ! -x "$BACKEND_BIN" ]; then
|
|
echo "error: built backend binary is not executable: ${BACKEND_BIN}" >&2
|
|
file "$BACKEND_BIN" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
ensure_backend_port_ready
|
|
ensure_frontend_port_ready
|
|
|
|
echo "starting backend on http://127.0.0.1:${BACKEND_PORT} ..."
|
|
local backend_pid
|
|
backend_pid="$(start_backend)"
|
|
wait_for_http "http://127.0.0.1:${BACKEND_PORT}/api/v2/workspaces" "backend" "$BACKEND_LOG" "$backend_pid"
|
|
|
|
echo "starting frontend on http://${FRONTEND_HOST}:${FRONTEND_PORT} ..."
|
|
local frontend_pid
|
|
frontend_pid="$(start_frontend)"
|
|
wait_for_http "http://${FRONTEND_HOST}:${FRONTEND_PORT}" "frontend" "$FRONTEND_LOG" "$frontend_pid"
|
|
|
|
echo ""
|
|
echo "project restarted"
|
|
echo "backend: http://127.0.0.1:${BACKEND_PORT} (pid $(backend_pid_summary))"
|
|
echo "frontend: http://${FRONTEND_HOST}:${FRONTEND_PORT} (pid $(frontend_pid_summary))"
|
|
echo "logs:"
|
|
echo " ${BACKEND_LOG}"
|
|
echo " ${FRONTEND_LOG}"
|
|
}
|
|
|
|
main "$@"
|