Files
piperust/server.sh
T
2026-10-17 15:49:20 +01:00

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