From e5f637bca6842b9ff02476b4ce22bfa05648245d Mon Sep 17 00:00:00 2001 From: librelad Date: Sun, 24 May 2026 15:01:31 +0100 Subject: [PATCH] refactor(service): make task processor service setup idempotent installLibrePortalWebUITaskService only wrote the unit if it didn't already exist, so env/User/mode changes never reached an existing install and a docker-type switch couldn't update the service. Make it converge: compute the desired unit for the current mode and only rewrite + daemon-reload + restart when it actually differs (otherwise just ensure enabled+running, no restart, so routine re-runs don't bounce the processor and kill in-flight tasks). The docker-type switcher now calls this idempotent setup (replacing the one-shot restart helper), so a swap updates the env AND restarts in one step. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../docker/type_switcher/swap_docker_type.sh | 4 +- scripts/webui/webui_install_systemd.sh | 106 +++++++++--------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/scripts/docker/type_switcher/swap_docker_type.sh b/scripts/docker/type_switcher/swap_docker_type.sh index 28e6c5c..b0ff5c3 100755 --- a/scripts/docker/type_switcher/swap_docker_type.sh +++ b/scripts/docker/type_switcher/swap_docker_type.sh @@ -75,7 +75,7 @@ dockerSwitcherSwap() dockerStartAllApps; databaseOptionInsert "docker_type" $CFG_DOCKER_INSTALL_TYPE; switchMigrateRestoreApps; - restartLibrePortalWebUITaskService; + installLibrePortalWebUITaskService; fi fi fi @@ -114,7 +114,7 @@ dockerSwitcherSwap() dockerStartAllApps; databaseOptionInsert "docker_type" $CFG_DOCKER_INSTALL_TYPE; switchMigrateRestoreApps; - restartLibrePortalWebUITaskService; + installLibrePortalWebUITaskService; fi fi elif [[ $flag == "cli" ]]; then diff --git a/scripts/webui/webui_install_systemd.sh b/scripts/webui/webui_install_systemd.sh index eebe079..3f1a86e 100755 --- a/scripts/webui/webui_install_systemd.sh +++ b/scripts/webui/webui_install_systemd.sh @@ -1,34 +1,37 @@ #!/bin/bash # LibrePortal Task Processor Systemd Service Setup -# Replaces crontabSetupTaskProcessor with systemd service -installLibrePortalWebUITaskService() +# Replaces crontabSetupTaskProcessor with systemd service. +# +# Idempotent: computes the desired unit for the CURRENT docker mode and only +# rewrites + daemon-reloads + restarts when it actually differs from what's on +# disk. So routine re-runs are no-ops (no needless restart that would kill an +# in-flight task), while a rooted<->rootless switch — which changes the env +# block — triggers exactly one rewrite + restart so the processor re-reads the +# new mode. Safe to call from install AND from the docker-type switcher. +installLibrePortalWebUITaskService() { - if [[ "$CFG_REQUIREMENT_WEBUI_SERVICE" == "true" ]]; then - local service_file="/etc/systemd/system/libreportal.service" - if [[ ! -f "$service_file" ]]; then - local task_processor_script="$install_scripts_dir/crontab/task/crontab_task_processor.sh" - local task_dir="$containers_dir/libreportal/frontend/data/tasks" - - # Update TASK_DIR in the task processor script - if [ -f "$task_processor_script" ]; then - sed -i "s|TASK_DIR=\".*\"|TASK_DIR=\"$task_dir\"|g" "$task_processor_script" - chmod +x "$task_processor_script" - else - isNotice "Task processor script not found" - fi + [[ "$CFG_REQUIREMENT_WEBUI_SERVICE" == "true" ]] || return 0 - # Rootless docker exposes the daemon at /run/user//docker.sock and - # depends on XDG_RUNTIME_DIR being set. Systemd units don't inherit user - # bashrc, so without these Environment= lines the processor would fall - # back to /var/run/docker.sock (which rootless does not create) and any - # `docker …` call inside the task would fail. Rootful gets no extras — - # the default /var/run path is already correct. - # - # The rootless daemon runs as the DOCKER INSTALL USER, so its socket lives in - # that user's runtime dir — not the manager's. Use the docker install user's - # uid here (matches dockerCommandRunInstallUser); pointing at the manager's - # uid was wrong — that socket doesn't exist. + local service_file="/etc/systemd/system/libreportal.service" + local task_processor_script="$install_scripts_dir/crontab/task/crontab_task_processor.sh" + local task_dir="$containers_dir/libreportal/frontend/data/tasks" + + # Point the processor at the task dir (idempotent). + if [ -f "$task_processor_script" ]; then + sed -i "s|TASK_DIR=\".*\"|TASK_DIR=\"$task_dir\"|g" "$task_processor_script" + chmod +x "$task_processor_script" + else + isNotice "Task processor script not found" + fi + + # Rootless docker exposes the daemon at /run/user//docker.sock and depends + # on XDG_RUNTIME_DIR being set. Systemd units don't inherit user bashrc, so + # without these Environment= lines the processor would fall back to + # /var/run/docker.sock (which rootless does not create). The rootless daemon + # runs as the DOCKER INSTALL USER, so its socket lives in THAT user's runtime + # dir (matches dockerCommandRunInstallUser). Rooted gets no extras — the + # default /var/run path is already correct. local service_env_block="" if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then local docker_install_uid @@ -37,8 +40,8 @@ installLibrePortalWebUITaskService() Environment=XDG_RUNTIME_DIR=/run/user/${docker_install_uid}" fi - # Create systemd service file - runSystem tee "$service_file" > /dev/null </dev/null | grep -q "task_processor.sh"; then - sudo -u $sudo_user_name crontab -l 2>/dev/null | grep -v "task_processor.sh" | sudo -u $sudo_user_name crontab - - isNotice "Removed task processor from crontab" - fi - - # Reload systemd and enable service - runSystem systemctl daemon-reload - runSystem systemctl enable libreportal.service >/dev/null 2>&1 - runSystem systemctl start libreportal.service - - isSuccessful "LibrePortal task processor service setup." - fi + local current="" + [[ -f "$service_file" ]] && current="$(sudo cat "$service_file" 2>/dev/null)" + + if [[ "$desired" != "$current" ]]; then + printf '%s\n' "$desired" | runSystem tee "$service_file" > /dev/null + runSystem systemctl daemon-reload + runSystem systemctl enable libreportal.service >/dev/null 2>&1 + runSystem systemctl restart libreportal.service + isSuccessful "LibrePortal task processor service installed/updated ($CFG_DOCKER_INSTALL_TYPE)." + else + # Unit already correct — ensure it's enabled + running, without a restart. + runSystem systemctl enable libreportal.service >/dev/null 2>&1 + sudo systemctl is-active --quiet libreportal.service || runSystem systemctl start libreportal.service + isSuccessful "LibrePortal task processor service already up to date." + fi + + # Drop the legacy crontab entry if present (superseded by the service). + if sudo -u "$sudo_user_name" crontab -l 2>/dev/null | grep -q "task_processor.sh"; then + sudo -u "$sudo_user_name" crontab -l 2>/dev/null | grep -v "task_processor.sh" | sudo -u "$sudo_user_name" crontab - + isNotice "Removed task processor from crontab" fi } - -# Restart the task processor after a docker-type switch. The processor reads the -# install type (rooted/rootless) ONCE at startup to decide how runFileOp writes -# into the docker-install-owned task dir, so a running instance keeps using the -# old mode until it's bounced. The switch is a CLI one-shot (not a processor -# task), so this won't kill an in-flight switch. -restartLibrePortalWebUITaskService() -{ - [[ "$CFG_REQUIREMENT_WEBUI_SERVICE" == "true" ]] || return 0 - [[ -f /etc/systemd/system/libreportal.service ]] || return 0 - runSystem systemctl restart libreportal.service 2>/dev/null - isSuccessful "Restarted task processor for $CFG_DOCKER_INSTALL_TYPE Docker mode" -}