Files
romm/docker/init_scripts/init
Michael Manganiello 63f84b78d5 misc: Create startup script to run initial tasks before main application
For steps that need to run before the web application starts, such as
scheduling tasks, this new `startup.py` script is introduced.

This fixes a recently introduced issue where task scheduling was not
being triggered, because of it being included in the
`if __name__ == "__main__":` block, which is not executed when
the application is run by Gunicorn in production environments.

We do not include this logic as part of FastAPI's lifespan
implementation, as running multiple workers with Gunicorn would
cause this logic to be executed multiple times.
2025-08-12 23:14:26 -03:00

310 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -o errexit # treat errors as fatal
set -o nounset # treat unset variables as an error
set -o pipefail # treat errors in pipes as fatal
shopt -s inherit_errexit # inherit errexit
LOGLEVEL="${LOGLEVEL:="info"}"
# make it possible to disable the inotify watcher process
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE="${ENABLE_RESCAN_ON_FILESYSTEM_CHANGE:="false"}"
ENABLE_SCHEDULED_RESCAN="${ENABLE_SCHEDULED_RESCAN:="false"}"
ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA="${ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA:="false"}"
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB="${ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB:="false"}"
# if REDIS_HOST is set, we assume that an external redis is used
REDIS_HOST="${REDIS_HOST:=""}"
# set DEFAULT_WEB_CONCURRENCY to 1 if not set by docker env to reduce resource usage
# (since backend is almost 100% async this won't block anything)
DEFAULT_WEB_CONCURRENCY=1
# logger colors
RED='\033[0;31m'
LIGHTMAGENTA='\033[0;95m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
RESET='\033[0;00m'
print_banner() {
local version
version=$(python3 -c "exec(open('__version__.py').read()); print(__version__)")
info_log " _____ __ __ "
info_log ' | __ \ | \/ |'
info_log ' | |__) |___ _ __ ___ | \ / |'
info_log " | _ // _ \\| '_ \` _ \\| |\\/| |"
info_log ' | | \ \ (_) | | | | | | | | |'
info_log ' |_| \_\___/|_| |_| |_|_| |_|'
info_log ""
info_log "The beautiful, powerful, self-hosted Rom manager and player"
info_log ""
info_log "Version: ${version}"
info_log ""
}
debug_log() {
# print debug log output if enabled
if [[ ${LOGLEVEL} == "debug" ]]; then
echo -e "${LIGHTMAGENTA}DEBUG: ${BLUE}[RomM]${LIGHTMAGENTA}[init]${CYAN}[$(date +"%Y-%m-%d %T")]${RESET}" "${@}" || true
fi
}
info_log() {
echo -e "${GREEN}INFO: ${BLUE}[RomM]${LIGHTMAGENTA}[init]${CYAN}[$(date +"%Y-%m-%d %T")]${RESET}" "${@}" || true
}
warn_log() {
echo -e "${YELLOW}WARNING: ${BLUE}[RomM]${LIGHTMAGENTA}[init]${CYAN}[$(date +"%Y-%m-%d %T")]${RESET}" "${@}" || true
}
error_log() {
echo -e "${RED}ERROR: ${BLUE}[RomM]${LIGHTMAGENTA}[init]${CYAN}[$(date +"%Y-%m-%d %T")]${RESET}" "${@}" || true
exit 1
}
# Commands to run initial startup tasks
run_startup() {
if ! PYTHONPATH="/backend:${PYTHONPATH-}" opentelemetry-instrument \
--service_name "${OTEL_SERVICE_NAME_PREFIX-}startup" \
python3 /backend/startup.py; then
error_log "Startup script failed, exiting"
fi
}
wait_for_gunicorn_socket() {
debug_log "Waiting for gunicorn socket file..."
local retries=60
while [[ ! -S /tmp/gunicorn.sock && retries -gt 0 ]]; do
sleep 0.5
((retries--))
done
if [[ -S /tmp/gunicorn.sock ]]; then
debug_log "Gunicorn socket file found"
else
error_log "Gunicorn socket file not found after waiting 30s"
fi
}
# function that runs or main process and creates a corresponding PID file,
start_bin_gunicorn() {
# cleanup potentially leftover socket
rm /tmp/gunicorn.sock -f
# commands to start our main application and store its PID to check for crashes
info_log "Starting backend"
opentelemetry-instrument \
--service_name "${OTEL_SERVICE_NAME_PREFIX-}api" \
gunicorn \
--bind=0.0.0.0:5000 \
--bind=unix:/tmp/gunicorn.sock \
--pid=/tmp/gunicorn.pid \
--forwarded-allow-ips="*" \
--worker-class uvicorn_worker.UvicornWorker \
--workers "${WEB_CONCURRENCY:-${DEFAULT_WEB_CONCURRENCY:-1}}" \
--error-logfile - \
--log-config /etc/gunicorn/logging.conf \
main:app &
}
# Commands to start nginx (handling PID creation internally)
start_bin_nginx() {
wait_for_gunicorn_socket
info_log "Starting nginx"
if [[ ${EUID} -ne 0 ]]; then
nginx
else
# if container runs as root, drop permissions
nginx -g 'user romm;'
fi
: "${ROMM_BASE_URL:=http://0.0.0.0:8080}"
info_log "🚀 RomM is now available at ${ROMM_BASE_URL}"
}
# Commands to start valkey-server (handling PID creation internally)
start_bin_valkey-server() {
info_log "Starting internal valkey"
if [[ -f /usr/local/etc/valkey/valkey.conf ]]; then
if [[ ${LOGLEVEL} == "debug" ]]; then
valkey-server /usr/local/etc/valkey/valkey.conf &
else
valkey-server /usr/local/etc/valkey/valkey.conf >/dev/null 2>&1 &
fi
else
if [[ ${LOGLEVEL} == "debug" ]]; then
valkey-server --dir /redis-data &
else
valkey-server --dir /redis-data >/dev/null 2>&1 &
fi
fi
VALKEY_PID=$!
echo "${VALKEY_PID}" >/tmp/valkey-server.pid
local host="127.0.0.1"
local port="6379"
local max_retries=120
local retry=0
debug_log "Waiting for internal valkey to be ready..."
# Temporarily disable errexit for this part of the script
set +o errexit
while ((retry < max_retries)); do
# Attempt to check if valkey TCP port is open
if (echo >/dev/tcp/"${host}"/"${port}") 2>/dev/null; then
debug_log "Internal valkey is ready and accepting connections"
set -o errexit # Re-enable errexit after success
return 0
fi
sleep 0.5
((retry++))
done
error_log "Internal valkey did not become ready after $((max_retries * 500))ms"
}
# Commands to start RQ scheduler
start_bin_rq_scheduler() {
info_log "Starting RQ scheduler"
RQ_REDIS_HOST=${REDIS_HOST:-127.0.0.1} \
RQ_REDIS_PORT=${REDIS_PORT:-6379} \
RQ_REDIS_USERNAME=${REDIS_USERNAME:-""} \
RQ_REDIS_PASSWORD=${REDIS_PASSWORD:-""} \
RQ_REDIS_DB=${REDIS_DB:-0} \
RQ_REDIS_SSL=${REDIS_SSL:-0} \
rqscheduler \
--path /backend \
--pid /tmp/rq_scheduler.pid &
}
# Commands to start RQ worker
start_bin_rq_worker() {
info_log "Starting RQ worker"
# Build Redis URL properly
local redis_url
if [[ -n ${REDIS_PASSWORD-} ]]; then
redis_url="redis${REDIS_SSL:+s}://${REDIS_USERNAME-}:${REDIS_PASSWORD}@${REDIS_HOST:-127.0.0.1}:${REDIS_PORT:-6379}/${REDIS_DB:-0}"
elif [[ -n ${REDIS_USERNAME-} ]]; then
redis_url="redis${REDIS_SSL:+s}://${REDIS_USERNAME}@${REDIS_HOST:-127.0.0.1}:${REDIS_PORT:-6379}/${REDIS_DB:-0}"
else
redis_url="redis${REDIS_SSL:+s}://${REDIS_HOST:-127.0.0.1}:${REDIS_PORT:-6379}/${REDIS_DB:-0}"
fi
# Set PYTHONPATH so RQ can find the tasks module
PYTHONPATH="/backend:${PYTHONPATH-}" rq worker \
--path /backend \
--pid /tmp/rq_worker.pid \
--url "${redis_url}" \
high default low &
}
start_bin_watcher() {
info_log "Starting watcher"
watchfiles \
--target-type command \
"opentelemetry-instrument --service_name '${OTEL_SERVICE_NAME_PREFIX-}watcher' python3 watcher.py" \
/romm/library &
WATCHER_PID=$!
echo "${WATCHER_PID}" >/tmp/watcher.pid
}
watchdog_process_pid() {
PROCESS=$1
if [[ -f "/tmp/${PROCESS}.pid" ]]; then
# Check if the pid we last wrote to our state file is actually active
PID=$(cat "/tmp/${PROCESS}.pid") || true
if [[ ! -d "/proc/${PID}" ]]; then
start_bin_"${PROCESS}"
fi
else
# Start process if we dont have a corresponding PID file
start_bin_"${PROCESS}"
fi
}
stop_process_pid() {
PROCESS=$1
if [[ -f "/tmp/${PROCESS}.pid" ]]; then
PID=$(cat "/tmp/${PROCESS}.pid") || true
if [[ -d "/proc/${PID}" ]]; then
info_log "Stopping ${PROCESS}"
kill "${PID}" || true
# wait for process exit
while [[ -e "/proc/${PID}" ]]; do sleep 0.1; done
fi
fi
}
shutdown() {
# shutdown in reverse order
stop_process_pid rq_worker
stop_process_pid rq_scheduler
stop_process_pid watcher
stop_process_pid nginx
stop_process_pid gunicorn
stop_process_pid valkey-server
}
# switch to backend directory
cd /backend || { error_log "/backend directory doesn't seem to exist"; }
print_banner
# setup trap handler
exited=0
trap 'exited=1 && shutdown' SIGINT SIGTERM EXIT
# clear any leftover PID files
rm /tmp/*.pid -f
# Start Valkey server if REDIS_HOST is not set (which would mean user is using an external Redis/Valkey)
if [[ -z ${REDIS_HOST} ]]; then
watchdog_process_pid valkey-server
else
info_log "REDIS_HOST is set, not starting internal valkey-server"
fi
# Run needed database migrations once at startup
info_log "Running database migrations"
if alembic upgrade head; then
info_log "Database migrations succeeded"
else
error_log "Failed to run database migrations"
fi
run_startup
# main loop
while ! ((exited)); do
watchdog_process_pid gunicorn
# only start the scheduler if enabled
if [[ ${ENABLE_SCHEDULED_RESCAN} == "true" || ${ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB} == "true" || ${ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA} == "true" ]]; then
watchdog_process_pid rq_scheduler
fi
watchdog_process_pid rq_worker
# only start the watcher if enabled
if [[ ${ENABLE_RESCAN_ON_FILESYSTEM_CHANGE} == "true" ]]; then
watchdog_process_pid watcher
fi
watchdog_process_pid nginx
# check for died processes every 5 seconds
sleep 5
done