#!/usr/bin/env bash # stemdeck dev server control: setup | start | stop | restart | status set -euo pipefail cd "$(dirname "$0")" HOST="${HOST:-0.0.0.0}" PORT="${PORT:-8000}" RELOAD="${RELOAD:-0}" # Treat the self-hosted server as a persistent, user-managed library (like the # desktop app): opt out of the 24h job TTL sweep so processed tracks are not # auto-deleted. Override with STEMDECK_PERSIST_LIBRARY=0 for disk-hygiene mode. export STEMDECK_PERSIST_LIBRARY="${STEMDECK_PERSIST_LIBRARY:-1}" FOREGROUND="${FOREGROUND:-0}" PID_FILE=".run/uvicorn.pid" LOG_FILE=".run/uvicorn.log" UVICORN=".venv/bin/uvicorn" mkdir -p .run is_running() { [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null } start() { if is_running; then echo "already running (pid $(cat "$PID_FILE"))" return 0 fi if [[ ! -x "$UVICORN" ]]; then echo "uvicorn not found at $UVICORN — run: uv sync" >&2 exit 1 fi echo "starting on http://$HOST:$PORT" # A long-lived SSE stream (the import queue view) keeps a connection open # for as long as a browser tab is open, and uvicorn waits for open # connections before it exits. Without a bound, Ctrl-C appears to hang. # Kept below stop()'s own 5 s deadline so the clean path finishes first and # the lifespan teardown (which reaps the demucs worker) actually runs. local args=(app.main:app --host "$HOST" --port "$PORT" --timeout-graceful-shutdown 2) if [[ "$RELOAD" == "1" ]]; then args+=(--reload) fi if [[ "$FOREGROUND" == "1" ]]; then echo $$ >"$PID_FILE" exec "$UVICORN" "${args[@]}" fi nohup "$UVICORN" "${args[@]}" >"$LOG_FILE" 2>&1 & echo $! >"$PID_FILE" for _ in {1..10}; do sleep 0.5 grep -q "Application startup complete\|Uvicorn running on" "$LOG_FILE" 2>/dev/null && break is_running || break done if is_running; then echo "started (pid $(cat "$PID_FILE"), log: $LOG_FILE)" else echo "failed to start — see $LOG_FILE" >&2 exit 1 fi } stop() { if ! is_running; then echo "not running" # Sweep a stray server for THIS port only. A bare # "uvicorn app.main:app" pattern also matches the backend that # StemDeck.app spawns, so it would kill the desktop app out from # under the user (#352). pkill -f "uvicorn app.main:app.*--port $PORT" 2>/dev/null || true rm -f "$PID_FILE" return 0 fi local pid pid=$(cat "$PID_FILE") echo "stopping pid $pid" kill "$pid" 2>/dev/null || true for _ in {1..10}; do kill -0 "$pid" 2>/dev/null || break sleep 0.5 done if kill -0 "$pid" 2>/dev/null; then echo "force-killing pid $pid" kill -9 "$pid" 2>/dev/null || true fi # No demucs sweep here. The separation worker is a child of the backend # and exits on its own when the backend dies -- its stdin and stderr # pipes close with the parent, which ends its read loop. The pattern # that used to be here ("python -m demucs") never matched it anyway: # the worker runs as "python -m app.pipeline.demucs_worker". rm -f "$PID_FILE" echo "stopped" } status() { if is_running; then echo "running (pid $(cat "$PID_FILE")) on http://$HOST:$PORT" else echo "not running" fi } setup() { local os case "$(uname -s)" in Darwin) os="macos" ;; Linux) os="linux" ;; *) echo "setup: unsupported OS '$(uname -s)' — install ffmpeg + uv manually" >&2 exit 1 ;; esac echo "detected: $os" if [[ "$os" == "macos" ]]; then if ! command -v brew >/dev/null 2>&1; then echo "setup: Homebrew not found — install from https://brew.sh first" >&2 exit 1 fi if ! command -v ffmpeg >/dev/null 2>&1; then echo "==> brew install ffmpeg" brew install ffmpeg else echo "ffmpeg: already installed ($(ffmpeg -version | head -n1))" fi if ! command -v uv >/dev/null 2>&1; then echo "==> brew install uv" brew install uv else echo "uv: already installed ($(uv --version))" fi else # linux: assume Debian/Ubuntu (apt). Other distros fall through to manual. if ! command -v apt-get >/dev/null 2>&1; then echo "setup: non-apt Linux detected — install ffmpeg + uv via your package manager" >&2 exit 1 fi if ! command -v ffmpeg >/dev/null 2>&1; then echo "==> sudo apt-get update && sudo apt-get install -y ffmpeg" sudo apt-get update sudo apt-get install -y ffmpeg else echo "ffmpeg: already installed ($(ffmpeg -version | head -n1))" fi if ! command -v uv >/dev/null 2>&1; then echo "==> installing uv via astral.sh installer" curl -LsSf https://astral.sh/uv/install.sh | sh # installer drops uv in ~/.local/bin; surface it for this shell export PATH="$HOME/.local/bin:$PATH" else echo "uv: already installed ($(uv --version))" fi fi echo "==> uv sync" uv sync --python 3.12 echo echo "setup complete. start the server with: ./run.sh start" } case "${1:-}" in setup) setup ;; start) start ;; stop) stop ;; restart) stop; start ;; status) status ;; *) echo "usage: $0 {setup|start|stop|restart|status}" >&2 exit 2 ;; esac