From e7ed3839edb7511310baa1b3d450c0d01f131b55 Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Mon, 10 Aug 2026 05:47:42 +0300 Subject: [PATCH] =?UTF-8?q?fix(package):=20=D0=BD=D0=B5=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D1=8F=D1=82=D1=8C=20=D1=81=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D1=8F=D1=8E=D1=89=D0=B5=D0=B3=D0=BE=D1=81=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=20postinst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Самообновление Tachyon — это компонентное задание, которое устанавливает пакет, чей postinst и вызывает remove_component_update_cache(). Очистка стирала каталог component-actions целиком, включая файл состояния того самого задания, которое ещё выполнялось. LuCI опрашивал задание, не находил его, не получал ни message, ни stderr и показывал фолбэк "Failed to execute" поверх успешно прошедшего обновления. Теперь очистка пропускает задания, помеченные running. Устаревшие результаты и кэш проверок обновлений убираются как раньше. Зависшее задание не залипнет: детект по PID и started_at помечает такие stale. Пути каталогов состояния стали переопределяемыми через окружение, чтобы тест мог прогнать luci-postinst, не трогая /var/run. --- tachyon/files/usr/lib/service/package.uc | 29 ++++++++-- tests/package_postinst_running_job.sh | 69 ++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 tests/package_postinst_running_job.sh diff --git a/tachyon/files/usr/lib/service/package.uc b/tachyon/files/usr/lib/service/package.uc index 473f4056..3c8e2ce5 100644 --- a/tachyon/files/usr/lib/service/package.uc +++ b/tachyon/files/usr/lib/service/package.uc @@ -22,6 +22,11 @@ const SING_BOX_BIN = env("TACHYON_SING_BOX_BIN", "/usr/bin/sing-box"); const SING_BOX_CRONET = env("TACHYON_SING_BOX_CRONET", "/usr/lib/libcronet.so"); const SING_BOX_MANAGED_MARKER = env("SB_MANAGED_SERVICE_MARKER", "Tachyon managed sing-box service for binary variants"); const PACKAGE_UPGRADE_STATE = env("TACHYON_PACKAGE_UPGRADE_STATE", "/tmp/tachyon-package-was-running"); +const RUNTIME_STATE_DIR = env("TACHYON_RUNTIME_STATE_DIR", "/var/run/tachyon"); +const COMPONENT_ACTION_DIR = env("TACHYON_UI_COMPONENT_ACTION_DIR", RUNTIME_STATE_DIR + "/component-actions"); +const COMPONENT_UPDATE_CHECK_DIR = env("TACHYON_COMPONENT_UPDATE_CHECK_CACHE_DIR", RUNTIME_STATE_DIR + "/component-update-checks"); +const COMPONENT_UPDATE_CHECK_TIMESTAMP = env("TACHYON_COMPONENT_UPDATE_CHECK_STATE_FILE", RUNTIME_STATE_DIR + "/component-update-check.timestamp"); +const COMPONENT_ACTION_LOCK = env("TACHYON_COMPONENT_ACTION_LOCK", RUNTIME_STATE_DIR + "/component-action.lock"); const PACKAGE_TEST_MODE = env("TACHYON_PACKAGE_TEST_MODE", "") != ""; // apk aborts and rolls the whole transaction back if a hook outlives its // patience, and the installer kills it even sooner. Anything that touches @@ -270,13 +275,27 @@ function remove_luci_index_cache() { } } +// A Tachyon self-update is itself a component job: the job installs the package +// whose postinst this is. Wiping the whole directory therefore deleted the state +// file of the job that was still running, so the UI polled a job that no longer +// existed, got no message and no stderr, and rendered its "Failed to execute" +// fallback over an update that had in fact succeeded. Only finished results are +// stale; a job still marked running must survive to write its own result. +function component_job_is_running(path) { + let data = fs.readfile(path); + if (data == null) + return false; + return match(as_string(data), /"running"[ \t]*:[ \t]*(true|1)/) != null; +} + function remove_component_update_cache() { - for (let path in fs.glob("/var/run/tachyon/component-update-checks/*.json")) - unlink_if_exists(path); - unlink_if_exists("/var/run/tachyon/component-update-check.timestamp"); - command_success_from_args([ "rm", "-rf", "/var/run/tachyon/component-action.lock" ]); - for (let path in fs.glob("/var/run/tachyon/component-actions/*")) + for (let path in fs.glob(COMPONENT_UPDATE_CHECK_DIR + "/*.json")) unlink_if_exists(path); + unlink_if_exists(COMPONENT_UPDATE_CHECK_TIMESTAMP); + command_success_from_args([ "rm", "-rf", COMPONENT_ACTION_LOCK ]); + for (let path in fs.glob(COMPONENT_ACTION_DIR + "/*")) + if (!component_job_is_running(path)) + unlink_if_exists(path); } function luci_postinst() { diff --git a/tests/package_postinst_running_job.sh b/tests/package_postinst_running_job.sh new file mode 100644 index 00000000..807ada0b --- /dev/null +++ b/tests/package_postinst_running_job.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -eo pipefail + +# A Tachyon self-update is itself a component job: the job installs the package +# whose postinst runs remove_component_update_cache(). That cleanup used to wipe +# the whole component-actions directory, deleting the state file of the very job +# that was still running. LuCI then polled a job that no longer existed, got +# neither a message nor stderr, and rendered its "Failed to execute" fallback +# over an update that had actually succeeded. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib" +PACKAGE_UC="$TACHYON_LIB/service/package.uc" +WORK_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +STATE_DIR="$WORK_DIR/run/tachyon" +ACTION_DIR="$STATE_DIR/component-actions" +CHECK_DIR="$STATE_DIR/component-update-checks" +mkdir -p "$ACTION_DIR" "$CHECK_DIR" + +RUNNING_JOB="$ACTION_DIR/1786306166-287510904.json" +FINISHED_JOB="$ACTION_DIR/1786306100-111111111.json" +STALE_CHECK="$CHECK_DIR/tachyon.json" +TIMESTAMP="$STATE_DIR/component-update-check.timestamp" + +printf '%s\n' '{ "success": true, "running": true, "kind": "component", "component": "tachyon", "action": "install", "message": "Component action is running", "started_at": 1786306166 }' >"$RUNNING_JOB" +printf '%s\n' '{ "success": true, "running": false, "kind": "component", "component": "sing_box", "action": "check_update", "message": "Latest version is installed" }' >"$FINISHED_JOB" +printf '%s\n' '{ "component": "tachyon", "status": "latest" }' >"$STALE_CHECK" +printf '%s\n' '1786306166' >"$TIMESTAMP" + +TACHYON_PACKAGE_TEST_MODE=1 \ +TACHYON_RUNTIME_STATE_DIR="$STATE_DIR" \ +TACHYON_UI_COMPONENT_ACTION_DIR="$ACTION_DIR" \ +TACHYON_COMPONENT_UPDATE_CHECK_CACHE_DIR="$CHECK_DIR" \ +TACHYON_COMPONENT_UPDATE_CHECK_STATE_FILE="$TIMESTAMP" \ +TACHYON_COMPONENT_ACTION_LOCK="$STATE_DIR/component-action.lock" \ +TACHYON_CONFIG_NAME=tachyon-package-job-test \ + ucode -L "$TACHYON_LIB" "$PACKAGE_UC" luci-postinst >/dev/null 2>&1 || + fail "luci-postinst must succeed in test mode" + +# The whole point: the in-flight job survives so its worker can write the result. +[ -f "$RUNNING_JOB" ] || + fail "postinst deleted the state file of a running component job; the UI then reports 'Failed to execute' over a successful update" + +# Finished results are genuinely stale and must still be cleared. +[ ! -f "$FINISHED_JOB" ] || + fail "postinst must still remove finished component job results" +[ ! -f "$STALE_CHECK" ] || + fail "postinst must still clear the component update check cache" +[ ! -f "$TIMESTAMP" ] || + fail "postinst must still clear the component update check timestamp" + +# Structural: no unconditional wipe of the job directory may come back. +sweep="$(sed -n '/^function remove_component_update_cache/,/^}/p' "$PACKAGE_UC" | sed 's://.*::')" +[ -n "$sweep" ] || fail "service/package.uc must define remove_component_update_cache" +grep -Fq 'component_job_is_running' <<<"$sweep" || + fail "remove_component_update_cache must skip jobs that are still running" + +printf 'package postinst running-job checks passed\n'