#!/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 # 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_SWITCH_TITLEDB="${ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB:="false"}" # Set INIT_DEBUG to false if not set by docker env # (this env var is currently undocumented and usually just needed for development purposes) INIT_DEBUG="${INIT_DEBUG:="false"}" # print debug log output if enabled debug_log() { if [[ ${INIT_DEBUG} == "true" ]]; then echo "DEBUG: [RomM][init][$(date +"%Y-%m-%d %T")]" "${@}" || true fi } info_log() { echo "INFO: [RomM][init][$(date +"%Y-%m-%d %T")]" "${@}" || true } warn_log() { echo "WARNING: [RomM][init][$(date +"%Y-%m-%d %T")]" "${@}" || true } error_log() { echo "ERROR: [RomM][init][$(date +"%Y-%m-%d %T")]" "${@}" || true exit 1 } wait_for_gunicorn_socket() { info_debug "Waiting for gunicorn socket file..." while [[ ! -S /tmp/gunicorn.sock ]]; do sleep 1 done info_debug "Gunicorn socket file found" } # 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" # TODO: Remove support for GUNICORN_WORKERS in future version. if [[ -n ${GUNICORN_WORKERS-} ]]; then warn_log "GUNICORN_WORKERS variable is deprecated, use WEB_CONCURRENCY instead" : "${WEB_CONCURRENCY:=${GUNICORN_WORKERS}}" fi gunicorn \ --access-logfile - \ --error-logfile - \ --worker-class uvicorn.workers.UvicornWorker \ --bind=0.0.0.0:5000 \ --bind=unix:/tmp/gunicorn.sock \ --pid=/tmp/gunicorn.pid \ --forwarded-allow-ips="*" \ --workers "${WEB_CONCURRENCY:-2}" \ main:app & } # Commands to start nginx (handling PID creation internally) start_bin_nginx() { wait_for_gunicorn_socket info_log "Starting internal nginx" if [[ ${EUID} -ne 0 ]]; then nginx else # if container runs as root, drop permissions nginx -g 'user romm;' fi } start_bin_valkey-server() { info_log "Starting internal valkey-server" # Check if /usr/local/etc/valkey/valkey.conf exists and use it if so if [[ -f /usr/local/etc/valkey/valkey.conf ]]; then valkey-server /usr/local/etc/valkey/valkey.conf & else valkey-server --dir /redis-data & fi VALKEY_PID=$! echo "${VALKEY_PID}" >/tmp/valkey-server.pid } # function that runs our independent python scripts and creates corresponding PID files, start_python() { SCRIPT="${1}" info_log "Starting ${SCRIPT}" python3 "${SCRIPT}.py" & WATCHER_PID=$! echo "${WATCHER_PID}" >"/tmp/${SCRIPT}.pid" } watchdog_process_pid() { TYPE=$1 PROCESS=$2 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 debug_log "${PROCESS} still running, no need to start" else if [[ ${TYPE} == "bin" ]]; then start_bin_"${PROCESS}" elif [[ ${TYPE} == "python" ]]; then start_python "${PROCESS}" fi fi else if [[ ${TYPE} == "bin" ]]; then start_bin_"${PROCESS}" elif [[ ${TYPE} == "python" ]]; then start_python "${PROCESS}" fi 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 scheduler stop_process_pid worker 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"; } # setup trap handler exited=0 trap 'exited=1 && shutdown' SIGINT SIGTERM EXIT # clear any leftover PID files rm /tmp/*.pid -f # function definition done, lets start our main loop while ! ((exited)); do # Start Valkey server if we dont have a corresponding PID file # and REDIS_HOST is not set (which would mean we're using an external Redis/Valkey) if [[ -z ${REDIS_HOST:=""} ]]; then watchdog_process_pid bin valkey-server else warn_log "REDIS_HOST is set, not starting internal valkey-server" fi # Run needed database migrations on startup, # but only if it was not successful since the last full docker container start if [[ ${ALEMBIC_SUCCESS:="false"} == "false" ]]; then if alembic upgrade head; then info_log "Database schema migrations succeeded" ALEMBIC_SUCCESS="true" else error_log "Something went horribly wrong when running the database migrations" fi else debug_log "Database schema already upgraded during current container lifecycle" fi # start backend if we dont have a corresponding PID file watchdog_process_pid bin gunicorn # start nginx if we dont have a corresponding PID file watchdog_process_pid bin nginx # start worker if we dont have a corresponding PID file info_log "Starting worker" watchdog_process_pid python worker # only start the scheduler if enabled if [[ ${ENABLE_SCHEDULED_RESCAN} == "true" || ${ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB} == "true" ]]; then # start scheduler if we dont have a corresponding PID file info_log "Starting scheduler" watchdog_process_pid python scheduler fi # only start the watcher if enabled if [[ ${ENABLE_RESCAN_ON_FILESYSTEM_CHANGE} == "true" ]]; then # start watcher if we dont have a corresponding PID file info_log "Starting watcher" watchdog_process_pid python watcher fi # check for died processes every 5 seconds sleep 5 done