#!/usr/bin/env bash # # audio-agent-engine — start / stop / restart helper. # # Usage: # ./server.sh start [--build] Start the server (optionally build first). # ./server.sh stop Stop the running server. # ./server.sh restart [--build] Stop then start (optionally build first). # ./server.sh status Show process / port / HTTP status. # ./server.sh logs [N] Tail the last N log lines (default 50, -f). # ./server.sh build Build the release binary only. # # Environment: # PROFILE=release (default) | debug Which Cargo profile to run. # RUST_LOG=info (default) Log verbosity; `audio_agent_engine::tts=debug` # and `audio_agent_engine::vad=debug` are always # appended (unless already present). # PORT=8787 (default) Must match SERVER_PORT in .env. # set -euo pipefail # --- Resolve project layout (script may be called from anywhere) ------------ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" PROFILE="${PROFILE:-release}" # INFO globally, DEBUG for the TTS and VAD subsystems (per-chunk streaming / # per-window VAD decisions). Each directive is appended even when RUST_LOG is # already exported (e.g. by an IDE or the parent shell as `RUST_LOG=info`), # otherwise those debug logs would be silently filtered out. RUST_LOG="${RUST_LOG:-info}" case "$RUST_LOG" in *audio_agent_engine::tts*) ;; *) RUST_LOG="$RUST_LOG,audio_agent_engine::tts=debug" ;; esac case "$RUST_LOG" in *audio_agent_engine::vad*) ;; *) RUST_LOG="$RUST_LOG,audio_agent_engine::vad=debug" ;; esac PORT="${PORT:-8787}" RUN_DIR="$SCRIPT_DIR/run" PID_FILE="$RUN_DIR/server.pid" LOG_FILE="$RUN_DIR/server.log" BIN="$SCRIPT_DIR/target/$PROFILE/audio-agent-engine" CERT_DIR="$SCRIPT_DIR/certs" CERT_FILE="$CERT_DIR/cert.pem" KEY_FILE="$CERT_DIR/key.pem" # --- Colors ----------------------------------------------------------------- if [ -t 1 ]; then C_RED='\033[31m'; C_GRN='\033[32m'; C_YLW='\033[33m'; C_DIM='\033[2m'; C_RST='\033[0m' else C_RED=''; C_GRN=''; C_YLW=''; C_DIM=''; C_RST='' fi info() { echo -e "${C_DIM}[server]${C_RST} $*"; } ok() { echo -e "${C_GRN}[server]${C_RST} $*"; } warn() { echo -e "${C_YLW}[server]${C_RST} $*"; } err() { echo -e "${C_RED}[server]${C_RST} $*" >&2; } # --- Helpers ---------------------------------------------------------------- # PID currently listening on $PORT (empty if none). listening_pid() { ss -ltnp 2>/dev/null | awk -v p=":$PORT" '$4 ~ p {print $NF}' \ | grep -oE 'pid=[0-9]+' | head -n1 | cut -d= -f2 } # PID recorded in the pid file, if the process is still alive. pidfile_pid() { [ -f "$PID_FILE" ] || return 0 local pid pid="$(cat "$PID_FILE" 2>/dev/null || true)" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "$pid" fi } is_running() { [ -n "$(pidfile_pid)" ] || [ -n "$(listening_pid)" ] } # True when .env enables HTTPS (both TLS_CERT and TLS_KEY set, non-empty). tls_enabled() { [ -f "$SCRIPT_DIR/.env" ] || return 1 grep -qE '^[[:space:]]*TLS_CERT=.+' "$SCRIPT_DIR/.env" && grep -qE '^[[:space:]]*TLS_KEY=.+' "$SCRIPT_DIR/.env" } # URL scheme the server will speak, based on .env. scheme() { if tls_enabled; then echo "https"; else echo "http"; fi } build() { info "building ($PROFILE)…" cargo build "--$PROFILE" ok "build complete → $BIN" } # Generate a self-signed certificate (SAN: localhost + this host's IPs). # Usage: cert [--force] cert() { local force="${1:-}" if [ -s "$CERT_FILE" ] && [ -s "$KEY_FILE" ] && [ "$force" != "--force" ]; then ok "certificate already present: $CERT_FILE (use 'cert --force' to regenerate)" return 0 fi if ! command -v openssl >/dev/null 2>&1; then err "openssl not found — install it or provide TLS_CERT/TLS_KEY manually" return 1 fi mkdir -p "$CERT_DIR" local san="DNS:localhost,IP:127.0.0.1,IP:::1" for ip in $(hostname -I 2>/dev/null); do san="$san,IP:$ip"; done [ -n "${EXTRA_SAN:-}" ] && san="$san,$EXTRA_SAN" info "generating self-signed certificate (SAN: $san)…" openssl req -x509 -newkey rsa:2048 -nodes -days 825 \ -keyout "$KEY_FILE" -out "$CERT_FILE" \ -subj "/CN=audio-agent-engine" -addext "subjectAltName=$san" >/dev/null 2>&1 chmod 600 "$KEY_FILE" 2>/dev/null || true ok "certificate → $CERT_FILE / $KEY_FILE" } start() { if is_running; then warn "already running (pid $(pidfile_pid || listening_pid)) — use 'restart'" return 0 fi if [ ! -x "$BIN" ]; then warn "binary not found: $BIN" build fi # HTTPS requested but certificate missing → generate one on the fly. if tls_enabled && { [ ! -s "$CERT_FILE" ] || [ ! -s "$KEY_FILE" ]; }; then cert fi mkdir -p "$RUN_DIR" local sch; sch="$(scheme)" info "starting on :$PORT ($sch, RUST_LOG=$RUST_LOG, profile=$PROFILE)…" # The server reads .env + models/ with paths relative to this directory, # so we launch it from $SCRIPT_DIR. nohup env RUST_LOG="$RUST_LOG" "$BIN" >> "$LOG_FILE" 2>&1 & echo $! > "$PID_FILE" local pid=$! # Wait for the port to accept connections (model load ~1.5 s). for _ in $(seq 1 40); do if [ -n "$(listening_pid)" ]; then ok "started (pid $pid) — $sch://localhost:$PORT/" return 0 fi sleep 0.5 done err "failed to start within ~20 s. Last log lines:" tail -n 20 "$LOG_FILE" || true rm -f "$PID_FILE" return 1 } stop() { local pid pid="$(pidfile_pid)" [ -z "$pid" ] && pid="$(listening_pid)" if [ -z "$pid" ]; then warn "not running" rm -f "$PID_FILE" return 0 fi info "stopping pid $pid…" kill "$pid" 2>/dev/null || true # Wait up to 10 s for a graceful exit, then force-kill. for _ in $(seq 1 20); do kill -0 "$pid" 2>/dev/null || break sleep 0.5 done if kill -0 "$pid" 2>/dev/null; then warn "still alive — sending SIGKILL" kill -9 "$pid" 2>/dev/null || true fi rm -f "$PID_FILE" ok "stopped" } restart() { stop start } status() { local pid; pid="$(pidfile_pid)" local lpid; lpid="$(listening_pid)" local sch; sch="$(scheme)" echo "profile : $PROFILE" echo "binary : $BIN $([ -x "$BIN" ] && echo '(ok)' || echo '(missing)')" echo "pid : ${pid:-${lpid:-none}}" echo "port : $PORT" echo "scheme : $sch" if tls_enabled; then echo "cert : $CERT_FILE $([ -s "$CERT_FILE" ] && echo '(ok)' || echo '(missing — run ./server.sh cert)')" fi if [ -n "$lpid" ]; then local code # -k: accept the self-signed certificate when probing. code="$(curl -sk -o /dev/null -w '%{http_code}' --max-time 5 \ "$sch://localhost:$PORT/health" || echo 000)" echo -e "http : ${C_GRN}$code${C_RST} ($sch://localhost:$PORT/)" else echo -e "http : ${C_RED}not listening${C_RST}" fi } logs() { local n="${1:-50}" [ -f "$LOG_FILE" ] || { warn "no log file yet ($LOG_FILE)"; return 0; } info "tailing $LOG_FILE (Ctrl-C to stop)…" tail -n "$n" -f "$LOG_FILE" } # --- Dispatch --------------------------------------------------------------- cmd="${1:-}"; shift || true # Allow `--build` as a flag for start/restart in any position. DO_BUILD=0 for arg in "$@"; do [ "$arg" = "--build" ] && DO_BUILD=1; done case "$cmd" in start) [ "$DO_BUILD" = 1 ] && build start ;; stop) stop ;; restart) [ "$DO_BUILD" = 1 ] && build restart ;; status) status ;; logs) logs "${1:-50}" ;; build) build ;; cert) cert "${1:-}" ;; *) cat < [args] start [--build] Start the server (--build recompiles first) stop Stop the running server restart [--build] Restart (--build recompiles first) status Show process / port / scheme / HTTP status logs [N] Tail the log file (default 50 lines, follow) build Build the release binary only cert [--force] Generate the self-signed HTTPS certificate HTTPS is served when .env sets both TLS_CERT and TLS_KEY (see .env.example). Browsers only grant microphone access on a secure context: use HTTPS, or open the page on localhost. Environment: PROFILE=$PROFILE RUST_LOG=$RUST_LOG PORT=$PORT EXTRA_SAN= EOF exit 1 ;; esac