| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| set -euo pipefail |
|
|
| ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| API_PORT="${LEXORA_API_PORT:-7862}" |
| WEB_PORT="${LEXORA_WEB_PORT:-3020}" |
| API_DIR="$ROOT/apps/api" |
| WEB_DIR="$ROOT/apps/web" |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| pid_cwd() { lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1 || true; } |
|
|
| |
| is_ours() { |
| local cwd; cwd="$(pid_cwd "$1")" |
| [ -n "$cwd" ] && [ "${cwd#"$ROOT"}" != "$cwd" ] |
| } |
|
|
| |
| port_pid() { lsof -tiTCP:"$1" -sTCP:LISTEN 2>/dev/null | head -1 || true; } |
|
|
| |
| |
| our_pid() { |
| local pid; pid="$(port_pid "$1")" |
| if [ -n "$pid" ] && is_ours "$pid"; then echo "$pid"; fi |
| } |
|
|
| api_pid() { our_pid "$API_PORT"; } |
| web_pid() { our_pid "$WEB_PORT"; } |
|
|
| |
| report_foreign() { |
| local port="$1" var="$2" pid; pid="$(port_pid "$port")" |
| [ -z "$pid" ] && return 1 |
| echo " port $port is held by pid $pid running from $(pid_cwd "$pid")" >&2 |
| echo " that is not this repo, so it will not be touched." >&2 |
| echo " set LEXORA_${var}_PORT=<free port> and retry." >&2 |
| return 0 |
| } |
|
|
| wait_for() { |
| local url="$1" name="$2" |
| for _ in $(seq 1 90); do |
| if curl -sf "$url" >/dev/null 2>&1; then echo " $name ready"; return 0; fi |
| sleep 1 |
| done |
| echo " $name did NOT come up -- see /tmp/lexora_$name.log" >&2 |
| return 1 |
| } |
|
|
| start_api() { |
| if [ -n "$(api_pid)" ]; then echo " api already running (pid $(api_pid))"; return 0; fi |
| if report_foreign "$API_PORT" API; then return 1; fi |
| cd "$ROOT" |
| PYTHONPATH="$API_DIR" nohup "$API_DIR/.venv/bin/python" -m uvicorn app.main:app \ |
| --host 127.0.0.1 --port "$API_PORT" --log-level warning \ |
| > /tmp/lexora_api.log 2>&1 & |
| wait_for "http://127.0.0.1:$API_PORT/api/health" "api" |
| } |
|
|
| start_web() { |
| if [ -n "$(web_pid)" ]; then echo " web already running (pid $(web_pid))"; return 0; fi |
| if report_foreign "$WEB_PORT" WEB; then return 1; fi |
| cd "$WEB_DIR" |
| |
| NEXT_PUBLIC_API_URL="${NEXT_PUBLIC_API_URL:-http://127.0.0.1:$API_PORT}" \ |
| nohup npm run dev > /tmp/lexora_web.log 2>&1 & |
| wait_for "http://127.0.0.1:$WEB_PORT" "web" |
| } |
|
|
| |
| |
| |
| |
| |
| kill_pid() { |
| local pid="$1" name="$2" |
| kill "$pid" 2>/dev/null || { echo " $name (pid $pid) already gone"; return 0; } |
| for _ in $(seq 1 100); do |
| kill -0 "$pid" 2>/dev/null || { echo " $name stopped (pid $pid)"; return 0; } |
| sleep 0.1 |
| done |
| echo " $name (pid $pid) ignored SIGTERM; sending SIGKILL" >&2 |
| kill -9 "$pid" 2>/dev/null || true |
| for _ in $(seq 1 20); do |
| kill -0 "$pid" 2>/dev/null || { echo " $name killed (pid $pid)"; return 0; } |
| sleep 0.1 |
| done |
| echo " $name (pid $pid) SURVIVED SIGKILL -- investigate before running tests" >&2 |
| return 1 |
| } |
|
|
| stop_all() { |
| local api web rc=0 |
| api="$(api_pid)"; web="$(web_pid)" |
| [ -n "$api" ] && { kill_pid "$api" api || rc=1; } |
| [ -n "$web" ] && { kill_pid "$web" web || rc=1; } |
| [ -z "$api$web" ] && echo " nothing of ours was running" |
| echo " nothing outside $ROOT was signalled" |
| return $rc |
| } |
|
|
| show() { |
| local name="$1" port="$2" mine foreign |
| mine="$(our_pid "$port")" |
| foreign="$(port_pid "$port")" |
| if [ -n "$mine" ]; then |
| echo " $name up pid $mine :$port" |
| elif [ -n "$foreign" ]; then |
| echo " $name FOREIGN pid $foreign :$port <- $(pid_cwd "$foreign")" |
| else |
| echo " $name down :$port" |
| fi |
| } |
|
|
| case "${1:-start}" in |
| start) echo "starting Lexora"; start_api; start_web ;; |
| api) start_api ;; |
| web) start_web ;; |
| stop) stop_all ;; |
| status) |
| show api "$API_PORT" |
| show web "$WEB_PORT" |
| curl -sf "http://127.0.0.1:$API_PORT/api/health" 2>/dev/null | head -c 200 || true |
| echo |
| ;; |
| *) echo "usage: scripts/dev.sh {start|stop|status|api|web}" >&2; exit 2 ;; |
| esac |
|
|