Merge pull request #169 from ReturnFI/beta

Reliable Scheduling & Smarter Scripts
This commit is contained in:
Whispering Wind
2025-05-16 13:25:07 +03:30
committed by GitHub
8 changed files with 292 additions and 143 deletions

View File

@ -1,20 +1,25 @@
# [1.9.2] - 2025-05-10 # [1.9.3] - 2025-05-16
## ✨ Changed ## ✨ Changed
### ✨ Features ### 🔧 System Improvements
🔄 feat: Merge traffic collection with user kicking for efficient enforcement
🧪 feat(api): Add user existence validation to `add-user` endpoint
🖥️ feat(frontend): Improve user management UI and error feedback in `users.html`
### 🧠 Refactors * 🕒 **feat:** Replace unreliable cron jobs with a systemd-based `HysteriaScheduler` service
📦 refactor: Migrate Hysteria restore functionality to Python * 🔐 **feat:** Add file locking to prevent concurrent access issues with `users.json`
🌐 refactor: Implement SNI changer in Python * ⏱️ **feat:** Schedule:
🛠️ refactor: Port TCP Brutal installation to Python
🎭 refactor: Rewrite masquerade management in Python3
⚙️ refactor(cli): Enhance error propagation and handling in CLI scripts
### 🛠️ System Enhancements * Traffic updates every 1 minute
🧩 update: Improve upgrade script with better cronjob management * Backups every 6 hours with isolated lock management
🛠️ enhance: Setup script with robust error handling and cleaner UX * 📝 **feat:** Add detailed logging for easier troubleshooting and monitoring
📦 update: Drop Debian 11 support to focus on supported OS versions * 👤 **feat:** Automatically add a default user after fresh installation
### 🛠️ Script Enhancements
* 📦 **refactor:** Create shared scheduler install function (used in both `install.sh` & `upgrade.sh`)
* ⚙️ **enhance:** Improve `upgrade.sh`:
* Add service checks
* Backup handling
* Color UI
* Robust error handling
* 🧼 **fix:** Improve uninstall script to clean up `HysteriaScheduler` service completely

View File

@ -2,6 +2,7 @@
source /etc/hysteria/core/scripts/path.sh source /etc/hysteria/core/scripts/path.sh
source /etc/hysteria/core/scripts/utils.sh source /etc/hysteria/core/scripts/utils.sh
source /etc/hysteria/core/scripts/scheduler.sh
define_colors define_colors
install_hysteria() { install_hysteria() {
@ -82,11 +83,9 @@ install_hysteria() {
chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh
chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py
(crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status --no-gui'") | crontab - if ! check_scheduler_service; then
# (crontab -l ; echo "0 3 */3 * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2' >/dev/null 2>&1") | crontab - setup_hysteria_scheduler
(crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab - fi
# (crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/scripts/hysteria2/kick.py' >/dev/null 2>&1") | crontab -
} }
if systemctl is-active --quiet hysteria-server.service; then if systemctl is-active --quiet hysteria-server.service; then
@ -100,6 +99,7 @@ else
if systemctl is-active --quiet hysteria-server.service; then if systemctl is-active --quiet hysteria-server.service; then
echo "Installation and configuration complete." echo "Installation and configuration complete."
python3 $CLI_PATH add-user --username default --traffic-limit 30 --expiration-days 30
else else
echo -e "${red}Error:${NC} Hysteria2 service is not active. Please check the logs for more details." echo -e "${red}Error:${NC} Hysteria2 service is not active. Please check the logs for more details."
fi fi

View File

@ -12,6 +12,7 @@ SERVICES = [
"hysteria-normal-sub.service", "hysteria-normal-sub.service",
"hysteria-singbox.service", "hysteria-singbox.service",
"hysteria-ip-limit.service", "hysteria-ip-limit.service",
"hysteria-scheduler.service",
] ]
def run_command(command, error_message): def run_command(command, error_message):

111
core/scripts/scheduler.py Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
import os
import sys
import time
import schedule
import logging
import subprocess
import fcntl
import datetime
from pathlib import Path
from paths import *
logging.basicConfig(
level=logging.WARNING,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("/var/log/hysteria_scheduler.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("HysteriaScheduler")
# Constants
BASE_DIR = Path("/etc/hysteria")
VENV_ACTIVATE = BASE_DIR / "hysteria2_venv/bin/activate"
# CLI_PATH = BASE_DIR / "core/cli.py"
LOCK_FILE = "/tmp/hysteria_scheduler.lock"
def acquire_lock():
try:
lock_fd = open(LOCK_FILE, 'w')
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return lock_fd
except IOError:
logger.warning("Another process is already running and has the lock")
return None
def release_lock(lock_fd):
if lock_fd:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
lock_fd.close()
def run_command(command, log_success=False):
activate_cmd = f"source {VENV_ACTIVATE}"
full_cmd = f"{activate_cmd} && {command}"
try:
result = subprocess.run(
full_cmd,
shell=True,
executable="/bin/bash",
capture_output=True,
text=True
)
if result.returncode != 0:
logger.error(f"Command failed: {full_cmd}")
logger.error(f"Error: {result.stderr}")
elif log_success:
logger.info(f"Command executed successfully: {full_cmd}")
return result.returncode == 0
except Exception as e:
logger.exception(f"Exception running command: {full_cmd}")
return False
def check_traffic_status():
lock_fd = acquire_lock()
if not lock_fd:
return
try:
success = run_command(f"python3 {CLI_PATH} traffic-status --no-gui", log_success=False)
if not success:
pass
finally:
release_lock(lock_fd)
def backup_hysteria():
lock_fd = acquire_lock()
if not lock_fd:
logger.warning("Skipping backup due to lock")
return
try:
run_command(f"python3 {CLI_PATH} backup-hysteria", log_success=True)
finally:
release_lock(lock_fd)
def main():
logger.info("Starting Hysteria Scheduler")
schedule.every(1).minutes.do(check_traffic_status)
schedule.every(6).hours.do(backup_hysteria)
backup_hysteria()
while True:
try:
schedule.run_pending()
time.sleep(1)
except KeyboardInterrupt:
logger.info("Shutting down scheduler")
break
except Exception as e:
logger.exception("Error in main loop")
time.sleep(60)
if __name__ == "__main__":
main()

42
core/scripts/scheduler.sh Normal file
View File

@ -0,0 +1,42 @@
#!/bin/bash
setup_hysteria_scheduler() {
chmod +x /etc/hysteria/core/scripts/scheduler.py
cat > /etc/systemd/system/hysteria-scheduler.service << 'EOF'
[Unit]
Description=Hysteria2 Scheduler Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/etc/hysteria
ExecStart=/etc/hysteria/hysteria2_venv/bin/python3 /etc/hysteria/core/scripts/scheduler.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hysteria-scheduler
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable hysteria-scheduler.service
systemctl start hysteria-scheduler.service
# wait 2
(crontab -l | grep -v "hysteria2_venv.*traffic-status" | grep -v "hysteria2_venv.*backup-hysteria") | crontab -
# return 0
}
check_scheduler_service() {
if systemctl is-active --quiet hysteria-scheduler.service; then
return 0
else
return 1
fi
}

View File

@ -2,6 +2,7 @@
declare -a services=( declare -a services=(
"hysteria-server.service" "hysteria-server.service"
"hysteria-scheduler.service"
"hysteria-webpanel.service" "hysteria-webpanel.service"
"hysteria-caddy.service" "hysteria-caddy.service"
"hysteria-telegram-bot.service" "hysteria-telegram-bot.service"

View File

@ -21,6 +21,7 @@ typing_extensions==4.13.2
urllib3==2.4.0 urllib3==2.4.0
yarl==1.20.0 yarl==1.20.0
hysteria2-api==0.1.2 hysteria2-api==0.1.2
schedule==1.2.2
# webpanel # webpanel
annotated-types==0.7.0 annotated-types==0.7.0

View File

@ -1,156 +1,144 @@
#!/bin/bash #!/bin/bash
cd /root/ set -euo pipefail
TEMP_DIR=$(mktemp -d) trap 'echo -e "\n❌ An error occurred. Aborting."; exit 1' ERR
# ========== Variables ==========
HYSTERIA_INSTALL_DIR="/etc/hysteria"
HYSTERIA_VENV_DIR="$HYSTERIA_INSTALL_DIR/hysteria2_venv"
REPO_URL="https://github.com/ReturnFI/Blitz"
REPO_BRANCH="main"
GEOSITE_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat"
GEOIP_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geoip.dat"
# ========== Color Setup ==========
GREEN=$(tput setaf 2)
RED=$(tput setaf 1)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
RESET=$(tput sgr0)
info() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] - ${RESET} $1"; }
success() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [OK] - ${RESET} $1"; }
warn() { echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] - ${RESET} $1"; }
error() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] - ${RESET} $1"; }
# ========== Backup Files ==========
cd /root
TEMP_DIR=$(mktemp -d)
FILES=( FILES=(
"/etc/hysteria/ca.key" "$HYSTERIA_INSTALL_DIR/ca.key"
"/etc/hysteria/ca.crt" "$HYSTERIA_INSTALL_DIR/ca.crt"
"/etc/hysteria/users.json" "$HYSTERIA_INSTALL_DIR/users.json"
"/etc/hysteria/config.json" "$HYSTERIA_INSTALL_DIR/config.json"
"/etc/hysteria/.configs.env" "$HYSTERIA_INSTALL_DIR/.configs.env"
"/etc/hysteria/core/scripts/telegrambot/.env" "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot/.env"
"/etc/hysteria/core/scripts/singbox/.env" "$HYSTERIA_INSTALL_DIR/core/scripts/singbox/.env"
"/etc/hysteria/core/scripts/normalsub/.env" "$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/.env"
"/etc/hysteria/core/scripts/webpanel/.env" "$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/.env"
"/etc/hysteria/core/scripts/webpanel/Caddyfile" "$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/Caddyfile"
) )
if crontab -l 2>/dev/null | grep -q "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py" || \ info "Backing up configuration files to: $TEMP_DIR"
crontab -l 2>/dev/null | grep -q "/etc/hysteria/core/scripts/hysteria2/kick.sh"; then
echo "Removing existing Hysteria cronjobs..."
crontab -l | grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status" | \
grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2" | \
grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria" | \
grep -v "/etc/hysteria/core/scripts/hysteria2/kick.sh" | \
crontab -
echo "Old Hysteria cronjobs removed successfully."
else
echo "No existing Hysteria cronjobs found. Skipping removal."
fi
echo "Backing up files to $TEMP_DIR"
for FILE in "${FILES[@]}"; do for FILE in "${FILES[@]}"; do
if [[ -f "$FILE" ]]; then
mkdir -p "$TEMP_DIR/$(dirname "$FILE")" mkdir -p "$TEMP_DIR/$(dirname "$FILE")"
cp "$FILE" "$TEMP_DIR/$FILE" cp -p "$FILE" "$TEMP_DIR/$FILE"
success "Backed up: $FILE"
else
warn "File not found: $FILE"
fi
done done
echo "Removing /etc/hysteria directory" # ========== Replace Installation ==========
rm -rf /etc/hysteria/ info "Removing old hysteria directory..."
rm -rf "$HYSTERIA_INSTALL_DIR"
echo "Cloning Blitz repository" info "Cloning Blitz repository (branch: $REPO_BRANCH)..."
git clone https://github.com/ReturnFI/Blitz /etc/hysteria git clone -q -b "$REPO_BRANCH" "$REPO_URL" "$HYSTERIA_INSTALL_DIR"
echo "Downloading geosite.dat and geoip.dat" # ========== Download Geo Data ==========
wget -O /etc/hysteria/geosite.dat https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat >/dev/null 2>&1 info "Downloading geosite.dat and geoip.dat..."
wget -O /etc/hysteria/geoip.dat https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geoip.dat >/dev/null 2>&1 wget -q -O "$HYSTERIA_INSTALL_DIR/geosite.dat" "$GEOSITE_URL"
wget -q -O "$HYSTERIA_INSTALL_DIR/geoip.dat" "$GEOIP_URL"
success "Geo data downloaded."
echo "Restoring backup files" # ========== Restore Backup ==========
info "Restoring configuration files..."
for FILE in "${FILES[@]}"; do for FILE in "${FILES[@]}"; do
cp "$TEMP_DIR/$FILE" "$FILE" BACKUP="$TEMP_DIR/$FILE"
if [[ -f "$BACKUP" ]]; then
cp -p "$BACKUP" "$FILE"
success "Restored: $FILE"
else
warn "Missing backup file: $BACKUP"
fi
done done
CONFIG_ENV="/etc/hysteria/.configs.env" # ========== Permissions ==========
if [ ! -f "$CONFIG_ENV" ]; then info "Setting ownership and permissions..."
echo ".configs.env not found, creating it with default values." chown hysteria:hysteria "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
echo "SNI=bts.com" > "$CONFIG_ENV" chmod 640 "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
fi
export $(grep -v '^#' "$CONFIG_ENV" | xargs 2>/dev/null) chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/singbox"
chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot"
if [[ -z "$IP4" ]]; then chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/user.sh"
echo "IP4 not found, fetching from ip.gs..." chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/kick.py"
IP4=$(curl -s -4 ip.gs || echo "")
echo "IP4=${IP4:-}" >> "$CONFIG_ENV"
fi
if [[ -z "$IP6" ]]; then # ========== Virtual Environment ==========
echo "IP6 not found, fetching from ip.gs..." info "Setting up virtual environment and installing dependencies..."
IP6=$(curl -s -6 ip.gs || echo "") cd "$HYSTERIA_INSTALL_DIR"
echo "IP6=${IP6:-}" >> "$CONFIG_ENV" python3 -m venv "$HYSTERIA_VENV_DIR"
fi source "$HYSTERIA_VENV_DIR/bin/activate"
pip install --upgrade pip >/dev/null
pip install -r requirements.txt >/dev/null
success "Python environment ready."
NORMALSUB_ENV="/etc/hysteria/core/scripts/normalsub/.env" # ========== Scheduler ==========
info "Ensuring scheduler is set..."
if [[ -f "$NORMALSUB_ENV" ]]; then if source "$HYSTERIA_INSTALL_DIR/core/scripts/scheduler.sh"; then
echo "Checking if SUBPATH exists in $NORMALSUB_ENV..." if ! check_scheduler_service; then
if setup_hysteria_scheduler; then
if ! grep -q '^SUBPATH=' "$NORMALSUB_ENV"; then success "Scheduler service configured."
echo "SUBPATH not found, generating a new one..."
SUBPATH=$(pwgen -s 32 1)
echo -e "\nSUBPATH=$SUBPATH" >> "$NORMALSUB_ENV"
else else
echo "SUBPATH already exists, no changes made." warn "Scheduler setup failed, but continuing upgrade..."
fi fi
else else
echo "$NORMALSUB_ENV not found. Skipping SUBPATH check." success "Scheduler already set."
fi fi
CONFIG_FILE="/etc/hysteria/config.json"
if [ -f "$CONFIG_FILE" ]; then
echo "Checking and converting pinSHA256 format in config.json"
if grep -q "pinSHA256.*=" "$CONFIG_FILE"; then
echo "Converting pinSHA256 from base64 to hex format"
HEX_FINGERPRINT=$(openssl x509 -noout -fingerprint -sha256 -inform pem -in /etc/hysteria/ca.crt | sed 's/.*=//;s///g')
sed -i "s|\"pinSHA256\": \"sha256/.*\"|\"pinSHA256\": \"$HEX_FINGERPRINT\"|" "$CONFIG_FILE"
echo "pinSHA256 converted to hex format: $HEX_FINGERPRINT"
else else
echo "pinSHA256 appears to already be in hex format or not present, no conversion needed" warn "Failed to source scheduler.sh, continuing without scheduler setup..."
fi
fi fi
echo "Setting ownership and permissions" # ========== Restart Services ==========
chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt SERVICES=(
chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt hysteria-caddy.service
chown -R hysteria:hysteria /etc/hysteria/core/scripts/singbox hysteria-server.service
chown -R hysteria:hysteria /etc/hysteria/core/scripts/telegrambot hysteria-scheduler.service
hysteria-telegram-bot.service
hysteria-normal-sub.service
hysteria-webpanel.service
hysteria-ip-limit.service
)
echo "Setting execute permissions for user.sh and kick.py" info "Restarting available services..."
chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh for SERVICE in "${SERVICES[@]}"; do
chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py if systemctl status "$SERVICE" &>/dev/null; then
systemctl restart "$SERVICE" && success "$SERVICE restarted." || warn "$SERVICE failed to restart."
else
warn "$SERVICE not found or not installed. Skipping..."
fi
done
cd /etc/hysteria # ========== Final Check ==========
python3 -m venv hysteria2_venv
source /etc/hysteria/hysteria2_venv/bin/activate
pip install -r requirements.txt
echo "Restarting hysteria-caddy service"
systemctl restart hysteria-caddy.service
echo "Restarting other hysteria services"
systemctl restart hysteria-server.service
systemctl restart hysteria-telegram-bot.service
systemctl restart hysteria-normal-sub.service
systemctl restart hysteria-webpanel.service
echo "Checking hysteria-server.service status"
if systemctl is-active --quiet hysteria-server.service; then if systemctl is-active --quiet hysteria-server.service; then
echo "Upgrade completed successfully" success "🎉 Upgrade completed successfully!"
else else
echo "Upgrade failed: hysteria-server.service is not active" warn "⚠️ hysteria-server.service is not active. Check logs if needed."
fi
echo "Adding new Hysteria cronjobs..."
if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py traffic-status --no-gui"; then
echo "Adding traffic-status cronjob..."
(crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status --no-gui'") | crontab -
else
echo "Traffic-status cronjob already exists. Skipping."
fi
if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py backup-hysteria"; then
echo "Adding backup-hysteria cronjob..."
(crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab -
else
echo "Backup-hysteria cronjob already exists. Skipping."
fi fi
# ========== Launch Menu ==========
sleep 10
chmod +x menu.sh chmod +x menu.sh
./menu.sh ./menu.sh