Verify Sing-box before the connection
This commit is contained in:
parent
35d94908f4
commit
ed0de4b2d9
14 changed files with 27 additions and 941 deletions
|
|
@ -1,102 +1,54 @@
|
||||||
class CommandNotFoundError(OSError):
|
class CommandNotFoundError(OSError):
|
||||||
|
|
||||||
def __init__(self, subject):
|
def __init__(self, subject):
|
||||||
|
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
super().__init__(f"Command '{subject}' could not be found.")
|
super().__init__(f"Command '{subject}' could not be found.")
|
||||||
|
|
||||||
|
|
||||||
class UnknownClientPathError(Exception):
|
class UnknownClientPathError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UnknownClientVersionError(Exception):
|
class UnknownClientVersionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UnknownConnectionTypeError(Exception):
|
class UnknownConnectionTypeError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UnknownTimeZoneError(Exception):
|
class UnknownTimeZoneError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTerminationError(Exception):
|
class ConnectionTerminationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PolicyAssignmentError(Exception):
|
class PolicyAssignmentError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PolicyInstatementError(Exception):
|
class PolicyInstatementError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PolicyRevocationError(Exception):
|
class PolicyRevocationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProfileDeletionError(Exception):
|
class ProfileDeletionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProfileModificationError(Exception):
|
class ProfileModificationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProfileStateConflictError(Exception):
|
class ProfileStateConflictError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProfileActivationError(Exception):
|
class ProfileActivationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProfileDeactivationError(Exception):
|
class ProfileDeactivationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedApplicationVersionError(Exception):
|
class UnsupportedApplicationVersionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ApplicationAlreadyInstalledError(Exception):
|
class ApplicationAlreadyInstalledError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MissingLocationError(Exception):
|
class MissingLocationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MissingSubscriptionError(Exception):
|
class MissingSubscriptionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidSubscriptionError(Exception):
|
class InvalidSubscriptionError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvoiceNotFoundError(Exception):
|
class InvoiceNotFoundError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvoiceExpiredError(Exception):
|
class InvoiceExpiredError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvoicePaymentFailedError(Exception):
|
class InvoicePaymentFailedError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ConnectionUnprotectedError(Exception):
|
class ConnectionUnprotectedError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileIntegrityError(Exception):
|
class FileIntegrityError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EndpointVerificationError(Exception):
|
class EndpointVerificationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
class SingboxNotInstalledException(Exception):
|
||||||
|
pass
|
||||||
|
|
@ -44,9 +44,10 @@ class ConnectionController:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
|
def establish_connection(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None):
|
||||||
|
|
||||||
from core.controllers.ConnectionController import ConnectionController
|
from core.controllers.ConnectionController import ConnectionController
|
||||||
|
|
||||||
|
ConnectionController.verify_singbox_installation()
|
||||||
|
|
||||||
connection = profile.connection
|
connection = profile.connection
|
||||||
|
|
||||||
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
|
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
|
||||||
|
|
@ -543,3 +544,25 @@ class ConnectionController:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def verify_singbox_installation():
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from core.Errors import SingboxNotInstalledException
|
||||||
|
|
||||||
|
singbox_binary = '/usr/bin/sing-box'
|
||||||
|
singbox_wrapper = '/usr/local/bin/hydraveil-singbox'
|
||||||
|
|
||||||
|
if not os.path.exists(singbox_binary):
|
||||||
|
raise SingboxNotInstalledException(f'sing-box binary not found at {singbox_binary}')
|
||||||
|
|
||||||
|
if not os.path.exists(singbox_wrapper):
|
||||||
|
raise SingboxNotInstalledException(f'sing-box wrapper not found at {singbox_wrapper}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run([singbox_binary, 'version'], capture_output=True, timeout=5, check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise SingboxNotInstalledException('sing-box binary check failed')
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise SingboxNotInstalledException('sing-box verification timeout')
|
||||||
98
install.sh
98
install.sh
|
|
@ -1,98 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# install.sh — hydra-veil core installer
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
LIB_DIR="$SCRIPT_DIR/lib"
|
|
||||||
|
|
||||||
trap 'echo ""; echo " [ERROR] Installer failed at line $LINENO — check: cat $LOG_FILE" >&2' ERR
|
|
||||||
|
|
||||||
for _lib in ui log input network steps; do
|
|
||||||
_path="$LIB_DIR/${_lib}.sh"
|
|
||||||
if [ ! -f "$_path" ]; then
|
|
||||||
echo "[ERROR] Missing lib: $_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
source "$_path"
|
|
||||||
done
|
|
||||||
|
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
|
||||||
--no-log) LOG_ENABLED="false" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
_log_init
|
|
||||||
|
|
||||||
clear
|
|
||||||
print_header "HYDRA-VEIL CORE INSTALLER" "v2.0.0"
|
|
||||||
printf " ${DIM}%s${RESET}\n" "Simplified Privacy"
|
|
||||||
printf " ${DIM}%s${RESET}\n" "$(date '+%Y-%m-%d %H:%M:%S') — $(uname -n)"
|
|
||||||
print_divider
|
|
||||||
|
|
||||||
print_section "Pre-flight"
|
|
||||||
log_info "User: ${USER}"
|
|
||||||
log_info "Home: ${HOME}"
|
|
||||||
log_info "Install dir: ${HV_BASE}"
|
|
||||||
require_internet
|
|
||||||
print_divider
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
label_info "hydra-veil se instalará en:"
|
|
||||||
printf " ${BCYAN}%s${RESET}\n\n" "$HV_BASE"
|
|
||||||
label_warn "sudo es requerido para sing-box, wrapper y sudoers."
|
|
||||||
echo ""
|
|
||||||
if ! ask_confirm "Continuar con la instalación?"; then
|
|
||||||
log_warn "Instalación cancelada."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
print_divider
|
|
||||||
|
|
||||||
step_preflight
|
|
||||||
step_repos
|
|
||||||
step_python_env
|
|
||||||
step_singbox
|
|
||||||
step_wrapper
|
|
||||||
step_sudoers
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_divider
|
|
||||||
printf "${BGREEN}"
|
|
||||||
cat << 'EOF'
|
|
||||||
_
|
|
||||||
_ _|_| _
|
|
||||||
| \_ _| | _/ |
|
|
||||||
| \__|___|__/ |
|
|
||||||
| | _ _ | |
|
|
||||||
| || | | || |
|
|
||||||
\_ || | | || _/
|
|
||||||
\_||_| |_||_/
|
|
||||||
______| |______
|
|
||||||
_/ \_
|
|
||||||
_/ S I M P L I F I E D \_
|
|
||||||
_/ P R I V A C Y \_
|
|
||||||
| _____ _____ |
|
|
||||||
| _/ |_ _| \_ |
|
|
||||||
|_/ |_ _| \_|
|
|
||||||
\_____/
|
|
||||||
|
|
||||||
EOF
|
|
||||||
printf "${RESET}"
|
|
||||||
print_divider
|
|
||||||
echo ""
|
|
||||||
label_ok "hydra-veil instalado correctamente"
|
|
||||||
label_ok "Todos los steps completados"
|
|
||||||
echo ""
|
|
||||||
label_info "Activar entorno:"
|
|
||||||
printf " ${CYAN}source %s/core/.venv/bin/activate${RESET}\n" "$HV_BASE"
|
|
||||||
echo ""
|
|
||||||
label_info "CLI disponible:"
|
|
||||||
printf " ${CYAN}python -m cli${RESET}\n"
|
|
||||||
echo ""
|
|
||||||
label_info "Wrapper disponible:"
|
|
||||||
printf " ${CYAN}sudo /usr/local/bin/hydraveil-singbox <config.json> start${RESET}\n"
|
|
||||||
echo ""
|
|
||||||
log_file_path
|
|
||||||
echo ""
|
|
||||||
print_divider
|
|
||||||
echo ""
|
|
||||||
32
lib/input.sh
32
lib/input.sh
|
|
@ -1,32 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
ask_input() {
|
|
||||||
local label="$1"
|
|
||||||
local default="${2:-}"
|
|
||||||
local prompt_default=""
|
|
||||||
[ -n "$default" ] && prompt_default=" ${DIM}[${default}]${RESET}"
|
|
||||||
|
|
||||||
printf " ${BCYAN}?${RESET} ${BWHITE}%s${RESET}%b " "$label" "$prompt_default"
|
|
||||||
read -r INPUT_RESULT
|
|
||||||
[ -z "$INPUT_RESULT" ] && INPUT_RESULT="$default"
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_confirm() {
|
|
||||||
local label="$1"
|
|
||||||
local default="${2:-y}"
|
|
||||||
local hint
|
|
||||||
if [ "$default" = "y" ]; then hint="${BGREEN}Y${RESET}${DIM}/n${RESET}"; else hint="${DIM}y/${RESET}${BRED}N${RESET}"; fi
|
|
||||||
|
|
||||||
printf " ${BYELLOW}?${RESET} ${BWHITE}%s${RESET} [%b] " "$label" "$hint"
|
|
||||||
read -r _confirm
|
|
||||||
_confirm="${_confirm:-$default}"
|
|
||||||
case "${_confirm,,}" in
|
|
||||||
y|yes) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_pause() {
|
|
||||||
local msg="${1:-Press ENTER to continue...}"
|
|
||||||
printf " ${DIM}%s${RESET}" "$msg"
|
|
||||||
read -r
|
|
||||||
}
|
|
||||||
52
lib/log.sh
52
lib/log.sh
|
|
@ -1,52 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
LOG_FILE="${LOG_FILE:-/tmp/hydra-veil-core.log}"
|
|
||||||
LOG_ENABLED="${LOG_ENABLED:-true}"
|
|
||||||
|
|
||||||
_log_init() {
|
|
||||||
if [ "$LOG_ENABLED" = "true" ]; then
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
|
||||||
echo "──────────────────────────────────────────" >> "$LOG_FILE"
|
|
||||||
echo " LOG START: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
|
|
||||||
echo "──────────────────────────────────────────" >> "$LOG_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_log_write() {
|
|
||||||
local level="$1"
|
|
||||||
local msg="$2"
|
|
||||||
if [ "$LOG_ENABLED" = "true" ]; then
|
|
||||||
printf "[%s] [%s] %s\n" "$(date '+%H:%M:%S')" "$level" "$msg" >> "$LOG_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
label_info "$1"
|
|
||||||
_log_write "INFO " "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_ok() {
|
|
||||||
label_ok "$1"
|
|
||||||
_log_write "OK " "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warn() {
|
|
||||||
label_warn "$1"
|
|
||||||
_log_write "WARN " "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
label_error "$1"
|
|
||||||
_log_write "ERROR" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_step() {
|
|
||||||
local msg="$1"
|
|
||||||
local current="$2"
|
|
||||||
local total="$3"
|
|
||||||
label_step "$msg" "$current" "$total"
|
|
||||||
_log_write "STEP " "[$current/$total] $msg"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_file_path() {
|
|
||||||
printf " ${DIM}Log file: ${CYAN}%s${RESET}\n" "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
NET_CHECK_HOST="${NET_CHECK_HOST:-8.8.8.8}"
|
|
||||||
NET_CHECK_TIMEOUT="${NET_CHECK_TIMEOUT:-3}"
|
|
||||||
|
|
||||||
check_internet() {
|
|
||||||
local host="${1:-$NET_CHECK_HOST}"
|
|
||||||
if ping -c 1 -W "$NET_CHECK_TIMEOUT" "$host" &>/dev/null; then
|
|
||||||
label_connected "Internet reachable ${DIM}(${host})${RESET}"
|
|
||||||
_log_write "NET " "Connected: $host"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
label_disconnect "No internet connection ${DIM}(${host})${RESET}"
|
|
||||||
_log_write "NET " "Disconnected: $host"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_host() {
|
|
||||||
local host="$1"
|
|
||||||
local label="${2:-$host}"
|
|
||||||
if ping -c 1 -W "$NET_CHECK_TIMEOUT" "$host" &>/dev/null 2>&1; then
|
|
||||||
label_connected "$label"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
label_disconnect "$label"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_url() {
|
|
||||||
local url="$1"
|
|
||||||
local label="${2:-$url}"
|
|
||||||
local http_code
|
|
||||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
||||||
--max-time "$NET_CHECK_TIMEOUT" "$url" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$http_code" =~ ^[23] ]]; then
|
|
||||||
label_connected "$label ${DIM}(HTTP ${http_code})${RESET}"
|
|
||||||
_log_write "NET " "URL OK [$http_code]: $url"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
label_disconnect "$label ${DIM}(HTTP ${http_code})${RESET}"
|
|
||||||
_log_write "NET " "URL FAIL [$http_code]: $url"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_internet() {
|
|
||||||
if ! check_internet; then
|
|
||||||
log_error "Internet connection required. Aborting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
57
lib/steps.sh
57
lib/steps.sh
|
|
@ -1,57 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
TOTAL_STEPS=6
|
|
||||||
|
|
||||||
HV_BASE="$HOME/.local/share/hydra-veil"
|
|
||||||
HV_CORE="$HV_BASE/core"
|
|
||||||
HV_CLI="$HV_BASE/cli"
|
|
||||||
HV_GUI="$HV_BASE/gui"
|
|
||||||
HV_ESSENTIALS="$HV_BASE/essentials"
|
|
||||||
HV_VENV="$HV_CORE/.venv"
|
|
||||||
|
|
||||||
INSTALL_DIR="$HOME"
|
|
||||||
HV_DATA_DIR="$HOME/data/hydra-veil"
|
|
||||||
HV_CONFIG_DIR="$HOME/.config/hydra-veil"
|
|
||||||
HV_CACHE_DIR="$HOME/.cache/hydra-veil"
|
|
||||||
HV_STATE_DIR="$HOME/.state/hydra-veil"
|
|
||||||
|
|
||||||
SINGBOX_BIN="/usr/bin/sing-box"
|
|
||||||
SINGBOX_WRAPPER="/usr/local/bin/hydraveil-singbox"
|
|
||||||
SINGBOX_VERSION="1.13.5"
|
|
||||||
SINGBOX_CONFIG_DIR="$HV_DATA_DIR/configs"
|
|
||||||
SINGBOX_PID_FILE="$HV_DATA_DIR/runtime/singbox.pid"
|
|
||||||
SINGBOX_LOG_FILE="$HV_DATA_DIR/runtime/singbox.log"
|
|
||||||
|
|
||||||
SUDOERS_PATH="/etc/sudoers.d/hydraveil"
|
|
||||||
|
|
||||||
declare -A REPO_URLS=(
|
|
||||||
["core"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-core"
|
|
||||||
["cli"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-cli"
|
|
||||||
["essentials"]="https://git.simplifiedprivacy.com/Support/sp-essentials"
|
|
||||||
["gui"]="https://git.simplifiedprivacy.com/Support/sp-hydra-veil-gui"
|
|
||||||
)
|
|
||||||
|
|
||||||
declare -A REPO_BRANCHES=(
|
|
||||||
["core"]="deploy"
|
|
||||||
["cli"]="zenaku"
|
|
||||||
["essentials"]="master"
|
|
||||||
["gui"]="master"
|
|
||||||
)
|
|
||||||
|
|
||||||
declare -A REPO_DIRS=(
|
|
||||||
["core"]="$HV_CORE"
|
|
||||||
["cli"]="$HV_CLI"
|
|
||||||
["essentials"]="$HV_ESSENTIALS"
|
|
||||||
["gui"]="$HV_GUI"
|
|
||||||
)
|
|
||||||
|
|
||||||
_STEPS_DIR="$SCRIPT_DIR/lib/steps"
|
|
||||||
|
|
||||||
for _step in preflight repos python_env singbox wrapper sudoers; do
|
|
||||||
_step_path="${_STEPS_DIR}/${_step}.sh"
|
|
||||||
if [ ! -f "$_step_path" ]; then
|
|
||||||
echo "[ERROR] Missing step: $_step_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
source "$_step_path"
|
|
||||||
done
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# lib/steps/preflight.sh — verificaciones previas
|
|
||||||
step_preflight() {
|
|
||||||
print_section "Pre-flight"
|
|
||||||
log_step "System checks" 1 $TOTAL_STEPS
|
|
||||||
|
|
||||||
_preflight_user
|
|
||||||
_preflight_python
|
|
||||||
_preflight_git
|
|
||||||
}
|
|
||||||
|
|
||||||
_preflight_user() {
|
|
||||||
log_info "Running as: ${USER} (uid=$(id -u))"
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
|
||||||
log_error "No correr el installer como root"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log_ok "User OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
_preflight_python() {
|
|
||||||
local found=""
|
|
||||||
for candidate in python3.12 python3.13; do
|
|
||||||
if command -v "$candidate" &>/dev/null; then
|
|
||||||
found="$candidate"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$found" ]; then
|
|
||||||
log_error "Se necesita Python 3.12 o 3.13"
|
|
||||||
log_info "Arch: sudo pacman -S python312"
|
|
||||||
log_info "Ubuntu: sudo apt install python3.12"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
PYTHON_BIN="$found"
|
|
||||||
log_ok "Python: $($PYTHON_BIN --version 2>&1)"
|
|
||||||
}
|
|
||||||
|
|
||||||
_preflight_git() {
|
|
||||||
if ! command -v git &>/dev/null; then
|
|
||||||
log_error "git no encontrado"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log_ok "git: $(git --version)"
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# lib/steps/python_env.sh — venv + pip install editable
|
|
||||||
|
|
||||||
step_python_env() {
|
|
||||||
print_section "Python Environment"
|
|
||||||
log_step "Virtual environment + dependencies" 3 $TOTAL_STEPS
|
|
||||||
|
|
||||||
_create_venv
|
|
||||||
_pip_install
|
|
||||||
}
|
|
||||||
|
|
||||||
_create_venv() {
|
|
||||||
if [ -d "$HV_VENV" ]; then
|
|
||||||
log_warn "venv ya existe: ${HV_VENV}"
|
|
||||||
if ask_confirm "Recrear venv?"; then
|
|
||||||
rm -rf "$HV_VENV"
|
|
||||||
else
|
|
||||||
log_skip "venv existente conservado"
|
|
||||||
source "$HV_VENV/bin/activate"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
start_spinner "Creando virtual environment (${PYTHON_BIN})..."
|
|
||||||
"$PYTHON_BIN" -m venv "$HV_VENV"
|
|
||||||
stop_spinner "venv creado → ${HV_VENV}"
|
|
||||||
|
|
||||||
source "$HV_VENV/bin/activate"
|
|
||||||
|
|
||||||
start_spinner "Actualizando pip..."
|
|
||||||
pip install --quiet --upgrade pip
|
|
||||||
stop_spinner "pip actualizado"
|
|
||||||
}
|
|
||||||
|
|
||||||
_pip_install() {
|
|
||||||
source "$HV_VENV/bin/activate"
|
|
||||||
|
|
||||||
if [ -f "$HV_GUI/pyproject.toml" ]; then
|
|
||||||
sed -i 's/sp-hydra-veil-core == [0-9.]*/sp-hydra-veil-core >= 2.3.0/' \
|
|
||||||
"$HV_GUI/pyproject.toml" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
for name in essentials core cli gui; do
|
|
||||||
local dir
|
|
||||||
case "$name" in
|
|
||||||
essentials) dir="$HV_ESSENTIALS" ;;
|
|
||||||
core) dir="$HV_CORE" ;;
|
|
||||||
cli) dir="$HV_CLI" ;;
|
|
||||||
gui) dir="$HV_GUI" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ ! -d "$dir" ]; then
|
|
||||||
log_warn "${name} no encontrado en ${dir} — omitiendo"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
start_spinner "Instalando ${name} (editable)..."
|
|
||||||
if pip install --quiet -e "$dir" 2>/tmp/pip_err_${name}; then
|
|
||||||
stop_spinner "${name} instalado"
|
|
||||||
else
|
|
||||||
stop_spinner
|
|
||||||
log_error "pip install falló para ${name}:"
|
|
||||||
tail -5 /tmp/pip_err_${name}
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
step_repos() {
|
|
||||||
print_section "Repositories"
|
|
||||||
log_step "Cloning repositories & configuring .env" 2 $TOTAL_STEPS
|
|
||||||
|
|
||||||
mkdir -p "$HV_BASE"
|
|
||||||
chmod 700 "$HV_BASE"
|
|
||||||
|
|
||||||
_clone_repos
|
|
||||||
_setup_env
|
|
||||||
}
|
|
||||||
|
|
||||||
_clone_repos() {
|
|
||||||
for name in essentials cli gui core; do
|
|
||||||
local url="${REPO_URLS[$name]}"
|
|
||||||
local branch="${REPO_BRANCHES[$name]}"
|
|
||||||
local dest="${REPO_DIRS[$name]}"
|
|
||||||
log_ok "HIJUEPUTA"
|
|
||||||
|
|
||||||
if [ -d "$dest" ]; then
|
|
||||||
log_warn "Repo ya existe: ${dest}"
|
|
||||||
rm -rf "$dest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git clone --branch "$branch" "$url" "$dest" 2>&1; then
|
|
||||||
log_error "git clone FAILED: $name"
|
|
||||||
log_error " URL: $url"
|
|
||||||
log_error " Branch: $branch"
|
|
||||||
log_error " Dest: $dest"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 700 "$dest"
|
|
||||||
log_ok "${name} clonado → ${dest}"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
_setup_env() {
|
|
||||||
local env_file="$HV_CORE/.env"
|
|
||||||
local example_file="$HV_CORE/.env.example"
|
|
||||||
|
|
||||||
if [ ! -f "$example_file" ]; then
|
|
||||||
log_error ".env.example no encontrado: ${example_file}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$env_file" ]; then
|
|
||||||
log_warn ".env ya existe — sobreescribiendo"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf " ${DIM}Fill in the variables. ENTER = leave empty.${RESET}\n"
|
|
||||||
print_divider
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
select_option \
|
|
||||||
"APP_ENV — application environment" \
|
|
||||||
"local development / testing" \
|
|
||||||
"production production / real domain"
|
|
||||||
|
|
||||||
case "$SELECT_RESULT" in
|
|
||||||
0) _app_env="local" ;;
|
|
||||||
1) _app_env="production" ;;
|
|
||||||
*) _app_env="production" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf " ${BCYAN}┌─${RESET} ${BWHITE}SP_API_BASE_URL_PRODUCTION${RESET} ${DIM}e.g. https://fake.simplifiedprivacy.org/api/v1${RESET}\n"
|
|
||||||
printf " ${BCYAN}›${RESET} "
|
|
||||||
read -r _api_url < /dev/tty
|
|
||||||
|
|
||||||
cat > "$env_file" << ENVEOF
|
|
||||||
APP_ENV=${_app_env}
|
|
||||||
SP_API_BASE_URL_LOCAL=http://simplifiedprivacy.test/api/v1
|
|
||||||
SP_API_BASE_URL_PRODUCTION=${_api_url}
|
|
||||||
HV_CORE_HOME=${HV_CORE}
|
|
||||||
INSTALL_DIR=${HOME}
|
|
||||||
CORE_DIR=${HV_CORE}
|
|
||||||
ENVEOF
|
|
||||||
|
|
||||||
chmod 600 "$env_file"
|
|
||||||
|
|
||||||
log_ok "APP_ENV=${_app_env}"
|
|
||||||
[ -n "$_api_url" ] && log_ok "SP_API_BASE_URL_PRODUCTION=${_api_url}" || label_skip "SP_API_BASE_URL_PRODUCTION — vacío"
|
|
||||||
log_ok "INSTALL_DIR=${HOME}"
|
|
||||||
log_ok ".env escrito → ${env_file}"
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# lib/steps/singbox.sh — instalar/verificar sing-box
|
|
||||||
|
|
||||||
step_singbox() {
|
|
||||||
print_section "sing-box"
|
|
||||||
log_step "Checking sing-box binary" 4 $TOTAL_STEPS
|
|
||||||
|
|
||||||
local found=""
|
|
||||||
for candidate in /usr/bin/sing-box /usr/local/bin/sing-box /bin/sing-box; do
|
|
||||||
[ -x "$candidate" ] && found="$candidate" && break
|
|
||||||
done
|
|
||||||
[ -z "$found" ] && found=$(command -v sing-box 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$found" ]; then
|
|
||||||
local ver
|
|
||||||
ver=$("$found" version 2>/dev/null | head -1 || echo "unknown")
|
|
||||||
log_ok "Encontrado: ${found} (${ver})"
|
|
||||||
if [ "$found" != "$SINGBOX_BIN" ]; then
|
|
||||||
sudo ln -sf "$found" "$SINGBOX_BIN"
|
|
||||||
log_ok "Symlink → ${SINGBOX_BIN}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
_install_singbox
|
|
||||||
fi
|
|
||||||
|
|
||||||
# setcap — sing-box crea TUN sin necesitar root completo
|
|
||||||
if command -v setcap &>/dev/null; then
|
|
||||||
sudo setcap cap_net_admin,cap_net_raw+ep "$SINGBOX_BIN"
|
|
||||||
log_ok "setcap cap_net_admin,cap_net_raw+ep → ${SINGBOX_BIN}"
|
|
||||||
else
|
|
||||||
log_warn "setcap no disponible — sing-box necesitará sudo para TUN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_ok "sing-box listo → ${SINGBOX_BIN}"
|
|
||||||
}
|
|
||||||
|
|
||||||
_install_singbox() {
|
|
||||||
log_info "sing-box no encontrado — instalando v${SINGBOX_VERSION}..."
|
|
||||||
|
|
||||||
local arch
|
|
||||||
arch=$(uname -m)
|
|
||||||
case "$arch" in
|
|
||||||
x86_64) arch="amd64" ;;
|
|
||||||
aarch64) arch="arm64" ;;
|
|
||||||
*)
|
|
||||||
log_error "Arquitectura no soportada: ${arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
local url="https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/sing-box-${SINGBOX_VERSION}-linux-${arch}.tar.gz"
|
|
||||||
local tmp_tar="/tmp/sing-box-$$.tar.gz"
|
|
||||||
local tmp_dir="/tmp/sing-box-$$"
|
|
||||||
|
|
||||||
start_spinner "Descargando sing-box v${SINGBOX_VERSION}..."
|
|
||||||
if wget -q --timeout=60 "$url" -O "$tmp_tar"; then
|
|
||||||
stop_spinner "Descarga completa"
|
|
||||||
else
|
|
||||||
stop_spinner
|
|
||||||
rm -f "$tmp_tar"
|
|
||||||
log_error "Descarga falló: ${url}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
start_spinner "Extrayendo..."
|
|
||||||
mkdir -p "$tmp_dir"
|
|
||||||
tar -xzf "$tmp_tar" -C "$tmp_dir" --strip-components=1
|
|
||||||
stop_spinner "Extraído"
|
|
||||||
|
|
||||||
sudo cp "$tmp_dir/sing-box" "$SINGBOX_BIN"
|
|
||||||
sudo chmod 755 "$SINGBOX_BIN"
|
|
||||||
rm -rf "$tmp_tar" "$tmp_dir"
|
|
||||||
log_ok "sing-box instalado: $($SINGBOX_BIN version | head -1)"
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# lib/steps/sudoers.sh — sudoers + directorios XDG
|
|
||||||
|
|
||||||
step_sudoers() {
|
|
||||||
print_section "Permissions & Directories"
|
|
||||||
log_step "sudoers + XDG directories" 6 $TOTAL_STEPS
|
|
||||||
|
|
||||||
_setup_sudoers
|
|
||||||
_create_directories
|
|
||||||
}
|
|
||||||
|
|
||||||
_setup_sudoers() {
|
|
||||||
local tmp
|
|
||||||
tmp=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$tmp" << EOF
|
|
||||||
# hydraveil — generado por installer $(date '+%Y-%m-%d')
|
|
||||||
# NO editar manualmente
|
|
||||||
Defaults!${SINGBOX_WRAPPER} !requiretty
|
|
||||||
${USER} ALL=(root) NOPASSWD: ${SINGBOX_WRAPPER}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if ! sudo visudo -c -f "$tmp" &>/dev/null; then
|
|
||||||
rm -f "$tmp"
|
|
||||||
log_error "sudoers inválido — abortando para evitar lockout"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo cp "$tmp" "$SUDOERS_PATH"
|
|
||||||
sudo chmod 440 "$SUDOERS_PATH"
|
|
||||||
rm -f "$tmp"
|
|
||||||
log_ok "sudoers instalado → ${SUDOERS_PATH}"
|
|
||||||
}
|
|
||||||
|
|
||||||
_create_directories() {
|
|
||||||
local -a dirs=(
|
|
||||||
# XDG data
|
|
||||||
"$HV_DATA_DIR/configs"
|
|
||||||
"$HV_DATA_DIR/runtime"
|
|
||||||
"$HV_DATA_DIR/applications"
|
|
||||||
"$HV_DATA_DIR/profiles"
|
|
||||||
"$HV_DATA_DIR/ticket_data"
|
|
||||||
"$HV_DATA_DIR/incidents"
|
|
||||||
# XDG config
|
|
||||||
"$HV_CONFIG_DIR/profiles"
|
|
||||||
"$HV_CONFIG_DIR/ticketing"
|
|
||||||
# XDG state
|
|
||||||
"$HV_STATE_DIR/sessions"
|
|
||||||
"$HV_STATE_DIR/tor"
|
|
||||||
# XDG cache
|
|
||||||
"$HV_CACHE_DIR"
|
|
||||||
)
|
|
||||||
|
|
||||||
for dir in "${dirs[@]}"; do
|
|
||||||
mkdir -p "$dir"
|
|
||||||
chmod 700 "$dir"
|
|
||||||
log_ok "→ ${dir}"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Log file con permisos de usuario ANTES de que el wrapper lo toque
|
|
||||||
local log_file="$SINGBOX_LOG_FILE"
|
|
||||||
if [ ! -f "$log_file" ]; then
|
|
||||||
touch "$log_file"
|
|
||||||
chmod 600 "$log_file"
|
|
||||||
log_ok "Log file creado → ${log_file}"
|
|
||||||
else
|
|
||||||
log_ok "Log file ya existe → ${log_file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# lib/steps/wrapper.sh — generar wrapper de privilegios para sing-box
|
|
||||||
|
|
||||||
step_wrapper() {
|
|
||||||
print_section "Security Wrapper"
|
|
||||||
log_step "Installing hydraveil-singbox wrapper" 5 $TOTAL_STEPS
|
|
||||||
|
|
||||||
[ -f "$SINGBOX_WRAPPER" ] && sudo rm -f "$SINGBOX_WRAPPER" && log_ok "Wrapper anterior eliminado"
|
|
||||||
|
|
||||||
sudo tee "$SINGBOX_WRAPPER" > /dev/null << WRAPPER
|
|
||||||
#!/bin/bash
|
|
||||||
# hydraveil-singbox — wrapper de privilegios para sing-box
|
|
||||||
# Generado por el installer. NO editar manualmente.
|
|
||||||
# Solo este binario tiene NOPASSWD en sudoers.
|
|
||||||
#
|
|
||||||
# Uso:
|
|
||||||
# sudo hydraveil-singbox <config.json> start
|
|
||||||
# sudo hydraveil-singbox "" stop
|
|
||||||
# sudo hydraveil-singbox "" log
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
CONFIG="\${1:-}"
|
|
||||||
ACTION="\${2:-}"
|
|
||||||
|
|
||||||
# ── Paths fijos (rutas XDG correctas — INSTALL_DIR=\$HOME) ────────────────────
|
|
||||||
SINGBOX_BIN="${SINGBOX_BIN}"
|
|
||||||
PID_FILE="${SINGBOX_PID_FILE}"
|
|
||||||
LOG_FILE="${SINGBOX_LOG_FILE}"
|
|
||||||
CONFIG_DIR="${SINGBOX_CONFIG_DIR}"
|
|
||||||
STOP_TIMEOUT=8
|
|
||||||
|
|
||||||
# ── Validar acción ────────────────────────────────────────────────────────────
|
|
||||||
if [[ "\$ACTION" != "start" && "\$ACTION" != "stop" && "\$ACTION" != "log" ]]; then
|
|
||||||
echo "Error: acción inválida '\$ACTION'. Uso: start|stop|log" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
||||||
_pid_running() {
|
|
||||||
local pid="\$1"
|
|
||||||
[[ "\$pid" =~ ^[0-9]+$ ]] && kill -0 "\$pid" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
_delete_tun() {
|
|
||||||
if ip link show tun0 &>/dev/null 2>&1; then
|
|
||||||
ip link delete tun0 2>/dev/null || true
|
|
||||||
echo "[wrapper] tun0 eliminada" >> "\$LOG_FILE" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_kill_by_pidfile() {
|
|
||||||
[ -f "\$PID_FILE" ] || return 0
|
|
||||||
local pid
|
|
||||||
pid=\$(cat "\$PID_FILE" 2>/dev/null || echo "")
|
|
||||||
if ! _pid_running "\$pid"; then
|
|
||||||
rm -f "\$PID_FILE"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
kill -TERM "\$pid" 2>/dev/null || true
|
|
||||||
echo "[wrapper] SIGTERM → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true
|
|
||||||
local elapsed=0
|
|
||||||
while _pid_running "\$pid" && (( elapsed < STOP_TIMEOUT * 2 )); do
|
|
||||||
sleep 0.5
|
|
||||||
(( elapsed++ )) || true
|
|
||||||
done
|
|
||||||
if _pid_running "\$pid"; then
|
|
||||||
kill -KILL "\$pid" 2>/dev/null || true
|
|
||||||
echo "[wrapper] SIGKILL → PID \$pid" >> "\$LOG_FILE" 2>/dev/null || true
|
|
||||||
sleep 0.3
|
|
||||||
fi
|
|
||||||
rm -f "\$PID_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── log ───────────────────────────────────────────────────────────────────────
|
|
||||||
if [[ "\$ACTION" == "log" ]]; then
|
|
||||||
[ -f "\$LOG_FILE" ] && tail -80 "\$LOG_FILE" || echo "Sin logs disponibles"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── stop ──────────────────────────────────────────────────────────────────────
|
|
||||||
if [[ "\$ACTION" == "stop" ]]; then
|
|
||||||
_kill_by_pidfile
|
|
||||||
_delete_tun
|
|
||||||
echo "[wrapper] sing-box detenido — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE" 2>/dev/null || true
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── start ─────────────────────────────────────────────────────────────────────
|
|
||||||
[[ -z "\$CONFIG" ]] && { echo "Error: config requerido para start" >&2; exit 1; }
|
|
||||||
[[ ! -f "\$CONFIG" ]] && { echo "Error: config no encontrado: \$CONFIG" >&2; exit 1; }
|
|
||||||
[[ ! -r "\$CONFIG" ]] && { echo "Error: config no legible: \$CONFIG" >&2; exit 1; }
|
|
||||||
|
|
||||||
_real_config=\$(realpath "\$CONFIG")
|
|
||||||
_real_config_dir=\$(realpath "\$CONFIG_DIR")
|
|
||||||
if [[ "\$_real_config" != "\$_real_config_dir"/* ]]; then
|
|
||||||
echo "Error: config fuera del directorio permitido" >&2
|
|
||||||
echo " Config: \$_real_config" >&2
|
|
||||||
echo " Permitido: \$_real_config_dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "\$CONFIG" 2>/dev/null; then
|
|
||||||
echo "Error: JSON inválido: \$CONFIG" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ ! -x "\$SINGBOX_BIN" ]] && { echo "Error: sing-box no encontrado: \$SINGBOX_BIN" >&2; exit 1; }
|
|
||||||
|
|
||||||
_kill_by_pidfile
|
|
||||||
_delete_tun
|
|
||||||
|
|
||||||
mkdir -p "\$(dirname "\$PID_FILE")" && chmod 700 "\$(dirname "\$PID_FILE")"
|
|
||||||
mkdir -p "\$(dirname "\$LOG_FILE")" && chmod 700 "\$(dirname "\$LOG_FILE")"
|
|
||||||
|
|
||||||
"\$SINGBOX_BIN" run -c "\$CONFIG" >> "\$LOG_FILE" 2>&1 &
|
|
||||||
_new_pid=\$!
|
|
||||||
|
|
||||||
sleep 0.3
|
|
||||||
if ! _pid_running "\$_new_pid"; then
|
|
||||||
echo "Error: sing-box terminó inmediatamente. Ver: \$LOG_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "\$_new_pid" > "\$PID_FILE"
|
|
||||||
chmod 600 "\$PID_FILE"
|
|
||||||
echo "[wrapper] sing-box iniciado — PID \$_new_pid — \$(date '+%Y-%m-%d %H:%M:%S')" >> "\$LOG_FILE"
|
|
||||||
echo "[wrapper] config: \$_real_config" >> "\$LOG_FILE"
|
|
||||||
exit 0
|
|
||||||
WRAPPER
|
|
||||||
|
|
||||||
sudo chmod 755 "$SINGBOX_WRAPPER"
|
|
||||||
log_ok "Wrapper instalado → ${SINGBOX_WRAPPER}"
|
|
||||||
|
|
||||||
# Verificar que el wrapper funciona
|
|
||||||
if sudo "$SINGBOX_WRAPPER" "" stop &>/dev/null; then
|
|
||||||
log_ok "Wrapper verificado (stop OK)"
|
|
||||||
else
|
|
||||||
log_warn "Wrapper instalado pero verificación falló — revisar manualmente"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
114
lib/ui.sh
114
lib/ui.sh
|
|
@ -1,114 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
RESET="\033[0m"
|
|
||||||
BOLD="\033[1m"
|
|
||||||
DIM="\033[2m"
|
|
||||||
|
|
||||||
BLACK="\033[0;30m"
|
|
||||||
RED="\033[0;31m"
|
|
||||||
GREEN="\033[0;32m"
|
|
||||||
YELLOW="\033[0;33m"
|
|
||||||
BLUE="\033[0;34m"
|
|
||||||
CYAN="\033[0;36m"
|
|
||||||
WHITE="\033[0;37m"
|
|
||||||
|
|
||||||
BRED="\033[1;31m"
|
|
||||||
BGREEN="\033[1;32m"
|
|
||||||
BYELLOW="\033[1;33m"
|
|
||||||
BBLUE="\033[1;34m"
|
|
||||||
BCYAN="\033[1;36m"
|
|
||||||
BWHITE="\033[1;37m"
|
|
||||||
|
|
||||||
print_header() {
|
|
||||||
local title="${1:-INSTALLER}"
|
|
||||||
local version="${2:-}"
|
|
||||||
local width=60
|
|
||||||
local line
|
|
||||||
printf -v line '%*s' "$width" ''
|
|
||||||
line="${line// /─}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf "${BCYAN}${line}${RESET}\n"
|
|
||||||
printf "${BCYAN} %-56s${RESET}\n" ""
|
|
||||||
printf "${BCYAN} ${BWHITE}%-54s${BCYAN} ${RESET}\n" "$title ${DIM}${version}${RESET}"
|
|
||||||
printf "${BCYAN} %-56s${RESET}\n" ""
|
|
||||||
printf "${BCYAN}${line}${RESET}\n"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
print_section() {
|
|
||||||
local label="$1"
|
|
||||||
echo ""
|
|
||||||
printf "${DIM}${CYAN}┌─${RESET} ${BWHITE}${label}${RESET}\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_divider() {
|
|
||||||
printf "${DIM}────────────────────────────────────────────────────────────${RESET}\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
label_ok() { printf " ${BGREEN}[ OK ]${RESET} %s\n" "$1"; }
|
|
||||||
label_warn() { printf " ${BYELLOW}[ WARN ]${RESET} %s\n" "$1"; }
|
|
||||||
label_error() { printf " ${BRED}[ ERROR]${RESET} %s\n" "$1"; }
|
|
||||||
label_info() { printf " ${BCYAN}[ INFO ]${RESET} %s\n" "$1"; }
|
|
||||||
label_skip() { printf " ${DIM}[ SKIP ]${RESET} %s\n" "$1"; }
|
|
||||||
label_simple() { printf " ${DIM}[SIMPLE]${RESET} %s\n" "$1"; }
|
|
||||||
label_connected() { printf " ${BGREEN}[CONNECTED ]${RESET} %s\n" "$1"; }
|
|
||||||
label_disconnect() { printf " ${BRED}[DISCONNECTED]${RESET} %s\n" "$1"; }
|
|
||||||
label_step() { printf " ${BBLUE}[ %-2s/%-2s ]${RESET} %s\n" "$2" "$3" "$1"; }
|
|
||||||
|
|
||||||
progress_bar() {
|
|
||||||
local current=$1
|
|
||||||
local total=$2
|
|
||||||
local label="${3:-}"
|
|
||||||
local width=40
|
|
||||||
local filled=$(( current * width / total ))
|
|
||||||
local empty=$(( width - filled ))
|
|
||||||
local pct=$(( current * 100 / total ))
|
|
||||||
|
|
||||||
local bar_filled bar_empty
|
|
||||||
printf -v bar_filled '%*s' "$filled" ''
|
|
||||||
printf -v bar_empty '%*s' "$empty" ''
|
|
||||||
bar_filled="${bar_filled// /#}"
|
|
||||||
bar_empty="${bar_empty// /·}"
|
|
||||||
|
|
||||||
printf "\r ${DIM}[${RESET}${BGREEN}${bar_filled}${RESET}${DIM}${bar_empty}${RESET}${DIM}]${RESET} ${BWHITE}%3d%%${RESET} ${DIM}%s${RESET} " \
|
|
||||||
"$pct" "$label"
|
|
||||||
}
|
|
||||||
|
|
||||||
progress_simulate() {
|
|
||||||
local steps="${1:-20}"
|
|
||||||
local label="${2:-loading}"
|
|
||||||
for i in $(seq 1 "$steps"); do
|
|
||||||
progress_bar "$i" "$steps" "$label"
|
|
||||||
sleep 0.04
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
SPINNER_PID=""
|
|
||||||
|
|
||||||
start_spinner() {
|
|
||||||
local msg="${1:-Processing...}"
|
|
||||||
local frames=('─' '\\' '│' '/')
|
|
||||||
(
|
|
||||||
local i=0
|
|
||||||
while true; do
|
|
||||||
printf "\r ${BCYAN}${frames[$i]}${RESET} ${DIM}%s${RESET} " "$msg"
|
|
||||||
i=$(( (i + 1) % 4 ))
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
SPINNER_PID=$!
|
|
||||||
disown "$SPINNER_PID" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
stop_spinner() {
|
|
||||||
local result_msg="${1:-}"
|
|
||||||
if [ -n "$SPINNER_PID" ]; then
|
|
||||||
kill "$SPINNER_PID" 2>/dev/null
|
|
||||||
wait "$SPINNER_PID" 2>/dev/null
|
|
||||||
SPINNER_PID=""
|
|
||||||
fi
|
|
||||||
printf "\r%*s\r" 60 ""
|
|
||||||
[ -n "$result_msg" ] && label_ok "$result_msg"
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue