#!/bin/bash # # Unified service lifecycle controller for saas-search. # Supports: start / stop / restart / status # set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" LOG_DIR="${PROJECT_ROOT}/logs" mkdir -p "${LOG_DIR}" CORE_SERVICES=("backend" "indexer" "frontend") OPTIONAL_SERVICES=("embedding" "translator" "reranker") LEGACY_SERVICES=("clip" "cnclip") all_services() { echo "${CORE_SERVICES[@]} ${OPTIONAL_SERVICES[@]} ${LEGACY_SERVICES[@]}" } load_env_file() { local env_file="${PROJECT_ROOT}/.env" if [ -f "${env_file}" ]; then set -a # shellcheck disable=SC1090 source "${env_file}" set +a fi } get_port() { local service="$1" case "${service}" in backend) echo "${API_PORT:-6002}" ;; indexer) echo "${INDEXER_PORT:-6004}" ;; frontend) echo "${FRONTEND_PORT:-6003}" ;; embedding) echo "${EMBEDDING_PORT:-6005}" ;; translator) echo "${TRANSLATION_PORT:-${TRANSLATOR_PORT:-6006}}" ;; reranker) echo "${RERANKER_PORT:-6007}" ;; clip) echo "${CLIP_PORT:-51000}" ;; cnclip) echo "${CNCLIP_PORT:-51000}" ;; *) echo "" ;; esac } pid_file() { local service="$1" case "${service}" in clip) echo "${LOG_DIR}/clip_service.pid" ;; cnclip) echo "${LOG_DIR}/cnclip_service.pid" ;; *) echo "${LOG_DIR}/${service}.pid" ;; esac } log_file() { local service="$1" echo "${LOG_DIR}/${service}.log" } service_start_cmd() { local service="$1" case "${service}" in backend) echo "./scripts/start_backend.sh" ;; indexer) echo "./scripts/start_indexer.sh" ;; frontend) echo "./scripts/start_frontend.sh" ;; embedding) echo "./scripts/start_embedding_service.sh" ;; translator) echo "./scripts/start_translator.sh" ;; reranker) echo "./scripts/start_reranker.sh" ;; clip) echo "./scripts/start_clip_service.sh" ;; cnclip) echo "./scripts/start_cnclip_service.sh" ;; *) return 1 ;; esac } wait_for_health() { local service="$1" local max_retries="${2:-30}" local interval_sec="${3:-1}" local port port="$(get_port "${service}")" local path="/health" case "${service}" in backend) path="/health" ;; indexer) path="/health" ;; frontend) path="/" ;; embedding) path="/health" ;; translator) path="/health" ;; reranker) path="/health" ;; *) return 0 ;; esac local i=0 while [ "${i}" -lt "${max_retries}" ]; do if curl -sf "http://127.0.0.1:${port}${path}" >/dev/null 2>&1; then return 0 fi i=$((i + 1)) sleep "${interval_sec}" done return 1 } is_running_by_pid() { local service="$1" local pf pf="$(pid_file "${service}")" if [ ! -f "${pf}" ]; then return 1 fi local pid pid="$(cat "${pf}" 2>/dev/null || true)" [ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null } is_running_by_port() { local service="$1" local port port="$(get_port "${service}")" [ -n "${port}" ] && lsof -ti:"${port}" >/dev/null 2>&1 } start_one() { local service="$1" cd "${PROJECT_ROOT}" local cmd cmd="$(service_start_cmd "${service}")" local pf lf pf="$(pid_file "${service}")" lf="$(log_file "${service}")" if is_running_by_pid "${service}" || is_running_by_port "${service}"; then echo "[skip] ${service} already running" return 0 fi case "${service}" in clip|cnclip) echo "[start] ${service} (managed by native script)" if [ "${service}" = "cnclip" ]; then CNCLIP_DEVICE="${CNCLIP_DEVICE:-cuda}" bash -lc "${cmd}" >> "${lf}" 2>&1 || true else bash -lc "${cmd}" >> "${lf}" 2>&1 || true fi if is_running_by_pid "${service}" || is_running_by_port "${service}"; then echo "[ok] ${service} started (log=${lf})" else echo "[warn] ${service} may not be running, inspect ${lf}" fi ;; backend|indexer|frontend|embedding|translator|reranker) echo "[start] ${service}" nohup bash -lc "${cmd}" > "${lf}" 2>&1 & local pid=$! echo "${pid}" > "${pf}" if wait_for_health "${service}"; then echo "[ok] ${service} healthy (pid=${pid}, log=${lf})" else echo "[warn] ${service} health check timeout, inspect ${lf}" fi ;; *) echo "[warn] ${service} unsupported start path" ;; esac } stop_one() { local service="$1" cd "${PROJECT_ROOT}" if [ "${service}" = "clip" ]; then echo "[stop] clip (managed by native script)" bash -lc "./scripts/stop_clip_service.sh" || true return 0 fi if [ "${service}" = "cnclip" ]; then echo "[stop] cnclip (managed by native script)" bash -lc "./scripts/stop_cnclip_service.sh" || true return 0 fi local pf pf="$(pid_file "${service}")" if [ -f "${pf}" ]; then local pid pid="$(cat "${pf}" 2>/dev/null || true)" if [ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null; then echo "[stop] ${service} pid=${pid}" kill -TERM "${pid}" 2>/dev/null || true sleep 1 if kill -0 "${pid}" 2>/dev/null; then kill -KILL "${pid}" 2>/dev/null || true fi fi rm -f "${pf}" fi local port port="$(get_port "${service}")" if [ -n "${port}" ]; then local pids pids="$(lsof -ti:${port} 2>/dev/null || true)" if [ -n "${pids}" ]; then echo "[stop] ${service} port=${port} pids=${pids}" for pid in ${pids}; do kill -TERM "${pid}" 2>/dev/null || true done sleep 1 pids="$(lsof -ti:${port} 2>/dev/null || true)" for pid in ${pids}; do kill -KILL "${pid}" 2>/dev/null || true done fi fi } status_one() { local service="$1" local port port="$(get_port "${service}")" local running="no" local pid_info="-" if is_running_by_pid "${service}"; then running="yes" pid_info="$(cat "$(pid_file "${service}")" 2>/dev/null || echo "-")" elif is_running_by_port "${service}"; then running="yes" pid_info="$(lsof -ti:${port} 2>/dev/null | tr '\n' ',' | sed 's/,$//' || echo "-")" fi printf "%-10s running=%-3s port=%-6s pid=%s\n" "${service}" "${running}" "${port:--}" "${pid_info}" } resolve_targets() { local scope="$1" shift || true if [ "$#" -gt 0 ]; then echo "$*" return fi case "${scope}" in start) local targets=("${CORE_SERVICES[@]}") if [ "${START_EMBEDDING:-0}" = "1" ]; then targets+=("embedding"); fi if [ "${START_TRANSLATOR:-0}" = "1" ]; then targets+=("translator"); fi if [ "${START_RERANKER:-0}" = "1" ]; then targets+=("reranker"); fi echo "${targets[@]}" ;; stop|restart|status) echo "$(all_services)" ;; *) echo "" ;; esac } usage() { cat <<'EOF' Usage: ./scripts/service_ctl.sh start [service...] ./scripts/service_ctl.sh stop [service...] ./scripts/service_ctl.sh restart [service...] ./scripts/service_ctl.sh status [service...] Default target set (when no service provided): start -> backend indexer frontend (+ optional by env flags) stop -> all known services restart -> all known services status -> all known services Optional startup flags: START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 ./run.sh CNCLIP_DEVICE=cuda|cpu ./scripts/service_ctl.sh start cnclip EOF } main() { if [ "$#" -lt 1 ]; then usage exit 1 fi local action="$1" shift || true load_env_file local targets targets="$(resolve_targets "${action}" "$@")" if [ -z "${targets}" ]; then usage exit 1 fi case "${action}" in start) for svc in ${targets}; do start_one "${svc}" done ;; stop) for svc in ${targets}; do stop_one "${svc}" done ;; restart) for svc in ${targets}; do stop_one "${svc}" done for svc in ${targets}; do start_one "${svc}" done ;; status) for svc in ${targets}; do status_one "${svc}" done ;; *) usage exit 1 ;; esac } main "$@"