Files
live_recorder/fnos/cmd/main
T

310 lines
10 KiB
Bash
Executable File

#!/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