#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" APP_HOME="$(cd "${SCRIPT_DIR}/.." && pwd)" CONFIG_FILE="${APP_HOME}/config/catalogsync.env" RUN_DIR="${APP_HOME}/run" LOCK_DIR="${RUN_DIR}/serve.lock" PID_FILE="${RUN_DIR}/serve.pid" # shellcheck source=./load_env.sh source "${SCRIPT_DIR}/load_env.sh" fail() { echo "[serve_console.sh] ERROR: $*" >&2 exit 1 } require_var() { local var_name="$1" if [[ -z "${!var_name:-}" ]]; then fail "Missing required config variable: ${var_name} (from ${CONFIG_FILE})" fi } acquire_serve_lock() { mkdir -p "${RUN_DIR}" if mkdir "${LOCK_DIR}" 2>/dev/null; then echo "$$" > "${LOCK_DIR}/owner_pid" return 0 fi local owner_pid="" if [[ -f "${LOCK_DIR}/owner_pid" ]]; then owner_pid="$(cat "${LOCK_DIR}/owner_pid" 2>/dev/null || true)" fi if [[ -n "${owner_pid}" ]] && kill -0 "${owner_pid}" 2>/dev/null; then fail "Another serve_console instance is running (owner_pid=${owner_pid})" fi rm -rf "${LOCK_DIR}" if ! mkdir "${LOCK_DIR}" 2>/dev/null; then fail "Cannot acquire serve lock: ${LOCK_DIR}" fi echo "$$" > "${LOCK_DIR}/owner_pid" } cleanup() { local exit_code=$? if [[ -n "${SERVER_PID:-}" ]] && kill -0 "${SERVER_PID}" 2>/dev/null; then kill -TERM "${SERVER_PID}" 2>/dev/null || true wait "${SERVER_PID}" 2>/dev/null || true fi rm -f "${PID_FILE}" rm -rf "${LOCK_DIR}" return "${exit_code}" } validate_port() { local port_value="$1" if ! [[ "${port_value}" =~ ^[0-9]+$ ]] || (( port_value < 1 || port_value > 65535 )); then fail "WEB_PORT must be an integer in range 1..65535: ${port_value}" fi } if [[ -f "${CONFIG_FILE}" ]]; then load_env_file "${CONFIG_FILE}" else fail "Config file not found: ${CONFIG_FILE}. Copy catalogsync.env.example to catalogsync.env first." fi for required_var in DB_PATH ENV_FILE WEB_HOST WEB_PORT LOG_DIR PYTHON_BIN VENV_DIR; do require_var "${required_var}" done validate_port "${WEB_PORT}" if [[ -n "${VENV_DIR:-}" && -x "${VENV_DIR}/bin/python" ]]; then PYTHON_BIN="${VENV_DIR}/bin/python" fi if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then fail "PYTHON_BIN is not executable or not found in PATH: ${PYTHON_BIN}" fi mkdir -p "${APP_HOME}/data" "${LOG_DIR}" "$(dirname "${DB_PATH}")" "$(dirname "${ENV_FILE}")" export PYTHONPATH="${APP_HOME}/app${PYTHONPATH:+:${PYTHONPATH}}" acquire_serve_lock trap cleanup EXIT INT TERM if [[ -f "${PID_FILE}" ]]; then existing_pid="$(cat "${PID_FILE}" 2>/dev/null || true)" if [[ -n "${existing_pid}" ]] && kill -0 "${existing_pid}" 2>/dev/null; then fail "Server process already running (pid=${existing_pid})" fi rm -f "${PID_FILE}" fi LOG_FILE="${LOG_DIR}/serve_console_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "${LOG_FILE}") 2>&1 echo "[serve_console.sh] logging to ${LOG_FILE}" # Run from the deployed app directory so `python -m musicdl...` does not # accidentally import an older checkout from the current working directory. cd "${APP_HOME}/app" "${PYTHON_BIN}" -m musicdl.catalogsync.cli serve \ --db "${DB_PATH}" \ --env-file "${ENV_FILE}" \ --host "${WEB_HOST}" \ --port "${WEB_PORT}" \ "$@" & SERVER_PID=$! echo "${SERVER_PID}" > "${PID_FILE}" echo "[serve_console.sh] server pid=${SERVER_PID}" wait "${SERVER_PID}"