#!/usr/bin/env python3 import os import sys import time import schedule import logging import subprocess import fcntl import datetime import json 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" 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: logger.error("Failed to run traffic-status command. Aborting check.") return if not os.path.exists(USERS_FILE): logger.warning(f"{USERS_FILE} not found. Skipping on-hold user check.") return try: with open(USERS_FILE, 'r') as f: users_data = json.load(f) except (json.JSONDecodeError, IOError) as e: logger.error(f"Error reading or parsing {USERS_FILE}: {e}") return users_updated = False today_date = datetime.datetime.now().strftime("%Y-%m-%d") for username, user_data in users_data.items(): is_on_hold = not user_data.get("account_creation_date") if is_on_hold: is_online = user_data.get("status") == "Online" if is_online: logger.info(f"On-hold user '{username}' connected. Activating account with creation date {today_date}.") user_data["account_creation_date"] = today_date users_updated = True else: if user_data.get("status") != "On-hold": user_data["status"] = "On-hold" users_updated = True if users_updated: try: with open(USERS_FILE, 'w') as f: json.dump(users_data, f, indent=4) logger.info("Successfully updated users.json for on-hold users.") except IOError as e: logger.error(f"Error writing updates to {USERS_FILE}: {e}") 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) # logger.info("Performing initial runs on startup...") check_traffic_status() backup_hysteria() # logger.info("Initial runs complete. Entering main loop.") 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()