service_ctl.sh 7.8 KB
#!/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 "$@"