#!/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" NODEJS_ROOT="${NODEJS_ROOT:-/var/apps/nodejs_v22/target}" NODE_BIN="$NODEJS_ROOT/bin/node" CURL_BIN="$RUNTIME_ROOT/bin/curl" CA_BUNDLE="$RUNTIME_ROOT/etc/ssl/certs/ca-certificates.crt" RUN_ROOT="$DATA_ROOT/run" LOG_ROOT="$DATA_ROOT/log" APP_PID_FILE="$RUN_ROOT/liverecorder.pid" LOG_MONITOR_PID_FILE="$RUN_ROOT/liverecorder-log-monitor.pid" APP_LOG="$LOG_ROOT/liverecorder.log" APP_LOG_MAX_BYTES=52428800 APP_LOG_RETAINED_FILES=3 ADMIN_PASSWORD_FILE="$DATA_ROOT/admin-password.seed" POSTGRES_ENROLLMENT_TOKEN_FILE="$DATA_ROOT/postgres-enrollment-token.seed" POSTGRES_CREDENTIALS_FILE="$DATA_ROOT/postgres-client.conf" SERVICE_PORT="${TRIM_SERVICE_PORT:-18080}" POSTGRES_SERVICE_API="${POSTGRES_SERVICE_API:-http://127.0.0.1:15433}" SYSTEM_PATH="${PATH:-/usr/local/bin:/usr/bin:/bin}" RUNTIME_PATH="$RUNTIME_ROOT/bin:$NODEJS_ROOT/bin:$SYSTEM_PATH" RUNTIME_LIBRARY_PATH="$RUNTIME_ROOT/lib" DATABASE_CONNECTION_STRING="" SHARED_DB_HOST="" SHARED_DB_PORT="" SHARED_DB_NAME="" SHARED_DB_USER="" SHARED_DB_PASSWORD="" log_message() { mkdir -p "$LOG_ROOT" printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$APP_LOG" } rotate_app_log() { [ -f "$APP_LOG" ] || return 0 size=$(wc -c <"$APP_LOG" 2>/dev/null | tr -d '[:space:]') case "$size" in ''|*[!0-9]*) return 0 ;; esac [ "$size" -ge "$APP_LOG_MAX_BYTES" ] || return 0 index=$APP_LOG_RETAINED_FILES rm -f "$APP_LOG.$index" while [ "$index" -gt 1 ]; do previous=$((index - 1)) if [ -f "$APP_LOG.$previous" ]; then mv "$APP_LOG.$previous" "$APP_LOG.$index" fi index=$previous done tail -c "$APP_LOG_MAX_BYTES" "$APP_LOG" >"$APP_LOG.1.tmp" 2>/dev/null || return 0 mv "$APP_LOG.1.tmp" "$APP_LOG.1" : >"$APP_LOG" } start_log_monitor() { rm -f "$LOG_MONITOR_PID_FILE" ( while app_pid >/dev/null 2>&1; do rotate_app_log sleep 60 done ) & printf '%s\n' "$!" >"$LOG_MONITOR_PID_FILE" } stop_log_monitor() { if [ -f "$LOG_MONITOR_PID_FILE" ]; then monitor_pid=$(sed -n '1p' "$LOG_MONITOR_PID_FILE" | tr -d '[:space:]') case "$monitor_pid" in ''|*[!0-9]*) ;; *) kill "$monitor_pid" 2>/dev/null || true ;; esac fi rm -f "$LOG_MONITOR_PID_FILE" } 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_native() { env LD_LIBRARY_PATH="$RUNTIME_LIBRARY_PATH" PATH="$RUNTIME_PATH" \ SSL_CERT_FILE="$CA_BUNDLE" CURL_CA_BUNDLE="$CA_BUNDLE" "$@" } read_credential_field() { key=$1 sed -n "s/^${key}=//p" "$POSTGRES_CREDENTIALS_FILE" | sed -n '1p' } configure_shared_credentials() { [ -s "$POSTGRES_CREDENTIALS_FILE" ] || return 1 SHARED_DB_HOST=$(read_credential_field host) SHARED_DB_PORT=$(read_credential_field port) SHARED_DB_NAME=$(read_credential_field database) SHARED_DB_USER=$(read_credential_field username) SHARED_DB_PASSWORD=$(read_credential_field password) [ "$SHARED_DB_HOST" = "127.0.0.1" ] || return 1 case "$SHARED_DB_PORT" in ''|*[!0-9]*) return 1 ;; esac case "$SHARED_DB_NAME$SHARED_DB_USER" in *[!a-z0-9_]*) return 1 ;; esac [ -n "$SHARED_DB_PASSWORD" ] || return 1 DATABASE_CONNECTION_STRING="Host=$SHARED_DB_HOST;Port=$SHARED_DB_PORT;Database=$SHARED_DB_NAME;Username=$SHARED_DB_USER;Password=$SHARED_DB_PASSWORD;SSL Mode=Disable;Timeout=15;Command Timeout=120;Keepalive=30" } enroll_shared_database() { [ -s "$POSTGRES_ENROLLMENT_TOKEN_FILE" ] || { log_message "缺少 PostgreSQL 共享服务接入令牌。请在升级或安装向导中重新填写。" return 1 } token=$(sed -n '1p' "$POSTGRES_ENROLLMENT_TOKEN_FILE") [ -n "$token" ] || return 1 response_file="$DATA_ROOT/postgres-enrollment-response.tmp" rm -f "$response_file" attempt=0 while [ "$attempt" -lt 60 ]; do if run_native "$CURL_BIN" \ --fail --silent --show-error \ --connect-timeout 3 --max-time 10 \ -H "Authorization: Bearer $token" \ -H 'Content-Type: application/json' \ --data '{"appId":"liverecorder","displayName":"Live Recorder","requestedExtensions":[]}' \ "$POSTGRES_SERVICE_API/internal/v1/enroll" >"$response_file" 2>>"$APP_LOG"; then break fi attempt=$((attempt + 1)) sleep 2 done unset token [ -s "$response_file" ] || { log_message "无法从 PostgreSQL 共享服务取得数据库凭据。" rm -f "$response_file" return 1 } host=$(sed -n 's/.*"host":"\([^"]*\)".*/\1/p' "$response_file") port=$(sed -n 's/.*"port":\([0-9][0-9]*\).*/\1/p' "$response_file") database=$(sed -n 's/.*"database":"\([^"]*\)".*/\1/p' "$response_file") username=$(sed -n 's/.*"username":"\([^"]*\)".*/\1/p' "$response_file") password=$(sed -n 's/.*"password":"\([^"]*\)".*/\1/p' "$response_file") [ "$host" = "127.0.0.1" ] && [ -n "$port" ] && [ -n "$database" ] && [ -n "$username" ] && [ -n "$password" ] || { log_message "PostgreSQL 共享服务返回了无效凭据。" rm -f "$response_file" return 1 } umask 077 { printf 'host=%s\n' "$host" printf 'port=%s\n' "$port" printf 'database=%s\n' "$database" printf 'username=%s\n' "$username" printf 'password=%s\n' "$password" } >"$POSTGRES_CREDENTIALS_FILE.tmp" mv "$POSTGRES_CREDENTIALS_FILE.tmp" "$POSTGRES_CREDENTIALS_FILE" chmod 0600 "$POSTGRES_CREDENTIALS_FILE" rm -f "$POSTGRES_ENROLLMENT_TOKEN_FILE" "$response_file" configure_shared_credentials } ensure_shared_credentials() { if configure_shared_credentials; then return 0 fi rm -f "$POSTGRES_CREDENTIALS_FILE" enroll_shared_database } select_database_connection() { ensure_shared_credentials || { log_message "无法连接 PostgreSQL 共享服务或取得数据库凭据,应用不会启动。" return 1 } } 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 } launch_app_process() { ( export ASPNETCORE_ENVIRONMENT=Production export ASPNETCORE_URLS="http://0.0.0.0:$SERVICE_PORT" export ConnectionStrings__DefaultConnection="$DATABASE_CONNECTION_STRING" 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 "$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" stop_log_monitor rotate_app_log select_database_connection || return 1 launch_attempt=1 launch_app_process pid=$APP_PROCESS_PID start_log_monitor 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() { stop_log_monitor if pid=$(app_pid); then kill "$pid" 2>/dev/null || true attempt=0 while kill -0 "$pid" 2>/dev/null && [ "$attempt" -lt 50 ]; 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" } case "${1:-status}" in start) start_app ;; stop) stop_app ;; restart) stop_app && start_app ;; status) if app_pid >/dev/null; then exit 0 fi exit 3 ;; *) printf 'usage: %s {start|stop|restart|status}\n' "$0" >&2; exit 2 ;; esac