#!/usr/bin/env bash set -euo pipefail ROOT="${MOBILITY_WORKBENCH_ROOT:-/mnt/DATA/git/meubility-workbench}" PYTHON="${PYTHON:-$ROOT/.venv/bin/python}" HOST="${MOBILITY_HOST:-127.0.0.1}" PORT="${MOBILITY_PORT:-8000}" OPEN_BROWSER="${OPEN_BROWSER:-1}" SAMPLE_MODE="${MOBILITY_SAMPLE_MODE:-missing}" # missing, always, never LOG_DIR="$ROOT/data/dev-launcher" SERVER_LOG="$LOG_DIR/server.log" URL="http://$HOST:$PORT" server_pid="" fail() { printf 'launch-dev: %s\n' "$*" >&2 exit 1 } port_is_free() { "$PYTHON" - "$1" "$2" <<'PY' import socket import sys host = sys.argv[1] port = int(sys.argv[2]) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((host, port)) except OSError: raise SystemExit(1) PY } wait_for_url() { "$PYTHON" - "$1" <<'PY' import sys import time import urllib.request url = sys.argv[1] deadline = time.monotonic() + 60 last_error = None while time.monotonic() < deadline: try: with urllib.request.urlopen(url, timeout=2) as response: if 200 <= response.status < 500: raise SystemExit(0) except Exception as exc: # noqa: BLE001 - printed only on timeout. last_error = exc time.sleep(1) print(f"Timed out waiting for {url}: {last_error}", file=sys.stderr) raise SystemExit(1) PY } configured_database() { "$PYTHON" - <<'PY' from app.config import settings kind = "sqlite" if settings.is_sqlite_database else "postgresql" if settings.is_postgresql_database else "other" print(f"{kind}\t{settings.database_url}") PY } cleanup() { if [ -n "${server_pid:-}" ] && kill -0 "$server_pid" 2>/dev/null; then kill "$server_pid" 2>/dev/null || true fi } trap cleanup EXIT INT TERM [ -x "$PYTHON" ] || fail "Python virtualenv not found at $PYTHON. Run: cd $ROOT && python -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt" mkdir -p "$LOG_DIR" : > "$SERVER_LOG" port_is_free "$HOST" "$PORT" || fail "$URL is already in use" cd "$ROOT" db_info="$(configured_database)" db_kind="$(printf '%s' "$db_info" | cut -f1)" db_url="$(printf '%s' "$db_info" | cut -f2-)" case "$SAMPLE_MODE" in always) printf 'Loading sample project. This clears project data in the configured database.\n' "$PYTHON" -m app.cli load-sample ;; missing) if [ "$db_kind" = "sqlite" ] && [ "$db_url" = "sqlite:///./data/workbench.sqlite" ] && [ ! -s "$ROOT/data/workbench.sqlite" ]; then printf 'Default SQLite database is missing. Loading sample project.\n' "$PYTHON" -m app.cli load-sample else "$PYTHON" -m app.cli init-db fi ;; never) "$PYTHON" -m app.cli init-db ;; *) fail "MOBILITY_SAMPLE_MODE must be missing, always, or never" ;; esac printf 'Starting Mobility Workbench at %s\n' "$URL" "$PYTHON" -m uvicorn app.main:app --host "$HOST" --port "$PORT" --reload >"$SERVER_LOG" 2>&1 & server_pid="$!" printf 'Waiting for %s\n' "$URL" wait_for_url "$URL" || { tail -n 80 "$SERVER_LOG" >&2 || true fail "server did not become reachable" } if [ "$OPEN_BROWSER" = "1" ] && command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" >/dev/null 2>&1 || true fi cat <