#!/usr/bin/env bash set -eo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon" TACHYON_INIT="$ROOT_DIR/tachyon/files/etc/init.d/tachyon" CLI_UC="$TACHYON_BIN" LIFECYCLE_UC="$ROOT_DIR/tachyon/files/usr/lib/service/lifecycle.uc" INITD_UC="$ROOT_DIR/tachyon/files/usr/lib/service/initd.uc" STATE_UC="$ROOT_DIR/tachyon/files/usr/lib/service/state.uc" UI_UC="$ROOT_DIR/tachyon/files/usr/lib/service/ui.uc" UPDATES_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updates.uc" SUBSCRIPTION_CACHE_UC="$ROOT_DIR/tachyon/files/usr/lib/subscription/cache.uc" NFQUEUE_RUNTIME_UC="$ROOT_DIR/tachyon/files/usr/lib/providers/nfqueue/runtime.uc" BYEDPI_RUNTIME_UC="$ROOT_DIR/tachyon/files/usr/lib/providers/byedpi/runtime.uc" PRIORITY_UC="$ROOT_DIR/tachyon/files/usr/lib/singbox/priority.uc" fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 } require_pattern() { local pattern="$1" local label="$2" grep -Fq "$pattern" "$LIFECYCLE_UC" || fail "$label" } reject_pattern() { local pattern="$1" local label="$2" ! grep -Fq "$pattern" "$TACHYON_BIN" || fail "$label" } require_file_pattern() { local file="$1" local pattern="$2" local label="$3" grep -Fq "$pattern" "$file" || fail "$label" } [ -r "$TACHYON_BIN" ] || fail "tachyon binary source is missing" [ -r "$TACHYON_BIN" ] || fail "tachyon entrypoint is missing" [ -r "$LIFECYCLE_UC" ] || fail "service/lifecycle.uc is missing" grep -Fq '#!/usr/bin/ucode' "$TACHYON_BIN" || fail "tachyon entrypoint must be a direct ucode executable" grep -Fq 'service/lifecycle.uc' "$CLI_UC" || fail "service/cli.uc must dispatch startup through service/lifecycle.uc" reject_pattern "trap " \ "tachyon shell entrypoint must not own startup fail-safe traps" reject_pattern "clear_startup_failsafe_trap" \ "tachyon shell entrypoint must not keep old trap helper" require_pattern 'module_background(UPDATES_UC, [ "list-update" ])' \ "startup list_update background job must be owned by service/lifecycle.uc" require_pattern 'module_background(DIAGNOSTICS_UC, [ "get-system-info" ])' \ "startup system-info background job must be owned by service/lifecycle.uc" require_pattern 'startup_config_fingerprint = external_config_fingerprint();' \ "startup must snapshot runtime-relevant config after validation" require_pattern 'mark_pending_reload_if_config_changed(startup_config_fingerprint, "config_changed_during_start")' \ "startup must queue reload when config changes while service is starting" awk ' /function restart_runtime_for_reload\(\)/ { in_restart = 1 } in_restart && /status = start_impl\(\);/ { saw_start_impl = 1 } in_restart && /dnsmasq_configure\(true\)/ { duplicated_dns = 1 } in_restart && /write-current-reload-state-clean/ { duplicated_state = 1 } in_restart && /DNS_FAILOVER_UC.*start-runtime/ { duplicated_failover = 1 } in_restart && /^}/ { done = 1; exit } END { exit done && saw_start_impl && !duplicated_dns && !duplicated_state && !duplicated_failover ? 0 : 1 } ' "$LIFECYCLE_UC" || fail "reload fallback restart must reuse the complete start_impl path without duplicating finalization" require_pattern 'function abort_reload(status, runtime_changed)' \ "reload failures must share one cleanup decision owner" require_pattern 'return abort_reload(status, true);' \ "reload failures after runtime mutation must clean the partial runtime" cleanup_function_line="$(awk '/function cleanup_failed_runtime\(\)/ { print NR; exit }' "$LIFECYCLE_UC")" abort_function_line="$(awk '/function abort_reload\(status, runtime_changed\)/ { print NR; exit }' "$LIFECYCLE_UC")" [ -n "$cleanup_function_line" ] && [ -n "$abort_function_line" ] && [ "$cleanup_function_line" -lt "$abort_function_line" ] || fail "cleanup_failed_runtime must be declared before abort_reload for ucode runtime calls" require_pattern 'pending_reload_log_context(reason)' \ "startup queued reload must be visible in logs" require_pattern 'return "current reload";' \ "reload-time queued reload must be distinguishable in logs" # Background spawns must route through the shared helpers, which close the # descriptors the child would otherwise inherit — the procd lock among them. # # These assertions used to require a literal `1000>&-` in each of these files, # which is why that string spread to 22 sites in 13 modules. It closed nothing: # no descriptor 1000 is ever opened, and every real one falls in 6..89. The test # pinned the appearance of the fix and would have gone on passing forever with # the leak intact. What matters is that each site delegates to a helper that # closes what is actually open; tests/background_spawn_fds.sh proves the helper # does that at runtime. require_file_pattern "$LIFECYCLE_UC" 'common.background_command(module_command(module_path, args))' \ "service/lifecycle.uc background modules must close inherited descriptors via the shared helper" require_file_pattern "$INITD_UC" 'background_command(shell_quote(init_script) + " reload pending")' \ "service/initd.uc pending reload worker must close inherited descriptors" require_file_pattern "$STATE_UC" 'background_command(shell_quote(init_script) + " reload pending")' \ "service/state.uc pending reload worker must close inherited descriptors" for background_owner in "$UI_UC" "$UPDATES_UC" "$SUBSCRIPTION_CACHE_UC" \ "$NFQUEUE_RUNTIME_UC" "$BYEDPI_RUNTIME_UC" "$PRIORITY_UC"; do require_file_pattern "$background_owner" 'common.background_command_with_pid(' \ "$(basename "$background_owner") workers must spawn through background_command_with_pid" done # initd.uc and state.uc sit on the early-boot path and carry their own copies of # the helper rather than pulling in core.common, so their prologue is checked # separately. Both copies and the shared one must ask the shell what it can close # before closing anything — see core/common.uc for why a fixed range is fatal. for prologue_owner in "$ROOT_DIR/tachyon/files/usr/lib/core/common.uc" "$INITD_UC" "$STATE_UC"; do require_file_pattern "$prologue_owner" 'eval \"exec 10<&-\"' \ "$(basename "$prologue_owner") descriptor prologue must probe the shell before closing high descriptors" done require_file_pattern "$TACHYON_INIT" 'TACHYON_LAST_START_STATUS="$?"' \ "init.d start_service must preserve backend start status for rc.common" require_file_pattern "$TACHYON_INIT" 'service_started()' \ "init.d must return preserved start status through rc.common service_started hook" awk ' /function cleanup_failed_runtime\(\)/ { in_cleanup = 1 } in_cleanup && /stop_main\(\);/ { saw_stop = 1 } in_cleanup && /dnsmasq_restore_fail_safe\(\);/ && saw_stop { saw_dns = 1 } in_cleanup && /mark_runtime_stopped_clean\(\);/ && saw_dns { ok = 1; exit } in_cleanup && /^}/ { exit } END { exit ok ? 0 : 1 } ' "$LIFECYCLE_UC" || fail "failed runtime cleanup must stop partial runtime, roll back DNS, and mark clean shutdown" grep -Fq 'cleanup_failed_runtime();' "$LIFECYCLE_UC" || fail "service failure paths must use failed runtime cleanup" require_pattern "tproxy-marking-rule4-present" \ "service stop must use direct ucode IPv4 tproxy marking rule check" require_pattern "tproxy-marking-rule6-present" \ "service stop must use direct ucode IPv6 tproxy marking rule check" require_pattern "tproxy-route4-present" \ "service stop must use direct ucode IPv4 tproxy route check" require_pattern "tproxy-route6-present" \ "service stop must use direct ucode IPv6 tproxy route check" grep -Fq 'require("core.uci")' "$LIFECYCLE_UC" || fail "service/lifecycle.uc must use core.uci for lifecycle UCI state" if grep -n -E 'uci -q|require\("uci"\)\.cursor|function uci_|uci_set\(' "$LIFECYCLE_UC" >/dev/null 2>&1; then fail "service/lifecycle.uc must not own direct UCI CLI/cursor helpers" fi reject_pattern "tproxy_marking_rule_present" \ "service stop must not call the removed shell tproxy marking helper" reject_pattern "tproxy_route_present" \ "service stop must not call the removed shell tproxy route helper" reject_pattern "has_tproxy_marking_rule" \ "service stop must not call the removed shell tproxy marking helper" reject_pattern "has_tproxy_route" \ "service stop must not call the removed shell tproxy route helper" printf 'service start trap checks passed\n'