#!/bin/bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" SESSION_NAME="${SESSION_NAME:-eryao-dev}" ENV_FILE="$ROOT_DIR/.env" ENV_LOADER="$ROOT_DIR/infra/scripts/lib/env.sh" usage() { echo "Usage: $0 {start|stop|restart}" echo "" echo "Commands:" echo " start Start local web/worker processes in tmux" echo " stop Stop tmux session and orphaned local processes" echo " restart Stop then start all app processes" exit 1 } load_env_if_exists() { # shellcheck disable=SC1090 . "$ENV_LOADER" load_env_file "$ENV_FILE" } is_port_in_use() { local port="$1" if command -v lsof >/dev/null 2>&1; then lsof -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1 return $? fi if command -v ss >/dev/null 2>&1; then ss -ltn "sport = :$port" | awk 'NR > 1 {exit 0} END {exit 1}' return $? fi return 1 } collect_listening_pids() { local port="$1" if command -v lsof >/dev/null 2>&1; then lsof -iTCP:"$port" -sTCP:LISTEN -t | sort -u return fi if command -v ss >/dev/null 2>&1; then ss -lptn "sport = :$port" | awk -F 'pid=' 'NF > 1 {split($2, tmp, ","); print tmp[1]}' | sort -u fi } kill_pids_gracefully() { local label="$1" shift local pids=("$@") local alive=() if [ "${#pids[@]}" -eq 0 ]; then return fi echo "Stopping ${label}: ${pids[*]}" kill -TERM "${pids[@]}" 2>/dev/null || true for _ in {1..10}; do alive=() for pid in "${pids[@]}"; do if kill -0 "$pid" 2>/dev/null; then alive+=("$pid") fi done if [ "${#alive[@]}" -eq 0 ]; then return fi sleep 1 done echo "Force killing ${label}: ${alive[*]}" kill -KILL "${alive[@]}" 2>/dev/null || true } kill_matching_processes() { local label="$1" local pattern="$2" local pids pids="$(pgrep -f "$pattern" || true)" if [ -z "$pids" ]; then return fi # shellcheck disable=SC2086 kill_pids_gracefully "$label" $pids } kill_listening_processes() { local label="$1" local port="$2" local pids pids="$(collect_listening_pids "$port" || true)" if [ -z "$pids" ]; then return fi # shellcheck disable=SC2086 kill_pids_gracefully "$label" $pids } start() { echo "=== Eryao App Up ===" echo "This script starts local web + worker processes in tmux." echo "Redis should be managed separately via docker-compose." echo "NOTE: Database migration must be run separately." echo "" if ! command -v tmux >/dev/null 2>&1; then echo "Error: tmux is required." >&2 exit 1 fi if [ ! -f "$ENV_FILE" ]; then echo "Error: env file not found at $ENV_FILE" >&2 exit 1 fi load_env_if_exists UVICORN_LOG_LEVEL="${ERYAO_RUNTIME__LOG_LEVEL:-info}" UVICORN_LOG_LEVEL="$(echo "$UVICORN_LOG_LEVEL" | tr '[:upper:]' '[:lower:]')" WEB_PORT="${ERYAO_WEB__PORT:-8000}" if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo "Error: tmux session '$SESSION_NAME' already exists." >&2 echo "Hint: tmux kill-session -t $SESSION_NAME" >&2 exit 1 fi if is_port_in_use "$WEB_PORT"; then echo "Error: web port ${WEB_PORT} is already in use." >&2 echo "Hint: run '$0 stop' or change ERYAO_WEB__PORT in .env" >&2 exit 1 fi if [ -z "${ERYAO_LLM__PROVIDER_KEYS__DEEPSEEK:-}" ]; then echo "Warning: ERYAO_LLM__PROVIDER_KEYS__DEEPSEEK is empty; deepseek calls may fail." >&2 fi WEB_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=web uv run uvicorn app:app --host ${ERYAO_WEB__HOST:-0.0.0.0} --port ${WEB_PORT} --workers ${ERYAO_WEB__WORKERS:-2} --log-level ${UVICORN_LOG_LEVEL}" WORKER_AGENT_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=worker-agent uv run taskiq worker core.taskiq.app:worker_agent_broker core.agentscope.runtime.tasks --workers ${ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY:-2}" WORKER_GENERAL_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=worker-general uv run taskiq worker core.taskiq.app:worker_general_broker core.agentscope.runtime.tasks v1.feedback.tasks --workers ${ERYAO_WORKER__GROUPS__GENERAL__CONCURRENCY:-1}" echo "Starting tmux web process in session '$SESSION_NAME'..." tmux new-session -d -s "$SESSION_NAME" -n web "bash -lc \"$WEB_CMD; echo '[web] exited'; exec bash\"" tmux new-window -t "$SESSION_NAME" -n worker-agent "bash -lc \"$WORKER_AGENT_CMD; echo '[worker-agent] exited'; exec bash\"" tmux new-window -t "$SESSION_NAME" -n worker-general "bash -lc \"$WORKER_GENERAL_CMD; echo '[worker-general] exited'; exec bash\"" echo "" echo "=== App Started ===" echo "Log files will be created in logs/ directory:" echo " - web.log, web.error.log" echo " - worker-agent.log, worker-agent.error.log" echo " - worker-general.log, worker-general.error.log" echo "" echo "tmux attach -t $SESSION_NAME" echo "tmux list-windows -t $SESSION_NAME" } stop() { echo "=== Eryao App Down ===" echo "Stopping tmux app processes (docker redis is not managed here)." load_env_if_exists WEB_PORT="${ERYAO_WEB__PORT:-8000}" if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo "Stopping tmux session '$SESSION_NAME'..." tmux kill-session -t "$SESSION_NAME" else echo "No tmux session '$SESSION_NAME' found." fi echo "Checking for orphaned processes..." kill_matching_processes "uvicorn" "uv run uvicorn app:app" kill_matching_processes "taskiq workers" "uv run taskiq worker core.taskiq.app:" kill_listening_processes "port ${WEB_PORT} listeners" "$WEB_PORT" if is_port_in_use "$WEB_PORT"; then echo "Warning: port ${WEB_PORT} is still in use after cleanup." >&2 echo "Hint: check process with 'lsof -iTCP:${WEB_PORT} -sTCP:LISTEN'" >&2 return 1 fi echo "Session stopped and cleaned up." } restart() { stop echo "" start } case "${1:-}" in start) start ;; stop) stop ;; restart) restart ;; *) usage ;; esac