276 lines
8.5 KiB
Bash
Executable File
276 lines
8.5 KiB
Bash
Executable File
#!/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. Apart from the model load (~1.5 s)
|
|
# startup also re-synthesizes the start-filler clip bank (one clip per
|
|
# START_FILLER_TEXTS phrase, generated before the socket is bound), which can
|
|
# take a while with a long phrase list — hence the generous timeout.
|
|
for _ in $(seq 1 120); 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 ~60 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 <<EOF
|
|
audio-agent-engine — control script
|
|
|
|
Usage: ./server.sh <command> [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=<extra SAN entries for 'cert'>
|
|
EOF
|
|
exit 1
|
|
;;
|
|
esac
|