diff --git a/install.sh b/install.sh index 305074c0..874a234a 100755 --- a/install.sh +++ b/install.sh @@ -10,10 +10,6 @@ set -e # Ensure sane PATH (Entware paths first for wget-ssl/curl from /opt/bin) export PATH="/opt/bin:/opt/sbin:$HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:$PATH" - - -# ======== lib/colors.sh ======== -# Terminal colors (disabled when not a TTY) if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' @@ -27,11 +23,6 @@ if [ -t 1 ]; then else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' NC='' fi - - -# ======== lib/log.sh ======== -# Logging functions - QUIET_MODE=0 log_info() { @@ -63,17 +54,10 @@ log_detail() { printf " ${CYAN}%-22s${NC}: %b\n" "$1" "$2" >&2 } -# Print a separator line log_sep() { [ "$QUIET_MODE" -eq 1 ] && return printf "${DIM}%s${NC}\n" "─────────────────────────────────────────" >&2 } - - -# ======== lib/utils.sh ======== -# Core utility functions - -# --- Configuration --- REPO_OWNER="DanielLavrushin" REPO_NAME="b4" BINARY_NAME="b4" @@ -81,7 +65,6 @@ TEMP_DIR="/tmp/b4_install_$$" WGET_INSECURE="" PROXY_BASE_URL="https://proxy.lavrush.in/github" -# --- Runtime state (set by platform/wizard) --- B4_BIN_DIR="" B4_DATA_DIR="" B4_CONFIG_FILE="" @@ -91,12 +74,10 @@ B4_SERVICE_NAME="" B4_PKG_MANAGER="" B4_PLATFORM="" -# --- Command existence check (works on BusyBox/minimal shells) --- command_exists() { command -v "$1" >/dev/null 2>&1 || which "$1" >/dev/null 2>&1 } -# --- Root check --- check_root() { if [ "$(id -u 2>/dev/null)" = "0" ]; then return 0 @@ -104,7 +85,6 @@ check_root() { if [ "$USER" = "root" ]; then return 0 fi - # Fallback: try writing to /etc if touch /etc/.b4_root_test 2>/dev/null; then rm -f /etc/.b4_root_test return 0 @@ -113,7 +93,6 @@ check_root() { exit 1 } -# --- Temp directory management --- setup_temp() { rm -rf "$TEMP_DIR" 2>/dev/null || true mkdir -p "$TEMP_DIR" || { log_err "Cannot create temp dir"; exit 1; } @@ -125,7 +104,6 @@ cleanup_temp() { trap cleanup_temp EXIT INT TERM -# --- Package manager detection --- detect_pkg_manager() { if [ -n "$B4_PKG_MANAGER" ]; then return 0 @@ -158,7 +136,6 @@ pkg_install() { esac } -# --- Architecture detection --- detect_architecture() { arch=$(uname -m) @@ -167,7 +144,6 @@ detect_architecture() { i386 | i486 | i586 | i686) echo "386" ;; aarch64 | arm64) echo "arm64" ;; armv7 | armv7l) - # Check for full ARMv7 VFP support, otherwise use armv5 for safety if [ -f /proc/cpuinfo ] && grep -qE "(vfpv[3-9])" /proc/cpuinfo 2>/dev/null && grep -qE "CPU architecture:\s*7" /proc/cpuinfo 2>/dev/null; then @@ -213,7 +189,6 @@ is_little_endian() { uname -m | grep -qi "el" && return 0 [ -f /proc/cpuinfo ] && grep -qi "little.endian\|byteorder.*little" /proc/cpuinfo 2>/dev/null && return 0 command_exists opkg && opkg print-architecture 2>/dev/null | grep -qi "mipsel\|mips64el" && return 0 - # ELF header check [ "$(dd if=/bin/sh bs=1 skip=5 count=1 2>/dev/null | od -An -tx1 | tr -d ' ')" = "01" ] && return 0 return 1 } @@ -225,7 +200,6 @@ is_softfloat() { return 1 } -# --- HTTPS support --- check_https_support() { if command_exists curl && curl -sI --max-time 5 "https://github.com" >/dev/null 2>&1; then return 0 @@ -233,7 +207,6 @@ check_https_support() { if command_exists wget && wget --spider -q --timeout=5 "https://github.com" 2>/dev/null; then return 0 fi - # Try with --no-check-certificate if command_exists wget && wget --spider -q --timeout=5 --no-check-certificate "https://github.com" 2>/dev/null; then WGET_INSECURE="--no-check-certificate" log_warn "HTTPS works only with --no-check-certificate (CA certs missing)" @@ -259,7 +232,6 @@ ensure_https_support() { return 1 } -# --- Download helpers --- convert_to_proxy_url() { url="$1" case "$url" in @@ -280,11 +252,9 @@ fetch_file() { return 1 fi - # Try direct if command_exists curl && curl -sfL --max-time 30 -o "$output" "$url" 2>/dev/null; then return 0; fi if command_exists wget && wget -q $WGET_INSECURE --timeout=30 -O "$output" "$url" 2>/dev/null; then return 0; fi - # Try proxy fallback proxy_url=$(convert_to_proxy_url "$url") if [ "$proxy_url" != "$url" ]; then log_warn "Direct download failed, trying proxy..." @@ -306,7 +276,6 @@ fetch_stdout() { result=$(wget -qO- $WGET_INSECURE --timeout=15 "$url" 2>/dev/null) && [ -n "$result" ] && echo "$result" && return 0 fi - # Proxy fallback proxy_url=$(convert_to_proxy_url "$url") if [ "$proxy_url" != "$url" ]; then if command_exists curl; then @@ -320,7 +289,6 @@ fetch_stdout() { return 1 } -# --- GitHub release helpers --- get_latest_version() { api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" version=$(fetch_stdout "$api_url" | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4) @@ -360,8 +328,6 @@ verify_checksum() { fi } -# --- Kernel module helpers --- -# Check if a kernel module is built-in (compiled into kernel, not loadable) _kmod_builtin() { _mod="$1" _kver=$(uname -r) @@ -372,28 +338,22 @@ _kmod_builtin() { return 1 } -# Check if a kernel module is available (loaded OR built-in) _kmod_available() { lsmod 2>/dev/null | grep -q "^$1" && return 0 _kmod_builtin "$1" && return 0 return 1 } -# --- Process management --- is_b4_running() { - # Check PID files first (most reliable) for pf in /var/run/b4.pid /opt/var/run/b4.pid; do if [ -f "$pf" ]; then _pid=$(cat "$pf" 2>/dev/null) [ -n "$_pid" ] && kill -0 "$_pid" 2>/dev/null && return 0 fi done - # Try pgrep -x (exact process name match — won't match scripts containing "b4") if command_exists pgrep; then pgrep -x "$BINARY_NAME" >/dev/null 2>&1 && return 0 fi - # Fallback: check ps for the actual b4 binary (not scripts mentioning b4) - # Match paths like /usr/bin/b4 or standalone "b4" command, exclude our own PID _mypid=$$ _ps_out=$(ps w 2>/dev/null || ps 2>/dev/null) || true if [ -n "$_ps_out" ]; then @@ -406,25 +366,21 @@ is_b4_running() { stop_b4() { if ! is_b4_running; then return 0; fi log_info "Stopping running b4 process..." - # Try PID file first for pf in /var/run/b4.pid /opt/var/run/b4.pid; do if [ -f "$pf" ]; then _pid=$(cat "$pf" 2>/dev/null) [ -n "$_pid" ] && kill "$_pid" 2>/dev/null || true fi done - # Then try pkill -x (exact name match) if command_exists pkill; then pkill -x "$BINARY_NAME" 2>/dev/null || true fi sleep 2 } -# --- Directory helpers --- is_writable_dir() { dir="$1" [ -d "$dir" ] && [ -w "$dir" ] && return 0 - # Try to create and test mkdir -p "$dir" 2>/dev/null && [ -w "$dir" ] && return 0 return 1 } @@ -443,7 +399,6 @@ ensure_dir() { return 0 } -# --- Check if user wants to exit --- check_exit() { case "$1" in [eEqQ] | exit | EXIT | quit | QUIT) @@ -453,27 +408,22 @@ check_exit() { esac } -# --- Read user input (works even when stdin is piped) --- -# Uses global _INPUT to avoid subshell issues with exit _INPUT="" read_input() { prompt="$1" default="$2" - # In quiet mode, always use default without prompting if [ "$QUIET_MODE" -eq 1 ] 2>/dev/null; then _INPUT="$default" return 0 fi printf "${CYAN}%b${NC}" "$prompt" >&2 read _INPUT /dev/null || _INPUT="$default" - # Strip carriage returns (some terminals/SSH clients send \r) _INPUT=$(printf '%s' "$_INPUT" | tr -d '\r') check_exit "$_INPUT" [ -z "$_INPUT" ] && _INPUT="$default" return 0 } -# --- Yes/No prompt --- confirm() { prompt="$1" default="${2:-y}" # default yes @@ -492,14 +442,8 @@ confirm() { *) [ "$default" = "y" ] && return 0 || return 1 ;; esac } - - -# ======== lib/wizard.sh ======== -# Interactive wizard — handles both manual and automatic modes - WIZARD_MODE="" # "auto" or "manual" -# Show the welcome banner and mode selection wizard_start() { echo "" printf "${BOLD}" @@ -533,12 +477,10 @@ wizard_start() { done } -# Run automatic detection and show results for review wizard_auto_detect() { log_header "Detecting system..." echo "" - # 1. Detect platform platform_auto_detect if [ -z "$B4_PLATFORM" ]; then log_err "Could not detect platform" @@ -546,16 +488,17 @@ wizard_auto_detect() { exit 1 fi - # 2. Load platform defaults + _user_bin_dir="$B4_BIN_DIR" + _user_data_dir="$B4_DATA_DIR" platform_call info + [ -n "$_user_bin_dir" ] && B4_BIN_DIR="$_user_bin_dir" + [ -n "$_user_data_dir" ] && B4_DATA_DIR="$_user_data_dir" + [ -n "$_user_data_dir" ] && B4_CONFIG_FILE="${_user_data_dir}/b4.json" - # 3. Detect architecture B4_ARCH=$(detect_architecture) - # 4. Detect package manager detect_pkg_manager - # 5. Show what was detected wizard_show_config echo "" @@ -566,12 +509,10 @@ wizard_auto_detect() { fi } -# Manual configuration — ask for every setting wizard_manual_configure() { log_header "Manual configuration" echo "" - # 1. Platform selection while true; do echo " Available platforms:" idx=1 @@ -599,30 +540,24 @@ wizard_manual_configure() { echo "" done - # Load platform defaults first platform_call info - # 2. Binary directory read_input "Binary directory [${B4_BIN_DIR}]: " "$B4_BIN_DIR" B4_BIN_DIR="$_INPUT" - # 3. Data/config directory read_input "Data directory [${B4_DATA_DIR}]: " "$B4_DATA_DIR" B4_DATA_DIR="$_INPUT" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" - # 4. Service type echo "" echo " Service types: systemd, procd, sysv, entware, none" read_input "Service type [${B4_SERVICE_TYPE}]: " "$B4_SERVICE_TYPE" B4_SERVICE_TYPE="$_INPUT" - # 5. Architecture auto_arch=$(detect_architecture) read_input "Architecture [${auto_arch}]: " "$auto_arch" B4_ARCH="$_INPUT" - # 6. Package manager detect_pkg_manager read_input "Package manager [${B4_PKG_MANAGER:-none}]: " "$B4_PKG_MANAGER" B4_PKG_MANAGER="$_INPUT" @@ -636,7 +571,6 @@ wizard_manual_configure() { fi } -# Display current configuration wizard_show_config() { log_sep pname="" @@ -651,7 +585,6 @@ wizard_show_config() { log_detail "Service type" "${B4_SERVICE_TYPE}" log_detail "Package manager" "${B4_PKG_MANAGER:-none}" - # Show enabled features if [ -n "$REGISTERED_FEATURES" ]; then echo "" log_detail "Features" "" @@ -664,7 +597,6 @@ wizard_show_config() { log_sep } -# Feature selection wizard (called during install) wizard_select_features() { if [ -z "$REGISTERED_FEATURES" ]; then return 0 @@ -689,20 +621,6 @@ wizard_select_features() { fi done } - - -# ======== platforms/_interface.sh ======== -# Platform registration and dispatch system -# -# Each platform file must define these functions (prefixed with platform__): -# name — Human-readable name -# match — Return 0 if this platform is detected, 1 otherwise -# info — Set B4_BIN_DIR, B4_DATA_DIR, B4_SERVICE_TYPE, etc. -# check_deps — Verify/install kernel modules and dependencies -# find_storage — Find writable storage (for routers with limited rootfs) -# -# Then register with: register_platform "" - REGISTERED_PLATFORMS="" register_platform() { @@ -710,21 +628,16 @@ register_platform() { REGISTERED_PLATFORMS="${REGISTERED_PLATFORMS} ${id}" } -# Dispatch a call to the active platform -# Usage: platform_call [args...] platform_call() { func="$1" shift platform_dispatch "$B4_PLATFORM" "$func" "$@" } -# Dispatch to a specific platform -# Usage: platform_dispatch [args...] platform_dispatch() { pid="$1" func="$2" shift 2 - # Build function name: platform__ fn="platform_${pid}_${func}" if type "$fn" >/dev/null 2>&1; then "$fn" "$@" @@ -733,17 +646,8 @@ platform_dispatch() { return 1 fi } - - -# ======== platforms/_detect.sh ======== -# Auto-detection: iterate registered platforms and find the best match -# -# Override with: B4_PLATFORM= environment variable - platform_auto_detect() { - # User override — most reliable if [ -n "$B4_PLATFORM" ]; then - # Verify the platform exists for p in $REGISTERED_PLATFORMS; do if [ "$p" = "$B4_PLATFORM" ]; then log_ok "Using user-specified platform: $B4_PLATFORM" @@ -755,8 +659,6 @@ platform_auto_detect() { exit 1 fi - # Try each registered platform's match function - # generic_linux is tried last since it's the catch-all _fallback="" for p in $REGISTERED_PLATFORMS; do [ "$p" = "generic_linux" ] && _fallback="generic_linux" && continue @@ -768,14 +670,12 @@ platform_auto_detect() { fi done - # Try generic_linux last (its match() excludes known router firmwares) if [ -n "$_fallback" ] && platform_dispatch "generic_linux" match 2>/dev/null; then B4_PLATFORM="generic_linux" log_ok "Detected platform: Generic Linux" return 0 fi - # Nothing matched specifically — still use generic_linux as safe default if [ -n "$_fallback" ]; then B4_PLATFORM="generic_linux" log_warn "Could not detect specific platform, defaulting to Generic Linux" @@ -784,22 +684,13 @@ platform_auto_detect() { return 1 } - - -# ======== platforms/generic-linux.sh ======== -# Platform: Generic Linux (Ubuntu, Debian, Fedora, Arch, Alpine, etc.) -# Covers any systemd-based or sysv-init desktop/server Linux - platform_generic_linux_name() { echo "Generic Linux (Ubuntu/Debian/Fedora/Arch/Alpine)" } platform_generic_linux_match() { - # Match any Linux with systemd or standard init.d - # This is the lowest-priority fallback — other platforms should match first [ "$(uname -s)" = "Linux" ] || return 1 - # Don't match if this looks like a router firmware [ -f /etc/openwrt_release ] && return 1 [ -f /etc/merlinwrt_release ] && return 1 [ -d /jffs ] && [ -d /opt/etc/init.d ] && return 1 # Merlin with Entware @@ -809,7 +700,6 @@ platform_generic_linux_match() { command_exists nvram && nvram get firmver 2>/dev/null | grep -qi "merlin" && return 1 [ -f /proc/device-tree/model ] && grep -qi "keenetic" /proc/device-tree/model 2>/dev/null && return 1 - # Match systemd or standard init command_exists systemctl && return 0 [ -d /etc/init.d ] && return 0 @@ -839,7 +729,6 @@ platform_generic_linux_info() { platform_generic_linux_check_deps() { missing="" - # Check basic tools if ! command_exists curl && ! command_exists wget; then missing="${missing} wget" fi @@ -857,10 +746,8 @@ platform_generic_linux_check_deps() { ensure_https_support || exit 1 - # Check kernel modules _generic_linux_check_kmods - # Recommended packages _generic_linux_check_recommended } @@ -894,47 +781,25 @@ _generic_linux_check_recommended() { } platform_generic_linux_find_storage() { - # Standard Linux — no special storage detection needed return 0 } register_platform "generic_linux" - - -# ======== platforms/keenetic.sh ======== -# Platform: Keenetic routers (NDMS OS with Entware) -# -# Key characteristics: -# - NDMS is a proprietary Linux-based OS (not OpenWrt) -# - Root filesystem is read-only -# - Entware provides /opt (USB or internal storage on newer models) -# - Uses Entware init system (rc.func or standalone scripts) -# - opkg is the package manager (via Entware) -# - Older models: MIPS (MT7621 — mipsle_softfloat) -# - Newer models: aarch64 -# - Kernel modules usually built into firmware -# - May not have /opt/etc/entware_release file - platform_keenetic_name() { echo "Keenetic (NDMS)" } platform_keenetic_match() { - # /proc/device-tree/model contains "keenetic" if [ -f /proc/device-tree/model ] && grep -qi "keenetic" /proc/device-tree/model 2>/dev/null; then return 0 fi - # NDMS-specific: /var/run/ndm exists or ndmc command available if [ -d /var/run/ndm ] || command_exists ndmc; then return 0 fi - # Keenetic with Entware but no entware_release file - # /opt writable + read-only /etc + no /jffs (not Merlin) + no openwrt_release if [ -d "/opt/sbin" ] && [ -w "/opt/sbin" ] && [ ! -w "/etc" ] && [ ! -d "/jffs" ] && [ ! -f /etc/openwrt_release ]; then - # Check if it looks like NDMS (has /tmp/ndm or similar) [ -d /tmp/ndm ] && return 0 fi @@ -950,7 +815,6 @@ platform_keenetic_info() { B4_SERVICE_NAME="S99b4" B4_PKG_MANAGER="opkg" - # Check if Entware is installed if [ ! -d "/opt/etc/init.d" ] && [ ! -f "/opt/bin/opkg" ]; then log_warn "Entware not detected!" log_info "Entware is required on Keenetic. To install:" @@ -959,7 +823,6 @@ platform_keenetic_info() { log_info " 3. For older models: plug in a USB drive and install Entware" log_info " More info: https://help.keenetic.com/hc/en-us/articles/360021214160" - # Try /tmp as last resort (non-persistent) if [ -d "/tmp" ] && [ -w "/tmp" ]; then log_warn "Falling back to /tmp (non-persistent, will not survive reboot)" B4_BIN_DIR="/tmp/b4" @@ -971,7 +834,6 @@ platform_keenetic_info() { } platform_keenetic_check_deps() { - # Check basic download tools if ! command_exists curl && ! command_exists wget; then log_warn "Neither curl nor wget found" if command_exists opkg; then @@ -987,10 +849,8 @@ platform_keenetic_check_deps() { ensure_https_support || exit 1 - # Kernel modules — on Keenetic, usually built into NDMS firmware _keenetic_load_kmods - # Recommended packages _keenetic_check_recommended } @@ -1021,7 +881,6 @@ _keenetic_check_recommended() { command_exists iptables || rec_missing="${rec_missing} iptables" command_exists nohup || rec_missing="${rec_missing} coreutils-nohup" - # SSL support if ! opkg list-installed 2>/dev/null | grep -q "^ca-certificates "; then rec_missing="${rec_missing} ca-certificates" fi @@ -1044,9 +903,6 @@ _keenetic_check_recommended() { } platform_keenetic_find_storage() { - # Keenetic storage priority: - # 1. /opt (Entware — USB or internal on newer models) - # 2. /tmp — volatile, absolute last resort if [ -d "/opt" ] && [ -w "/opt" ]; then return 0 @@ -1060,27 +916,12 @@ platform_keenetic_find_storage() { } register_platform "keenetic" - - -# ======== platforms/merlinwrt.sh ======== -# Platform: Asus Merlin (Asuswrt-Merlin firmware with Entware) -# -# Key characteristics: -# - Root filesystem is read-only (squashfs) -# - /jffs is a persistent writable JFFS2 partition -# - Entware provides /opt (usually on USB or /jffs) -# - Uses Entware's rc.func init system (not systemd, not procd) -# - opkg is the package manager (via Entware) -# - Kernel modules are usually built into firmware - platform_merlinwrt_name() { echo "Asus Merlin (Asuswrt-Merlin)" } platform_merlinwrt_match() { - # Check for Merlin-specific indicators - # nvram firmware version contains "merlin" if command_exists nvram; then fw=$(nvram get firmver 2>/dev/null) bw=$(nvram get buildno 2>/dev/null) @@ -1089,14 +930,10 @@ platform_merlinwrt_match() { fi fi - # /jffs exists and is writable (Merlin signature) - # plus Entware init structure if [ -d "/jffs" ] && [ -w "/jffs" ] && [ -d "/opt/etc/init.d" ]; then - # Additional check: rc.func exists (Entware on Merlin) [ -f "/opt/etc/init.d/rc.func" ] && return 0 fi - # /etc/merlinwrt_release (some builds) [ -f "/etc/merlinwrt_release" ] && return 0 return 1 @@ -1111,7 +948,6 @@ platform_merlinwrt_info() { B4_SERVICE_NAME="S99b4" B4_PKG_MANAGER="opkg" - # Check if Entware is actually installed if [ ! -d "/opt/etc/init.d" ]; then log_warn "Entware not detected!" log_info "Entware is required for MerlinWRT. Install it first:" @@ -1120,7 +956,6 @@ platform_merlinwrt_info() { log_info " 3. Go to Administration > System > Enable Entware" log_info " Or visit: https://github.com/Entware/Entware/wiki/Install-on-Asuswrt-Merlin" - # Fallback to /jffs if available if [ -d "/jffs" ] && [ -w "/jffs" ]; then log_warn "Falling back to /jffs (limited space, Entware recommended)" B4_BIN_DIR="/jffs/b4" @@ -1132,7 +967,6 @@ platform_merlinwrt_info() { } platform_merlinwrt_check_deps() { - # Check basic download tools if ! command_exists curl && ! command_exists wget; then log_warn "Neither curl nor wget found" if command_exists opkg; then @@ -1148,11 +982,8 @@ platform_merlinwrt_check_deps() { ensure_https_support || exit 1 - # Kernel modules — on Merlin, most are built into firmware - # Just try to load them, don't panic if they fail _merlinwrt_load_kmods - # Recommended packages via Entware opkg _merlinwrt_check_recommended } @@ -1182,7 +1013,6 @@ _merlinwrt_check_recommended() { command_exists iptables || rec_missing="${rec_missing} iptables" command_exists nohup || rec_missing="${rec_missing} coreutils-nohup" - # Check SSL support packages if ! opkg list-installed 2>/dev/null | grep -q "^ca-certificates "; then rec_missing="${rec_missing} ca-certificates" fi @@ -1200,10 +1030,6 @@ _merlinwrt_check_recommended() { } platform_merlinwrt_find_storage() { - # Merlin storage priority: - # 1. /opt (Entware on USB) — preferred, most space - # 2. /jffs — persistent but limited (~60MB typically) - # 3. /tmp — volatile, last resort if [ -d "/opt" ] && [ -w "/opt" ]; then return 0 @@ -1223,48 +1049,28 @@ platform_merlinwrt_find_storage() { } register_platform "merlinwrt" - - -# ======== platforms/openwrt.sh ======== -# Platform: OpenWrt -# -# Key characteristics: -# - Embedded Linux distribution for routers and embedded devices -# - /etc/openwrt_release identifies the system -# - Uses procd as init system (OpenWrt 15.05+) or sysv for older versions -# - opkg is the package manager -# - Root filesystem is often SquashFS overlay with limited space -# - /tmp is tmpfs (volatile) -# - External storage may be mounted at /mnt/* or /opt (extroot/USB) -# - Kernel modules may need to be installed via opkg - platform_openwrt_name() { echo "OpenWrt" } platform_openwrt_match() { - # Primary: /etc/openwrt_release exists [ -f /etc/openwrt_release ] && return 0 - # Secondary: /etc/os-release contains openwrt if [ -f /etc/os-release ]; then grep -qi "openwrt" /etc/os-release 2>/dev/null && return 0 fi - # Tertiary: board.json exists (OpenWrt-specific) [ -f /etc/board.json ] && return 0 return 1 } platform_openwrt_info() { - # Default paths — overlay root has limited space B4_BIN_DIR="/usr/bin" B4_DATA_DIR="/etc/b4" B4_CONFIG_FILE="${B4_DATA_DIR}/b4.json" B4_PKG_MANAGER="opkg" - # Init system: procd on modern OpenWrt, sysv fallback if [ -f /sbin/procd ] || command_exists procd; then B4_SERVICE_TYPE="procd" B4_SERVICE_DIR="/etc/init.d" @@ -1277,9 +1083,7 @@ platform_openwrt_info() { B4_SERVICE_TYPE="none" fi - # Prefer external storage if available (/opt from extroot or USB) if [ -d "/opt" ] && [ -w "/opt" ]; then - # Check if /opt has meaningful space (not just an empty dir on overlay) _opt_avail=$(df /opt 2>/dev/null | tail -1 | awk '{print $4}') if [ -n "$_opt_avail" ] && [ "$_opt_avail" -gt 10000 ] 2>/dev/null; then B4_BIN_DIR="/opt/bin" @@ -1288,7 +1092,6 @@ platform_openwrt_info() { fi fi - # Check for USB/external mounts with space if [ "$B4_BIN_DIR" = "/usr/bin" ]; then for mnt in /mnt/sda1 /mnt/sda2 /mnt/mmcblk* /mnt/usb*; do if [ -d "$mnt" ] && [ -w "$mnt" ]; then @@ -1306,7 +1109,6 @@ platform_openwrt_info() { } platform_openwrt_check_deps() { - # Check basic download tools if ! command_exists curl && ! command_exists wget; then log_warn "Neither curl nor wget found" log_info "Installing wget-ssl..." @@ -1320,10 +1122,8 @@ platform_openwrt_check_deps() { ensure_https_support || exit 1 - # Kernel modules _openwrt_load_kmods - # Recommended packages _openwrt_check_recommended } @@ -1347,7 +1147,6 @@ _openwrt_check_recommended() { command_exists jq || rec_missing="${rec_missing} jq" command_exists iptables || rec_missing="${rec_missing} iptables" - # SSL support if ! command_exists curl || ! curl -sI --max-time 3 "https://github.com" >/dev/null 2>&1; then if ! opkg list-installed 2>/dev/null | grep -q "^ca-certificates "; then rec_missing="${rec_missing} ca-certificates" @@ -1370,10 +1169,6 @@ _openwrt_check_recommended() { } platform_openwrt_find_storage() { - # OpenWrt storage priority: - # 1. /opt (extroot or USB) — has space - # 2. External mounts at /mnt/* - # 3. Root overlay — very limited space if [ -d "/opt" ] && [ -w "/opt" ]; then _opt_avail=$(df /opt 2>/dev/null | tail -1 | awk '{print $4}') @@ -1388,7 +1183,6 @@ platform_openwrt_find_storage() { fi done - # Check root overlay space _root_avail=$(df / 2>/dev/null | tail -1 | awk '{print $4}') if [ -n "$_root_avail" ] && [ "$_root_avail" -lt 2000 ] 2>/dev/null; then log_warn "Root filesystem has very little space ($(df -h / 2>/dev/null | tail -1 | awk '{print $4}') available)" @@ -1400,20 +1194,6 @@ platform_openwrt_find_storage() { } register_platform "openwrt" - - -# ======== features/_interface.sh ======== -# Feature registration and dispatch system -# -# Each feature file must define these functions (prefixed with feature__): -# name — Human-readable name -# description — Short description -# default_enabled — "yes" or "no" -# run — Execute the feature (install/configure) -# remove — Undo/clean up the feature -# -# Then register with: register_feature "" - REGISTERED_FEATURES="" ENABLED_FEATURES="" @@ -1422,7 +1202,6 @@ register_feature() { REGISTERED_FEATURES="${REGISTERED_FEATURES} ${id}" } -# Dispatch to a specific feature feature_dispatch() { fid="$1" func="$2" @@ -1436,7 +1215,6 @@ feature_dispatch() { fi } -# Run all enabled features features_run() { for f in $ENABLED_FEATURES; do fname=$(feature_dispatch "$f" name) @@ -1445,18 +1223,11 @@ features_run() { done } -# Remove all registered features features_remove() { for f in $REGISTERED_FEATURES; do feature_dispatch "$f" remove || true done } - - -# ======== features/geoip.sh ======== -# Feature: GeoIP data (geoip.dat) -# Downloads v2ray-format geoip database for IP-based filtering - GEOIP_SOURCES="1|Loyalsoldier|https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download 2|RUNET Freedom|https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release 3|B4 GeoIP (recommended)|https://github.com/DanielLavrushin/b4geoip/releases/latest/download" @@ -1474,7 +1245,6 @@ feature_geoip_default_enabled() { } feature_geoip_run() { - # Default source (B4 GeoIP = 3) base_url=$(echo "$GEOIP_SOURCES" | grep "^3|" | cut -d'|' -f3) save_dir="$B4_DATA_DIR" @@ -1482,7 +1252,6 @@ feature_geoip_run() { log_sep echo "" - # Select source echo " Available geoip sources:" echo "$GEOIP_SOURCES" | while IFS='|' read -r num name _url; do [ -n "$num" ] && printf " ${BOLD}%s${NC}) %s\n" "$num" "$name" @@ -1494,9 +1263,8 @@ feature_geoip_run() { _sel_url=$(echo "$GEOIP_SOURCES" | grep "^${_INPUT}|" | cut -d'|' -f3) || true [ -n "$_sel_url" ] && base_url="$_sel_url" || log_warn "Invalid selection, using default" - # Check if config already has a geoip path if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then - existing=$(jq -r '.system.geo.ipdat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) + existing=$(jq -r '.system.geo.ipdat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$existing" ] && [ "$existing" != "null" ]; then save_dir=$(dirname "$existing") log_info "Found existing geoip path: $save_dir" @@ -1509,7 +1277,6 @@ feature_geoip_run() { ensure_dir "$save_dir" "GeoIP directory" || return 1 - # Download log_info "Downloading geoip.dat..." if ! fetch_file "${base_url}/geoip.dat" "${save_dir}/geoip.dat"; then log_err "Failed to download geoip.dat" @@ -1519,7 +1286,6 @@ feature_geoip_run() { log_ok "geoip.dat downloaded to ${save_dir}" - # Update config (uses shared helper from geosite.sh) _geo_update_config "ipdat_path" "${save_dir}/geoip.dat" "ipdat_url" "${base_url}/geoip.dat" } @@ -1528,12 +1294,6 @@ feature_geoip_remove() { } register_feature "geoip" - - -# ======== features/geosite.sh ======== -# Feature: GeoSite data (geosite.dat) -# Downloads v2ray-format geosite database for domain categorization - GEOSITE_SOURCES="1|Loyalsoldier|https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download 2|RUNET Freedom (recommended)|https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/release" @@ -1550,7 +1310,6 @@ feature_geosite_default_enabled() { } feature_geosite_run() { - # Default source (RUNET Freedom = 2) base_url=$(echo "$GEOSITE_SOURCES" | grep "^2|" | cut -d'|' -f3) save_dir="$B4_DATA_DIR" @@ -1558,7 +1317,6 @@ feature_geosite_run() { log_sep echo "" - # Select source echo " Available geosite sources:" echo "$GEOSITE_SOURCES" | while IFS='|' read -r num name _url; do [ -n "$num" ] && printf " ${BOLD}%s${NC}) %s\n" "$num" "$name" @@ -1570,9 +1328,8 @@ feature_geosite_run() { _sel_url=$(echo "$GEOSITE_SOURCES" | grep "^${_INPUT}|" | cut -d'|' -f3) || true [ -n "$_sel_url" ] && base_url="$_sel_url" || log_warn "Invalid selection, using default" - # Check if config already has a geosite path if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then - existing=$(jq -r '.system.geo.sitedat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) + existing=$(jq -r '.system.geo.sitedat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$existing" ] && [ "$existing" != "null" ]; then save_dir=$(dirname "$existing") log_info "Found existing geosite path: $save_dir" @@ -1585,7 +1342,6 @@ feature_geosite_run() { ensure_dir "$save_dir" "GeoSite directory" || return 1 - # Download log_info "Downloading geosite.dat..." if ! fetch_file "${base_url}/geosite.dat" "${save_dir}/geosite.dat"; then log_err "Failed to download geosite.dat" @@ -1595,7 +1351,6 @@ feature_geosite_run() { log_ok "geosite.dat downloaded to ${save_dir}" - # Update config _geo_update_config "sitedat_path" "${save_dir}/geosite.dat" "sitedat_url" "${base_url}/geosite.dat" } @@ -1604,9 +1359,6 @@ feature_geosite_remove() { } register_feature "geosite" - -# --- Shared helpers used by both geosite and geoip features --- - _geo_update_config() { path_key="$1" path_val="$2" @@ -1620,7 +1372,6 @@ _geo_update_config() { fi if [ ! -f "$B4_CONFIG_FILE" ]; then - # Create minimal config with just this geo key jq -n \ --arg pv "$path_val" \ --arg uv "$url_val" \ @@ -1630,7 +1381,6 @@ _geo_update_config() { return 0 fi - # Update existing config — merge into system.geo, preserving other keys tmp="${B4_CONFIG_FILE}.tmp" if jq \ --arg pv "$path_val" \ @@ -1649,11 +1399,10 @@ _geo_remove_file() { config_key="$1" filename="$2" - # Try reading path from config for cfg in "$B4_CONFIG_FILE" /etc/b4/b4.json /opt/etc/b4/b4.json; do [ -f "$cfg" ] || continue if command_exists jq; then - fpath=$(jq -r ".system.geo.${config_key} // empty" "$cfg" 2>/dev/null) + fpath=$(jq -r ".system.geo.${config_key} // empty" "$cfg" 2>/dev/null) || true if [ -n "$fpath" ] && [ -f "$fpath" ]; then log_info "Found ${filename}: ${fpath}" if [ "$QUIET_MODE" -eq 1 ] || confirm "Remove ${filename}?" "y"; then @@ -1666,7 +1415,6 @@ _geo_remove_file() { fi done - # Fallback: check default locations for dir in /etc/b4 /opt/etc/b4; do if [ -f "${dir}/${filename}" ]; then log_info "Found ${filename}: ${dir}/${filename}" @@ -1679,12 +1427,6 @@ _geo_remove_file() { fi done } - - -# ======== features/https.sh ======== -# Feature: HTTPS for B4 web interface -# Detects existing TLS certificates on the system and configures b4 to use them - feature_https_name() { echo "HTTPS web interface" } @@ -1694,7 +1436,6 @@ feature_https_description() { } feature_https_default_enabled() { - # Only suggest if certificates exist _https_detect_certs >/dev/null 2>&1 && echo "yes" || echo "no" } @@ -1749,7 +1490,6 @@ feature_https_run() { } _https_detect_certs() { - # Common certificate locations on various systems if [ -f "/etc/uhttpd.crt" ] && [ -f "/etc/uhttpd.key" ]; then echo "/etc/uhttpd.crt|/etc/uhttpd.key|OpenWrt uhttpd" return 0 @@ -1770,23 +1510,6 @@ feature_https_remove() { } register_feature "https" - - -# ======== services/_interface.sh ======== -# Service registration and dispatch system -# -# Each service file must define these functions (prefixed with service__): -# install — Write the service/init script to disk -# remove — Stop and delete the service/init script -# start — Start the b4 service -# stop — Stop the b4 service -# -# Then register with: register_service "" -# -# Required globals when service functions are called: -# B4_SERVICE_TYPE, B4_SERVICE_DIR, B4_SERVICE_NAME -# B4_BIN_DIR, B4_DATA_DIR, B4_CONFIG_FILE, BINARY_NAME - REGISTERED_SERVICES="" register_service() { @@ -1794,16 +1517,12 @@ register_service() { REGISTERED_SERVICES="${REGISTERED_SERVICES} ${id}" } -# Dispatch to the active service type -# Usage: service_call [args...] service_call() { func="$1" shift service_dispatch "$B4_SERVICE_TYPE" "$func" "$@" } -# Dispatch to a specific service type -# Usage: service_dispatch [args...] service_dispatch() { sid="$1" func="$2" @@ -1816,17 +1535,9 @@ service_dispatch() { return 1 fi } - - -# ======== services/entware.sh ======== -# Service type: entware -# Manages b4 using Entware's init.d system (rc.func or standalone) -# Used by Keenetic (NDMS) and Asus Merlin (Asuswrt-Merlin) - service_entware_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 - # Remove stale service file rm -f "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" 2>/dev/null || true if [ -f "${B4_SERVICE_DIR}/rc.func" ]; then @@ -1947,12 +1658,6 @@ service_entware_stop() { } register_service "entware" - - -# ======== services/none.sh ======== -# Service type: none -# No-op service management — used when no init system is available - service_none_install() { log_warn "No init system configured — b4 will not start automatically" log_info "Start manually: ${B4_BIN_DIR}/${BINARY_NAME} --config ${B4_CONFIG_FILE}" @@ -1972,12 +1677,6 @@ service_none_stop() { } register_service "none" - - -# ======== services/procd.sh ======== -# Service type: procd -# Manages b4 using OpenWrt's procd init system - service_procd_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 @@ -2025,7 +1724,6 @@ EOF chmod +x "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" log_ok "Procd init script created: ${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" - # Enable the service to start on boot "${B4_SERVICE_DIR}/${B4_SERVICE_NAME}" enable 2>/dev/null || true log_info "Service enabled for boot" } @@ -2054,12 +1752,6 @@ service_procd_stop() { } register_service "procd" - - -# ======== services/systemd.sh ======== -# Service type: systemd -# Manages b4 as a systemd unit on standard Linux systems - service_systemd_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 @@ -2108,12 +1800,6 @@ service_systemd_stop() { } register_service "systemd" - - -# ======== services/sysv.sh ======== -# Service type: sysv -# Manages b4 using a traditional SysV init.d script - service_sysv_install() { ensure_dir "$B4_SERVICE_DIR" "Service directory" || return 1 @@ -2197,25 +1883,23 @@ service_sysv_stop() { } register_service "sysv" - - -# ======== actions/install.sh ======== -# Action: Install b4 - action_install() { version="$1" force_arch="$2" check_root - # --- Wizard --- if [ "$QUIET_MODE" -eq 1 ]; then WIZARD_MODE="auto" + _user_bin_dir="$B4_BIN_DIR" + _user_data_dir="$B4_DATA_DIR" platform_auto_detect platform_call info + [ -n "$_user_bin_dir" ] && B4_BIN_DIR="$_user_bin_dir" + [ -n "$_user_data_dir" ] && B4_DATA_DIR="$_user_data_dir" + [ -n "$_user_data_dir" ] && B4_CONFIG_FILE="${_user_data_dir}/b4.json" B4_ARCH="${force_arch:-$(detect_architecture)}" detect_pkg_manager - # Enable all default features in quiet mode for f in $REGISTERED_FEATURES; do fdefault=$(feature_dispatch "$f" default_enabled) [ "$fdefault" = "yes" ] && ENABLED_FEATURES="${ENABLED_FEATURES} ${f}" @@ -2232,21 +1916,17 @@ action_install() { ;; esac - # Override arch if user forced it [ -n "$force_arch" ] && B4_ARCH="$force_arch" - # Feature selection wizard_select_features fi echo "" log_header "Installing B4" - # --- Check dependencies --- log_info "Checking dependencies..." platform_call check_deps - # --- Resolve version --- if [ -z "$version" ]; then log_info "Fetching latest version..." version=$(get_latest_version) @@ -2254,12 +1934,10 @@ action_install() { log_ok "Version: ${version}" log_ok "Architecture: ${B4_ARCH}" - # --- Prepare directories --- ensure_dir "$B4_BIN_DIR" "Binary directory" || exit 1 ensure_dir "$B4_DATA_DIR" "Data directory" || exit 1 setup_temp - # --- Download & install binary --- file_name="${BINARY_NAME}-linux-${B4_ARCH}.tar.gz" download_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${version}/${file_name}" archive_path="${TEMP_DIR}/${file_name}" @@ -2270,11 +1948,9 @@ action_install() { exit 1 fi - # Verify checksum sha_url="${download_url}.sha256" _cs_ret=0 verify_checksum "$archive_path" "$sha_url" || _cs_ret=$? - # exit code 2 = actual SHA256 mismatch (corrupted/tampered download) if [ "$_cs_ret" -eq 2 ]; then log_warn "Checksum mismatch — download may be corrupted" if ! confirm "Continue anyway?"; then @@ -2282,7 +1958,6 @@ action_install() { fi fi - # Extract log_info "Extracting..." cd "$TEMP_DIR" tar -xzf "$archive_path" || { log_err "Failed to extract archive"; exit 1; } @@ -2293,43 +1968,35 @@ action_install() { exit 1 fi - # Stop running instance stop_b4 - # Backup existing binary if [ -f "${B4_BIN_DIR}/${BINARY_NAME}" ]; then ts=$(date '+%Y%m%d_%H%M%S') mv "${B4_BIN_DIR}/${BINARY_NAME}" "${B4_BIN_DIR}/${BINARY_NAME}.backup.${ts}" log_info "Existing binary backed up" fi - # Install mv "${BINARY_NAME}" "${B4_BIN_DIR}/" 2>/dev/null || cp "${BINARY_NAME}" "${B4_BIN_DIR}/" || { log_err "Failed to install binary to ${B4_BIN_DIR}" exit 1 } chmod +x "${B4_BIN_DIR}/${BINARY_NAME}" - # Verify if "${B4_BIN_DIR}/${BINARY_NAME}" --version >/dev/null 2>&1; then installed_ver=$("${B4_BIN_DIR}/${BINARY_NAME}" --version 2>&1 | head -1) log_ok "Binary installed: ${installed_ver}" - # Clean old backups rm -f "${B4_BIN_DIR}/${BINARY_NAME}".backup.* 2>/dev/null || true else log_warn "Binary installed but version check failed" fi - # --- Install service --- log_info "Setting up service..." service_call install - # --- Run enabled features --- if [ -n "$ENABLED_FEATURES" ]; then features_run fi - # --- Summary --- _install_summary "$version" } @@ -2345,20 +2012,17 @@ _install_summary() { log_detail "Service" "${B4_SERVICE_TYPE}" log_sep - # Check if binary is in PATH if ! echo "$PATH" | grep -q "$B4_BIN_DIR"; then log_warn "$B4_BIN_DIR is not in PATH" log_info "Consider: ln -s ${B4_BIN_DIR}/${BINARY_NAME} /usr/bin/${BINARY_NAME}" fi - # Show web interface info _show_web_info echo "" log_info "To see all options: ${B4_BIN_DIR}/${BINARY_NAME} --help" echo "" - # Offer to start service if [ "$QUIET_MODE" -eq 0 ] && [ "$B4_SERVICE_TYPE" != "none" ]; then if confirm "Start B4 service now?"; then service_call start || true @@ -2375,12 +2039,11 @@ _show_web_info() { protocol="http" if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then - web_port=$(jq -r '.system.web_server.port // 7000' "$B4_CONFIG_FILE" 2>/dev/null) - tls=$(jq -r '.system.web_server.tls_cert // ""' "$B4_CONFIG_FILE" 2>/dev/null) + web_port=$(jq -r '.system.web_server.port // 7000' "$B4_CONFIG_FILE" 2>/dev/null) || true + tls=$(jq -r '.system.web_server.tls_cert // ""' "$B4_CONFIG_FILE" 2>/dev/null) || true [ -n "$tls" ] && protocol="https" fi - # Try to get LAN IP lan_ip="" if command_exists ip; then lan_ip=$(ip -4 addr show br0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1) @@ -2392,17 +2055,11 @@ _show_web_info() { log_info "Web interface: ${protocol}://${lan_ip}:${web_port}" fi } - - -# ======== actions/remove.sh ======== -# Action: Remove b4 - action_remove() { check_root log_header "Removing B4" - # Detect platform if not set if [ -z "$B4_PLATFORM" ]; then platform_auto_detect || true if [ -n "$B4_PLATFORM" ]; then @@ -2410,18 +2067,14 @@ action_remove() { fi fi - # Find config file — check all known locations _remove_find_config - # Stop running process stop_b4 - # Remove service if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then log_info "Removing service..." service_call remove 2>/dev/null || true else - # Manual cleanup of known service locations for svc in \ /etc/systemd/system/b4.service \ /etc/init.d/b4 \ @@ -2434,10 +2087,8 @@ action_remove() { command_exists systemctl && systemctl daemon-reload 2>/dev/null || true fi - # Remove features (geodat etc. — reads paths from config) features_remove - # Remove binary from known locations for dir in /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /tmp/b4; do if [ -f "${dir}/${BINARY_NAME}" ]; then rm -f "${dir}/${BINARY_NAME}" @@ -2446,10 +2097,8 @@ action_remove() { fi done - # Ask about config directories _remove_config_dirs - # Cleanup rm -f /var/run/b4.pid 2>/dev/null || true rm -f /var/log/b4.log 2>/dev/null || true @@ -2458,15 +2107,12 @@ action_remove() { echo "" } -# Find the active config file so features can read paths from it _remove_find_config() { - # Already set by platform detection or user override if [ -n "$B4_CONFIG_FILE" ] && [ -f "$B4_CONFIG_FILE" ]; then log_info "Using config: $B4_CONFIG_FILE" return 0 fi - # Search known locations for cfg in /etc/b4/b4.json /opt/etc/b4/b4.json /etc/storage/b4/b4.json; do if [ -f "$cfg" ]; then B4_CONFIG_FILE="$cfg" @@ -2479,18 +2125,16 @@ _remove_find_config() { log_warn "No config file found" } -# Remove config directories, but list what's inside first _remove_config_dirs() { - # Collect unique config dirs to check checked="" for cfg_dir in "$B4_DATA_DIR" /etc/b4 /opt/etc/b4 /etc/storage/b4; do [ -z "$cfg_dir" ] && continue [ -d "$cfg_dir" ] || continue - # Skip if already checked - echo "$checked" | grep -q "$cfg_dir" && continue + case " $checked " in + *" $cfg_dir "*) continue ;; + esac checked="${checked} ${cfg_dir}" - # Show remaining contents remaining=$(ls -1 "$cfg_dir" 2>/dev/null) if [ -n "$remaining" ]; then log_info "Remaining files in ${cfg_dir}:" @@ -2507,19 +2151,14 @@ _remove_config_dirs() { fi done } - - -# ======== actions/update.sh ======== -# Action: Update b4 to latest version - action_update() { - force_arch="$1" + target_ver="$1" + force_arch="$2" check_root log_header "Updating B4" - # Detect platform if [ -z "$B4_PLATFORM" ]; then platform_auto_detect || true if [ -n "$B4_PLATFORM" ]; then @@ -2527,9 +2166,9 @@ action_update() { fi fi - # Find existing binary existing_bin="" for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin; do + [ -z "$dir" ] && continue if [ -f "${dir}/${BINARY_NAME}" ]; then existing_bin="${dir}/${BINARY_NAME}" B4_BIN_DIR="$dir" @@ -2542,21 +2181,25 @@ action_update() { exit 1 fi - # Get current version - current_ver=$("$existing_bin" --version 2>&1 | head -1) || current_ver="unknown" + _ver_full=$("$existing_bin" --version 2>&1) || _ver_full="" + current_ver=$(echo "$_ver_full" | grep -i "version" | head -1) + [ -z "$current_ver" ] && current_ver="unknown" log_info "Current: ${current_ver}" - # Detect arch from existing binary or system if [ -n "$force_arch" ]; then B4_ARCH="$force_arch" else B4_ARCH=$(detect_architecture) fi - # Get latest version - log_info "Checking for updates..." - latest_ver=$(get_latest_version) - log_info "Latest: ${latest_ver}" + if [ -n "$target_ver" ]; then + latest_ver="$target_ver" + log_info "Target: ${latest_ver}" + else + log_info "Checking for updates..." + latest_ver=$(get_latest_version) + log_info "Latest: ${latest_ver}" + fi if [ "$current_ver" = "$latest_ver" ] || echo "$current_ver" | grep -q "$latest_ver"; then log_ok "Already up to date" @@ -2570,7 +2213,6 @@ action_update() { fi fi - # Download and install setup_temp file_name="${BINARY_NAME}-linux-${B4_ARCH}.tar.gz" @@ -2580,7 +2222,6 @@ action_update() { log_info "Downloading ${latest_ver}..." fetch_file "$download_url" "$archive_path" || { log_err "Download failed"; exit 1; } - # Verify sha_url="${download_url}.sha256" _cs_ret=0 verify_checksum "$archive_path" "$sha_url" || _cs_ret=$? @@ -2591,11 +2232,9 @@ action_update() { fi fi - # Extract cd "$TEMP_DIR" tar -xzf "$archive_path" || { log_err "Extraction failed"; exit 1; } - # Stop, backup, replace stop_b4 ts=$(date '+%Y%m%d_%H%M%S') @@ -2606,7 +2245,6 @@ action_update() { { log_err "Failed to replace binary"; exit 1; } chmod +x "$existing_bin" - # Verify if "$existing_bin" --version >/dev/null 2>&1; then new_ver=$("$existing_bin" --version 2>&1 | head -1) log_ok "Updated to: ${new_ver}" @@ -2615,7 +2253,6 @@ action_update() { log_warn "Updated binary failed version check" fi - # Restart service if it was running if [ -n "$B4_SERVICE_TYPE" ] && [ "$B4_SERVICE_TYPE" != "none" ]; then log_info "Restarting service..." service_call start 2>/dev/null || true @@ -2625,15 +2262,9 @@ action_update() { log_ok "Update complete" echo "" } - - -# ======== actions/sysinfo.sh ======== -# Action: Show system diagnostics - action_sysinfo() { log_header "B4 System Diagnostics" - # --- System info --- log_sep log_detail "Hostname" "$(hostname 2>/dev/null || cat /proc/sys/kernel/hostname 2>/dev/null || echo 'unknown')" log_detail "Kernel" "$(uname -r)" @@ -2642,14 +2273,12 @@ action_sysinfo() { [ -f /etc/os-release ] && log_detail "Distribution" "$(. /etc/os-release && echo "$PRETTY_NAME")" [ -f /etc/openwrt_release ] && log_detail "OpenWrt" "$(. /etc/openwrt_release && echo "$DISTRIB_DESCRIPTION")" - # CPU cpu_cores="" if [ -f /proc/cpuinfo ]; then cpu_cores=$(grep -c "^processor" /proc/cpuinfo 2>/dev/null) fi [ -n "$cpu_cores" ] && log_detail "CPU cores" "$cpu_cores" - # Memory if [ -f /proc/meminfo ]; then mem_total=$(awk '/^MemTotal:/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null) mem_avail=$(awk '/^MemAvailable:/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null) @@ -2659,11 +2288,14 @@ action_sysinfo() { fi fi - # Platform detection (save/restore to avoid leaking into wizard) _saved_platform="$B4_PLATFORM" _saved_bin_dir="$B4_BIN_DIR" _saved_data_dir="$B4_DATA_DIR" + _saved_config_file="$B4_CONFIG_FILE" _saved_service_type="$B4_SERVICE_TYPE" + _saved_service_dir="$B4_SERVICE_DIR" + _saved_service_name="$B4_SERVICE_NAME" + _saved_pkg_manager="$B4_PKG_MANAGER" platform_auto_detect 2>/dev/null || true if [ -n "$B4_PLATFORM" ]; then pname=$(platform_dispatch "$B4_PLATFORM" name 2>/dev/null) @@ -2674,7 +2306,6 @@ action_sysinfo() { log_detail "Service type" "${B4_SERVICE_TYPE}" fi - # --- B4 status --- log_sep found_bin="" @@ -2682,10 +2313,8 @@ action_sysinfo() { for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin /tmp/b4; do [ -z "$dir" ] && continue if [ -f "${dir}/${BINARY_NAME}" ] && [ -x "${dir}/${BINARY_NAME}" ]; then - # Run in subshell to suppress kernel crash messages (e.g. "Segmentation fault") _ver_exit=0 _ver_full=$(sh -c "\"${dir}/${BINARY_NAME}\" --version 2>/dev/null" 2>/dev/null) || _ver_exit=$? - # Binary crashed (segfault=139, abort=134, etc.) if [ "$_ver_exit" -gt 128 ] 2>/dev/null; then _bin_crashed="${dir}/${BINARY_NAME}" continue @@ -2701,7 +2330,6 @@ action_sysinfo() { if [ -n "$found_bin" ]; then log_detail "Binary" "$found_bin" log_detail "Version" "$_ver_out" - # Warn if found binary is not in the expected directory if [ -n "$B4_BIN_DIR" ] && [ -n "$_bin_crashed" ]; then log_detail "WARNING" "${RED}${_bin_crashed} crashes (segfault) — wrong architecture?${NC}" fi @@ -2724,7 +2352,6 @@ action_sysinfo() { log_detail "Binary" "${YELLOW}not found${NC}" fi - # Config file cfg_file="" for cfg in "$B4_CONFIG_FILE" /etc/b4/b4.json /opt/etc/b4/b4.json; do [ -z "$cfg" ] && continue @@ -2732,11 +2359,9 @@ action_sysinfo() { done [ -n "$cfg_file" ] && log_detail "Config" "$cfg_file" - # Running status + details from config and process if is_b4_running; then log_detail "Service status" "${GREEN}running${NC}" - # Get PID and process details b4_pid="" for pf in /var/run/b4.pid /opt/var/run/b4.pid; do if [ -f "$pf" ] && kill -0 "$(cat "$pf")" 2>/dev/null; then @@ -2748,7 +2373,6 @@ action_sysinfo() { [ -z "$b4_pid" ] && b4_pid=$(pgrep -f "${BINARY_NAME}" 2>/dev/null | head -1) if [ -n "$b4_pid" ]; then - # Memory usage if [ -f "/proc/${b4_pid}/status" ]; then mem_kb=$(awk '/^VmRSS:/ {print $2}' "/proc/${b4_pid}/status" 2>/dev/null) if [ -n "$mem_kb" ]; then @@ -2757,7 +2381,6 @@ action_sysinfo() { fi fi - # Uptime if [ -f "/proc/${b4_pid}/stat" ]; then proc_start=$(awk '{print $22}' "/proc/${b4_pid}/stat" 2>/dev/null) clk_tck=$(getconf CLK_TCK 2>/dev/null || echo 100) @@ -2782,12 +2405,11 @@ action_sysinfo() { log_detail "Service status" "${YELLOW}not running${NC}" fi - # Config-derived info (queue number, worker threads, geodat paths) if [ -n "$cfg_file" ] && command_exists jq; then - queue_num=$(jq -r '.system.queue_num // empty' "$cfg_file" 2>/dev/null) - workers=$(jq -r '.system.workers // empty' "$cfg_file" 2>/dev/null) - geosite=$(jq -r '.system.geo.sitedat_path // empty' "$cfg_file" 2>/dev/null) - geoip=$(jq -r '.system.geo.ipdat_path // empty' "$cfg_file" 2>/dev/null) + queue_num=$(jq -r '.system.queue_num // empty' "$cfg_file" 2>/dev/null) || true + workers=$(jq -r '.system.workers // empty' "$cfg_file" 2>/dev/null) || true + geosite=$(jq -r '.system.geo.sitedat_path // empty' "$cfg_file" 2>/dev/null) || true + geoip=$(jq -r '.system.geo.ipdat_path // empty' "$cfg_file" 2>/dev/null) || true [ -n "$queue_num" ] && [ "$queue_num" != "null" ] && log_detail "Queue number" "$queue_num" [ -n "$workers" ] && [ "$workers" != "null" ] && log_detail "Worker threads" "$workers" @@ -2804,7 +2426,6 @@ action_sysinfo() { log_sep - # --- Kernel modules --- echo "" log_info "Kernel modules:" for mod in xt_NFQUEUE nfnetlink_queue xt_connbytes xt_multiport nf_conntrack; do @@ -2817,7 +2438,6 @@ action_sysinfo() { fi done - # Functional test — does NFQUEUE actually work? if command_exists iptables; then if iptables -t mangle -C B4_TEST -j NFQUEUE --queue-num 0 2>/dev/null; then iptables -t mangle -D B4_TEST -j NFQUEUE --queue-num 0 2>/dev/null || true @@ -2833,10 +2453,8 @@ action_sysinfo() { fi fi - # --- Tools & dependencies --- echo "" log_info "Required tools:" - # Firewall if command_exists iptables; then printf " ${GREEN}found${NC} iptables\n" >&2 elif command_exists nft; then @@ -2844,7 +2462,6 @@ action_sysinfo() { else printf " ${RED}missing${NC} iptables or nft ${DIM}(firewall required)${NC}\n" >&2 fi - # Archive for tool in tar; do if command_exists "$tool"; then printf " ${GREEN}found${NC} %s\n" "$tool" >&2 @@ -2852,7 +2469,6 @@ action_sysinfo() { printf " ${RED}missing${NC} %s ${DIM}(required for install)${NC}\n" >&2 fi done - # curl/wget with HTTPS check if command_exists curl; then if curl -sI --max-time 5 "https://github.com" >/dev/null 2>&1; then printf " ${GREEN}found${NC} curl ${GREEN}(HTTPS OK)${NC}\n" >&2 @@ -2885,7 +2501,6 @@ action_sysinfo() { esac fi done - # Show secondary download tool if primary exists if command_exists curl && command_exists wget; then printf " ${GREEN}found${NC} wget ${DIM}(fallback downloader)${NC}\n" >&2 elif command_exists wget && ! command_exists curl; then @@ -2894,12 +2509,10 @@ action_sysinfo() { printf " ${DIM} ---${NC} wget ${DIM}(not needed, curl available)${NC}\n" >&2 fi - # Package manager echo "" detect_pkg_manager log_detail "Package manager" "${B4_PKG_MANAGER:-none}" - # --- Storage --- echo "" log_info "Storage:" _sysinfo_shown_devs="" @@ -2908,10 +2521,8 @@ action_sysinfo() { _sysinfo_show_storage "$dir" fi done - # Auto-discover mounted USB/external storage under /mnt for dir in /mnt/*; do [ -d "$dir" ] || continue - # Skip already-shown entries case "$dir" in /mnt/sda1) continue ;; esac _sysinfo_show_storage "$dir" done @@ -2919,16 +2530,18 @@ action_sysinfo() { echo "" log_sep - # Restore globals so sysinfo doesn't leak into wizard B4_PLATFORM="$_saved_platform" B4_BIN_DIR="$_saved_bin_dir" B4_DATA_DIR="$_saved_data_dir" + B4_CONFIG_FILE="$_saved_config_file" B4_SERVICE_TYPE="$_saved_service_type" + B4_SERVICE_DIR="$_saved_service_dir" + B4_SERVICE_NAME="$_saved_service_name" + B4_PKG_MANAGER="$_saved_pkg_manager" } _sysinfo_show_storage() { _dir="$1" - # Get underlying device to avoid showing the same filesystem twice _dev=$(df "$_dir" 2>/dev/null | tail -1 | awk '{print $1}') case "$_sysinfo_shown_devs" in *"|${_dev}|"*) return 0 ;; # already shown @@ -2939,17 +2552,11 @@ _sysinfo_show_storage() { [ ! -w "$_dir" ] && writable="ro" printf " %-20s %s available (%s)\n" "$_dir" "${avail:-?}" "$writable" >&2 } - - -# ======== main.sh ======== -# Main entry point — argument parsing and dispatch - main() { ACTION="install" VERSION="" FORCE_ARCH="" - # Parse arguments for arg in "$@"; do case "$arg" in --remove | --uninstall | -r) @@ -2978,11 +2585,10 @@ main() { esac done - # Dispatch case "$ACTION" in install) action_install "$VERSION" "$FORCE_ARCH" ;; remove) action_remove ;; - update) action_update "$FORCE_ARCH" ;; + update) action_update "$VERSION" "$FORCE_ARCH" ;; sysinfo) action_sysinfo ;; esac } diff --git a/installer/_build.sh b/installer/_build.sh index aba820fe..42a8a429 100755 --- a/installer/_build.sh +++ b/installer/_build.sh @@ -25,9 +25,13 @@ append() { cat "$file" >>"$OUTPUT" else echo "" >>"$OUTPUT" - echo "# ======== ${basename} ========" >>"$OUTPUT" - # Skip shebang line, strip leading blank lines - tail -n +2 "$file" | sed '/./,$!d' >>"$OUTPUT" + # Skip shebang, strip comment-only lines (outside heredocs), collapse leading blanks + tail -n +2 "$file" | awk ' + /<<[[:space:]]*EOF/ || /<<[[:space:]]*'\''EOF'\''/ { heredoc=1 } + heredoc { print; if (/^EOF$/) heredoc=0; next } + /^[[:space:]]*#/ { next } + { print } + ' | sed '/./,$!d' >>"$OUTPUT" fi echo "" >>"$OUTPUT" @@ -79,5 +83,8 @@ done # 7. Main entry point append "${SCRIPT_DIR}/main.sh" +# Collapse multiple consecutive blank lines +sed -i '/^$/N;/^\n$/d' "$OUTPUT" + chmod +x "$OUTPUT" echo "Built: $OUTPUT ($(wc -l <"$OUTPUT") lines)" diff --git a/installer/actions/install.sh b/installer/actions/install.sh index ede7a109..96d01604 100644 --- a/installer/actions/install.sh +++ b/installer/actions/install.sh @@ -10,8 +10,15 @@ action_install() { # --- Wizard --- if [ "$QUIET_MODE" -eq 1 ]; then WIZARD_MODE="auto" + # Preserve user overrides (--bin-dir, --data-dir) before platform defaults + _user_bin_dir="$B4_BIN_DIR" + _user_data_dir="$B4_DATA_DIR" platform_auto_detect platform_call info + # Re-apply user overrides + [ -n "$_user_bin_dir" ] && B4_BIN_DIR="$_user_bin_dir" + [ -n "$_user_data_dir" ] && B4_DATA_DIR="$_user_data_dir" + [ -n "$_user_data_dir" ] && B4_CONFIG_FILE="${_user_data_dir}/b4.json" B4_ARCH="${force_arch:-$(detect_architecture)}" detect_pkg_manager # Enable all default features in quiet mode @@ -174,8 +181,8 @@ _show_web_info() { protocol="http" if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then - web_port=$(jq -r '.system.web_server.port // 7000' "$B4_CONFIG_FILE" 2>/dev/null) - tls=$(jq -r '.system.web_server.tls_cert // ""' "$B4_CONFIG_FILE" 2>/dev/null) + web_port=$(jq -r '.system.web_server.port // 7000' "$B4_CONFIG_FILE" 2>/dev/null) || true + tls=$(jq -r '.system.web_server.tls_cert // ""' "$B4_CONFIG_FILE" 2>/dev/null) || true [ -n "$tls" ] && protocol="https" fi diff --git a/installer/actions/remove.sh b/installer/actions/remove.sh index 02dc0b15..57a04ed6 100644 --- a/installer/actions/remove.sh +++ b/installer/actions/remove.sh @@ -90,8 +90,10 @@ _remove_config_dirs() { for cfg_dir in "$B4_DATA_DIR" /etc/b4 /opt/etc/b4 /etc/storage/b4; do [ -z "$cfg_dir" ] && continue [ -d "$cfg_dir" ] || continue - # Skip if already checked - echo "$checked" | grep -q "$cfg_dir" && continue + # Skip if already checked (exact match to avoid substring false positives) + case " $checked " in + *" $cfg_dir "*) continue ;; + esac checked="${checked} ${cfg_dir}" # Show remaining contents diff --git a/installer/actions/sysinfo.sh b/installer/actions/sysinfo.sh index defe8d90..2330cca2 100644 --- a/installer/actions/sysinfo.sh +++ b/installer/actions/sysinfo.sh @@ -34,7 +34,11 @@ action_sysinfo() { _saved_platform="$B4_PLATFORM" _saved_bin_dir="$B4_BIN_DIR" _saved_data_dir="$B4_DATA_DIR" + _saved_config_file="$B4_CONFIG_FILE" _saved_service_type="$B4_SERVICE_TYPE" + _saved_service_dir="$B4_SERVICE_DIR" + _saved_service_name="$B4_SERVICE_NAME" + _saved_pkg_manager="$B4_PKG_MANAGER" platform_auto_detect 2>/dev/null || true if [ -n "$B4_PLATFORM" ]; then pname=$(platform_dispatch "$B4_PLATFORM" name 2>/dev/null) @@ -155,10 +159,10 @@ action_sysinfo() { # Config-derived info (queue number, worker threads, geodat paths) if [ -n "$cfg_file" ] && command_exists jq; then - queue_num=$(jq -r '.system.queue_num // empty' "$cfg_file" 2>/dev/null) - workers=$(jq -r '.system.workers // empty' "$cfg_file" 2>/dev/null) - geosite=$(jq -r '.system.geo.sitedat_path // empty' "$cfg_file" 2>/dev/null) - geoip=$(jq -r '.system.geo.ipdat_path // empty' "$cfg_file" 2>/dev/null) + queue_num=$(jq -r '.system.queue_num // empty' "$cfg_file" 2>/dev/null) || true + workers=$(jq -r '.system.workers // empty' "$cfg_file" 2>/dev/null) || true + geosite=$(jq -r '.system.geo.sitedat_path // empty' "$cfg_file" 2>/dev/null) || true + geoip=$(jq -r '.system.geo.ipdat_path // empty' "$cfg_file" 2>/dev/null) || true [ -n "$queue_num" ] && [ "$queue_num" != "null" ] && log_detail "Queue number" "$queue_num" [ -n "$workers" ] && [ "$workers" != "null" ] && log_detail "Worker threads" "$workers" @@ -294,7 +298,11 @@ action_sysinfo() { B4_PLATFORM="$_saved_platform" B4_BIN_DIR="$_saved_bin_dir" B4_DATA_DIR="$_saved_data_dir" + B4_CONFIG_FILE="$_saved_config_file" B4_SERVICE_TYPE="$_saved_service_type" + B4_SERVICE_DIR="$_saved_service_dir" + B4_SERVICE_NAME="$_saved_service_name" + B4_PKG_MANAGER="$_saved_pkg_manager" } _sysinfo_show_storage() { diff --git a/installer/actions/update.sh b/installer/actions/update.sh index 649df242..bb94030c 100644 --- a/installer/actions/update.sh +++ b/installer/actions/update.sh @@ -2,7 +2,8 @@ # Action: Update b4 to latest version action_update() { - force_arch="$1" + target_ver="$1" + force_arch="$2" check_root @@ -19,6 +20,7 @@ action_update() { # Find existing binary existing_bin="" for dir in "$B4_BIN_DIR" /usr/local/bin /usr/bin /usr/sbin /opt/bin /opt/sbin; do + [ -z "$dir" ] && continue if [ -f "${dir}/${BINARY_NAME}" ]; then existing_bin="${dir}/${BINARY_NAME}" B4_BIN_DIR="$dir" @@ -32,7 +34,9 @@ action_update() { fi # Get current version - current_ver=$("$existing_bin" --version 2>&1 | head -1) || current_ver="unknown" + _ver_full=$("$existing_bin" --version 2>&1) || _ver_full="" + current_ver=$(echo "$_ver_full" | grep -i "version" | head -1) + [ -z "$current_ver" ] && current_ver="unknown" log_info "Current: ${current_ver}" # Detect arch from existing binary or system @@ -42,10 +46,15 @@ action_update() { B4_ARCH=$(detect_architecture) fi - # Get latest version - log_info "Checking for updates..." - latest_ver=$(get_latest_version) - log_info "Latest: ${latest_ver}" + # Get target version + if [ -n "$target_ver" ]; then + latest_ver="$target_ver" + log_info "Target: ${latest_ver}" + else + log_info "Checking for updates..." + latest_ver=$(get_latest_version) + log_info "Latest: ${latest_ver}" + fi if [ "$current_ver" = "$latest_ver" ] || echo "$current_ver" | grep -q "$latest_ver"; then log_ok "Already up to date" diff --git a/installer/features/geoip.sh b/installer/features/geoip.sh index fe049949..01cb784f 100644 --- a/installer/features/geoip.sh +++ b/installer/features/geoip.sh @@ -41,7 +41,7 @@ feature_geoip_run() { # Check if config already has a geoip path if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then - existing=$(jq -r '.system.geo.ipdat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) + existing=$(jq -r '.system.geo.ipdat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$existing" ] && [ "$existing" != "null" ]; then save_dir=$(dirname "$existing") log_info "Found existing geoip path: $save_dir" diff --git a/installer/features/geosite.sh b/installer/features/geosite.sh index 626ff2d4..a67e8b68 100644 --- a/installer/features/geosite.sh +++ b/installer/features/geosite.sh @@ -40,7 +40,7 @@ feature_geosite_run() { # Check if config already has a geosite path if [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then - existing=$(jq -r '.system.geo.sitedat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) + existing=$(jq -r '.system.geo.sitedat_path // empty' "$B4_CONFIG_FILE" 2>/dev/null) || true if [ -n "$existing" ] && [ "$existing" != "null" ]; then save_dir=$(dirname "$existing") log_info "Found existing geosite path: $save_dir" @@ -121,7 +121,7 @@ _geo_remove_file() { for cfg in "$B4_CONFIG_FILE" /etc/b4/b4.json /opt/etc/b4/b4.json; do [ -f "$cfg" ] || continue if command_exists jq; then - fpath=$(jq -r ".system.geo.${config_key} // empty" "$cfg" 2>/dev/null) + fpath=$(jq -r ".system.geo.${config_key} // empty" "$cfg" 2>/dev/null) || true if [ -n "$fpath" ] && [ -f "$fpath" ]; then log_info "Found ${filename}: ${fpath}" if [ "$QUIET_MODE" -eq 1 ] || confirm "Remove ${filename}?" "y"; then diff --git a/installer/lib/wizard.sh b/installer/lib/wizard.sh index 67f201b6..a0ef6c39 100644 --- a/installer/lib/wizard.sh +++ b/installer/lib/wizard.sh @@ -50,8 +50,13 @@ wizard_auto_detect() { exit 1 fi - # 2. Load platform defaults + # 2. Load platform defaults (preserve user overrides) + _user_bin_dir="$B4_BIN_DIR" + _user_data_dir="$B4_DATA_DIR" platform_call info + [ -n "$_user_bin_dir" ] && B4_BIN_DIR="$_user_bin_dir" + [ -n "$_user_data_dir" ] && B4_DATA_DIR="$_user_data_dir" + [ -n "$_user_data_dir" ] && B4_CONFIG_FILE="${_user_data_dir}/b4.json" # 3. Detect architecture B4_ARCH=$(detect_architecture) diff --git a/installer/main.sh b/installer/main.sh index 27a80bc6..26b0fcf7 100644 --- a/installer/main.sh +++ b/installer/main.sh @@ -39,7 +39,7 @@ main() { case "$ACTION" in install) action_install "$VERSION" "$FORCE_ARCH" ;; remove) action_remove ;; - update) action_update "$FORCE_ARCH" ;; + update) action_update "$VERSION" "$FORCE_ARCH" ;; sysinfo) action_sysinfo ;; esac }