Without PYTHONUNBUFFERED, when stderr is piped through '2>&1 | sed', Python fully buffers stderr output. JSON log lines are small and never fill the buffer, so they never appear in real time (or at all for sporadic HTTP request logs). Startup logs appeared because the buffer flushed on process exit during Alembic migration. Also kept --reload removed as it breaks stderr capture from child process.
235 lines
6.7 KiB
Bash
Executable File
235 lines
6.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# #region run [C:2] [TYPE Module]
|
|
# @PURPOSE: Utility script for run
|
|
# @COMPLEXITY: 1
|
|
|
|
# Project Launch Script
|
|
# Automates setup and concurrent execution of backend and frontend servers.
|
|
|
|
set -e
|
|
|
|
# Default configuration
|
|
BACKEND_PORT=${BACKEND_PORT:-8000}
|
|
FRONTEND_PORT=${FRONTEND_PORT:-5173}
|
|
SKIP_INSTALL=false
|
|
|
|
# Help message
|
|
show_help() {
|
|
echo "Usage: ./run.sh [options]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --help Show this help message"
|
|
echo " --skip-install Skip dependency checks and installation"
|
|
echo ""
|
|
echo "Environment Variables:"
|
|
echo " BACKEND_PORT Port for the backend server (default: 8000)"
|
|
echo " FRONTEND_PORT Port for the frontend server (default: 5173)"
|
|
}
|
|
|
|
# Parse arguments
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case $1 in
|
|
--help) show_help; exit 0 ;;
|
|
--skip-install) SKIP_INSTALL=true ;;
|
|
*) echo "Unknown parameter passed: $1"; show_help; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
echo "Starting Project Launch Script..."
|
|
|
|
# Environment validation
|
|
validate_env() {
|
|
echo "Validating environment..."
|
|
|
|
if ! command -v python3 &> /dev/null; then
|
|
echo "Error: python3 is not installed."
|
|
exit 1
|
|
fi
|
|
|
|
if ! python3 -c 'import sys; exit(0) if sys.version_info >= (3, 9) else exit(1)'; then
|
|
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
|
echo "Error: python3 version 3.9 or higher is required. Found $PYTHON_VERSION"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v npm &> /dev/null; then
|
|
echo "Error: npm is not installed."
|
|
exit 1
|
|
fi
|
|
|
|
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
|
echo "Environment validation passed (Python $PYTHON_VERSION, npm $(npm -v))"
|
|
}
|
|
|
|
validate_env
|
|
|
|
# Database connectivity preflight
|
|
check_database() {
|
|
# Keep resolution order aligned with backend/src/core/database.py defaults.
|
|
local DB_URL="${DATABASE_URL:-${POSTGRES_URL:-postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools}}"
|
|
|
|
# SQLite does not require external service.
|
|
if [[ "$DB_URL" == sqlite* ]]; then
|
|
echo "Database preflight: sqlite detected, skipping PostgreSQL connectivity check."
|
|
return
|
|
fi
|
|
|
|
local DB_HOST DB_PORT
|
|
read -r DB_HOST DB_PORT < <(
|
|
python3 - "$DB_URL" <<'PY'
|
|
import sys
|
|
from urllib.parse import urlparse
|
|
|
|
url = sys.argv[1]
|
|
if "://" not in url:
|
|
print("localhost 5432")
|
|
raise SystemExit(0)
|
|
|
|
# Support SQLAlchemy schemes like postgresql+psycopg2://...
|
|
scheme, rest = url.split("://", 1)
|
|
parsed = urlparse(f"{scheme.split('+', 1)[0]}://{rest}")
|
|
host = parsed.hostname or "localhost"
|
|
port = parsed.port or 5432
|
|
print(f"{host} {port}")
|
|
PY
|
|
)
|
|
|
|
local check_cmd
|
|
check_cmd='import socket,sys; socket.create_connection((sys.argv[1], int(sys.argv[2])), timeout=1).close()'
|
|
|
|
if python3 -c "$check_cmd" "$DB_HOST" "$DB_PORT" >/dev/null 2>&1; then
|
|
echo "Database preflight: reachable at ${DB_HOST}:${DB_PORT}."
|
|
return
|
|
fi
|
|
|
|
echo "Database preflight: cannot connect to ${DB_HOST}:${DB_PORT}."
|
|
|
|
# For local development defaults, attempt to auto-start bundled PostgreSQL.
|
|
if [ "$DB_HOST" = "localhost" ] && [ "$DB_PORT" = "5432" ] && command -v docker >/dev/null 2>&1; then
|
|
if [ -f "docker-compose.yml" ]; then
|
|
echo "Attempting to start local PostgreSQL via docker compose (service: db)..."
|
|
docker compose up -d db || true
|
|
fi
|
|
fi
|
|
|
|
for _ in {1..20}; do
|
|
if python3 -c "$check_cmd" "$DB_HOST" "$DB_PORT" >/dev/null 2>&1; then
|
|
echo "Database preflight: reachable at ${DB_HOST}:${DB_PORT}."
|
|
return
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
echo "Error: PostgreSQL is unavailable at ${DB_HOST}:${DB_PORT}."
|
|
echo "Run: docker compose up -d db"
|
|
echo "Or set DATABASE_URL/POSTGRES_URL to a reachable database."
|
|
exit 1
|
|
}
|
|
|
|
check_database
|
|
|
|
# Backend dependency management
|
|
setup_backend() {
|
|
if [ "$SKIP_INSTALL" = true ]; then
|
|
echo "Skipping backend installation..."
|
|
return
|
|
fi
|
|
|
|
echo "Setting up backend..."
|
|
cd backend
|
|
if [ ! -d ".venv" ]; then
|
|
echo "Creating virtual environment..."
|
|
python3 -m venv .venv
|
|
fi
|
|
|
|
source .venv/bin/activate
|
|
if [ -f "requirements.txt" ]; then
|
|
echo "Installing backend dependencies..."
|
|
pip install -r requirements.txt
|
|
else
|
|
echo "Warning: backend/requirements.txt not found."
|
|
fi
|
|
cd ..
|
|
}
|
|
|
|
# Frontend dependency management
|
|
setup_frontend() {
|
|
if [ "$SKIP_INSTALL" = true ]; then
|
|
echo "Skipping frontend installation..."
|
|
return
|
|
fi
|
|
|
|
echo "Setting up frontend..."
|
|
cd frontend
|
|
if [ ! -d "node_modules" ]; then
|
|
echo "Installing frontend dependencies..."
|
|
npm install
|
|
else
|
|
echo "frontend/node_modules already exists. Skipping npm install."
|
|
fi
|
|
cd ..
|
|
}
|
|
|
|
setup_backend
|
|
setup_frontend
|
|
|
|
# Cleanup function for graceful shutdown
|
|
cleanup() {
|
|
echo ""
|
|
echo "Stopping services..."
|
|
if [ -n "$BACKEND_PID" ]; then
|
|
kill $BACKEND_PID 2>/dev/null || true
|
|
fi
|
|
if [ -n "$FRONTEND_PID" ]; then
|
|
kill $FRONTEND_PID 2>/dev/null || true
|
|
fi
|
|
echo "Services stopped."
|
|
exit 0
|
|
}
|
|
|
|
# Trap SIGINT (Ctrl+C)
|
|
trap cleanup SIGINT
|
|
|
|
# Start Backend
|
|
start_backend() {
|
|
echo -e "\033[0;34m[Backend]\033[0m Starting on port $BACKEND_PORT..."
|
|
cd backend
|
|
local -a uvicorn_env_args=()
|
|
if [ -f ".venv/bin/activate" ]; then
|
|
source .venv/bin/activate
|
|
else
|
|
echo -e "\033[0;31m[Backend]\033[0m Warning: .venv/bin/activate not found. Attempting to run without venv."
|
|
fi
|
|
if [ -f ".env" ]; then
|
|
uvicorn_env_args=(--env-file .env)
|
|
fi
|
|
# PYTHONUNBUFFERED=1 prevents stderr buffering when piped through sed.
|
|
# Without it, JSON logs from superset_tools_app get stuck in the buffer
|
|
# and never appear in real time (or at all, for small log lines).
|
|
# NOTE: --reload also breaks stderr capture (subprocess child loses the pipe).
|
|
# For hot-reload during development, run uvicorn directly instead:
|
|
# cd backend && source .venv/bin/activate && uvicorn src.app:app --reload --port 8000
|
|
PYTHONUNBUFFERED=1 python3 -m uvicorn src.app:app --port "$BACKEND_PORT" "${uvicorn_env_args[@]}" 2>&1 | sed "s/^/$(echo -e '\033[0;34m[Backend]\033[0m ') /" &
|
|
BACKEND_PID=$!
|
|
cd ..
|
|
}
|
|
|
|
# Start Frontend
|
|
start_frontend() {
|
|
echo -e "\033[0;32m[Frontend]\033[0m Starting on port $FRONTEND_PORT..."
|
|
cd frontend
|
|
# Use a subshell to prefix output
|
|
npm run dev -- --port "$FRONTEND_PORT" 2>&1 | sed "s/^/$(echo -e '\033[0;32m[Frontend]\033[0m ') /" &
|
|
FRONTEND_PID=$!
|
|
cd ..
|
|
}
|
|
|
|
start_backend
|
|
start_frontend
|
|
|
|
echo "Services are running. Press Ctrl+C to stop."
|
|
wait
|
|
|
|
# [/DEF:run:Module]
|