#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" RUN_DIR="$ROOT_DIR/run" ENV_FILE="${APP_ENV_FILE:-$ROOT_DIR/.env}" if [ -f "$ENV_FILE" ]; then set -a # shellcheck disable=SC1090 . "$ENV_FILE" set +a fi APP_BIND="${APP_BIND:-0.0.0.0}" APP_PORT="${APP_PORT:-2026}" TARGETS_FILE="${TARGETS_FILE:-$ROOT_DIR/config/targets.json}" YUMI_INTERNAL_HOST="${YUMI_INTERNAL_HOST:-127.0.0.1}" YUMI_INTERNAL_PORT="${YUMI_INTERNAL_PORT:-2027}" HAIYI_INTERNAL_HOST="${HAIYI_INTERNAL_HOST:-127.0.0.1}" HAIYI_INTERNAL_PORT="${HAIYI_INTERNAL_PORT:-2028}" GATEWAY_BIND="$APP_BIND" GATEWAY_PORT="$APP_PORT" GATEWAY_SYSTEMD_SERVICE_NAME="hy-app-monitor.service" YUMI_SYSTEMD_SERVICE_NAME="hy-app-monitor-yumi.service" HAIYI_SYSTEMD_SERVICE_NAME="hy-app-monitor-haiyi.service" if [ -x "$ROOT_DIR/.venv/bin/python" ]; then PYTHON_BIN_DEFAULT="$ROOT_DIR/.venv/bin/python" else PYTHON_BIN_DEFAULT="python3" fi PYTHON_BIN="${PYTHON_BIN:-$PYTHON_BIN_DEFAULT}" mkdir -p "$RUN_DIR" suite_service_names() { printf '%s\n' gateway yumi haiyi } service_display_name() { case "$1" in gateway) echo "gateway" ;; yumi) echo "yumi" ;; haiyi) echo "haiyi" ;; *) echo "unknown service: $1" >&2 return 1 ;; esac } service_unit_name() { case "$1" in gateway) echo "$GATEWAY_SYSTEMD_SERVICE_NAME" ;; yumi) echo "$YUMI_SYSTEMD_SERVICE_NAME" ;; haiyi) echo "$HAIYI_SYSTEMD_SERVICE_NAME" ;; *) echo "unknown service: $1" >&2 return 1 ;; esac } service_entry_file() { case "$1" in gateway) echo "$ROOT_DIR/server.py" ;; yumi) echo "$ROOT_DIR/yumi_server.py" ;; haiyi) echo "$ROOT_DIR/haiyi_server.py" ;; *) echo "unknown service: $1" >&2 return 1 ;; esac } service_bind() { case "$1" in gateway) echo "$GATEWAY_BIND" ;; yumi) echo "$YUMI_INTERNAL_HOST" ;; haiyi) echo "$HAIYI_INTERNAL_HOST" ;; *) echo "unknown service: $1" >&2 return 1 ;; esac } service_port() { case "$1" in gateway) echo "$GATEWAY_PORT" ;; yumi) echo "$YUMI_INTERNAL_PORT" ;; haiyi) echo "$HAIYI_INTERNAL_PORT" ;; *) echo "unknown service: $1" >&2 return 1 ;; esac } service_base_path() { case "$1" in gateway) echo "" ;; yumi) echo "/yumi" ;; haiyi) echo "/haiyi" ;; *) echo "unknown service: $1" >&2 return 1 ;; esac } service_pid_file() { echo "$RUN_DIR/hy-app-monitor-$1.pid" } service_log_file() { echo "$RUN_DIR/hy-app-monitor-$1.log" } service_command_pattern() { basename "$(service_entry_file "$1")" } has_systemd_unit() { local name="$1" if ! command -v systemctl >/dev/null 2>&1; then return 1 fi systemctl cat "$(service_unit_name "$name")" >/dev/null 2>&1 } has_full_systemd_suite() { local name for name in $(suite_service_names); do if ! has_systemd_unit "$name"; then return 1 fi done return 0 } systemd_main_pid() { local name="$1" local main_pid if ! has_systemd_unit "$name"; then return 1 fi main_pid="$(systemctl show --property MainPID --value "$(service_unit_name "$name")" 2>/dev/null || true)" if [ -z "${main_pid:-}" ] || [ "$main_pid" = "0" ]; then return 1 fi echo "$main_pid" } pid_matches_project() { local name="$1" local pid="$2" if [ -z "${pid:-}" ] || ! kill -0 "$pid" >/dev/null 2>&1; then return 1 fi local command command="$(ps -p "$pid" -o command= 2>/dev/null || true)" if [[ "$command" != *"$(service_command_pattern "$name")"* ]]; then return 1 fi if command -v lsof >/dev/null 2>&1; then local expected_port expected_port="$(service_port "$name")" if ! lsof -a -p "$pid" -iTCP:"$expected_port" -sTCP:LISTEN >/dev/null 2>&1; then return 1 fi fi if command -v lsof >/dev/null 2>&1; then local cwd cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1)" if [ -n "$cwd" ] && [ "$cwd" != "$ROOT_DIR" ]; then return 1 fi fi return 0 } discover_project_pid() { local name="$1" if ! command -v lsof >/dev/null 2>&1; then return 1 fi local pid while IFS= read -r pid; do if pid_matches_project "$name" "$pid"; then echo "$pid" return 0 fi done < <(lsof -tiTCP:"$(service_port "$name")" -sTCP:LISTEN 2>/dev/null || true) return 1 } read_pid() { local name="$1" local pid_file pid_file="$(service_pid_file "$name")" if [ -f "$pid_file" ]; then local pid pid="$(cat "$pid_file")" if pid_matches_project "$name" "$pid"; then echo "$pid" return 0 fi fi discover_project_pid "$name" } is_running() { local name="$1" local pid local pid_file pid_file="$(service_pid_file "$name")" pid="$(read_pid "$name" || true)" if [ -z "${pid:-}" ]; then rm -f "$pid_file" return 1 fi if ! kill -0 "$pid" >/dev/null 2>&1; then rm -f "$pid_file" return 1 fi echo "$pid" >"$pid_file" return 0 } stop_legacy_process() { local name="$1" local pid local pid_file local managed_pid pid_file="$(service_pid_file "$name")" pid="$(read_pid "$name" || true)" managed_pid="$(systemd_main_pid "$name" || true)" if [ -n "${pid:-}" ] && [ -n "${managed_pid:-}" ] && [ "$pid" = "$managed_pid" ]; then rm -f "$pid_file" return 0 fi if [ -n "${pid:-}" ] && kill -0 "$pid" >/dev/null 2>&1; then kill "$pid" >/dev/null 2>&1 || true local _i for _i in $(seq 1 10); do if ! kill -0 "$pid" >/dev/null 2>&1; then break fi sleep 1 done kill -9 "$pid" >/dev/null 2>&1 || true fi rm -f "$pid_file" } stop_all_legacy_processes() { local name for name in $(suite_service_names); do stop_legacy_process "$name" done } export_service_env() { local name="$1" export APP_BIND export APP_PORT export APP_BASE_PATH export TARGETS_FILE export PYTHONUNBUFFERED=1 APP_BIND="$(service_bind "$name")" APP_PORT="$(service_port "$name")" APP_BASE_PATH="$(service_base_path "$name")" export APP_BIND APP_PORT APP_BASE_PATH TARGETS_FILE PYTHONUNBUFFERED } start_local_service() { local name="$1" local pid_file local log_file local entry_file if is_running "$name"; then echo "$(service_display_name "$name") already running pid=$(read_pid "$name")" return 0 fi if [ "$name" = "yumi" ] && [ ! -f "$TARGETS_FILE" ]; then echo "targets file not found: $TARGETS_FILE" >&2 return 1 fi pid_file="$(service_pid_file "$name")" log_file="$(service_log_file "$name")" entry_file="$(service_entry_file "$name")" export_service_env "$name" nohup "$PYTHON_BIN" "$entry_file" >>"$log_file" 2>&1 & local pid=$! echo "$pid" >"$pid_file" sleep 1 if kill -0 "$pid" >/dev/null 2>&1; then echo "$(service_display_name "$name") started pid=$pid port=$(service_port "$name")" return 0 fi echo "$(service_display_name "$name") failed to start" >&2 tail -n 40 "$log_file" >&2 || true return 1 } stop_local_service() { local name="$1" local pid_file pid_file="$(service_pid_file "$name")" if ! is_running "$name"; then echo "$(service_display_name "$name") not running" rm -f "$pid_file" return 0 fi local pid pid="$(read_pid "$name")" kill "$pid" >/dev/null 2>&1 || true local _i for _i in $(seq 1 10); do if ! kill -0 "$pid" >/dev/null 2>&1; then rm -f "$pid_file" echo "$(service_display_name "$name") stopped" return 0 fi sleep 1 done kill -9 "$pid" >/dev/null 2>&1 || true rm -f "$pid_file" echo "$(service_display_name "$name") force stopped" } status_local_service() { local name="$1" if is_running "$name"; then echo "$(service_display_name "$name") running pid=$(read_pid "$name") port=$(service_port "$name")" return 0 fi echo "$(service_display_name "$name") stopped" return 1 } start_systemd_suite() { local name for name in yumi haiyi gateway; do systemctl start "$(service_unit_name "$name")" done for name in $(suite_service_names); do systemctl is-active "$(service_unit_name "$name")" done } stop_systemd_suite() { local name for name in gateway yumi haiyi; do systemctl stop "$(service_unit_name "$name")" >/dev/null 2>&1 || true done } restart_systemd_suite() { local name for name in yumi haiyi gateway; do systemctl restart "$(service_unit_name "$name")" done for name in $(suite_service_names); do systemctl is-active "$(service_unit_name "$name")" done } status_systemd_suite() { local all_active=0 local name for name in $(suite_service_names); do if systemctl is-active --quiet "$(service_unit_name "$name")"; then echo "$(service_display_name "$name") running under systemd port=$(service_port "$name")" else echo "$(service_display_name "$name") stopped" all_active=1 fi done return "$all_active" }