Make the project start more reliably in the current Windows bash setup, add a safe root .env.example for onboarding, and lower the backend Python floor to 3.11 to match the validated local environment.
337 lines
8.6 KiB
Bash
337 lines
8.6 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
BACKEND_DIR="$SCRIPT_DIR/backend"
|
|
FRONTEND_DIR="$SCRIPT_DIR/frontend"
|
|
LOG_DIR="$SCRIPT_DIR/logs"
|
|
ROOT_ENV="$SCRIPT_DIR/.env"
|
|
BACKEND_ENV="$BACKEND_DIR/.env"
|
|
BACKEND_ENV_EXAMPLE="$BACKEND_DIR/.env.example"
|
|
FRONTEND_ENV_LOCAL="$FRONTEND_DIR/.env.local"
|
|
HOST_OS="$(uname -s)"
|
|
USE_WINDOWS_TOOLS=false
|
|
|
|
if command -v cmd.exe >/dev/null 2>&1 || command -v powershell.exe >/dev/null 2>&1; then
|
|
USE_WINDOWS_TOOLS=true
|
|
fi
|
|
|
|
if [[ "$USE_WINDOWS_TOOLS" == true ]]; then
|
|
BACKEND_PYTHON="$BACKEND_DIR/.venv/Scripts/python.exe"
|
|
FRONTEND_NPM="npm.cmd"
|
|
else
|
|
BACKEND_PYTHON="$BACKEND_DIR/.venv/bin/python"
|
|
FRONTEND_NPM="npm"
|
|
fi
|
|
|
|
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
|
BACKEND_LOG="$LOG_DIR/backend-start-${RUN_ID}.log"
|
|
BACKEND_ERR_LOG="$LOG_DIR/backend-start-${RUN_ID}.err.log"
|
|
FRONTEND_LOG="$LOG_DIR/frontend-start-${RUN_ID}.log"
|
|
FRONTEND_ERR_LOG="$LOG_DIR/frontend-start-${RUN_ID}.err.log"
|
|
KILL_PORT=false
|
|
ENABLE_RELOAD=false
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--kill-port)
|
|
KILL_PORT=true
|
|
;;
|
|
--reload)
|
|
ENABLE_RELOAD=true
|
|
;;
|
|
*)
|
|
echo "[ERROR] Unsupported argument: $arg"
|
|
echo "[ERROR] Usage: bash start.sh [--kill-port] [--reload]"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
port_in_use() {
|
|
"$BACKEND_PYTHON" - "$1" <<'PY'
|
|
import socket, sys
|
|
port = int(sys.argv[1])
|
|
with socket.socket() as sock:
|
|
sock.settimeout(0.2)
|
|
sys.exit(0 if sock.connect_ex(("127.0.0.1", port)) == 0 else 1)
|
|
PY
|
|
}
|
|
|
|
to_windows_path() {
|
|
local path="$1"
|
|
if [[ "$path" =~ ^/mnt/([a-zA-Z])/(.*)$ ]]; then
|
|
printf '%s:/%s' "${BASH_REMATCH[1]^}" "${BASH_REMATCH[2]}"
|
|
elif [[ "$path" =~ ^/cygdrive/([a-zA-Z])/(.*)$ ]]; then
|
|
printf '%s:/%s' "${BASH_REMATCH[1]^}" "${BASH_REMATCH[2]}"
|
|
elif [[ "$path" =~ ^/([a-zA-Z])/(.*)$ ]]; then
|
|
printf '%s:/%s' "${BASH_REMATCH[1]^}" "${BASH_REMATCH[2]}"
|
|
else
|
|
printf '%s' "$path"
|
|
fi
|
|
}
|
|
|
|
quote_ps() {
|
|
local value="$1"
|
|
value="$(printf '%s' "$value" | sed "s/'/''/g")"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
sync_backend_env() {
|
|
if [[ -f "$BACKEND_ENV" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if [[ -f "$ROOT_ENV" ]]; then
|
|
cp "$ROOT_ENV" "$BACKEND_ENV"
|
|
echo "[INFO] Created backend/.env from root .env"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -f "$BACKEND_ENV_EXAMPLE" ]]; then
|
|
cp "$BACKEND_ENV_EXAMPLE" "$BACKEND_ENV"
|
|
echo "[INFO] Created backend/.env from backend/.env.example"
|
|
echo "[INFO] Review $BACKEND_ENV before first run."
|
|
return 0
|
|
fi
|
|
|
|
echo "[ERROR] backend/.env was not found and could not be initialized."
|
|
echo "[ERROR] Run: bash setup.sh"
|
|
exit 1
|
|
}
|
|
|
|
read_env_value() {
|
|
local key="$1"
|
|
local file="$2"
|
|
grep "^${key}=" "$file" | cut -d'=' -f2- | tr -d '\r' || true
|
|
}
|
|
|
|
write_frontend_env() {
|
|
local backend_host="$1"
|
|
local backend_port="$2"
|
|
printf 'VITE_API_URL=http://%s:%s\n' "$backend_host" "$backend_port" > "$FRONTEND_ENV_LOCAL"
|
|
}
|
|
|
|
kill_port_process() {
|
|
local port="$1"
|
|
|
|
if [[ "$USE_WINDOWS_TOOLS" == true ]]; then
|
|
local pids
|
|
pids="$(powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique" | tr -d '\r' || true)"
|
|
|
|
if [[ -z "$pids" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
while IFS= read -r pid; do
|
|
[[ -z "$pid" ]] && continue
|
|
cmd.exe /c "taskkill /PID $pid /T /F" >/dev/null 2>&1 || true
|
|
powershell.exe -NoProfile -Command "Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue" >/dev/null 2>&1 || true
|
|
done <<< "$pids"
|
|
return 0
|
|
fi
|
|
|
|
local pids
|
|
if command -v ss >/dev/null 2>&1; then
|
|
pids="$(ss -ltnp "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | sort -u || true)"
|
|
elif command -v lsof >/dev/null 2>&1; then
|
|
pids="$(lsof -ti tcp:"$port" 2>/dev/null | sort -u || true)"
|
|
else
|
|
return 0
|
|
fi
|
|
|
|
if [[ -z "$pids" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
while IFS= read -r pid; do
|
|
[[ -z "$pid" ]] && continue
|
|
kill "$pid" >/dev/null 2>&1 || true
|
|
done <<< "$pids"
|
|
}
|
|
|
|
kill_process_tree() {
|
|
local pid="$1"
|
|
[[ -z "$pid" ]] && return 0
|
|
kill "$pid" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
cleanup() {
|
|
local exit_code=$?
|
|
trap - INT TERM EXIT
|
|
if [[ -n "${FRONTEND_PID:-}" ]]; then
|
|
kill_process_tree "$FRONTEND_PID"
|
|
fi
|
|
if [[ -n "${BACKEND_PID:-}" ]]; then
|
|
kill_process_tree "$BACKEND_PID"
|
|
fi
|
|
exit "$exit_code"
|
|
}
|
|
trap cleanup INT TERM EXIT
|
|
|
|
echo "=========================================="
|
|
echo " Jarvis - Start"
|
|
echo "=========================================="
|
|
echo
|
|
|
|
sync_backend_env
|
|
|
|
if [[ ! -f "$BACKEND_ENV" ]]; then
|
|
echo "[ERROR] Backend env file was not found: $BACKEND_ENV"
|
|
echo "[ERROR] Run: bash setup.sh"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$USE_WINDOWS_TOOLS" == true ]]; then
|
|
if [[ ! -f "$BACKEND_PYTHON" ]]; then
|
|
echo "[ERROR] Backend virtual environment Python was not found: $BACKEND_PYTHON"
|
|
echo "[ERROR] Run: bash setup.sh"
|
|
exit 1
|
|
fi
|
|
if ! command -v "$FRONTEND_NPM" >/dev/null 2>&1; then
|
|
echo "[ERROR] npm.cmd was not found."
|
|
echo "[ERROR] Install Node.js and ensure npm is on PATH."
|
|
exit 1
|
|
fi
|
|
else
|
|
if [[ ! -x "$BACKEND_PYTHON" ]]; then
|
|
echo "[ERROR] Backend virtual environment Python was not found: $BACKEND_PYTHON"
|
|
echo "[ERROR] Run: bash setup.sh"
|
|
exit 1
|
|
fi
|
|
if ! command -v "$FRONTEND_NPM" >/dev/null 2>&1; then
|
|
echo "[ERROR] npm was not found."
|
|
echo "[ERROR] Install Node.js and ensure npm is on PATH."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$USE_WINDOWS_TOOLS" == true ]] && ! command -v powershell.exe >/dev/null 2>&1; then
|
|
echo "[ERROR] powershell.exe was not found."
|
|
exit 1
|
|
fi
|
|
|
|
BACKEND_HOST="$(read_env_value HOST "$BACKEND_ENV")"
|
|
BACKEND_PORT="$(read_env_value PORT "$BACKEND_ENV")"
|
|
|
|
[[ -z "$BACKEND_HOST" ]] && BACKEND_HOST="127.0.0.1"
|
|
|
|
if [[ -z "$BACKEND_PORT" ]]; then
|
|
echo "[ERROR] PORT was not found in $BACKEND_ENV"
|
|
echo "[ERROR] Set PORT and run start.sh again."
|
|
exit 1
|
|
fi
|
|
|
|
if ! [[ "$BACKEND_PORT" =~ ^[0-9]+$ ]]; then
|
|
echo "[ERROR] PORT must be numeric in $BACKEND_ENV: $BACKEND_PORT"
|
|
exit 1
|
|
fi
|
|
|
|
if (( BACKEND_PORT < 1 || BACKEND_PORT > 65535 )); then
|
|
echo "[ERROR] PORT must be between 1 and 65535 in $BACKEND_ENV: $BACKEND_PORT"
|
|
exit 1
|
|
fi
|
|
|
|
write_frontend_env "$BACKEND_HOST" "$BACKEND_PORT"
|
|
|
|
BACKEND_ARGS=(-m uvicorn app.main:app --host "$BACKEND_HOST" --port "$BACKEND_PORT")
|
|
if [[ "$ENABLE_RELOAD" == true ]]; then
|
|
BACKEND_ARGS+=(--reload)
|
|
fi
|
|
|
|
echo "[1/3] Backend environment ready."
|
|
echo "[2/3] Frontend environment ready."
|
|
echo
|
|
|
|
echo "[3/3] Start services..."
|
|
if port_in_use "$BACKEND_PORT"; then
|
|
if [[ "$KILL_PORT" == true ]]; then
|
|
echo "[INFO] Port ${BACKEND_PORT} is in use. Killing existing process..."
|
|
kill_port_process "$BACKEND_PORT"
|
|
sleep 2
|
|
fi
|
|
fi
|
|
|
|
if port_in_use "$BACKEND_PORT"; then
|
|
echo "[ERROR] Port ${BACKEND_PORT} is already in use."
|
|
echo "[ERROR] Stop the existing service first, or run: bash start.sh --kill-port"
|
|
exit 1
|
|
fi
|
|
|
|
(
|
|
cd "$BACKEND_DIR"
|
|
exec "$BACKEND_PYTHON" "${BACKEND_ARGS[@]}"
|
|
) >"$BACKEND_LOG" 2>"$BACKEND_ERR_LOG" &
|
|
BACKEND_PID="$!"
|
|
|
|
echo "Waiting for backend..."
|
|
backend_ready=false
|
|
for _ in $(seq 1 30); do
|
|
if port_in_use "$BACKEND_PORT"; then
|
|
backend_ready=true
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [[ "$backend_ready" != true ]]; then
|
|
echo "[ERROR] Backend did not start successfully."
|
|
echo "[ERROR] Check logs: $BACKEND_LOG $BACKEND_ERR_LOG"
|
|
exit 1
|
|
fi
|
|
|
|
FRONTEND_HOST="0.0.0.0"
|
|
FRONTEND_BROWSER_HOST="localhost"
|
|
FRONTEND_PORT="5173"
|
|
|
|
echo "Starting frontend on port ${FRONTEND_PORT}..."
|
|
if [[ "$USE_WINDOWS_TOOLS" == true ]]; then
|
|
(
|
|
cd "$FRONTEND_DIR"
|
|
exec cmd.exe /c npm.cmd run dev -- --host "$FRONTEND_HOST"
|
|
) >"$FRONTEND_LOG" 2>"$FRONTEND_ERR_LOG" &
|
|
FRONTEND_PID="$!"
|
|
else
|
|
(
|
|
cd "$FRONTEND_DIR"
|
|
exec "$FRONTEND_NPM" run dev -- --host "$FRONTEND_HOST"
|
|
) >"$FRONTEND_LOG" 2>"$FRONTEND_ERR_LOG" &
|
|
FRONTEND_PID="$!"
|
|
fi
|
|
|
|
echo
|
|
echo "=========================================="
|
|
echo " Started"
|
|
echo
|
|
echo " Backend: http://${BACKEND_HOST}:${BACKEND_PORT}"
|
|
echo " Frontend: http://${FRONTEND_BROWSER_HOST}:${FRONTEND_PORT}"
|
|
echo " API docs: http://${BACKEND_HOST}:${BACKEND_PORT}/docs"
|
|
echo
|
|
echo " Logs:"
|
|
echo " - $BACKEND_LOG"
|
|
echo " - $BACKEND_ERR_LOG"
|
|
echo " - $FRONTEND_LOG"
|
|
echo " - $FRONTEND_ERR_LOG"
|
|
echo "=========================================="
|
|
echo
|
|
echo "Press Ctrl+C to stop both services."
|
|
echo
|
|
|
|
while true; do
|
|
if ! kill -0 "$BACKEND_PID" >/dev/null 2>&1; then
|
|
echo "[ERROR] Backend process exited."
|
|
echo "[ERROR] Check logs: $BACKEND_LOG $BACKEND_ERR_LOG"
|
|
exit 1
|
|
fi
|
|
|
|
if ! kill -0 "$FRONTEND_PID" >/dev/null 2>&1; then
|
|
echo "[ERROR] Frontend process exited."
|
|
echo "[ERROR] Check logs: $FRONTEND_LOG $FRONTEND_ERR_LOG"
|
|
exit 1
|
|
fi
|
|
|
|
sleep 2
|
|
done
|