# coding: utf-8 import json import os import socket import subprocess import sys import time from functools import wraps, partial from typing import Optional, Dict, Callable import fcntl os.chdir("/www/server/panel") sys.path.insert(0, "class/") sys.path.insert(0, "class_v2/") SETUP_PATH = "/www/server" DATA_PATH = os.path.join(SETUP_PATH, "panel/data") PLUGINS_PATH = os.path.join(SETUP_PATH, "panel/plugin") DAEMON_SERVICE = os.path.join(DATA_PATH, "daemon_service.pl") DAEMON_SERVICE_LOCK = os.path.join(DATA_PATH, "daemon_service_lock.pl") DAEMON_RESTART_RECORD = os.path.join(DATA_PATH, "daemon_restart_record.pl") MANUAL_FLAG = os.path.join(SETUP_PATH, "panel/data/mod_push_data", "manual_flag.pl") def read_file(filename: str): fp = None try: fp = open(filename, "rb") f_body_bytes: bytes = fp.read() f_body = f_body_bytes.decode("utf-8", errors='ignore') fp.close() return f_body except Exception: return False finally: if fp and not fp.closed: fp.close() def run_command(cmd, timeout=5) -> str: try: result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=timeout, check=False ) output = result.stdout.strip() return output if output else "" except subprocess.TimeoutExpired as t: write_logs(f"Command execution timed out: {cmd}, error: {t}") return "" except Exception as e: write_logs(f"Command execution failed: {cmd}, error: {e}") return "" def service_shop_name(services_name: str) -> str: # 对应商店名字 shop_name = { "postfix": "mail_sys", "pgsql": "pgsql_manager", "pure-ftpd": "pureftpd", "php-fpm-52": "php-5.2", "php-fpm-53": "php-5.3", "php-fpm-54": "php-5.4", "php-fpm-55": "php-5.5", "php-fpm-56": "php-5.6", "php-fpm-70": "php-7.0", "php-fpm-71": "php-7.1", "php-fpm-72": "php-7.2", "php-fpm-73": "php-7.3", "php-fpm-74": "php-7.4", "php-fpm-80": "php-8.0", "php-fpm-81": "php-8.1", "php-fpm-82": "php-8.2", "php-fpm-83": "php-8.3", "php-fpm-84": "php-8.4", "php-fpm-85": "php-8.5", } return shop_name.get(services_name, services_name) def pretty_title(services_name: str) -> str: title = { "apache": "Apache", "nginx": "Nginx", "openlitespeed": "OpenLiteSpeed", "redis": "Redis", "mysql": "MySQL/MariaDB", "mongodb": "MongoDB", "pgsql": "PostgreSQL", "pure-ftpd": "Pure-FTPd", "memcached": "Memcached", "ssh": "SSH", "postfix": "Postfix", "pdns": "PowerDNS", # PHP-FPM "php-fpm-52": "PHP 5.2 FPM", "php-fpm-53": "PHP 5.3 FPM", "php-fpm-54": "PHP 5.4 FPM", "php-fpm-55": "PHP 5.5 FPM", "php-fpm-56": "PHP 5.6 FPM", "php-fpm-70": "PHP 7.0 FPM", "php-fpm-71": "PHP 7.1 FPM", "php-fpm-72": "PHP 7.2 FPM", "php-fpm-73": "PHP 7.3 FPM", "php-fpm-74": "PHP 7.4 FPM", "php-fpm-80": "PHP 8.0 FPM", "php-fpm-81": "PHP 8.1 FPM", "php-fpm-82": "PHP 8.2 FPM", "php-fpm-83": "PHP 8.3 FPM", "php-fpm-84": "PHP 8.4 FPM", "php-fpm-85": "PHP 8.5 FPM", # plugins "btwaf": "YakPanel WAF", "fail2ban": "Fail2Ban", } return title.get(services_name, services_name) # ======================================================= def ssh_ver() -> str: version_info = run_command(["ssh", "-V"]) if version_info and "OpenSSH" in version_info: return version_info.split(",")[0] return "" def postfix_ver() -> str: # mail_version = x.x.x output = run_command(["/usr/sbin/postconf", "mail_version"]) if output and "=" in output: return output.split("=")[-1].strip() return "" def pgsql_pid() -> str: pid_str = run_command(["pgrep", "-o", "postgres"]) if pid_str and pid_str.isdigit(): return pid_str return "" def pgsql_ver() -> str: # psql (PostgreSQL) 18.0 output = run_command([f"{SETUP_PATH}/pgsql/bin/psql", "--version"]) if output: from re import search match = search(r"(\d+\.\d+(\.\d+)?)", output) if match: return match.group(1) org_file = f"{SETUP_PATH}/pgsql/data/PG_VERSION", if os.path.exists(str(org_file)): try: with open(str(org_file), "r") as f: version = f.read().strip() return version except: return "" return "" def pdns_pid() -> str: pid_str = run_command(["pgrep", "-x", "pdns_server"]) if pid_str and pid_str.isdigit(): return pid_str return "" def pdns_ver() -> str: # PowerDNS Authoritative Server x.x.x output = run_command(["pdns_server", "--version"]) if not output: return "" from re import search match = search(r"(\d+\.\d+\.\d+)", output) if match: return match.group(1) if "PowerDNS" in output: return output.split()[-1] return "" def waf_pid() -> str: pid_str = run_command(["pgrep", "-x", "BT-WAF"]) if pid_str and pid_str.isdigit(): return pid_str return "" def pluign_ver(plugin_name: str) -> str: info = f"{PLUGINS_PATH}/{plugin_name}/info.json" if not os.path.exists(info): return "" try: with open(info, "r") as f: json_data = json.load(f) ret = json_data.get("versions", "") return ret except: pass return "" SERVICES_MAP = { # ========================= core base ============================= "panel": ( "Yak-Panel", f"{SETUP_PATH}/panel/logs/panel.pid", f"{SETUP_PATH}/panel/init.sh", f"{SETUP_PATH}/nginx/version.pl", ), "apache": ( "httpd", f"{SETUP_PATH}/apache/logs/httpd.pid", "/etc/init.d/httpd", f"{SETUP_PATH}/apache/version.pl", ), "nginx": ( "nginx", f"{SETUP_PATH}/nginx/logs/nginx.pid", "/etc/init.d/nginx", f"{SETUP_PATH}/nginx/version.pl", ), "openlitespeed": ( "litespeed", "/tmp/lshttpd/lshttpd.pid", "/usr/local/lsws/bin/lswsctrl", "/usr/local/lsws/VERSION", ), "redis": ( "redis-server", f"{SETUP_PATH}/redis/redis.pid", "/etc/init.d/redis", f"{SETUP_PATH}/redis/version.pl", ), "mysql": ( "mysqld", "/tmp/mysql.sock", "/etc/init.d/mysqld", f"{SETUP_PATH}/mysql/version.pl", ), "mongodb": ( "mongod", f"{SETUP_PATH}/mongodb/log/configsvr.pid", "/etc/init.d/mongodb", f"{SETUP_PATH}/mongodb/version.pl", ), "pgsql": ( "postgres", pgsql_pid, "/etc/init.d/pgsql", pgsql_ver, ), "pure-ftpd": ( "pure-ftpd", "/var/run/pure-ftpd.pid", "/etc/init.d/pure-ftpd", f"{SETUP_PATH}/pure-ftpd/version.pl", ), "memcached": ( "memcached", "/var/run/memcached.pid", "/etc/init.d/memcached", "/usr/local/memcached/version_check.pl", ), "ssh": ( "sshd", "/var/run/sshd.pid", "/etc/init.d/ssh", ssh_ver, ), "postfix": ( "master", "/var/spool/postfix/pid/master.pid", "/etc/init.d/postfix", postfix_ver, ), "pdns": ( # "pdns_server", pdns_pid, "/usr/sbin/pdns_server", "pdns_server", pdns_pid, "/www/server/panel/class_v2/ssl_dnsV2/aadns.pl", pdns_ver, ), # ======================== PHP-FPM ============================ "php-fpm-52": ("php-fpm", f"{SETUP_PATH}/php/52/var/run/php-fpm.pid", "/etc/init.d/php-fpm-52", f"{SETUP_PATH}/php/52/version.pl"), "php-fpm-53": ("php-fpm", f"{SETUP_PATH}/php/53/var/run/php-fpm.pid", "/etc/init.d/php-fpm-53", f"{SETUP_PATH}/php/53/version.pl"), "php-fpm-54": ("php-fpm", f"{SETUP_PATH}/php/54/var/run/php-fpm.pid", "/etc/init.d/php-fpm-54", f"{SETUP_PATH}/php/54/version.pl"), "php-fpm-55": ("php-fpm", f"{SETUP_PATH}/php/55/var/run/php-fpm.pid", "/etc/init.d/php-fpm-55", f"{SETUP_PATH}/php/55/version.pl"), "php-fpm-56": ("php-fpm", f"{SETUP_PATH}/php/56/var/run/php-fpm.pid", "/etc/init.d/php-fpm-56", f"{SETUP_PATH}/php/56/version.pl"), "php-fpm-70": ("php-fpm", f"{SETUP_PATH}/php/70/var/run/php-fpm.pid", "/etc/init.d/php-fpm-70", f"{SETUP_PATH}/php/70/version.pl"), "php-fpm-71": ("php-fpm", f"{SETUP_PATH}/php/71/var/run/php-fpm.pid", "/etc/init.d/php-fpm-71", f"{SETUP_PATH}/php/71/version.pl"), "php-fpm-72": ("php-fpm", f"{SETUP_PATH}/php/72/var/run/php-fpm.pid", "/etc/init.d/php-fpm-72", f"{SETUP_PATH}/php/72/version.pl"), "php-fpm-73": ("php-fpm", f"{SETUP_PATH}/php/73/var/run/php-fpm.pid", "/etc/init.d/php-fpm-73", f"{SETUP_PATH}/php/73/version.pl"), "php-fpm-74": ("php-fpm", f"{SETUP_PATH}/php/74/var/run/php-fpm.pid", "/etc/init.d/php-fpm-74", f"{SETUP_PATH}/php/74/version.pl"), "php-fpm-80": ("php-fpm", f"{SETUP_PATH}/php/80/var/run/php-fpm.pid", "/etc/init.d/php-fpm-80", f"{SETUP_PATH}/php/80/version.pl"), "php-fpm-81": ("php-fpm", f"{SETUP_PATH}/php/81/var/run/php-fpm.pid", "/etc/init.d/php-fpm-81", f"{SETUP_PATH}/php/81/version.pl"), "php-fpm-82": ("php-fpm", f"{SETUP_PATH}/php/82/var/run/php-fpm.pid", "/etc/init.d/php-fpm-82", f"{SETUP_PATH}/php/82/version.pl"), "php-fpm-83": ("php-fpm", f"{SETUP_PATH}/php/83/var/run/php-fpm.pid", "/etc/init.d/php-fpm-83", f"{SETUP_PATH}/php/83/version.pl"), "php-fpm-84": ("php-fpm", f"{SETUP_PATH}/php/84/var/run/php-fpm.pid", "/etc/init.d/php-fpm-84", f"{SETUP_PATH}/php/84/version_check.pl"), "php-fpm-85": ("php-fpm", f"{SETUP_PATH}/php/85/var/run/php-fpm.pid", "/etc/init.d/php-fpm-85", f"{SETUP_PATH}/php/85/version_check.pl"), # ======================== plugins ============================ # nginx : btwaf # apache: btwaf_httpd "btwaf": ( # "BT-WAF", f"{PLUGINS_PATH}/btwaf/BT-WAF.pid", f"/etc/init.d/btwaf", "BT-WAF", waf_pid, f"/etc/init.d/btwaf", partial(pluign_ver, "btwaf"), ), "fail2ban": ( "fail2ban-server", f"{PLUGINS_PATH}/fail2ban/fail2ban.pid", "/etc/init.d/fail2ban", partial(pluign_ver, "fail2ban"), ), } # 日志 def write_logs(msg: str, logger_name: str = "Service Daemon"): try: from public import M t = time.strftime('%Y-%m-%d %X', time.localtime()) M("logs").add("uid,username,type,log,addtime", (1, "system", logger_name, msg, t)) except: pass # 手动干预 def manual_flag(server_name: str = None, open_: str = None) -> Optional[dict]: if not server_name: # only read return DaemonManager.manual_safe_read() # 人为干预 if open_ in ["start", "restart"]: # 激活服务检查 return DaemonManager.active_daemon(server_name) elif open_ == "stop": # 跳过服务检查 return DaemonManager.skip_daemon(server_name) return DaemonManager.manual_safe_read() # 管理服务助手 class ServicesHelper: def __init__(self, nick_name: str = None): self.nick_name = nick_name self._serviced = None self._pid_source = None self._bash = None self._ver_source = None self._pid_cache = None self._ver_cache = None self._install_cache = None self._info_inited = False def __check_pid_process(self, pid: int) -> bool: try: # 是否活跃 with open(f"/proc/{pid}/stat", "r") as f: if f.read().split()[2] == "Z": return False # 僵尸进程 # 进程是否名字匹配 with open(f"/proc/{pid}/comm", "r") as f: proc_name = f.read().strip() # 特殊处理 mysql if self.nick_name != "mysql": return proc_name == self._serviced or proc_name == self.nick_name else: return proc_name in ["mysqld", "mariadbd"] except (FileNotFoundError, IndexError): return False except Exception: return False def _init_info(self) -> None: if self._info_inited: return if not self.nick_name or not isinstance(self.nick_name, str): self._info_inited = True return map_info = SERVICES_MAP.get(self.nick_name) if map_info: self._serviced, self._pid_source, self._bash, self._ver_source = map_info self._info_inited = True @property def pid(self): """返回pid文件路径或pid值""" if self._pid_cache is None: self._init_info() if isinstance(self._pid_source, Callable): try: pid_val = self._pid_source() self._pid_cache = int(pid_val) if pid_val and pid_val.isdigit() else pid_val except Exception: self._pid_cache = self._pid_source else: self._pid_cache = self._pid_source return self._pid_cache @property def version(self) -> str: """返回服务版本号""" if self._ver_cache is None: self._init_info() if isinstance(self._ver_source, Callable): try: self._ver_cache = self._ver_source() except: self._ver_cache = "" elif isinstance(self._ver_source, str) and os.path.exists(self._ver_source): try: with open(self._ver_source, "r") as f: self._ver_cache = f.read().strip() except: self._ver_cache = "" else: self._ver_cache = "" return str(self._ver_cache).strip() @property def is_install(self) -> bool: """判断是否安装""" if self._install_cache is not None: return self._install_cache self._init_info() self._install_cache = False # waf特殊处理 if self.nick_name == "btwaf": if os.path.exists(f"{PLUGINS_PATH}/btwaf"): self._install_cache = True return self._install_cache # postfix特殊处理 if self.nick_name == "postfix": if os.path.exists(f"{PLUGINS_PATH}/mail_sys"): self._install_cache = True return self._install_cache if any([ self._serviced and os.path.exists(f"/etc/init.d/{self._serviced}"), self.nick_name and os.path.exists(f"/etc/init.d/{self.nick_name}"), self._bash and os.path.exists(self._bash), ]): self._install_cache = True return self._install_cache @property def shop_name(self) -> str: return service_shop_name(self.nick_name) @property def pretty_title(self) -> str: return pretty_title(self.nick_name) @property def is_running(self) -> bool: """ 判断进程是否存活 pid: 进程pid文件 str 或 int serviced: 进程服务名称 nick_name: 进程别名 """ if not self.is_install: return False if not self.pid: return False if isinstance(self.pid, int): return self.__check_pid_process(self.pid) if isinstance(self.pid, str): if not os.path.exists(self.pid): return False if self.pid.endswith(".pid"): try: with open(self.pid, "r") as f: temp_pid = int(f.read().strip()) return self.__check_pid_process(temp_pid) except (ValueError, FileNotFoundError): return False except Exception: return False elif self.pid.endswith(".sock"): try: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: s.settimeout(0.1) s.connect(self.pid) return True except (socket.timeout, ConnectionRefusedError, FileNotFoundError): return False except Exception: return False # not str and not int return False def script(self, act: str, logger_name: str = "Service Daemon") -> None: if not self.is_install or act not in ["start", "stop", "restart", "status"]: return try: # "try to {act} [{self.nick_name}]..." self._init_info() bash_path = self._bash if self._bash else f"/etc/init.d/{self._serviced}" # if isinstance(self.pid, str) and ( # self.pid.endswith(".sock") or self.pid.endswith(".pid") # ): # try: # os.remove(self.pid) # except: # pass if self.nick_name == "panel": if not os.path.exists(f"{SETUP_PATH}/panel/init.sh"): from public import get_url os.system(f"curl -k {get_url()}/install/update_7.x_en.sh|bash &") cmd = ["bash", bash_path, act] elif self.nick_name == "pdns": cmd = ["systemctl", act, "pdns"] else: cmd = [bash_path, act] subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True ) write_logs(f"Service [ {self.nick_name} ] {act}", logger_name) except Exception as e: print(str(e)) write_logs(f"Failed to {act} {self.nick_name}, error: {e}", logger_name) # 守护服务管理 class DaemonManager: _ensured = False @classmethod def __ensure(cls): if cls._ensured: return if not os.path.exists(DAEMON_SERVICE_LOCK): with open(DAEMON_SERVICE_LOCK, "w") as _: pass if not os.path.exists(DAEMON_RESTART_RECORD): with open(DAEMON_RESTART_RECORD, "w") as fm: fm.write(json.dumps({})) os.makedirs(os.path.dirname(MANUAL_FLAG), exist_ok=True) if not os.path.exists(MANUAL_FLAG): with open(MANUAL_FLAG, "w") as fm: fm.write(json.dumps({})) if not os.path.exists(DAEMON_SERVICE): with open(DAEMON_SERVICE, "w") as fm: fm.write(json.dumps([])) cls._ensured = True @staticmethod def read_lock(func): @wraps(func) def wrapper(*args, **kwargs): DaemonManager.__ensure() with open(DAEMON_SERVICE_LOCK, "r") as lock_file: fcntl.flock(lock_file, fcntl.LOCK_SH) try: return func(*args, **kwargs) finally: fcntl.flock(lock_file, fcntl.LOCK_UN) return wrapper @staticmethod def write_lock(func): @wraps(func) def wrapper(*args, **kwargs): DaemonManager.__ensure() with open(DAEMON_SERVICE_LOCK, "r+") as lock_file: fcntl.flock(lock_file, fcntl.LOCK_EX) try: return func(*args, **kwargs) finally: fcntl.flock(lock_file, fcntl.LOCK_UN) return wrapper @staticmethod @write_lock def operate_daemon(service_name: str, flag: int = 0) -> list: """ flag: 0 add, 1 del """ with open(DAEMON_SERVICE, "r+") as f: try: service = json.load(f) except json.JSONDecodeError: service = [] if flag == 0: service.append(service_name) elif flag == 1: service = [x for x in service if x != service_name] service = list(set(service)) f.seek(0) # noinspection PyTypeChecker json.dump(service, f) f.truncate() return service @staticmethod @write_lock def operate_manual_flag(service_name: str, flag: int = 0) -> dict: """ flag: 0 normal, 1 manual closed """ with open(MANUAL_FLAG, "r+") as f: try: service = json.load(f) except json.JSONDecodeError: service = {} service[service_name] = flag f.seek(0) # noinspection PyTypeChecker json.dump(service, f) f.truncate() return service @staticmethod def remove_daemon(service_name: str) -> list: """移除守护进程服务""" return DaemonManager.operate_daemon(service_name, 1) @staticmethod def add_daemon(service_name: str) -> list: """添加守护进程服务""" return DaemonManager.operate_daemon(service_name, 0) @staticmethod def skip_daemon(service_name: str) -> dict: """跳过服务检查""" return DaemonManager.operate_manual_flag(service_name, 1) @staticmethod def active_daemon(service_name: str) -> dict: """激活服务检查""" return DaemonManager.operate_manual_flag(service_name, 0) @staticmethod @read_lock def safe_read(): """服务守护进程服务列表""" try: res = read_file(DAEMON_SERVICE) return json.loads(res) if res else [] except: return [] @staticmethod @read_lock def manual_safe_read(): """手动干预服务字典, 0: 需要干预, 1: 被手动关闭的""" try: manual = read_file(MANUAL_FLAG) return json.loads(manual) if manual else {} except: return {} @staticmethod def update_restart_record(service_name: str, max_count: int) -> bool: try: with open(DAEMON_RESTART_RECORD, "r+") as f: fcntl.flock(f, fcntl.LOCK_EX) try: record = json.load(f) except json.JSONDecodeError: record = {} count = record.get(service_name, 0) if count >= max_count: return True record[service_name] = count + 1 f.seek(0) json.dump(record, f) f.truncate() return False except Exception: return True @staticmethod def safe_read_restart_record() -> Dict[str, int]: try: with open(DAEMON_RESTART_RECORD, "r") as f: fcntl.flock(f, fcntl.LOCK_SH) record = json.load(f) except Exception: record = {} return record # 服务守护 class RestartServices: COUNT = 30 @staticmethod def __keep_flag_right(manual_info: dict) -> None: try: with open(MANUAL_FLAG, "r+") as f: try: fcntl.flock(f.fileno(), fcntl.LOCK_EX) f.seek(0) # noinspection PyTypeChecker json.dump(manual_info, f) f.truncate() except: print("Error writing manual flag file") finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) except Exception as e: print("Error keep_flag_right:", e) def _overhead(self, nick_name) -> bool: return DaemonManager.update_restart_record( nick_name, self.COUNT ) @DaemonManager.read_lock def main(self): manaul = read_file(MANUAL_FLAG) services = read_file(DAEMON_SERVICE) try: manual_info = json.loads(manaul) if manaul else {} check_list = json.loads(services) if services else [] check_list = ["panel"] + check_list # panel 强制守护 except Exception: manual_info = {} check_list = ["panel"] record = DaemonManager.safe_read_restart_record() for service in [ x for x in list(set(check_list)) if record.get(x, 0) < self.COUNT ]: obj = ServicesHelper(service) if not obj.is_install: continue if not obj.is_running: if int(manual_info.get(obj.nick_name, 0)) == 1: # service closed maually, skip continue # if obj.nick_name != "panel": # write_logs(f"Service [ {obj.nick_name} ] is Not Running, Try to start it...") if not self._overhead(obj.nick_name): obj.script("restart") if obj.is_running and manual_info.get(obj.nick_name) == 1: # service is running, fix the wrong flag manual_info[obj.nick_name] = 0 # under lock file read lock self.__keep_flag_right(manual_info) def first_time_installed(data: dict) -> None: """ 首次安装服务启动守护进程服务 """ # todo 虽然支持, 但是守护目前不干预, 前端没开放 exculde = [ "pgsql", "fail2ban", "btwaf", "ssh", "pdns", "php-fpm", "memcached" ] if not data: return try: for service in SERVICES_MAP.keys(): # support service if service in exculde: continue if "php-fpm" in service: continue pl_name = f"{DATA_PATH}/first_installed_flag_{service}.pl" if data.get(service): # panel installed setup = data[service].get("setup", False) if setup is False and os.path.exists(pl_name): os.remove(pl_name) elif setup is True and not os.path.exists(pl_name): DaemonManager.add_daemon(service) with open(pl_name, "w") as f: f.write("1") else: pass except: pass if __name__ == "__main__": if len(sys.argv) == 2: service_name = sys.argv[1] if service_name in SERVICES_MAP: helper = ServicesHelper(service_name) print(f"Attempting to restart service: {service_name}") helper.script("restart") print(f"'{service_name}' restart.") write_logs(f"Service [ {service_name} ] restart ...") else: print(f"Error: Service '{service_name}' not found.") print("Available services are:") for key in SERVICES_MAP.keys(): print(f" - {key}") elif len(sys.argv) == 1: pass else: print("Usage: python restart_services.py [service_name]")