#!/usr/bin/env bash set -eo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib" INITD_UC="$TACHYON_LIB/service/initd.uc" STATE_UC="$TACHYON_LIB/service/state.uc" INITD="$ROOT_DIR/tachyon/files/etc/init.d/tachyon" WORK_DIR="$(mktemp -d)" cleanup() { rm -rf "$WORK_DIR" } trap cleanup EXIT fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 } initd_ucode() { ucode -L "$TACHYON_LIB" "$INITD_UC" "$@" } config_file="$WORK_DIR/tachyon" guard_file="$WORK_DIR/internal-config-change" sync_file="$WORK_DIR/service-triggers.sync" printf '%s\n' "config-version=1" >"$config_file" config_hash="$(md5sum "$config_file" | awk '{print $1}')" printf '%s\n%s\n' 1000 "$config_hash" >"$guard_file" initd_ucode initd-guard-matches-current-config "$guard_file" "$config_file" 1010 >/dev/null || fail "valid internal config guard should match current config" if initd_ucode initd-guard-matches-current-config "$guard_file" "$config_file" 1031 >/dev/null 2>&1; then fail "stale internal config guard should not match" fi printf '%s\n%s\n' 1000 "$config_hash" >"$guard_file" initd_ucode initd-should-skip-internal-config-reload on_config_change "$guard_file" "$config_file" on_config_change 1010 >/dev/null || fail "matching internal config guard should skip config-change reload" [ ! -e "$guard_file" ] || fail "matching internal config guard should be consumed" printf '%s\n%s\n' 1000 "$config_hash" >"$guard_file" if initd_ucode initd-should-skip-internal-config-reload manual "$guard_file" "$config_file" on_config_change 1010 >/dev/null 2>&1; then fail "manual reload should not be skipped by internal config guard" fi [ -e "$guard_file" ] || fail "non-config-change reason should not consume internal config guard" printf '%s\n%s\n' 1000 "bad-hash" >"$guard_file" if initd_ucode initd-should-skip-internal-config-reload on_config_change "$guard_file" "$config_file" on_config_change 1010 >/dev/null 2>&1; then fail "mismatched internal config guard should not skip reload" fi [ ! -e "$guard_file" ] || fail "mismatched internal config guard should be consumed" printf '%s\n' 1 >"$sync_file" initd_ucode initd-service-trigger-sync-requested "$sync_file" >/dev/null || fail "service trigger sync marker should request sync" [ ! -e "$sync_file" ] || fail "service trigger sync marker should be consumed" printf '%s\n' 0 >"$sync_file" if initd_ucode initd-service-trigger-sync-requested "$sync_file" >/dev/null 2>&1; then fail "service trigger sync marker with value 0 should not request sync" fi [ ! -e "$sync_file" ] || fail "service trigger sync marker value 0 should be consumed" initd_ucode initd-should-ignore-config-change-reload on_config_change on_config_change 0 0 >/dev/null || fail "disabled stopped service should ignore config-change reload" if initd_ucode initd-should-ignore-config-change-reload manual on_config_change 0 0 >/dev/null 2>&1; then fail "manual reload should not be ignored" fi if initd_ucode initd-should-ignore-config-change-reload on_config_change on_config_change 1 0 >/dev/null 2>&1; then fail "running service should not ignore config-change reload" fi initd_ucode initd-should-ignore-config-change-reload on_config_change on_config_change 0 1 >/dev/null || fail "stopped service should ignore config-change reload even when autostart is enabled" initd_ucode initd-should-queue-config-change-reload on_config_change on_config_change 0 start >/dev/null || fail "config-change reload during service start should be queued" if initd_ucode initd-should-queue-config-change-reload on_config_change on_config_change 0 "" >/dev/null 2>&1; then fail "stopped service without active service action should not queue config-change reload" fi if initd_ucode initd-should-queue-config-change-reload on_config_change on_config_change 1 reload >/dev/null 2>&1; then fail "running service should not use stopped-service queue path" fi pending_file="$WORK_DIR/reload.pending" if TACHYON_PENDING_RELOAD_FILE="$pending_file" \ TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$guard_file" \ TACHYON_CONFIG_FILE="$config_file" \ initd_ucode reload-begin-fixture on_config_change 123 0 1 start >/dev/null 2>&1; then fail "queued config-change reload should not run immediately while service is starting" fi [ -f "$pending_file" ] || fail "config-change reload during service start should create pending reload" grep -Fq 'reason=on_config_change' "$pending_file" || fail "pending reload should preserve config-change reason" rm -f "$pending_file" if TACHYON_PENDING_RELOAD_FILE="$pending_file" \ TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$guard_file" \ TACHYON_CONFIG_FILE="$config_file" \ initd_ucode reload-begin-fixture pending 123 1 1 start >/dev/null 2>&1; then fail "pending reload should not run while another service action is active" fi [ -f "$pending_file" ] || fail "pending reload should stay pending while another service action is active" grep -Fq 'reason=pending' "$pending_file" || fail "pending reload should preserve pending reason while delayed by active action" start_retry_file="$WORK_DIR/start.retry" if initd_ucode start-retry-pending "$start_retry_file" >/dev/null 2>&1; then fail "missing start retry marker should not be pending" fi initd_ucode mark-start-retry "$start_retry_file" start_failed >/dev/null || fail "failed start should create WAN retry marker" initd_ucode start-retry-pending "$start_retry_file" >/dev/null || fail "created start retry marker should be pending" grep -Fq 'reason=start_failed' "$start_retry_file" || fail "start retry marker should preserve failure reason" initd_ucode clear-start-retry "$start_retry_file" if initd_ucode start-retry-pending "$start_retry_file" >/dev/null 2>&1; then fail "cleared start retry marker should not be pending" fi retry_pid_file="$WORK_DIR/start-retry.pid" retry_call_file="$WORK_DIR/start-retry.called" retry_service="$WORK_DIR/tachyon-init" cat >"$retry_service" <>"$retry_call_file" EOF chmod +x "$retry_service" TACHYON_SERVICE_INIT="$retry_service" \ TACHYON_START_RETRY_PID_FILE="$retry_pid_file" \ TACHYON_START_RETRY_DELAY_SECONDS=1 \ initd_ucode schedule-start-retry >/dev/null || fail "failed start should schedule an automatic retry" [ -s "$retry_pid_file" ] || fail "scheduled automatic retry should record its worker pid" for _ in 1 2 3 4 5; do [ -s "$retry_call_file" ] && break sleep 1 done grep -Fxq 'retry_start_on_wan_up' "$retry_call_file" || fail "scheduled automatic retry should call the marker-gated retry action" [ ! -e "$retry_pid_file" ] || fail "automatic retry worker should clear its pid before retrying" [ "$(initd_ucode retry-start-on-wan-up-action 1 1 1)" = "skip_running" ] || fail "WAN retry should skip when runtime is already running" [ "$(initd_ucode retry-start-on-wan-up-action 0 0 1)" = "skip_disabled" ] || fail "WAN retry should skip when autostart is disabled" [ "$(initd_ucode retry-start-on-wan-up-action 0 1 0)" = "skip_no_retry" ] || fail "WAN retry should skip stopped service without failed-start marker" [ "$(initd_ucode retry-start-on-wan-up-action 0 1 1)" = "restart" ] || fail "WAN retry should restart only after a recorded failed start" initd_ucode initd-should-restore-dnsmasq-on-start-fixture "" 0 >/dev/null || fail "unclean shutdown should request dnsmasq restore on start" if initd_ucode initd-should-restore-dnsmasq-on-start-fixture triggered 0 >/dev/null 2>&1; then fail "triggered start should not restore dnsmasq" fi if initd_ucode initd-should-restore-dnsmasq-on-start-fixture "" 1 >/dev/null 2>&1; then fail "clean shutdown should not restore dnsmasq" fi start_plan="$(initd_ucode start-plan-fixture "" 0 1 "wan vpn0" 123 1)" case "$start_plan" in *"INITD_BIN_OK='1'"* ) ;; *) fail "start-plan must expose executable binary state" ;; esac case "$start_plan" in *"INITD_BADWAN_NETDEV='wan vpn0'"* ) ;; *) fail "start-plan must derive Bad WAN netdev from UCI settings" ;; esac start_plan_triggered="$(initd_ucode start-plan-fixture triggered 0 1 "wan vpn0" 123 1)" case "$start_plan_triggered" in *"INITD_BADWAN_NETDEV='wan vpn0'"* ) ;; *) fail "triggered start should keep Bad WAN netdev plan" ;; esac cat >"$WORK_DIR/settings.json" <<'JSON' { "settings": { "enable_badwan_interface_monitoring": "1", "badwan_monitored_interfaces": [ "wan", "vpn0", "wwan" ], "badwan_reload_delay": "3500" } } JSON trigger_plan="$(initd_ucode trigger-plan-fixture "$WORK_DIR/settings.json")" case "$trigger_plan" in *"delay 3500"* ) ;; *) fail "trigger-plan must emit Bad WAN reload delay" ;; esac case "$trigger_plan" in *"interface interface.*.up vpn0"* ) ;; *) fail "trigger-plan must include non-wan Bad WAN interface triggers" ;; esac case "$trigger_plan" in *"interface interface.*.up wan "*"retry_start_on_wan_up"* ) ;; *) fail "trigger-plan must keep WAN cold-start retry trigger" ;; esac for legacy in \ service_trigger_sync_requested \ current_config_hash \ internal_config_guard_matches_current_config \ should_skip_internal_config_reload \ should_restore_dnsmasq_on_start; do if grep -Fq "$legacy" "$INITD"; then fail "init.d must not keep shell decision function $legacy" fi done grep -Fq 'service/initd.uc' "$INITD" || fail "init.d must delegate init.d decisions to service/initd.uc" grep -Fq 'start-service' "$INITD" || fail "init.d must start through a complete ucode start entrypoint" grep -Fq 'mode == "start-service"' "$INITD_UC" || fail "service/initd.uc must expose the complete start entrypoint" grep -Fq 'stop-service' "$INITD" || fail "init.d must stop through a complete ucode stop entrypoint" grep -Fq 'mode == "stop-service"' "$INITD_UC" || fail "service/initd.uc must expose the complete stop entrypoint" grep -Fq 'reload-service' "$INITD" || fail "init.d must reload through a complete ucode reload entrypoint" grep -Fq 'mode == "reload-service"' "$INITD_UC" || fail "service/initd.uc must expose the complete reload entrypoint" grep -Fq 'status-service' "$INITD" || fail "init.d status must delegate to service/initd.uc" grep -Fq 'mode == "status-service"' "$INITD_UC" || fail "service/initd.uc must expose the complete status entrypoint" grep -Fq 'retry-start-on-wan-up' "$INITD" || fail "init.d WAN retry must delegate to service/initd.uc" grep -Fq 'mode == "retry-start-on-wan-up"' "$INITD_UC" || fail "service/initd.uc must expose the complete WAN retry entrypoint" grep -Fq 'mode == "retry-start-on-wan-up-action"' "$INITD_UC" || fail "service/initd.uc must expose the WAN retry decision fixture" grep -Fq 'schedule_start_retry(START_RETRY_PID_FILE, START_RETRY_DELAY_SECONDS)' "$INITD_UC" || fail "failed service start must schedule a retry even if WAN is already up" grep -Fq 'start_retry_pending(START_RETRY_FILE)' "$INITD_UC" || fail "WAN retry must be gated by a failed-start marker" service_enabled_line="$(awk '/function service_is_enabled\(\)/ { print NR; exit }' "$INITD_UC")" retry_start_line="$(awk '/function retry_start_on_wan_up\(/ { print NR; exit }' "$INITD_UC")" [ -n "$service_enabled_line" ] && [ -n "$retry_start_line" ] && [ "$service_enabled_line" -lt "$retry_start_line" ] || fail "service_is_enabled must be declared before retry_start_on_wan_up for ucode runtime calls" grep -Fq 'trigger-plan' "$INITD" || fail "init.d must get procd trigger plan from service/initd.uc" if grep -Fq 'eval ' "$INITD"; then fail "init.d must not eval ucode-generated shell plans" fi if grep -Fq 'runtime_is_running' "$INITD"; then fail "init.d must not keep shell runtime status decisions" fi if grep -E -n 'TACHYON_(CONFIG_FILE|RELOAD_LOCK_DIR|RUNTIME_STATE_DIR|PENDING_RELOAD_FILE|SERVICE_TRIGGER_SYNC_FILE|INTERNAL_CONFIG_TRIGGER_GUARD|CONFIG_CHANGE_REASON|BIN|SERVICE_INIT)=' "$INITD" >/dev/null 2>&1; then fail "init.d must not pass internal orchestration paths through shell env" fi if grep -n -E 'require\("uci"\)\.cursor|uci -q|uci", "-q"' "$INITD_UC" >/dev/null 2>&1; then fail "service/initd.uc must use core.uci instead of direct UCI cursor/CLI access" fi if grep -E -n '(^|[[:space:]])config_(load|get|get_bool|set)[[:space:]]' "$INITD" >/dev/null 2>&1; then fail "init.d must not read UCI directly" fi if grep -Fq 'TACHYON_STATE_UC' "$INITD" || grep -Fq 'TACHYON_UI_UC' "$INITD" || grep -Fq 'TACHYON_STATUS_UC' "$INITD"; then fail "init.d must not orchestrate state/UI/status modules directly" fi if grep -Fq 'procd_set_param command "$TACHYON_BIN" start' "$INITD"; then fail "init.d must not hand the one-shot start operation to procd as a daemon" fi if grep -Fq 'initd-should-' "$STATE_UC"; then fail "service/state.uc must not keep init.d decision owner modes" fi printf 'init.d state checks passed\n'