-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.sh
More file actions
executable file
·211 lines (190 loc) · 7.6 KB
/
Copy pathrun.sh
File metadata and controls
executable file
·211 lines (190 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/env bash
#
# run.sh — start all Singularity services locally with one command.
#
# Brings up the infrastructure containers (Postgres, Redis, Qdrant) via
# Docker Compose, then runs the application processes locally:
# - API server (uvicorn, :8000)
# - Research worker (arq)
# - Frontend (next dev, :3000)
#
# All application logs are streamed to ./logs/ and the processes are torn
# down together on Ctrl-C.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
LOG_DIR="$ROOT/logs"
mkdir -p "$LOG_DIR"
# ---------------------------------------------------------------------------
# Load a KEY=VALUE .env file into the environment.
#
# Pydantic Settings and Next.js read their .env files on their own, but some
# code paths use raw os.getenv (e.g. SINGULARITY_MODAL_ENABLED in
# api/services/chat_stream.py), which only sees variables present in the
# process environment. Exporting the file here keeps every path consistent.
# Existing environment values win, so an explicit override on the command line
# (or an already-loaded parent file) is respected.
# ---------------------------------------------------------------------------
load_env_file() {
local file="$1"
[[ -f "$file" ]] || return 0
echo "==> Loading environment from ${file#$ROOT/}..."
local line key
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip blanks and comments; only accept KEY=VALUE lines.
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
[[ "$line" != *=* ]] && continue
key="${line%%=*}"
key="$(printf '%s' "$key" | tr -d '[:space:]')"
# Don't clobber a value already set in the environment.
if [[ -z "${!key:-}" ]]; then
export "$line"
fi
done < "$file"
}
load_env_file "$ROOT/.env"
# ---------------------------------------------------------------------------
# Resolve the Python environment (prefer the project virtualenv).
# ---------------------------------------------------------------------------
if [[ -x "$ROOT/.venv/bin/python" ]]; then
PY="$ROOT/.venv/bin/python"
BIN="$ROOT/.venv/bin"
else
PY="$(command -v python3)"
BIN="$(dirname "$PY")"
fi
# Pick the docker compose invocation available on this machine.
if docker compose version >/dev/null 2>&1; then
DC="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
DC="docker-compose"
else
echo "error: docker compose is required to start the infrastructure services" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Decide which infrastructure to start based on the database backend.
# SINGULARITY_USE_SQLITE=true (local dev) needs only Redis; otherwise the
# backend targets Postgres, so bring up Postgres and Qdrant too.
# The value is read from the environment first, falling back to .env.
# ---------------------------------------------------------------------------
use_sqlite="${SINGULARITY_USE_SQLITE:-}"
if [[ -z "$use_sqlite" && -f "$ROOT/.env" ]]; then
use_sqlite="$(grep -E '^\s*SINGULARITY_USE_SQLITE\s*=' "$ROOT/.env" | tail -1 | cut -d= -f2- | tr -d '[:space:]' || true)"
fi
case "$(printf '%s' "$use_sqlite" | tr '[:upper:]' '[:lower:]')" in
1 | true | yes | on)
# Local dev: SQLite for the relational store, but the research worker still
# ingests reports into Qdrant, so bring up the local Qdrant container and
# point the app at it (unless QDRANT_URL is already set, e.g. to point at a
# cloud instance). Prod leaves QDRANT_URL set in the environment/.env.
INFRA_SERVICES=(redis qdrant)
DB_BACKEND="SQLite (local)"
: "${QDRANT_URL:=http://localhost:6333}"
export QDRANT_URL
;;
*)
INFRA_SERVICES=(postgres redis qdrant)
DB_BACKEND="PostgreSQL"
;;
esac
# ---------------------------------------------------------------------------
# Preflight: refuse to start when an app port is already taken. A stale
# frontend/API from a previous session otherwise makes the new process exit
# immediately, and the watchdog below then tears everything down — which looks
# like the backend "stopping on its own".
# ---------------------------------------------------------------------------
for port in 8000 3000; do
holder="$(lsof -nP -iTCP:$port -sTCP:LISTEN -t 2>/dev/null | head -1 || true)"
if [[ -n "$holder" ]]; then
echo "error: port $port is already in use by:" >&2
ps -p "$holder" -o pid,etime,command | tail -1 >&2
echo "Stop it first (kill $holder), then rerun ./run.sh." >&2
exit 1
fi
done
PIDS=()
cleanup() {
echo
echo "==> Shutting down application processes..."
for pid in "${PIDS[@]:-}"; do
if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
wait 2>/dev/null || true
echo "==> Stopping infrastructure containers..."
$DC stop "${INFRA_SERVICES[@]}" >/dev/null 2>&1 || true
echo "==> Done."
}
trap cleanup INT TERM EXIT
# ---------------------------------------------------------------------------
# 1. Infrastructure
# ---------------------------------------------------------------------------
echo "==> Database backend: $DB_BACKEND"
echo "==> Starting infrastructure (${INFRA_SERVICES[*]})..."
$DC up -d "${INFRA_SERVICES[@]}"
echo "==> Waiting for services to be healthy..."
for _ in $(seq 1 30); do
all_healthy=1
for svc in "${INFRA_SERVICES[@]}"; do
# Only services with a healthcheck report a health status; treat an empty
# status (e.g. qdrant, which has none) as ready.
health=$($DC ps "$svc" --format '{{.Health}}' 2>/dev/null || echo "")
if [[ -n "$health" && "$health" != "healthy" ]]; then
all_healthy=0
fi
done
[[ "$all_healthy" == 1 ]] && break
sleep 1
done
# ---------------------------------------------------------------------------
# 2. Database migrations
# ---------------------------------------------------------------------------
echo "==> Applying database migrations (alembic upgrade head)..."
"$BIN/alembic" upgrade head
# ---------------------------------------------------------------------------
# 3. Application processes
# ---------------------------------------------------------------------------
NAMES=()
echo "==> Starting API server (:8000)..."
"$BIN/uvicorn" api.main:app --host 0.0.0.0 --port 8000 --reload \
>"$LOG_DIR/api.log" 2>&1 &
PIDS+=($!)
NAMES+=("API server (logs/api.log)")
echo "==> Starting research worker (arq)..."
"$BIN/arq" api.research_worker.WorkerSettings \
>"$LOG_DIR/worker.log" 2>&1 &
PIDS+=($!)
NAMES+=("research worker (logs/worker.log)")
echo "==> Starting frontend (:3000)..."
(
# Load the frontend's own env explicitly. Next.js also auto-loads
# frontend/.env, but exporting it here makes the values available to the
# process regardless of how it is launched. Root .env values already
# exported above are not overwritten (existing values win).
load_env_file "$ROOT/frontend/.env"
cd "$ROOT/frontend"
[[ -d node_modules ]] || npm install
npm run dev
) >"$LOG_DIR/frontend.log" 2>&1 &
PIDS+=($!)
NAMES+=("frontend (logs/frontend.log)")
echo
echo "All services started:"
echo " API -> http://localhost:8000 (logs: logs/api.log)"
echo " Worker -> arq research worker (logs: logs/worker.log)"
echo " Frontend -> http://localhost:3000 (logs: logs/frontend.log)"
echo
echo "Press Ctrl-C to stop everything."
# Wait on the application processes; exit (and trigger cleanup) when any dies.
# ``wait -n`` is bash 4+, but macOS ships bash 3.2, so poll the PIDs instead.
while true; do
for i in "${!PIDS[@]}"; do
if ! kill -0 "${PIDS[$i]}" 2>/dev/null; then
echo "==> ${NAMES[$i]} exited; shutting everything down. Check its log for the cause."
exit 1
fi
done
sleep 2
done