224 lines
9.8 KiB
Bash
Executable file
224 lines
9.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# Bifröst lokal starten: PostgreSQL, Migrationen, DB-Dienst, Web und Worker in einem Terminal (Logs mit Präfix).
|
||
# Strg+C stoppt alles, was das Skript gestartet hat; die Container laufen weiter.
|
||
#
|
||
# scripts/dev.sh # Web auf http://127.0.0.1:8765, Worker, DB-Dienst PostgreSQL (Port 8791)
|
||
# scripts/dev.sh --oracle # dazu Oracle-Container und DB-Dienst für Oracle (Port 8792)
|
||
# scripts/dev.sh --port 8000 # anderer Port für Web
|
||
# scripts/dev.sh --setup # Ersteinrichtung: .env anlegen, Master-Key, Demo-Nutzer
|
||
# scripts/dev.sh --reset # von null: lokale Datenbank und data/ löschen (mit Rückfrage), dann wie
|
||
# # --setup und zusätzlich die Demo-Workspaces anlegen (aiw demo)
|
||
# scripts/dev.sh --reset --yes # ohne Rückfrage; mit --oracle auch die Oracle-Daten
|
||
#
|
||
# Die DB-Dienste registriert man einmal in Bifröst als Datenbank-Treiber (Administration → Datenbank-Treiber);
|
||
# das Skript zeigt URL und Schlüssel beim Start.
|
||
# Einstellungen kommen aus .env. Für lokale Quellen setzt das Skript AIW_DB_ALLOW_LOCAL und AIW_MCP_ALLOW_LOCAL
|
||
# auf true, sofern .env nichts anderes sagt.
|
||
set -euo pipefail
|
||
|
||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||
cd "$ROOT"
|
||
|
||
PORT=8765
|
||
ORACLE=false
|
||
SETUP=false
|
||
RESET=false
|
||
YES=false
|
||
# Schlüssel nur für die lokal gestarteten DB-Dienste
|
||
DEV_KEY="dev-schluessel-12345678"
|
||
|
||
# Hilfe: der Kommentarblock am Anfang
|
||
usage() { awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"; }
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--oracle) ORACLE=true ;;
|
||
--port) PORT=${2:?Port fehlt}; shift ;;
|
||
--setup) SETUP=true ;;
|
||
--reset) RESET=true; SETUP=true ;;
|
||
--yes) YES=true ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) echo "Unbekannte Option: $1"; usage; exit 1 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
say() { printf '\033[1m==> %s\033[0m\n' "$*"; }
|
||
fail() { printf '\033[31m%s\033[0m\n' "$*" >&2; exit 1; }
|
||
|
||
# Zeilen eines Prozesses mit Namen versehen (portabel, auch mit der Bash von macOS)
|
||
prefix() { while IFS= read -r line; do printf '[%-6s] %s\n' "$1" "$line"; done; }
|
||
|
||
# Lauscht etwas auf dem Port? Mit lsof, sonst (z. B. Linux ohne lsof) per Verbindungsversuch
|
||
port_busy() {
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
|
||
else
|
||
(exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# Wer belegt den Port? (für die Fehlermeldung; leer, wenn es sich nicht feststellen lässt)
|
||
port_owner() {
|
||
command -v lsof >/dev/null 2>&1 || return 0
|
||
lsof -nP -iTCP:"$1" -sTCP:LISTEN 2>/dev/null | awk 'NR == 2 { print $1 " (PID " $2 ")" }'
|
||
}
|
||
|
||
env_value() { grep -E "^$1=" .env 2>/dev/null | tail -1 | cut -d= -f2-; }
|
||
|
||
command -v docker >/dev/null || fail "Docker fehlt."
|
||
command -v uv >/dev/null || fail "uv fehlt (https://docs.astral.sh/uv/)."
|
||
|
||
# Von null: lokale Daten löschen. .env bleibt (Master-Key, Demo-Passwort), Docker-Images auch.
|
||
if $RESET; then
|
||
port_busy "$PORT" && fail "Auf Port $PORT läuft noch etwas (Bifröst?). Erst stoppen, dann --reset."
|
||
echo "Gelöscht werden ALLE lokalen Daten von Bifröst: Datenbank (Workspaces, Chats, Nutzer, Modelle, API-Keys,"
|
||
echo "Einstellungen) samt Shop-Datenbank der Demo, dazu Uploads, Git-Cache und Demo-Repositories unter data/."
|
||
$ORACLE && echo "Mit --oracle zusätzlich die Daten des Oracle-Containers."
|
||
if ! $YES; then
|
||
# Ohne Terminal (Skript, CI) nicht auf eine Antwort warten: dort ausdrücklich --yes
|
||
[ -t 0 ] || fail "Abgebrochen, nichts gelöscht: Die Rückfrage braucht ein Terminal (sonst --yes)."
|
||
printf "Fortfahren? Zum Bestätigen 'ja' eingeben: "
|
||
read -r answer || answer=""
|
||
[ "$answer" = "ja" ] || fail "Abgebrochen, nichts gelöscht."
|
||
fi
|
||
say "PostgreSQL-Container und -Daten löschen"
|
||
docker compose rm -sfv postgres >/dev/null
|
||
docker volume rm -f ai-workspace_aiw-postgres18 >/dev/null
|
||
if $ORACLE; then
|
||
say "Oracle-Container und -Daten löschen"
|
||
docker compose --profile oracle rm -sfv oracle >/dev/null
|
||
docker volume rm -f ai-workspace_aiw-oracle >/dev/null
|
||
fi
|
||
# Ablagen aus .env, sofern sie im Projekt liegen; data/ immer
|
||
for dir in data "$(env_value AIW_UPLOAD_DIR)" "$(env_value AIW_GIT_CACHE_DIR)"; do
|
||
[ -n "$dir" ] || continue
|
||
case "$dir" in
|
||
/*|*..*) echo "Übersprungen (außerhalb des Projekts, bitte selbst prüfen): $dir" ;;
|
||
*) if [ -e "$dir" ]; then say "$dir/ löschen"; rm -rf -- "$dir"; fi ;;
|
||
esac
|
||
done
|
||
fi
|
||
|
||
if $SETUP; then
|
||
if [ -f .env ]; then
|
||
say ".env gibt es schon, bleibt unverändert"
|
||
else
|
||
say ".env aus .env.example anlegen"
|
||
cp .env.example .env
|
||
fi
|
||
uv sync
|
||
if [ -z "$(env_value AIW_SECRET_KEY)" ]; then
|
||
say "Master-Key erzeugen (AIW_SECRET_KEY)"
|
||
key=$(uv run aiw gen-key | tail -1)
|
||
grep -q '^AIW_SECRET_KEY=' .env && sed -i '' '/^AIW_SECRET_KEY=/d' .env
|
||
printf 'AIW_SECRET_KEY=%s\n' "$key" >> .env
|
||
fi
|
||
if [ -z "$(env_value AIW_DEMO_PASSWORD)" ]; then
|
||
echo "Hinweis: AIW_DEMO_PASSWORD in .env setzen, sonst ist die Demo-Anmeldung (admin, entwicklung, fachbereich, freigabe) aus."
|
||
fi
|
||
fi
|
||
|
||
[ -f .env ] || fail "Keine .env. Ersteinrichtung mit: scripts/dev.sh --setup"
|
||
[ -n "${AIW_SECRET_KEY:-$(env_value AIW_SECRET_KEY)}" ] || fail "AIW_SECRET_KEY fehlt in .env (scripts/dev.sh --setup)."
|
||
port_busy "$PORT" && fail "Port $PORT ist belegt $(port_owner "$PORT") (läuft Bifröst schon?). Anderer Port: --port 8000"
|
||
WORKER_PORT=${AIW_WORKER_HTTP_PORT:-$(env_value AIW_WORKER_HTTP_PORT)}
|
||
WORKER_PORT=${WORKER_PORT:-9101}
|
||
port_busy "$WORKER_PORT" && fail "Port $WORKER_PORT (Health des Workers) ist belegt $(port_owner "$WORKER_PORT") \
|
||
– läuft noch ein Worker? Stoppen oder AIW_WORKER_HTTP_PORT in .env ändern."
|
||
|
||
say "PostgreSQL starten (127.0.0.1:55433)"
|
||
docker compose up -d --wait postgres
|
||
|
||
say "Migrationen"
|
||
uv run aiw migrate
|
||
$SETUP && { say "Demo-Nutzer anlegen"; uv run aiw seed-demo; }
|
||
|
||
: "${AIW_DB_ALLOW_LOCAL:=$(env_value AIW_DB_ALLOW_LOCAL)}"
|
||
: "${AIW_MCP_ALLOW_LOCAL:=$(env_value AIW_MCP_ALLOW_LOCAL)}"
|
||
export AIW_DB_ALLOW_LOCAL=${AIW_DB_ALLOW_LOCAL:-true}
|
||
export AIW_MCP_ALLOW_LOCAL=${AIW_MCP_ALLOW_LOCAL:-true}
|
||
export AIW_WORKER_HTTP_HOST=${AIW_WORKER_HTTP_HOST:-127.0.0.1}
|
||
export AIW_COOKIE_SECURE=${AIW_COOKIE_SECURE:-false}
|
||
export PYTHONUNBUFFERED=1
|
||
# Stürzt Python in nativem Code ab (z. B. SIGILL, SIGSEGV), zeigt das den Python-Stack bis zur Bibliothek
|
||
export PYTHONFAULTHANDLER=1
|
||
# psycopg ohne Binärpaket (reine Python-Variante mit der libpq des Systems), z. B. für VMs ohne AVX:
|
||
# PSYCOPG_IMPL=python in .env oder der Umgebung; gilt für Web, Worker und den DB-Dienst
|
||
: "${PSYCOPG_IMPL:=$(env_value PSYCOPG_IMPL)}"
|
||
[ -n "$PSYCOPG_IMPL" ] && export PSYCOPG_IMPL && say "psycopg-Variante: $PSYCOPG_IMPL"
|
||
|
||
trap 'trap - INT TERM EXIT; echo; say "Stoppe Web, Worker und DB-Dienste"; kill 0 2>/dev/null; wait 2>/dev/null' INT TERM EXIT
|
||
# Gestartete Prozesse: PID, Name und Befehl zum Einzelstart (parallele Listen, Bash 3 von macOS kennt keine Maps)
|
||
PIDS=()
|
||
NAMES=()
|
||
COMMANDS=()
|
||
started() { PIDS+=("$1"); NAMES+=("$2"); COMMANDS+=("$3"); }
|
||
|
||
HINTS=()
|
||
|
||
# DB-Dienst starten, außer er läuft schon auf dem Port
|
||
db_service() {
|
||
local engine=$1 port=$2
|
||
if curl -sf "http://127.0.0.1:$port/ready" >/dev/null 2>&1; then
|
||
say "DB-Dienst $engine läuft schon auf Port $port (Schlüssel muss $DEV_KEY sein)"
|
||
else
|
||
port_busy "$port" && fail "Port $port ist belegt, aber dort antwortet kein DB-Dienst."
|
||
say "DB-Dienst $engine starten (Port $port)"
|
||
(cd services/db-mcp && DBMCP_API_KEYS="dev:$DEV_KEY" DBMCP_ALLOW_LOCAL=true DBMCP_PORT=$port \
|
||
DBMCP_BIND=127.0.0.1 "$ROOT/.venv/bin/python" -m "dbmcp.$engine" 2>&1 | prefix "$engine") &
|
||
started $! "DB-Dienst $engine (Port $port)" \
|
||
"cd services/db-mcp && DBMCP_API_KEYS=dev:$DEV_KEY DBMCP_ALLOW_LOCAL=true DBMCP_PORT=$port ../../.venv/bin/python -m dbmcp.$engine"
|
||
for _ in $(seq 50); do curl -sf "http://127.0.0.1:$port/ready" >/dev/null 2>&1 && break; sleep 0.2; done
|
||
fi
|
||
HINTS+=("$engine: URL http://localhost:$port/mcp Schlüssel des Dienstes $DEV_KEY")
|
||
}
|
||
|
||
if $ORACLE; then
|
||
say "Oracle starten (127.0.0.1:55434, Service FREEPDB1; der erste Start dauert ein paar Minuten)"
|
||
docker compose --profile oracle up -d --wait oracle
|
||
db_service oracle 8792
|
||
fi
|
||
db_service postgres 8791
|
||
|
||
if $RESET; then
|
||
say "Demo-Workspaces anlegen (aiw demo)"
|
||
uv run aiw demo
|
||
fi
|
||
|
||
say "Web starten: http://127.0.0.1:$PORT"
|
||
(uv run aiw web --port "$PORT" 2>&1 | prefix web) &
|
||
started $! "Web (Port $PORT)" "uv run aiw web --port $PORT"
|
||
say "Worker starten"
|
||
(uv run aiw worker 2>&1 | prefix worker) &
|
||
started $! "Worker" "uv run aiw worker"
|
||
|
||
if $RESET; then
|
||
say "Fertig von null. Noch offen: als admin ein Modell einrichten (Administration → Modelle und Zugänge)."
|
||
say "Alles Weitere (Munin, Hugin, Dokumenten-Auslese, Bitbucket …): docs/checkliste.md"
|
||
say "DB-Dienste (PostgreSQL ist für die Demo schon registriert):"
|
||
else
|
||
say "DB-Dienste, einmalig registrieren unter Administration → Datenbank-Treiber, dann Tools abrufen und einstufen:"
|
||
fi
|
||
for hint in "${HINTS[@]}"; do echo " $hint"; done
|
||
|
||
# Endet ein Prozess (z. B. Fehler beim Start), sagen welcher und alles stoppen
|
||
while true; do
|
||
for i in "${!PIDS[@]}"; do
|
||
if ! kill -0 "${PIDS[$i]}" 2>/dev/null; then
|
||
code=0
|
||
wait "${PIDS[$i]}" 2>/dev/null || code=$?
|
||
echo
|
||
printf '\033[31m%s hat sich beendet (Exit-Code %s).\033[0m\n' "${NAMES[$i]}" "$code" >&2
|
||
case $code in
|
||
132) echo "Signal SIGILL: Die CPU kennt eine Anweisung einer nativen Bibliothek nicht (oft VM ohne AVX/AVX2;" \
|
||
"CPU-Typ der VM auf „host“ stellen). Welche Bibliothek, zeigt der Stack mit „Fatal Python error“ oben." >&2 ;;
|
||
134|139) echo "Absturz in nativem Code (Signal $((code - 128))); Stack mit „Fatal Python error“ oben." >&2 ;;
|
||
esac
|
||
echo "Seine Ausgabe steht oben mit Präfix; fehlt sie, den Prozess einzeln starten und die Meldung ansehen:" >&2
|
||
echo " ${COMMANDS[$i]}" >&2
|
||
exit 1
|
||
fi
|
||
done
|
||
sleep 1
|
||
done
|