Refactor Tachyon installer into resilient transactional flow (#61)
Replace the monolithic installer with a staged transactional flow, add atomic locking, APK lock waiting/diagnostics, tmpfs and overlay preflight, verified release downloads, recovery snapshots, package transaction healthchecks, and active installer regression tests.
This commit is contained in:
+711
-2995
File diff suppressed because it is too large
Load Diff
@@ -3,173 +3,55 @@ set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
MAKEFILE="$ROOT_DIR/tachyon/Makefile"
|
||||
MIGRATION="$ROOT_DIR/tachyon/files/usr/lib/config/migration.uc"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
[ -r "$INSTALLER" ] || fail "install.sh is missing"
|
||||
[ -r "$MAKEFILE" ] || fail "tachyon/Makefile is missing"
|
||||
[ -r "$MIGRATION" ] || fail "runtime migration module is missing"
|
||||
|
||||
# 1. Extract embedded ucode helper and verify its syntax
|
||||
helper="$WORK_DIR/install-json.uc"
|
||||
# Installer v3 deliberately does not duplicate legacy conversion logic. Package
|
||||
# postinst owns the move into /etc/config/tachyon and the runtime migration module
|
||||
# owns schema conversion.
|
||||
if grep -Fq 'installer_cleanup_legacy' "$INSTALLER"; then
|
||||
fail "installer must not embed legacy cleanup/migration ownership"
|
||||
fi
|
||||
if grep -Fq 'migrate-podkop' "$INSTALLER"; then
|
||||
fail "installer must delegate legacy schema migration to package/runtime code"
|
||||
fi
|
||||
|
||||
grep -Fq 'if [ -f /etc/config/netshift ]; then' "$MAKEFILE" ||
|
||||
fail "package postinst must detect NetShift config"
|
||||
grep -Fq 'elif [ -f /etc/config/forkop ]; then' "$MAKEFILE" ||
|
||||
fail "package postinst must detect Forkop config"
|
||||
grep -Fq 'elif [ -f /etc/config/podkop ]; then' "$MAKEFILE" ||
|
||||
fail "package postinst must detect Podkop config"
|
||||
grep -Fq '/usr/lib/tachyon/config/migration.uc migrate-podkop' "$MAKEFILE" ||
|
||||
fail "legacy package postinst must call migrate-podkop"
|
||||
grep -Fq 'migrate-podkop' "$MIGRATION" ||
|
||||
fail "runtime migration module must expose migrate-podkop"
|
||||
|
||||
# Before package hooks can move/remove legacy files, the installer takes a safety
|
||||
# snapshot of every supported legacy config.
|
||||
for path in \
|
||||
/etc/config/netshift \
|
||||
/etc/config/forkop \
|
||||
/etc/config/forkop_plus \
|
||||
/etc/config/podkop \
|
||||
/etc/config/podkop_plus; do
|
||||
grep -Fq "$path" "$INSTALLER" ||
|
||||
fail "installer recovery snapshot must include $path"
|
||||
done
|
||||
|
||||
grep -Fq 'snapshot_state || fail "Could not create recovery snapshot"' "$INSTALLER" ||
|
||||
fail "recovery snapshot must be created before package installation"
|
||||
awk '
|
||||
/cat > "\$helper_path" <<'\''EOF'\''/ { capture = 1; next }
|
||||
capture && /^EOF$/ { exit }
|
||||
capture { print }
|
||||
' "$INSTALLER" > "$helper"
|
||||
[ -s "$helper" ] || fail "failed to extract embedded installer ucode helper"
|
||||
/^main\(\)/ { in_main=1 }
|
||||
in_main && /snapshot_state/ && !snapshot { snapshot=NR }
|
||||
in_main && /install_core_transaction/ && !install { install=NR }
|
||||
END { exit !(snapshot > 0 && install > snapshot) }
|
||||
' "$INSTALLER" || fail "legacy configs must be snapshotted before package hooks run"
|
||||
|
||||
if command -v ucode >/dev/null 2>&1; then
|
||||
ucode -c -o /dev/null "$helper" || fail "embedded ucode helper failed syntax check"
|
||||
fi
|
||||
|
||||
# 2. Test installer_cleanup_legacy with mock Forkop environment
|
||||
MOCK_ROOT="$WORK_DIR/rootfs"
|
||||
mkdir -p "$MOCK_ROOT/etc/config"
|
||||
mkdir -p "$MOCK_ROOT/etc/init.d"
|
||||
mkdir -p "$MOCK_ROOT/usr/bin"
|
||||
mkdir -p "$MOCK_ROOT/usr/lib/forkop"
|
||||
mkdir -p "$MOCK_ROOT/tmp"
|
||||
mkdir -p "$MOCK_ROOT/var/run/forkop"
|
||||
|
||||
cat >"$MOCK_ROOT/etc/config/forkop" <<'EOF'
|
||||
config settings 'settings'
|
||||
option enabled '1'
|
||||
EOF
|
||||
|
||||
cat >"$MOCK_ROOT/etc/init.d/forkop" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
case "$1" in
|
||||
enabled) exit 0 ;;
|
||||
running) exit 0 ;;
|
||||
stop|disable) exit 0 ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod 0755 "$MOCK_ROOT/etc/init.d/forkop"
|
||||
|
||||
cat >"$MOCK_ROOT/usr/bin/forkop" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
case "$1" in
|
||||
get_status) printf '{"running":1}\n' ;;
|
||||
restore_dnsmasq) exit 0 ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod 0755 "$MOCK_ROOT/usr/bin/forkop"
|
||||
|
||||
# Mock opkg
|
||||
MOCK_BIN="$WORK_DIR/bin"
|
||||
mkdir -p "$MOCK_BIN"
|
||||
cat >"$MOCK_BIN/opkg" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
case "$1" in
|
||||
list-installed)
|
||||
printf '%s\n' \
|
||||
'forkop - 1.0.4' \
|
||||
'luci-app-forkop - 1.0.4' \
|
||||
'luci-i18n-forkop-ru - 1.0.4'
|
||||
;;
|
||||
remove)
|
||||
shift
|
||||
[ "$1" = "--force-depends" ] && shift
|
||||
printf 'removed: %s\n' "$1" >> "$TMP_DIR/opkg-removed.log"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "$MOCK_BIN/opkg"
|
||||
|
||||
# Run installer-cleanup-legacy in mock environment
|
||||
if command -v ucode >/dev/null 2>&1; then
|
||||
export PATH="$MOCK_BIN:$PATH"
|
||||
export TMP_DIR="$WORK_DIR"
|
||||
|
||||
state_output="$(
|
||||
TACHYON_LEGACY_DETECTED=1 \
|
||||
TACHYON_INSTALLER_OPKG_BIN="$MOCK_BIN/opkg" \
|
||||
TACHYON_INSTALLER_TACHYON_INIT="$WORK_DIR/no-init" \
|
||||
TACHYON_INSTALLER_TACHYON_BIN="$WORK_DIR/no-bin" \
|
||||
TACHYON_INSTALLER_TACHYON_LIB="$WORK_DIR/no-lib" \
|
||||
TACHYON_INSTALLER_TACHYON_UCI_DEFAULTS="$WORK_DIR/no-defaults" \
|
||||
TACHYON_INSTALLER_TACHYON_LUCI_VIEW="$WORK_DIR/no-view" \
|
||||
TACHYON_INSTALLER_MENU_JSON="$WORK_DIR/no-menu" \
|
||||
TACHYON_INSTALLER_ACL_JSON="$WORK_DIR/no-acl" \
|
||||
TACHYON_INSTALLER_RU_LMO="$WORK_DIR/no-ru-lmo" \
|
||||
TACHYON_INSTALLER_EN_LMO="$WORK_DIR/no-en-lmo" \
|
||||
TACHYON_INSTALLER_RU_LUA="$WORK_DIR/no-ru-lua" \
|
||||
TACHYON_INSTALLER_EN_LUA="$WORK_DIR/no-en-lua" \
|
||||
TACHYON_INSTALLER_LEGACY_INIT="$WORK_DIR/no-leg-init" \
|
||||
TACHYON_INSTALLER_LEGACY_BIN="$WORK_DIR/no-leg-bin" \
|
||||
TACHYON_INSTALLER_LEGACY_LIB="$WORK_DIR/no-leg-lib" \
|
||||
TACHYON_INSTALLER_LEGACY_UCI_DEFAULTS="$WORK_DIR/no-leg-defaults" \
|
||||
TACHYON_INSTALLER_LEGACY_LUCI_VIEW="$WORK_DIR/no-leg-view" \
|
||||
TACHYON_INSTALLER_LEGACY_MENU_JSON="$WORK_DIR/no-leg-menu" \
|
||||
TACHYON_INSTALLER_LEGACY_ACL_JSON="$WORK_DIR/no-leg-acl" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_INIT="$WORK_DIR/no-base-init" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_BIN="$WORK_DIR/no-base-bin" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_LIB="$WORK_DIR/no-base-lib" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_UCI_DEFAULTS="$WORK_DIR/no-base-defaults" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_LUCI_VIEW="$WORK_DIR/no-base-view" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_MENU_JSON="$WORK_DIR/no-base-menu" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_ACL_JSON="$WORK_DIR/no-base-acl" \
|
||||
TACHYON_INSTALLER_LEGACY_BASE_I18N="$WORK_DIR/no-base-i18n" \
|
||||
TACHYON_INSTALLER_LEGACY_BRAND="podkop" \
|
||||
TACHYON_INSTALLER_LEGACY_BACKEND="podkop-plus" \
|
||||
ucode "$helper" installer-cleanup-legacy
|
||||
)"
|
||||
|
||||
printf '%s\n' "$state_output" | grep -Fxq 'TACHYON_LEGACY_DETECTED=1' ||
|
||||
fail "installer_cleanup_legacy must report TACHYON_LEGACY_DETECTED=1 when Forkop is installed"
|
||||
|
||||
# 3. Test post-install when TACHYON_LEGACY_DETECTED=1
|
||||
# reset_settings must NOT be executed
|
||||
cat >"$WORK_DIR/mock-tachyon-bin" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
if [ "$1" = "reset_settings" ]; then
|
||||
echo "CALLED_RESET_SETTINGS" >> "$TMP_DIR/reset_settings.called"
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "$WORK_DIR/mock-tachyon-bin"
|
||||
|
||||
TACHYON_WAS_INSTALLED=0 \
|
||||
TACHYON_LEGACY_DETECTED=1 \
|
||||
TACHYON_WAS_ENABLED=1 \
|
||||
TACHYON_WAS_RUNNING=1 \
|
||||
TACHYON_INSTALLER_TACHYON_BIN="$WORK_DIR/mock-tachyon-bin" \
|
||||
TACHYON_INSTALLER_TACHYON_INIT="$WORK_DIR/mock-tachyon-bin" \
|
||||
ucode "$helper" installer-post-install
|
||||
|
||||
if [ -f "$WORK_DIR/reset_settings.called" ]; then
|
||||
fail "installer_post_install must not invoke reset_settings when legacy migration is active"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Static checks on install.sh to ensure Forkop / Netshift migration contracts
|
||||
grep -Fq 'TACHYON_FORKOP_MIGRATION=1' "$INSTALLER" ||
|
||||
fail "install.sh must set TACHYON_FORKOP_MIGRATION flag"
|
||||
|
||||
grep -Fq 'TACHYON_NETSHIFT_MIGRATION=1' "$INSTALLER" ||
|
||||
fail "install.sh must set TACHYON_NETSHIFT_MIGRATION flag"
|
||||
|
||||
grep -Fq 'legacy_detected_before="$TACHYON_LEGACY_DETECTED"' "$INSTALLER" ||
|
||||
fail "cleanup_legacy_installation must guard TACHYON_LEGACY_DETECTED against being overwritten to 0"
|
||||
|
||||
grep -Fq 'installer_package_installed("forkop")' "$INSTALLER" ||
|
||||
fail "installer_cleanup_legacy must detect forkop packages"
|
||||
|
||||
grep -Fq 'installer_package_installed("netshift")' "$INSTALLER" ||
|
||||
fail "installer_cleanup_legacy must detect netshift packages"
|
||||
|
||||
grep -Fq '"/etc/config/forkop"' "$INSTALLER" ||
|
||||
fail "installer must reference /etc/config/forkop for migration and cleanup"
|
||||
|
||||
printf 'Forkop migration installer tests passed successfully.\n'
|
||||
printf 'PASS: legacy migration ownership and installer recovery contract\n'
|
||||
|
||||
@@ -1,138 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# A rebuilt release keeps its version string, so "the package is installed and the
|
||||
# version matches" stays true even when the package manager rolled the upgrade
|
||||
# back — which is how a failed install used to end with a green success banner.
|
||||
# The only thing that moves is the commit SHA baked into the installed constants
|
||||
# module, so these checks pin how the installer reads it.
|
||||
set -eo pipefail
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1" actual="$2" label="$3"
|
||||
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
|
||||
}
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
[ -r "$INSTALLER" ] || fail "install.sh must exist"
|
||||
|
||||
# The function is lifted out of the installer rather than sourced: install.sh runs
|
||||
# main() at the bottom, so sourcing it would attempt a real installation. Only the
|
||||
# hardcoded path is redirected, so the extraction logic under test is the shipped
|
||||
# one, character for character.
|
||||
sed -n '/^installed_backend_sha() {/,/^}/p' "$INSTALLER" >"$WORK_DIR/sha.sh"
|
||||
[ -s "$WORK_DIR/sha.sh" ] ||
|
||||
fail "install.sh must define installed_backend_sha for rollback detection"
|
||||
grep -Fq '/usr/lib/tachyon/core/constants.uc' "$WORK_DIR/sha.sh" ||
|
||||
fail "installed_backend_sha must read the installed constants module"
|
||||
sed 's#/usr/lib/tachyon/core/constants.uc#'"$WORK_DIR"'/constants.uc#' \
|
||||
"$WORK_DIR/sha.sh" >"$WORK_DIR/sha-under-test.sh"
|
||||
# Package manager errors are fatal. The old installer tried to infer success after
|
||||
# a non-zero apk result and could accidentally accept a rolled-back upgrade.
|
||||
grep -Fq 'install_core_transaction || fail "Tachyon package transaction failed"' "$INSTALLER" ||
|
||||
fail "package transaction failures must abort the install"
|
||||
if grep -Fq 'Package manager reported non-critical errors during installation' "$INSTALLER"; then
|
||||
fail "installer must not convert package-manager failures into success"
|
||||
fi
|
||||
|
||||
read_sha() {
|
||||
local constants="$1"
|
||||
rm -f "$WORK_DIR/constants.uc"
|
||||
[ "$constants" = "" ] || cp "$constants" "$WORK_DIR/constants.uc"
|
||||
(
|
||||
# shellcheck disable=SC1090
|
||||
. "$WORK_DIR/sha-under-test.sh"
|
||||
installed_backend_sha
|
||||
)
|
||||
}
|
||||
# Backend, LuCI and optional i18n are installed in one package-manager invocation.
|
||||
grep -Fq 'pkg_install_local_bundle "$@"' "$INSTALLER" ||
|
||||
fail "release packages must be submitted as one transaction"
|
||||
grep -Fq 'apk_run apk-transaction "$PACKAGE_TIMEOUT_SECONDS" add --allow-untrusted "$@"' "$INSTALLER" ||
|
||||
fail "APK install must be one bounded transaction"
|
||||
grep -Fq 'run_logged_timeout opkg-transaction "$PACKAGE_TIMEOUT_SECONDS" opkg install' "$INSTALLER" ||
|
||||
fail "opkg install must be one bounded transaction"
|
||||
|
||||
printf ' c.TACHYON_COMMIT_SHA = env("TACHYON_COMMIT_SHA", "ba69e3d2");\n' \
|
||||
>"$WORK_DIR/stamped.uc"
|
||||
assert_eq "ba69e3d2" "$(read_sha "$WORK_DIR/stamped.uc")" "a stamped build must report its SHA"
|
||||
# The historical 120s package timeout caused valid postinst hooks to be killed and
|
||||
# APK to roll the upgrade back. Keep a larger package transaction budget.
|
||||
grep -Fq 'PACKAGE_TIMEOUT_SECONDS=420' "$INSTALLER" ||
|
||||
fail "package transaction timeout must allow slow OpenWrt package hooks"
|
||||
|
||||
# An unprocessed tree keeps the literal placeholder; treating it as a SHA would
|
||||
# make every install look like a rollback.
|
||||
printf ' c.TACHYON_COMMIT_SHA = env("TACHYON_COMMIT_SHA", "__COMPILED_COMMIT_SHA__");\n' \
|
||||
>"$WORK_DIR/placeholder.uc"
|
||||
assert_eq "" "$(read_sha "$WORK_DIR/placeholder.uc")" "the build placeholder must not read as a SHA"
|
||||
# Before the package manager runs, old rc.common/procd lock holders must be
|
||||
# cleared. Old Tachyon releases could otherwise block prerm until APK rolled back.
|
||||
grep -Fq 'release_tachyon_init_lock' "$INSTALLER" ||
|
||||
fail "installer must release stale Tachyon init locks"
|
||||
grep -Fq '/proc/[0-9]*/fd/1000' "$INSTALLER" ||
|
||||
fail "installer must find leaked procd lock holders by fd 1000"
|
||||
grep -Fq '*procd_tachyon*|*tachyon*lock*' "$INSTALLER" ||
|
||||
fail "fd sweep must be limited to Tachyon-owned locks"
|
||||
grep -Fq 'is_protected_pid' "$INSTALLER" ||
|
||||
fail "installer must protect its own process ancestry"
|
||||
grep -Fq '99-tachyon-wan|flock 1000|/etc/init.d/tachyon' "$INSTALLER" ||
|
||||
fail "installer must clear old rc.common waiters"
|
||||
|
||||
# The Makefile substitutes "unknown" when git is unavailable at build time. It
|
||||
# never changes between builds, so it must not be compared as a SHA either.
|
||||
printf ' c.TACHYON_COMMIT_SHA = env("TACHYON_COMMIT_SHA", "unknown");\n' \
|
||||
>"$WORK_DIR/unknown.uc"
|
||||
assert_eq "" "$(read_sha "$WORK_DIR/unknown.uc")" "an unstamped build must not read as a SHA"
|
||||
# The lock release must happen before packages are handed to apk/opkg.
|
||||
awk '
|
||||
/^main\(\)/ { in_main=1 }
|
||||
in_main && /prepare_transaction/ && !prepare { prepare=NR }
|
||||
in_main && /install_core_transaction/ && !install { install=NR }
|
||||
END { exit !(prepare > 0 && install > prepare) }
|
||||
' "$INSTALLER" || fail "stale init locks must be released before package install"
|
||||
|
||||
assert_eq "" "$(read_sha "")" "a missing constants module must yield no SHA"
|
||||
# A failed later stage restores the safety snapshot rather than leaving user
|
||||
# configuration rewritten by package hooks.
|
||||
grep -Fq 'TX_ACTIVE=1' "$INSTALLER" || fail "transaction recovery guard missing"
|
||||
grep -Fq 'TX_COMMITTED=1' "$INSTALLER" || fail "transaction commit guard missing"
|
||||
grep -Fq 'restore_snapshot' "$INSTALLER" || fail "failed transaction must restore snapshot"
|
||||
grep -Fq 'chmod 0600 /etc/config/tachyon' "$INSTALLER" ||
|
||||
fail "restored Tachyon config must remain private"
|
||||
|
||||
# A rolled-back rebuild is only detectable by comparing SHAs, so the backend
|
||||
# install must refuse to accept a mere presence check when the SHA did not move.
|
||||
grep -Fq 'rolled the upgrade back' "$INSTALLER" ||
|
||||
fail "install.sh must fail loudly when the backend SHA did not move after install"
|
||||
|
||||
# An empty "before" SHA is not evidence of a fresh install: installed_backend_sha
|
||||
# rejects both the placeholder and "unknown", which is exactly what a stale build
|
||||
# on disk reports. Presence before the install is what separates an upgrade from a
|
||||
# first install, so the rollback check must consult it — otherwise a rolled-back
|
||||
# upgrade of an unstamped build reaches the "installed — continuing" branch and the
|
||||
# run marches on over a failure.
|
||||
sed -n '/^install_backend_package() {/,/^}/p' "$INSTALLER" >"$WORK_DIR/backend.sh"
|
||||
[ -s "$WORK_DIR/backend.sh" ] || fail "install.sh must define install_backend_package"
|
||||
grep -Fq 'backend_present_before' "$WORK_DIR/backend.sh" ||
|
||||
fail "install_backend_package must record whether tachyon was installed before the upgrade"
|
||||
grep -Fq 'still reports no commit SHA' "$WORK_DIR/backend.sh" ||
|
||||
fail "an upgrade that leaves the installed build without a SHA must fail, not warn and continue"
|
||||
|
||||
# 120s was below the time the package hooks legitimately need to stop and restart
|
||||
# the service; being killed there makes apk roll the transaction back.
|
||||
for pm in apk opkg; do
|
||||
if grep -nE "(apk_with_lock_retry|run_logged_timeout) \"$pm\" 120 $pm (add|install)" "$INSTALLER" >/dev/null; then
|
||||
fail "$pm package install must not keep the 120s ceiling that rolled upgrades back"
|
||||
fi
|
||||
done
|
||||
|
||||
# Bounding the hooks only helps the build being installed; during THIS upgrade the
|
||||
# package manager runs the prerm of whatever is already on disk, and every release
|
||||
# up to 1.2.66 shipped one with no timeout. It blocks on rc.common's `flock -w
|
||||
# 1000`, so the installer has to free that lock itself before invoking the package
|
||||
# manager — otherwise the pre-upgrade hook outlives apk's patience and the whole
|
||||
# transaction is rolled back.
|
||||
sed -n '/^function installer_release_init_lock() {/,/^}/p' "$INSTALLER" >"$WORK_DIR/lock.uc"
|
||||
[ -s "$WORK_DIR/lock.uc" ] ||
|
||||
fail "install.sh must define installer_release_init_lock to unwedge the old prerm hook"
|
||||
|
||||
for pattern in '99-tachyon-wan|flock 1000' '/etc/init.d/tachyon'; do
|
||||
grep -Fq "$pattern" "$WORK_DIR/lock.uc" ||
|
||||
fail "installer_release_init_lock must clear '$pattern' waiters"
|
||||
done
|
||||
|
||||
# Killing by name misses the actual holders. procd_lock() does `exec 1000>` on the
|
||||
# lock file and `flock 1000` with no -w, so every background spawn inheriting fd 1000
|
||||
# keeps the lock. On a live router the holders were watchdog workers, logread, the
|
||||
# zapret2 supervisors, nfqws2 and a curl — none of which match a tachyon name.
|
||||
grep -Fq '/fd/1000' "$WORK_DIR/lock.uc" ||
|
||||
fail "installer_release_init_lock must clear lock holders by descriptor, not only by name"
|
||||
grep -Fq 'procd_tachyon' "$WORK_DIR/lock.uc" ||
|
||||
fail "installer_release_init_lock must only kill holders of Tachyon's own procd lock"
|
||||
|
||||
# The installer runs as a child of /usr/bin/tachyon during an upgrade, so a sweep that
|
||||
# does not exclude its own ancestors kills the process performing the install.
|
||||
grep -Fq '/stat' "$WORK_DIR/lock.uc" ||
|
||||
fail "installer_release_init_lock must walk /proc/<pid>/stat to spare its own ancestors"
|
||||
|
||||
# ucode resolves names lexically and does not hoist, so a call placed above the
|
||||
# declaration silently becomes null and the lock is never released.
|
||||
lock_decl="$(grep -m 1 -n '^function installer_release_init_lock() {' "$INSTALLER" | cut -d: -f1)"
|
||||
lock_call="$(grep -m 1 -n 'installer_release_init_lock();' "$INSTALLER" | cut -d: -f1)"
|
||||
[ -n "$lock_decl" ] && [ -n "$lock_call" ] ||
|
||||
fail "installer_release_init_lock must be both declared and called"
|
||||
[ "$lock_decl" -lt "$lock_call" ] ||
|
||||
fail "installer_release_init_lock is called before its declaration (ucode does not hoist)"
|
||||
|
||||
# The lock has to be dropped as part of stopping the service, i.e. before any
|
||||
# package is handed to the package manager.
|
||||
sed -n '/^function installer_cleanup_legacy() {/,/^}/p' "$INSTALLER" >"$WORK_DIR/cleanup.uc"
|
||||
grep -Fq 'installer_release_init_lock();' "$WORK_DIR/cleanup.uc" ||
|
||||
fail "installer_cleanup_legacy must release the init lock before the packages are installed"
|
||||
# Successful completion is only printed after the runtime healthcheck and commit.
|
||||
awk '
|
||||
/^main\(\)/ { in_main=1 }
|
||||
in_main && /healthcheck/ && !health { health=NR }
|
||||
in_main && /commit_transaction/ && !commit { commit=NR }
|
||||
in_main && /installed successfully/ && !success { success=NR }
|
||||
END { exit !(health > 0 && commit > health && success > commit) }
|
||||
' "$INSTALLER" || fail "success must be gated by healthcheck and transaction commit"
|
||||
|
||||
printf 'PASS: installer rollback detection\n'
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
[ -r "$INSTALLER" ] || fail "install.sh is missing"
|
||||
sh -n "$INSTALLER" || fail "install.sh must be valid POSIX shell"
|
||||
|
||||
grep -Fq 'mkdir "$LOCK_DIR"' "$INSTALLER" || fail "installer lock must use atomic mkdir"
|
||||
grep -Fq 'Removing stale installer lock' "$INSTALLER" || fail "stale installer lock must be recoverable"
|
||||
|
||||
grep -Fq 'trap cleanup EXIT' "$INSTALLER" || fail "cleanup must run on EXIT"
|
||||
grep -Fq 'trap on_int INT' "$INSTALLER" || fail "INT handler missing"
|
||||
grep -Fq 'on_int() { exit 130; }' "$INSTALLER" || fail "INT must return 130"
|
||||
grep -Fq 'on_term() { exit 143; }' "$INSTALLER" || fail "TERM must return 143"
|
||||
if grep -Fq 'trap cleanup EXIT HUP INT TERM' "$INSTALLER"; then
|
||||
fail "cleanup-only signal trap can resume installation after a signal"
|
||||
fi
|
||||
|
||||
awk '
|
||||
/^main\(\)/ { in_main=1 }
|
||||
in_main && /sync_time/ && !sync { sync=NR }
|
||||
in_main && /ensure_bootstrap_ucode_runtime/ && !bootstrap { bootstrap=NR }
|
||||
END { exit !(sync > 0 && bootstrap > sync) }
|
||||
' "$INSTALLER" || fail "sync_time must happen before bootstrap package/network operations"
|
||||
|
||||
grep -Fq 'apk --wait "$APK_LOCK_WAIT_SECONDS"' "$INSTALLER" || fail "apk --wait support is required"
|
||||
grep -Fq 'diagnose_apk_lock' "$INSTALLER" || fail "APK lock diagnosis is required"
|
||||
if grep -Fq 'sanitize_apk_world' "$INSTALLER"; then
|
||||
fail "installer must not mutate /etc/apk/world as generic APK cleanup"
|
||||
fi
|
||||
|
||||
grep -Fq 'TACHYON_DOWNLOAD_BYTES=' "$INSTALLER" || fail "release download size accounting missing"
|
||||
grep -Fq 'free_kb /tmp' "$INSTALLER" || fail "/tmp free-space check missing"
|
||||
grep -Fq 'MIN_TMP_HEADROOM_KB' "$INSTALLER" || fail "/tmp safety headroom missing"
|
||||
|
||||
grep -Fq 'pkg_install_local_bundle "$@"' "$INSTALLER" || fail "bundle transaction helper is not used"
|
||||
grep -Fq 'apk-transaction' "$INSTALLER" || fail "APK transaction tag missing"
|
||||
grep -Fq 'opkg-transaction' "$INSTALLER" || fail "opkg transaction tag missing"
|
||||
|
||||
awk '
|
||||
/^main\(\)/ { in_main=1 }
|
||||
in_main && /download_release/ && !download { download=NR }
|
||||
in_main && /snapshot_state/ && !snapshot { snapshot=NR }
|
||||
in_main && /prepare_transaction/ && !prepare { prepare=NR }
|
||||
in_main && /install_core_transaction/ && !install { install=NR }
|
||||
in_main && /healthcheck/ && !health { health=NR }
|
||||
in_main && /commit_transaction/ && !commit { commit=NR }
|
||||
END { exit !(download > 0 && snapshot > download && prepare > snapshot && install > prepare && health > install && commit > health) }
|
||||
' "$INSTALLER" || fail "transaction phase ordering is unsafe"
|
||||
|
||||
grep -Fq 'restore_snapshot' "$INSTALLER" || fail "failed transaction must restore recovery snapshot"
|
||||
grep -Fq 'TX_COMMITTED' "$INSTALLER" || fail "transaction commit guard missing"
|
||||
|
||||
grep -Fq '/fd/1000' "$INSTALLER" || fail "procd fd1000 lock-holder cleanup missing"
|
||||
grep -Fq 'is_protected_pid' "$INSTALLER" || fail "installer must protect its own process ancestry"
|
||||
grep -Fq '99-tachyon-wan|flock 1000|/etc/init.d/tachyon' "$INSTALLER" || fail "legacy init-lock waiter cleanup missing"
|
||||
|
||||
for pattern in 'tachyon_' 'luci-app-tachyon_' 'luci-i18n-tachyon-ru_' 'sha256sums.txt'; do
|
||||
grep -Fq "$pattern" "$INSTALLER" || fail "release asset contract missing: $pattern"
|
||||
done
|
||||
grep -Fq 'Checksum mismatch' "$INSTALLER" || fail "checksum failure path missing"
|
||||
|
||||
if grep -qi 'jsdelivr' "$INSTALLER"; then
|
||||
fail "GitHub release assets must not use the jsDelivr Git-tree endpoint"
|
||||
fi
|
||||
|
||||
grep -Fq '/usr/bin/tachyon component_action sing_box "$_action"' "$INSTALLER" || fail "sing-box must be delegated to component_action"
|
||||
|
||||
export TACHYON_INSTALLER_TEST=1
|
||||
# shellcheck source=/dev/null
|
||||
. "$INSTALLER"
|
||||
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEST_TMP"' EXIT
|
||||
LOCK_DIR="$TEST_TMP/lock"
|
||||
acquire_lock || fail "first atomic lock acquisition failed"
|
||||
[ -f "$LOCK_DIR/pid" ] || fail "lock PID was not recorded"
|
||||
release_lock
|
||||
[ ! -e "$LOCK_DIR" ] || fail "lock was not released"
|
||||
|
||||
start="$(date +%s)"
|
||||
if run_with_deadline 1 sh -c 'sleep 5'; then
|
||||
fail "deadline watchdog accepted an over-deadline command"
|
||||
fi
|
||||
elapsed="$(( $(date +%s) - start ))"
|
||||
[ "$elapsed" -lt 4 ] || fail "deadline watchdog did not stop the command promptly"
|
||||
run_with_deadline 3 sh -c 'exit 0' || fail "deadline helper changed a successful exit status"
|
||||
|
||||
printf 'PASS: ultimate installer transaction contract\n'
|
||||
@@ -8,24 +8,17 @@ TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
||||
REAL_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
[ -r "$RESET_UC" ] || fail "service/reset.uc must own reset-to-defaults logic"
|
||||
[ -r "$TACHYON_BIN" ] || fail "tachyon entrypoint is missing"
|
||||
|
||||
grep -Fq 'reset_settings: [ "service/reset.uc", "reset-settings", 1 ]' "$TACHYON_BIN" ||
|
||||
fail "tachyon entrypoint must dispatch reset_settings to service/reset.uc"
|
||||
grep -Fq 'reset_settings' "$TACHYON_BIN" ||
|
||||
fail "tachyon entrypoint must list reset_settings in the command table"
|
||||
grep -Fq 'reset_settings [no-start]' "$TACHYON_BIN" ||
|
||||
fail "tachyon help must document reset_settings"
|
||||
|
||||
mkdir -p "$WORK_DIR/lib/defaults"
|
||||
cat >"$WORK_DIR/default-config" <<'EOF'
|
||||
@@ -39,15 +32,14 @@ cp "$WORK_DIR/default-config" "$WORK_DIR/lib/defaults/config"
|
||||
mkdir -p \
|
||||
"$WORK_DIR/persistent/subscription-cache" \
|
||||
"$WORK_DIR/persistent/rulesets" \
|
||||
"$WORK_DIR/persistent/tailscale/server-main"
|
||||
printf '%s\n' 'stale-cache' >"$WORK_DIR/persistent/subscription-cache/cache.json"
|
||||
printf '%s\n' 'stale-ruleset' >"$WORK_DIR/persistent/rulesets/community.srs"
|
||||
printf '%s\n' 'tailscale-identity' >"$WORK_DIR/persistent/tailscale/server-main/node.key"
|
||||
mkdir -p "$WORK_DIR/runtime" "$WORK_DIR/sing-box-tmp"
|
||||
printf '%s\n' '{"log":{}}' >"$WORK_DIR/sing-box-tmp/config.json"
|
||||
mkdir -p "$(dirname "$WORK_DIR/sing-box-config")"
|
||||
printf '%s\n' '{"log":{}}' >"$WORK_DIR/sing-box-config"
|
||||
printf '%s\n' 'custom-user-config' >"$WORK_DIR/config"
|
||||
"$WORK_DIR/persistent/tailscale/server-main" \
|
||||
"$WORK_DIR/runtime" \
|
||||
"$WORK_DIR/sing-box-tmp"
|
||||
printf '%s\n' stale >"$WORK_DIR/persistent/subscription-cache/cache.json"
|
||||
printf '%s\n' stale >"$WORK_DIR/persistent/rulesets/community.srs"
|
||||
printf '%s\n' identity >"$WORK_DIR/persistent/tailscale/server-main/node.key"
|
||||
printf '%s\n' '{}' >"$WORK_DIR/sing-box-config"
|
||||
printf '%s\n' custom >"$WORK_DIR/config"
|
||||
|
||||
: >"$WORK_DIR/bin.log"
|
||||
: >"$WORK_DIR/init.log"
|
||||
@@ -56,97 +48,48 @@ cat >"$WORK_DIR/fake-bin" <<'SH'
|
||||
printf '%s\n' "$*" >> "$TACHYON_RESET_BIN_LOG"
|
||||
exit 0
|
||||
SH
|
||||
chmod 0755 "$WORK_DIR/fake-bin"
|
||||
cat >"$WORK_DIR/fake-init" <<'SH'
|
||||
#!/usr/bin/env sh
|
||||
printf '%s\n' "$*" >> "$TACHYON_RESET_INIT_LOG"
|
||||
exit 0
|
||||
SH
|
||||
chmod 0755 "$WORK_DIR/fake-init"
|
||||
chmod 0755 "$WORK_DIR/fake-bin" "$WORK_DIR/fake-init"
|
||||
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" -L "$REAL_LIB" "$RESET_UC" reset-settings no-start >"$WORK_DIR/result.json"
|
||||
grep -Fq '"success":true' "$WORK_DIR/result.json" ||
|
||||
fail "reset-settings must report success"
|
||||
run_reset() {
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" -L "$REAL_LIB" "$RESET_UC" reset-settings "$@"
|
||||
}
|
||||
|
||||
grep -Fxq 'stop' "$WORK_DIR/bin.log" ||
|
||||
fail "reset must stop the service through the backend entrypoint"
|
||||
grep -Fxq 'stop' "$WORK_DIR/init.log" ||
|
||||
fail "reset must stop the service through init.d"
|
||||
if grep -Fxq 'start' "$WORK_DIR/init.log"; then
|
||||
fail "reset no-start must not start the service"
|
||||
fi
|
||||
|
||||
cmp -s "$WORK_DIR/config" "$WORK_DIR/default-config" ||
|
||||
fail "reset must restore the config file to factory defaults"
|
||||
# busybox-based images may lack stat(1); ucode's fs module is always present.
|
||||
run_reset no-start >"$WORK_DIR/result.json"
|
||||
grep -Fq '"success":true' "$WORK_DIR/result.json" || fail "reset-settings must report success"
|
||||
grep -Fxq 'stop' "$WORK_DIR/bin.log" || fail "reset must stop through backend entrypoint"
|
||||
grep -Fxq 'stop' "$WORK_DIR/init.log" || fail "reset must stop through init.d"
|
||||
if grep -Fxq 'start' "$WORK_DIR/init.log"; then fail "no-start reset must not start service"; fi
|
||||
cmp -s "$WORK_DIR/config" "$WORK_DIR/default-config" || fail "reset must restore factory config"
|
||||
[ "$(ucode -e "let fs = require('fs'); printf('%o', fs.stat('$WORK_DIR/config').mode & 511);")" = "600" ] ||
|
||||
fail "reset config must be chmod 600"
|
||||
[ ! -e "$WORK_DIR/persistent/subscription-cache" ] || fail "reset must clear subscription cache"
|
||||
[ ! -e "$WORK_DIR/persistent/rulesets" ] || fail "reset must clear ruleset cache"
|
||||
[ "$(cat "$WORK_DIR/persistent/tailscale/server-main/node.key")" = identity ] || fail "reset must preserve Tailscale identity"
|
||||
[ ! -e "$WORK_DIR/runtime" ] || fail "reset must clear runtime state"
|
||||
[ ! -e "$WORK_DIR/sing-box-tmp" ] || fail "reset must clear sing-box temporary state"
|
||||
[ ! -e "$WORK_DIR/sing-box-config" ] || fail "reset must remove generated sing-box config"
|
||||
|
||||
[ ! -e "$WORK_DIR/persistent/subscription-cache" ] ||
|
||||
fail "reset must remove stale subscription caches"
|
||||
[ ! -e "$WORK_DIR/persistent/rulesets" ] ||
|
||||
fail "reset must remove stale ruleset caches"
|
||||
[ "$(cat "$WORK_DIR/persistent/tailscale/server-main/node.key")" = 'tailscale-identity' ] ||
|
||||
fail "reset must preserve the Tailscale identity"
|
||||
[ ! -e "$WORK_DIR/runtime" ] ||
|
||||
fail "reset must remove the runtime state directory"
|
||||
[ ! -e "$WORK_DIR/sing-box-tmp" ] ||
|
||||
fail "reset must remove the sing-box temporary directory"
|
||||
[ ! -e "$WORK_DIR/sing-box-config" ] ||
|
||||
fail "reset must remove the sing-box config file"
|
||||
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" -L "$REAL_LIB" "$RESET_UC" reset-settings >"$WORK_DIR/result.json"
|
||||
grep -Fxq 'start' "$WORK_DIR/init.log" ||
|
||||
fail "reset without no-start must start the service"
|
||||
|
||||
TACHYON_LIB="$WORK_DIR/lib" \
|
||||
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
||||
TACHYON_SERVICE_INIT="$WORK_DIR/fake-init" \
|
||||
TACHYON_CONFIG_PATH="$WORK_DIR/config" \
|
||||
TACHYON_PERSISTENT_DIR="$WORK_DIR/persistent" \
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
TACHYON_SING_BOX_TMP_DIR="$WORK_DIR/sing-box-tmp" \
|
||||
TACHYON_SING_BOX_CONFIG_PATH="$WORK_DIR/sing-box-config" \
|
||||
TACHYON_RESET_BIN_LOG="$WORK_DIR/bin.log" \
|
||||
TACHYON_RESET_INIT_LOG="$WORK_DIR/init.log" \
|
||||
ucode -L "$WORK_DIR/lib" -L "$REAL_LIB" "$RESET_UC" bogus-mode 2>"$WORK_DIR/usage.err" &&
|
||||
fail "reset module must reject unknown modes"
|
||||
grep -Fq 'Usage' "$WORK_DIR/usage.err" ||
|
||||
fail "reset module must print usage for unknown modes"
|
||||
|
||||
grep -Fq 'reset_settings [no-start]' "$TACHYON_BIN" ||
|
||||
fail "tachyon help must document reset_settings"
|
||||
grep -Fq 'reset_settings' "$TACHYON_BIN" ||
|
||||
fail "tachyon failsafe dnsmasq restore must cover reset_settings"
|
||||
|
||||
# The installer must wipe leftover state on a fresh install but keep user
|
||||
# configuration on upgrades and legacy migrations.
|
||||
grep -Fq 'TACHYON_WAS_INSTALLED' "$INSTALLER" ||
|
||||
fail "installer must remember whether Tachyon was installed before"
|
||||
grep -Fq 'reset_settings", "no-start"' "$INSTALLER" ||
|
||||
fail "installer post-install must delegate fresh-install defaults to reset_settings"
|
||||
if grep -n -E 'reset_settings|TACHYON_WAS_INSTALLED' "$INSTALLER" | grep -qE 'rm -f |uci |sed -i '; then
|
||||
fail "installer must not own reset/state shell logic"
|
||||
# Installer v3 preserves configuration and delegates package initialization to
|
||||
# package hooks/runtime modules. It must never implement reset semantics itself.
|
||||
grep -Fq 'snapshot_state()' "$INSTALLER" || fail "installer must snapshot config before package changes"
|
||||
grep -Fq 'TACHYON_WAS_INSTALLED' "$INSTALLER" || fail "installer must remember previous install state"
|
||||
if grep -n -E '(^|[;&|[:space:]])uci[[:space:]]+-q|sed -i .*/etc/config/tachyon' "$INSTALLER" >/dev/null; then
|
||||
fail "installer shell must not own UCI reset/mutation logic"
|
||||
fi
|
||||
|
||||
printf 'reset settings contract checks passed\n'
|
||||
|
||||
@@ -2,52 +2,48 @@
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
# 1. Structural checks in CLI and backend ucode
|
||||
grep -Fq 'delete_section' "$ROOT_DIR/tachyon/files/usr/bin/tachyon" ||
|
||||
fail "tachyon CLI must declare delete_section command"
|
||||
|
||||
grep -Fq 'cascade_delete_section' "$ROOT_DIR/tachyon/files/usr/lib/config/connections.uc" ||
|
||||
fail "connections.uc must implement cascade_delete_section"
|
||||
|
||||
grep -Fq 'cli_delete_section' "$ROOT_DIR/tachyon/files/usr/lib/config/connections.uc" ||
|
||||
fail "connections.uc must export cli_delete_section"
|
||||
|
||||
grep -Fq 'priority_group' "$ROOT_DIR/tachyon/files/usr/lib/config/migration.uc" ||
|
||||
fail "migration.uc CHILD_ITEM_TYPES must include priority_group"
|
||||
|
||||
grep -Fq 'priority_level' "$ROOT_DIR/tachyon/files/usr/lib/config/migration.uc" ||
|
||||
fail "migration.uc CHILD_ITEM_TYPES must include priority_level"
|
||||
|
||||
# 2. Structural checks in LuCI JS view
|
||||
grep -Fq 'cascadeDeleteSection' "$ROOT_DIR/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js" ||
|
||||
fail "section.js must implement cascadeDeleteSection"
|
||||
|
||||
grep -Fq 'cascadeDeleteSection(section_id)' "$ROOT_DIR/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js" ||
|
||||
fail "section.js remove handler must invoke cascadeDeleteSection"
|
||||
|
||||
grep -Fq 'cascadeDeleteSection' "$ROOT_DIR/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/tachyon.js" ||
|
||||
fail "tachyon.js must invoke cascadeDeleteSection on section remove/add"
|
||||
|
||||
# 3. Installer safety checks
|
||||
grep -Fq 'backup_existing_config' "$ROOT_DIR/install.sh" ||
|
||||
fail "install.sh must invoke backup_existing_config"
|
||||
# 3. Installer safety contract: never mutate user configuration before it is
|
||||
# snapshotted, and verify the resulting runtime before declaring success.
|
||||
INSTALLER="$ROOT_DIR/install.sh"
|
||||
grep -Fq 'snapshot_state()' "$INSTALLER" ||
|
||||
fail "install.sh must provide a recovery snapshot stage"
|
||||
grep -Fq '/etc/config/tachyon' "$INSTALLER" ||
|
||||
fail "installer snapshot/healthcheck must cover /etc/config/tachyon"
|
||||
grep -Fq 'chmod 0600 /etc/config/tachyon' "$INSTALLER" ||
|
||||
fail "installer must keep Tachyon config private"
|
||||
grep -Fq 'healthcheck()' "$INSTALLER" ||
|
||||
fail "installer must verify the installed runtime"
|
||||
grep -Fq '/usr/share/luci/menu.d/luci-app-tachyon.json' "$INSTALLER" ||
|
||||
fail "installer healthcheck must verify the LuCI menu"
|
||||
|
||||
grep -Fq 'path_exists("/etc/config/tachyon")' "$ROOT_DIR/install.sh" ||
|
||||
fail "install.sh must check /etc/config/tachyon before considering installation fresh"
|
||||
|
||||
grep -Fq 'path_executable(INSTALLER_TACHYON_BIN)' "$ROOT_DIR/install.sh" ||
|
||||
fail "install.sh must check INSTALLER_TACHYON_BIN before considering installation fresh"
|
||||
awk '
|
||||
/^main\(\)/ { in_main=1 }
|
||||
in_main && /snapshot_state/ && !snapshot { snapshot=NR }
|
||||
in_main && /install_core_transaction/ && !install { install=NR }
|
||||
END { exit !(snapshot > 0 && install > snapshot) }
|
||||
' "$INSTALLER" || fail "installer must snapshot configuration before package hooks run"
|
||||
|
||||
printf 'section cascade deletion and installer checks passed\n'
|
||||
|
||||
Reference in New Issue
Block a user