#!/usr/bin/env sh set -eu PROJECT_DIR="/opt/people-flow" CONFIG_TEMPLATE="${PROJECT_DIR}/config/config.example.yaml" CONFIG_PATH="${CONFIG_PATH:-${PROJECT_DIR}/config/local.yaml}" OUTPUT_DIR="${OUTPUT_DIR:-${PROJECT_DIR}/outputs}" RTSP_URL="${RTSP_URL:-}" API_HOST="${API_HOST:-0.0.0.0}" API_PORT="${API_PORT:-18082}" RTSP_STALL_TIMEOUT_SECONDS="${RTSP_STALL_TIMEOUT_SECONDS:-180}" DEEPFACE_CACHE_DIR="/root/.deepface/weights" DEEPFACE_SOURCE_DIR="${PROJECT_DIR}/weights/deepface" mkdir -p "${OUTPUT_DIR}" "$(dirname "${CONFIG_PATH}")" "${DEEPFACE_CACHE_DIR}" if [ -d "${DEEPFACE_SOURCE_DIR}" ]; then find "${DEEPFACE_SOURCE_DIR}" -maxdepth 1 -name '*.h5' -exec cp {} "${DEEPFACE_CACHE_DIR}/" \; fi if [ ! -f "${CONFIG_PATH}" ]; then cp "${CONFIG_TEMPLATE}" "${CONFIG_PATH}" fi python - "$CONFIG_PATH" "$RTSP_URL" "$OUTPUT_DIR" <<'PY' from pathlib import Path import sys import yaml config_path = Path(sys.argv[1]) rtsp_url = sys.argv[2] output_dir = sys.argv[3] raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} runtime = raw.setdefault("runtime", {}) if rtsp_url: runtime["rtsp_url"] = rtsp_url runtime["output_dir"] = output_dir yolo = raw.setdefault("yolo", {}) yolo.setdefault("model_path", "weights/yolo11n.pt") config_path.write_text( yaml.safe_dump(raw, allow_unicode=True, sort_keys=False), encoding="utf-8", ) PY RTSP_OUTPUT_SUBDIR="$(python - "$CONFIG_PATH" <<'PY' from pathlib import Path import sys import yaml config_path = Path(sys.argv[1]) raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} rtsp = raw.get("rtsp") or {} print(rtsp.get("output_subdir", "rtsp_stream")) PY )" RTSP_STATUS_PATH="${OUTPUT_DIR}/${RTSP_OUTPUT_SUBDIR}/worker_status.json" exec python - "$CONFIG_PATH" "$API_HOST" "$API_PORT" "$RTSP_STATUS_PATH" "$RTSP_STALL_TIMEOUT_SECONDS" <<'PY' from pathlib import Path import signal import subprocess import sys import time from src.people_flow.worker_status import worker_status_stall_reason config_path, api_host, api_port, status_path_raw, stall_timeout_raw = sys.argv[1:6] status_path = Path(status_path_raw) stall_timeout_seconds = max(float(stall_timeout_raw), 30.0) commands = [ [sys.executable, "main.py", "--config", config_path, "rtsp"], [ sys.executable, "main.py", "--config", config_path, "manage-api", "--host", api_host, "--port", api_port, ], ] processes = [subprocess.Popen(command) for command in commands] supervisor_started_at = time.time() def stop_all(excluded_index=None): for index, process in enumerate(processes): if index == excluded_index or process.poll() is not None: continue process.terminate() deadline = time.time() + 10 for index, process in enumerate(processes): if index == excluded_index or process.poll() is not None: continue timeout = max(0, deadline - time.time()) try: process.wait(timeout=timeout) except subprocess.TimeoutExpired: process.kill() def stale_reason(): if processes[0].poll() is not None: return None return worker_status_stall_reason( status_path, started_at=supervisor_started_at, max_age_seconds=stall_timeout_seconds, ) def terminate_all(signum, _frame): stop_all() raise SystemExit(128 + signum) for handled_signal in (signal.SIGINT, signal.SIGTERM): signal.signal(handled_signal, terminate_all) while True: for index, process in enumerate(processes): return_code = process.poll() if return_code is None: continue stop_all(excluded_index=index) raise SystemExit(return_code) reason = stale_reason() if reason is not None: print(reason, flush=True) stop_all() raise SystemExit(1) time.sleep(0.5) PY