#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" UPDATER="${SCRIPT_DIR}/update.sh" ENABLED="false" MANIFEST="${SCRIPT_DIR}/manifest.production.json" SOURCE_POLICY="" BIN_ROOT="/opt/selective-vpn/bin" TARGET="" COMPONENTS_RAW="" ROLLOUT_STAGE="stable" COHORT_ID="" FORCE_ROLLOUT=0 SIGNATURE_MODE="" MIN_INTERVAL_SEC=21600 JITTER_SEC=0 FORCE_NOW=0 DRY_RUN=0 STATE_DIR="" LOCK_FILE="" usage() { cat <<'EOF' Usage: auto_update.sh [--enabled true|false] [--manifest PATH] [--source-policy PATH] [--bin-root DIR] [--target OS-ARCH] [--component NAME[,NAME...]] [--rollout-stage stable|canary|any] [--cohort-id 0..99] [--signature-mode off|optional|required] [--min-interval-sec N] [--jitter-sec N] [--state-dir DIR] [--lock-file PATH] [--force-rollout] [--force-now] [--dry-run] Description: Opt-in scheduler wrapper around update.sh. Default behavior is disabled; when enabled, it enforces interval gating and lock. Examples: ./scripts/transport-packaging/auto_update.sh --enabled true ./scripts/transport-packaging/auto_update.sh --enabled true --component singbox,phoenix --min-interval-sec 3600 EOF } require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "[transport-auto-update] missing required command: ${cmd}" >&2 exit 1 fi } bool_normalize() { local raw raw="$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)" case "$raw" in 1|true|yes|on) echo "true" ;; 0|false|no|off|"") echo "false" ;; *) echo "[transport-auto-update] invalid boolean value: ${1}" >&2 exit 1 ;; esac } int_validate_non_negative() { local name="$1" local value="$2" if [[ ! "$value" =~ ^[0-9]+$ ]]; then echo "[transport-auto-update] ${name} must be a non-negative integer" >&2 exit 1 fi } while [[ $# -gt 0 ]]; do case "$1" in --enabled) ENABLED="${2:-}" shift 2 ;; --manifest) MANIFEST="${2:-}" shift 2 ;; --source-policy) SOURCE_POLICY="${2:-}" shift 2 ;; --bin-root) BIN_ROOT="${2:-}" shift 2 ;; --target) TARGET="${2:-}" shift 2 ;; --component) COMPONENTS_RAW="${2:-}" shift 2 ;; --rollout-stage) ROLLOUT_STAGE="${2:-}" shift 2 ;; --cohort-id) COHORT_ID="${2:-}" shift 2 ;; --signature-mode) SIGNATURE_MODE="${2:-}" shift 2 ;; --min-interval-sec) MIN_INTERVAL_SEC="${2:-}" shift 2 ;; --jitter-sec) JITTER_SEC="${2:-}" shift 2 ;; --state-dir) STATE_DIR="${2:-}" shift 2 ;; --lock-file) LOCK_FILE="${2:-}" shift 2 ;; --force-rollout) FORCE_ROLLOUT=1 shift ;; --force-now) FORCE_NOW=1 shift ;; --dry-run) DRY_RUN=1 shift ;; -h|--help) usage exit 0 ;; *) echo "[transport-auto-update] unknown argument: $1" >&2 usage >&2 exit 1 ;; esac done ENABLED="$(bool_normalize "$ENABLED")" int_validate_non_negative "min-interval-sec" "$MIN_INTERVAL_SEC" int_validate_non_negative "jitter-sec" "$JITTER_SEC" if [[ "$ENABLED" != "true" ]]; then echo "[transport-auto-update] disabled (opt-in mode)" exit 0 fi if [[ ! -x "$UPDATER" ]]; then echo "[transport-auto-update] updater not found or not executable: ${UPDATER}" >&2 exit 1 fi if [[ ! -f "$MANIFEST" ]]; then echo "[transport-auto-update] manifest not found: ${MANIFEST}" >&2 exit 1 fi if [[ -n "$SOURCE_POLICY" && ! -f "$SOURCE_POLICY" ]]; then echo "[transport-auto-update] source policy not found: ${SOURCE_POLICY}" >&2 exit 1 fi require_cmd flock require_cmd date if [[ -z "$STATE_DIR" ]]; then STATE_DIR="${BIN_ROOT}/.packaging/auto-update" fi mkdir -p "$STATE_DIR" if [[ -z "$LOCK_FILE" ]]; then LOCK_FILE="${STATE_DIR}/auto-update.lock" fi last_run_file="${STATE_DIR}/last_run_epoch" last_success_file="${STATE_DIR}/last_success_epoch" last_error_file="${STATE_DIR}/last_error" echo "[transport-auto-update] enabled=true" echo "[transport-auto-update] manifest=${MANIFEST}" echo "[transport-auto-update] state_dir=${STATE_DIR}" echo "[transport-auto-update] min_interval_sec=${MIN_INTERVAL_SEC}" if [[ -n "$COMPONENTS_RAW" ]]; then echo "[transport-auto-update] components=${COMPONENTS_RAW}" fi exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "[transport-auto-update] skip: another auto-update process is running" exit 0 fi now_epoch="$(date +%s)" last_run_epoch=0 if [[ -f "$last_run_file" ]]; then last_run_epoch="$(cat "$last_run_file" 2>/dev/null || echo 0)" [[ "$last_run_epoch" =~ ^[0-9]+$ ]] || last_run_epoch=0 fi elapsed=$((now_epoch - last_run_epoch)) if [[ "$FORCE_NOW" -ne 1 && "$elapsed" -lt "$MIN_INTERVAL_SEC" ]]; then echo "[transport-auto-update] skip: interval gate (elapsed=${elapsed}s < ${MIN_INTERVAL_SEC}s)" exit 0 fi if [[ "$JITTER_SEC" -gt 0 && "$FORCE_NOW" -ne 1 ]]; then jitter=$((RANDOM % (JITTER_SEC + 1))) if [[ "$jitter" -gt 0 ]]; then echo "[transport-auto-update] jitter sleep: ${jitter}s" sleep "$jitter" fi fi cmd=("$UPDATER" "--manifest" "$MANIFEST" "--bin-root" "$BIN_ROOT" "--rollout-stage" "$ROLLOUT_STAGE") if [[ -n "$SOURCE_POLICY" ]]; then cmd+=("--source-policy" "$SOURCE_POLICY") fi if [[ -n "$TARGET" ]]; then cmd+=("--target" "$TARGET") fi if [[ -n "$COMPONENTS_RAW" ]]; then cmd+=("--component" "$COMPONENTS_RAW") fi if [[ -n "$COHORT_ID" ]]; then cmd+=("--cohort-id" "$COHORT_ID") fi if [[ -n "$SIGNATURE_MODE" ]]; then cmd+=("--signature-mode" "$SIGNATURE_MODE") fi if [[ "$FORCE_ROLLOUT" -eq 1 ]]; then cmd+=("--force-rollout") fi if [[ "$DRY_RUN" -eq 1 ]]; then cmd+=("--dry-run") fi echo "[transport-auto-update] run: ${cmd[*]}" if "${cmd[@]}"; then date +%s >"$last_run_file" date +%s >"$last_success_file" : >"$last_error_file" echo "[transport-auto-update] success" exit 0 fi rc=$? date +%s >"$last_run_file" { echo "ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "exit_code=${rc}" echo "cmd=${cmd[*]}" } >"$last_error_file" echo "[transport-auto-update] failed rc=${rc}" >&2 exit "$rc"