#!/bin/bash set -u PACKAGE_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) APP_ROOT="${TRIM_APPDEST:-$PACKAGE_ROOT/app}" DATA_ROOT="${TRIM_PKGVAR:-$PACKAGE_ROOT/var}" VOLUME_ROOT="${TRIM_APPDEST_VOL:-$DATA_ROOT/volume}" RECORD_ROOT="${LIVE_RECORDER_RECORD_ROOT:-$VOLUME_ROOT/@appshare/liverecorder/records}" RUNTIME_ROOT="$APP_ROOT/runtime" SERVER="$APP_ROOT/server/LiveRecorder.WebApi" PG_BIN="$RUNTIME_ROOT/usr/lib/postgresql/15/bin" PG_SHARE="$RUNTIME_ROOT/usr/share/postgresql/15" PG_LIB="$RUNTIME_ROOT/usr/lib/postgresql/15/lib" NODE_BIN="$RUNTIME_ROOT/bin/node" CURL_BIN="$RUNTIME_ROOT/bin/curl" CA_BUNDLE="$RUNTIME_ROOT/etc/ssl/certs/ca-certificates.crt" PG_DATA="$DATA_ROOT/postgres" RUN_ROOT="$DATA_ROOT/run" LOG_ROOT="$DATA_ROOT/log" APP_PID_FILE="$RUN_ROOT/liverecorder.pid" APP_LOG="$LOG_ROOT/liverecorder.log" PG_LOG="$LOG_ROOT/postgresql.log" ADMIN_PASSWORD_FILE="$DATA_ROOT/admin-password.seed" PG_PORT="${LIVE_RECORDER_POSTGRES_PORT:-54329}" SERVICE_PORT="${TRIM_SERVICE_PORT:-18080}" SYSTEM_PATH="${PATH:-/usr/local/bin:/usr/bin:/bin}" RUNTIME_PATH="$RUNTIME_ROOT/bin:$SYSTEM_PATH" RUNTIME_LIBRARY_PATH="$RUNTIME_ROOT/lib:$PG_LIB" log_message() { mkdir -p "$LOG_ROOT" printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$APP_LOG" } app_pid() { if [ -f "$APP_PID_FILE" ]; then pid=$(sed -n '1p' "$APP_PID_FILE" | tr -d '[:space:]') if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then printf '%s' "$pid" return 0 fi fi return 1 } run_pg() { env LD_LIBRARY_PATH="$RUNTIME_LIBRARY_PATH" PATH="$PG_BIN:$RUNTIME_PATH" "$@" } run_native() { env LD_LIBRARY_PATH="$RUNTIME_LIBRARY_PATH" PATH="$RUNTIME_PATH" \ SSL_CERT_FILE="$CA_BUNDLE" CURL_CA_BUNDLE="$CA_BUNDLE" "$@" } system_media_tools_available() { missing_tools="" for tool_name in ffmpeg ffprobe; do if ! PATH="$SYSTEM_PATH" command -v "$tool_name" >/dev/null 2>&1; then missing_tools="$missing_tools $tool_name" fi done if [ -n "$missing_tools" ]; then log_message "缺少 fnOS 系统媒体工具:${missing_tools# }。请先在系统环境中安装 FFmpeg(必须同时提供 ffmpeg 与 ffprobe)。" return 1 fi return 0 } postgres_running() { [ -x "$PG_BIN/pg_ctl" ] || return 1 [ -f "$PG_DATA/PG_VERSION" ] || return 1 run_pg "$PG_BIN/pg_ctl" -D "$PG_DATA" status >/dev/null 2>&1 } initialize_postgres() { if [ -f "$PG_DATA/PG_VERSION" ]; then return 0 fi mkdir -p "$PG_DATA" "$RUN_ROOT" chmod 0700 "$PG_DATA" "$RUN_ROOT" if ! run_pg "$PG_BIN/initdb" \ -D "$PG_DATA" \ -L "$PG_SHARE" \ --username=liverecorder \ --auth-local=trust \ --auth-host=reject \ --encoding=UTF8 \ --no-locale >>"$PG_LOG" 2>&1; then log_message "PostgreSQL 初始化失败。" return 1 fi { printf "listen_addresses = ''\n" printf "port = %s\n" "$PG_PORT" printf "unix_socket_directories = '%s'\n" "$RUN_ROOT" printf "max_connections = 40\n" printf "shared_buffers = '64MB'\n" printf "timezone = 'UTC'\n" printf "log_timezone = 'UTC'\n" } >>"$PG_DATA/postgresql.conf" return 0 } start_postgres() { if postgres_running; then return 0 fi initialize_postgres || return 1 if ! run_pg "$PG_BIN/pg_ctl" -D "$PG_DATA" -l "$PG_LOG" -w start; then log_message "PostgreSQL 启动失败。" return 1 fi database_exists=$(run_pg "$PG_BIN/psql" \ -h "$RUN_ROOT" -p "$PG_PORT" -U liverecorder -d postgres -Atqc \ "SELECT 1 FROM pg_database WHERE datname = 'live_recorder'" 2>/dev/null || true) if [ "$database_exists" != "1" ]; then if ! run_pg "$PG_BIN/createdb" \ -h "$RUN_ROOT" -p "$PG_PORT" -U liverecorder live_recorder >>"$PG_LOG" 2>&1; then log_message "Live Recorder 数据库创建失败。" return 1 fi fi return 0 } stop_postgres() { if postgres_running; then # A first-run migration can dirty enough pages that a NAS needs more # than pg_ctl's 60-second default to finish the shutdown checkpoint. # Do not let restart race a database that is still shutting down. if ! run_pg "$PG_BIN/pg_ctl" -D "$PG_DATA" -m fast -t 180 -w stop >>"$PG_LOG" 2>&1; then log_message "PostgreSQL 未能在 180 秒内安全停止,已取消后续重启。" return 1 fi fi } launch_app_process() { ( export ASPNETCORE_ENVIRONMENT=Production export ASPNETCORE_URLS="http://0.0.0.0:$SERVICE_PORT" export ConnectionStrings__DefaultConnection="Host=$RUN_ROOT;Port=$PG_PORT;Database=live_recorder;Username=liverecorder;Timeout=15;Command Timeout=120;Keepalive=30" export LIVE_RECORDER_DEFAULT_OUTPUT_ROOT="$RECORD_ROOT" if [ -f "$ADMIN_PASSWORD_FILE" ]; then LIVE_RECORDER_DEFAULT_ADMIN_PASSWORD=$(sed -n '1p' "$ADMIN_PASSWORD_FILE") export LIVE_RECORDER_DEFAULT_ADMIN_PASSWORD fi export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 export DOTNET_BUNDLE_EXTRACT_BASE_DIR="$DATA_ROOT/dotnet-bundle" export XDG_CACHE_HOME="$DATA_ROOT/cache" export TMPDIR="$DATA_ROOT/tmp" export LD_LIBRARY_PATH="$RUNTIME_LIBRARY_PATH" export PATH="$RUNTIME_PATH" export SSL_CERT_FILE="$CA_BUNDLE" export CURL_CA_BUNDLE="$CA_BUNDLE" mkdir -p "$XDG_CACHE_HOME" "$TMPDIR" cd "$APP_ROOT/server" || exit 1 exec "$SERVER" ) >>"$APP_LOG" 2>&1 & APP_PROCESS_PID=$! printf '%s\n' "$APP_PROCESS_PID" >"$APP_PID_FILE" } start_app() { mkdir -p "$DATA_ROOT" "$RUN_ROOT" "$LOG_ROOT" "$RECORD_ROOT" "$DATA_ROOT/dotnet-bundle" chmod 0700 "$DATA_ROOT" "$RUN_ROOT" "$DATA_ROOT/dotnet-bundle" 2>/dev/null || true if [ ! -x "$SERVER" ]; then log_message "应用程序不存在或不可执行:$SERVER" return 1 fi if [ ! -x "$PG_BIN/postgres" ] || [ ! -x "$NODE_BIN" ] || [ ! -x "$CURL_BIN" ] || [ ! -s "$CA_BUNDLE" ]; then log_message "FPK 原生运行环境不完整。" return 1 fi system_media_tools_available || return 1 if pid=$(app_pid); then log_message "应用已运行,PID $pid。" return 0 fi rm -f "$APP_PID_FILE" start_postgres || return 1 launch_attempt=1 launch_app_process pid=$APP_PROCESS_PID attempt=0 while [ "$attempt" -lt 90 ]; do if run_native "$CURL_BIN" -fsS "http://127.0.0.1:$SERVICE_PORT/health/ready" >/dev/null 2>&1; then rm -f "$ADMIN_PASSWORD_FILE" log_message "应用启动成功,PID $pid,端口 $SERVICE_PORT。" return 0 fi if ! kill -0 "$pid" 2>/dev/null; then if [ "$launch_attempt" -ge 3 ]; then break fi log_message "应用启动进程提前退出,3 秒后重试($launch_attempt/3)。" sleep 3 launch_attempt=$((launch_attempt + 1)) launch_app_process pid=$APP_PROCESS_PID fi attempt=$((attempt + 1)) sleep 1 done log_message "应用未能在 90 秒内就绪。" stop_app return 1 } stop_app() { if pid=$(app_pid); then kill "$pid" 2>/dev/null || true attempt=0 while kill -0 "$pid" 2>/dev/null && [ "$attempt" -lt 30 ]; do sleep 1 attempt=$((attempt + 1)) done if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true fi fi rm -f "$APP_PID_FILE" stop_postgres } case "${1:-status}" in start) start_app ;; stop) stop_app ;; restart) stop_app && start_app ;; status) if app_pid >/dev/null && postgres_running; then exit 0 fi exit 3 ;; *) printf 'usage: %s {start|stop|restart|status}\n' "$0" >&2; exit 2 ;; esac