Files
openswarm/run/local.sh

245 lines
9.3 KiB
Bash
Executable File

#!/bin/bash
# The comment above is shebang, DO NOT REMOVE
SCRIPT_ABSPATH="$(readlink -f "${BASH_SOURCE[0]}")"
RUN_DIR_ROOT="$(dirname "$SCRIPT_ABSPATH")"
PROJECT_ROOT="$(dirname "$RUN_DIR_ROOT")"
# shellcheck source=utils/platform.sh
source "$RUN_DIR_ROOT/utils/platform.sh"
ensure_lf "$SCRIPT_ABSPATH"
chmod +x "$SCRIPT_ABSPATH"
BLUE='\033[0;34m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
RESET='\033[0m'
BACKEND_PID=""
FRONTEND_PID=""
ELECTRON_PID=""
SHUTTING_DOWN=false
cleanup() {
$SHUTTING_DOWN && return
SHUTTING_DOWN=true
echo ""
echo -e "${YELLOW}${BOLD}Gracefully shutting down all services...${RESET}"
# Send SIGTERM to top-level PIDs only (not recursively) so uvicorn
# can run its lifespan teardown before child processes are killed.
for pid in $ELECTRON_PID $FRONTEND_PID; do
[[ -n "$pid" ]] && kill_tree "$pid" TERM
done
if [[ -n "$BACKEND_PID" ]]; then
if [[ "$IS_WINDOWS" == "true" ]]; then
kill_tree "$BACKEND_PID" TERM
else
kill -TERM "$BACKEND_PID" 2>/dev/null
fi
fi
local elapsed=0
while (( elapsed < 10 )); do
local alive=false
for pid in $ELECTRON_PID $BACKEND_PID $FRONTEND_PID; do
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && alive=true
done
$alive || break
sleep 1
((elapsed++))
done
for pid in $ELECTRON_PID $BACKEND_PID $FRONTEND_PID; do
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && kill_tree "$pid" KILL
done
wait 2>/dev/null
echo -e "${GREEN}${BOLD}All services stopped.${RESET}"
}
trap 'cleanup; exit 0' INT TERM
trap cleanup EXIT
# --- Ensure bundled uv/uvx for MCP servers ---
UV_BIN_DIR="$PROJECT_ROOT/backend/uv-bin"
if [ ! -f "$UV_BIN_DIR/uvx${EXE_EXT}" ]; then
echo -e "${YELLOW}${BOLD}[uv]${RESET} Downloading uv/uvx..."
mkdir -p "$UV_BIN_DIR"
ARCH=$(uname -m)
# NOTE: do not name this `TMP` -- on Windows, `TMP` is an exported env var
# and bash assignment preserves the export attribute, so child processes
# (e.g. uv) would inherit it and crash once we `rm -rf` the directory.
UV_DL_TMP=$(mktemp -d)
if [[ "$IS_WINDOWS" == "true" ]]; then
case "$ARCH" in
aarch64|arm64) UV_PKG="uv-aarch64-pc-windows-msvc.zip" ;;
*) UV_PKG="uv-x86_64-pc-windows-msvc.zip" ;;
esac
curl -fsSL "https://github.com/astral-sh/uv/releases/latest/download/${UV_PKG}" -o "$UV_DL_TMP/uv.zip"
# Use Python's zipfile module: GNU tar in MSYS/Git Bash can't read zips,
# and Windows' libarchive tar.exe is shadowed on PATH by GNU tar.
# The Windows zip stores uv.exe/uvx.exe at the archive root (no wrapper dir),
# unlike the Linux/macOS tarballs which have a uv-<triple>/ prefix.
"$PY" -m zipfile -e "$(py_path "$UV_DL_TMP/uv.zip")" "$(py_path "$UV_DL_TMP")"
cp "$UV_DL_TMP/uv.exe" "$UV_BIN_DIR/uv.exe"
cp "$UV_DL_TMP/uvx.exe" "$UV_BIN_DIR/uvx.exe"
elif [[ "$IS_MAC" == "true" ]]; then
if [[ "$ARCH" == "arm64" ]]; then
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar xz -C "$UV_DL_TMP"
cp "$UV_DL_TMP/uv-aarch64-apple-darwin/uv" "$UV_BIN_DIR/uv"
cp "$UV_DL_TMP/uv-aarch64-apple-darwin/uvx" "$UV_BIN_DIR/uvx"
else
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-apple-darwin.tar.gz" | tar xz -C "$UV_DL_TMP"
cp "$UV_DL_TMP/uv-x86_64-apple-darwin/uv" "$UV_BIN_DIR/uv"
cp "$UV_DL_TMP/uv-x86_64-apple-darwin/uvx" "$UV_BIN_DIR/uvx"
fi
chmod +x "$UV_BIN_DIR/uv" "$UV_BIN_DIR/uvx"
else
case "$ARCH" in
aarch64|arm64) UV_PKG_BASE="uv-aarch64-unknown-linux-gnu" ;;
*) UV_PKG_BASE="uv-x86_64-unknown-linux-gnu" ;;
esac
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/${UV_PKG_BASE}.tar.gz" | tar xz -C "$UV_DL_TMP"
cp "$UV_DL_TMP/$UV_PKG_BASE/uv" "$UV_BIN_DIR/uv"
cp "$UV_DL_TMP/$UV_PKG_BASE/uvx" "$UV_BIN_DIR/uvx"
chmod +x "$UV_BIN_DIR/uv" "$UV_BIN_DIR/uvx"
fi
rm -rf "$UV_DL_TMP"
fi
# --- Read ports from config (path passed via argv so MSYS converts it) ---
BACKEND_PORT=$("$PY" -c "import json,sys; print(json.load(open(sys.argv[1]))['backend']['dev'])" "$PROJECT_ROOT/ports.config.json")
FRONTEND_PORT=$("$PY" -c "import json,sys; print(json.load(open(sys.argv[1]))['frontend']['dev'])" "$PROJECT_ROOT/ports.config.json")
# --- Run structural linter (warnings only, non-blocking) ---
LINT_OUTPUT=$("$PY" "$PROJECT_ROOT/linter/lint.py" --root "$PROJECT_ROOT" 2>&1)
LINT_EXIT=$?
if [ $LINT_EXIT -ne 0 ]; then
echo ""
echo -e "${YELLOW}${BOLD}[structlint] Violations found:${RESET}"
echo "$LINT_OUTPUT" | grep -v "^structlint:" | while IFS= read -r line; do
printf '%b %s%b\n' "$YELLOW" "$line" "$RESET"
done
LINT_COUNT=$(echo "$LINT_OUTPUT" | grep -oE '[0-9]+ error' | head -1 | grep -oE '[0-9]+')
echo -e "${YELLOW}${BOLD} ${LINT_COUNT} violation(s) — fix or add exceptions in linter/config/config.json${RESET}"
echo ""
fi
# --- Start backend ---
echo -e "${BLUE}${BOLD}[backend]${RESET} Starting backend server..."
bash "$PROJECT_ROOT/backend/run.sh" > >(
while IFS= read -r line; do
printf "${BLUE}${BOLD}[backend]${RESET} %s\n" "$line"
done
) 2>&1 &
BACKEND_PID=$!
track_winpid "$BACKEND_PID"
# --- Wait for backend to become healthy ---
echo -e "${YELLOW}${BOLD}Waiting for backend (http://localhost:${BACKEND_PORT}) to be ready...${RESET}"
MAX_WAIT=120
elapsed=0
while (( elapsed < MAX_WAIT )); do
if curl -s -o /dev/null --connect-timeout 1 "http://localhost:${BACKEND_PORT}/" 2>/dev/null; then
echo -e "${GREEN}${BOLD}Backend is ready!${RESET}"
break
fi
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo -e "${RED}${BOLD}Backend process exited before becoming ready.${RESET}"
exit 1
fi
sleep 2
((elapsed += 2))
done
if (( elapsed >= MAX_WAIT )); then
echo -e "${RED}${BOLD}Backend did not become ready within ${MAX_WAIT}s. Aborting.${RESET}"
exit 1
fi
# --- Start frontend ---
echo -e "${GREEN}${BOLD}[frontend]${RESET} Starting frontend dev server..."
bash "$PROJECT_ROOT/frontend/run.sh" > >(
while IFS= read -r line; do
printf "${GREEN}${BOLD}[frontend]${RESET} %s\n" "$line"
done
) 2>&1 &
FRONTEND_PID=$!
track_winpid "$FRONTEND_PID"
# --- Wait for frontend dev server to become available ---
echo -e "${YELLOW}${BOLD}Waiting for frontend (http://localhost:${FRONTEND_PORT}) to be ready...${RESET}"
FRONTEND_MAX_WAIT=60
frontend_elapsed=0
while (( frontend_elapsed < FRONTEND_MAX_WAIT )); do
if curl -s -o /dev/null --connect-timeout 1 "http://localhost:${FRONTEND_PORT}/" 2>/dev/null; then
echo -e "${GREEN}${BOLD}Frontend is ready!${RESET}"
break
fi
if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then
echo -e "${RED}${BOLD}Frontend process exited before becoming ready.${RESET}"
exit 1
fi
sleep 2
((frontend_elapsed += 2))
done
if (( frontend_elapsed >= FRONTEND_MAX_WAIT )); then
echo -e "${RED}${BOLD}Frontend did not become ready within ${FRONTEND_MAX_WAIT}s. Aborting.${RESET}"
exit 1
fi
# --- Install Electron dependencies if needed ---
MAGENTA='\033[0;35m'
if [ ! -d "$PROJECT_ROOT/electron/node_modules" ]; then
echo -e "${MAGENTA}${BOLD}[electron]${RESET} Installing dependencies..."
(cd "$PROJECT_ROOT/electron" && npm install) 2>&1 | while IFS= read -r line; do
printf "${MAGENTA}${BOLD}[electron]${RESET} %s\n" "$line"
done
fi
# --- Sign Electron VMP for DRM (macOS only; Windows uses a different DRM flow) ---
if [[ "$IS_WINDOWS" != "true" ]] && [ -f "$PROJECT_ROOT/electron/scripts/sign-vmp.sh" ]; then
echo -e "${YELLOW}${BOLD}[vmp]${RESET} Checking VMP signature..."
bash "$PROJECT_ROOT/electron/scripts/sign-vmp.sh" 2>&1 | while IFS= read -r line; do
printf "${YELLOW}${BOLD}%s${RESET}\n" "$line"
done
fi
# --- Start Electron in dev mode ---
echo -e "${MAGENTA}${BOLD}[electron]${RESET} Launching Electron dev shell..."
(cd "$PROJECT_ROOT/electron" && unset ELECTRON_RUN_AS_NODE && ELECTRON_DEV=1 npx electron .) > >(
while IFS= read -r line; do
printf "${MAGENTA}${BOLD}[electron]${RESET} %s\n" "$line"
done
) 2>&1 &
ELECTRON_PID=$!
track_winpid "$ELECTRON_PID"
echo ""
echo -e "${BOLD}All services are running. Press Ctrl+C to stop.${RESET}"
echo -e " Backend: ${BLUE}http://localhost:${BACKEND_PORT}${RESET}"
echo -e " Frontend: ${GREEN}http://localhost:${FRONTEND_PORT}${RESET}"
echo -e " Electron: ${MAGENTA}dev shell (pid $ELECTRON_PID)${RESET}"
echo ""
# --- Monitor: if any service exits, tear down all ---
while ! $SHUTTING_DOWN; do
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
echo -e "${RED}${BOLD}Backend process exited unexpectedly. Shutting down...${RESET}"
exit 1
fi
if [[ -n "$FRONTEND_PID" ]] && ! kill -0 "$FRONTEND_PID" 2>/dev/null; then
echo -e "${RED}${BOLD}Frontend process exited unexpectedly. Shutting down...${RESET}"
exit 1
fi
if [[ -n "$ELECTRON_PID" ]] && ! kill -0 "$ELECTRON_PID" 2>/dev/null; then
echo -e "${YELLOW}${BOLD}Electron process exited. Shutting down...${RESET}"
exit 0
fi
sleep 3
done