#!/bin/python # coding: utf-8 # +------------------------------------------------------------------- # | YakPanel # +------------------------------------------------------------------- # | Copyright (c) 2015-2016 YakPanel(www.yakpanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: yakpanel # +------------------------------------------------------------------- # ------------------------------ # YakPanel background scheduled tasks # ------------------------------ import gc import os import shutil import sqlite3 import subprocess import sys import threading import time import traceback from datetime import datetime, timedelta from typing import Optional, Tuple import psutil try: import ujson as json except ImportError: try: os.system("btpip install ujson") import ujson as json except: import json sys.path.insert(0, "/www/server/panel/class/") try: from public.hook_import import hook_import hook_import() except: pass import db from panelTask import bt_task from script.restart_services import RestartServices from YakTask.brain import SimpleBrain from YakTask.conf import ( BASE_PATH, PYTHON_BIN, exlogPath, isTask, logger, ) def write_file(path: str, content: str, mode='w'): try: fp = open(path, mode) fp.write(content) fp.close() return True except: try: fp = open(path, mode, encoding="utf-8") fp.write(content) fp.close() return True except: return False 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 exec_shell(cmdstring, timeout=None, shell=True, cwd=None): """ @name 执行命令 @param cmdstring 命令 [必传] @param timeout 超时时间 @param shell 是否通过shell运行 @return 命令执行结果 """ try: result = subprocess.run( cmdstring, shell=shell, cwd=cwd, timeout=timeout, capture_output=True, text=True, # 直接以文本模式处理输出 encoding='utf-8', errors='ignore', env=os.environ ) return result.stdout, result.stderr except subprocess.TimeoutExpired: return 'Timed out', '' except Exception: return '', traceback.format_exc() task_obj = bt_task() task_obj.not_web = True bt_box_task = task_obj.start_task_new def task_ExecShell(fucn_name: str, **kw): """ 仅运行 /www/server/panel/YakTask/task_script.py 下的包装函数 可通过 kw 参数扩展前置检查,例如: - kw['paths_exists']: List [str] (例如, 检查邮局插件是否存在, 任何一个检查不存在则不执行 paths_exists=['/www/server/panel/plugin/mail_sys/mail_sys_main.py', '/www/vmail']) """ if PYTHON_BIN in fucn_name: raise ValueError("valid function name required") if kw.get("paths_exists") and isinstance(kw["paths_exists"], list): for p in kw["paths_exists"]: try: if not os.path.exists(str(p)): logger.debug(f"Skip task [{fucn_name}]: path not exists") return except Exception as e: raise ValueError(f"Invalid path in paths_exists: '{p}', error: {e}") cmd = f"{PYTHON_BIN} /www/server/panel/YakTask/task_script.py {fucn_name}" _, err = exec_shell(cmd) if err: raise Exception(err) # 系统监控全局缓存 _system_task_state = { "table_ensure": False, # {"timestamp": 0, "total_up": 0, "total_down": 0, "up_packets": {}, "down_packets": {}} "last_network_io": {}, # {"timestamp": 0, "read_count": 0, "write_count": 0, "read_bytes": 0, "write_bytes": 0, "read_time": 0, "write_time": 0} "last_disk_io": {}, # {pid: (create_time, cpu_time, disk_read, disk_write, timestamp)} "last_process_cache": {}, # {pid: (inactive_count, last_check_time)} "inactive_process_cache": {}, "last_clear_time": 0, "last_cpu_times": None, } # 系统监控任务 def systemTask(): def get_cpu_percent_smooth() -> float: """窗口时间内CPU占用率""" try: current_times = psutil.cpu_times() last_times = _system_task_state.get("last_cpu_times") if not last_times: _system_task_state["last_cpu_times"] = current_times return psutil.cpu_percent(interval=0.1) all_delta = sum(current_times) - sum(last_times) if all_delta == 0.0: return 0.0 idle_delta = getattr(current_times, "idle", 0) - getattr(last_times, "idle", 0) cpu_percent = ((all_delta - idle_delta) / all_delta) * 100 _system_task_state["last_cpu_times"] = current_times return max(0.0, min(100.0, cpu_percent)) except Exception: return psutil.cpu_percent(interval=0.1) def get_mem_used_percent() -> float: """内存使用率""" try: mem = psutil.virtual_memory() total = mem.total / 1024 / 1024 free = mem.free / 1024 / 1024 buffers = getattr(mem, "buffers", 0) / 1024 / 1024 cached = getattr(mem, "cached", 0) / 1024 / 1024 used = total - free - buffers - cached return used / (total / 100.0) if total else 1.0 except Exception: return 1.0 # noinspection PyUnusedLocal,PyTypeChecker def get_swap_used_percent() -> float: """ 获取Swap内存已占用的百分比(返回0.0~100.0,异常时返回100.0) 逻辑:Swap使用率 = (已使用Swap / 总Swap容量) * 100% """ try: # 获取Swap内存信息(psutil.swap_memory()返回namedtuple) swap = psutil.swap_memory() # Swap总容量(bytes → MB,和物理内存计算单位保持一致) swap_total = swap.total / 1024 / 1024 # Swap已使用量(bytes → MB) swap_used = swap.used / 1024 / 1024 swap_free = swap.free / 1024 / 1024 # 避免除以0(无Swap分区时) if swap_total == 0: return 0.0, 0, 0, 0 # 无Swap时默认返回100%(或根据需求改0.0) # 计算使用率(保留2位小数,确保返回float类型) used_percent = round((swap_used / swap_total) * 100.0, 2) # 边界值修正(防止因系统浮点误差导致超过100%) # return min(used_percent, 100.0),swap.total/1024,swap.used/1024,swap.free/1024 return min(used_percent, 100.0), swap.total, swap.used, swap.free # 捕获所有异常,返回100%(和物理内存函数的异常返回逻辑一致) except Exception: return 100.0, 0, 0, 0 def get_load_average() -> Tuple[float, float, float, float]: """负载平均""" try: one, five, fifteen = os.getloadavg() max_v = psutil.cpu_count() * 2 lpro = round((one / max_v) * 100, 2) if max_v else 0 if lpro > 100: lpro = 100 return lpro, float(one), float(five), float(fifteen) except Exception: return 0.0, 0.0, 0.0, 0.0 def get_network_io() -> Optional[dict]: """网络IO""" try: network_io = psutil.net_io_counters(pernic=True) ret = { 'total_up': sum(v.bytes_sent for v in network_io.values()), 'total_down': sum(v.bytes_recv for v in network_io.values()), 'timestamp': time.time(), 'down_packets': {k: v.bytes_recv for k, v in network_io.items()}, 'up_packets': {k: v.bytes_sent for k, v in network_io.items()} } last_io = _system_task_state["last_network_io"] if not last_io: _system_task_state["last_network_io"] = ret return None diff_t = (ret["timestamp"] - last_io.get("timestamp", 0)) * 1024 # 转KB if diff_t <= 0: return None res = { 'up': round((ret['total_up'] - last_io.get('total_up', 0)) / diff_t, 2), 'down': round((ret['total_down'] - last_io.get('total_down', 0)) / diff_t, 2), 'total_up': ret['total_up'], 'total_down': ret['total_down'], 'down_packets': { k: round((v - last_io.get('down_packets', {}).get(k, 0)) / diff_t, 2) for k, v in ret['down_packets'].items() }, 'up_packets': { k: round((v - last_io.get('up_packets', {}).get(k, 0)) / diff_t, 2) for k, v in ret['up_packets'].items() } } _system_task_state["last_network_io"] = ret return res except Exception: return None def get_disk_io() -> Optional[dict]: """磁盘IO""" if not os.path.exists('/proc/diskstats'): return None try: disk_io = psutil.disk_io_counters() if not disk_io: return None ret = { 'read_count': disk_io.read_count, 'write_count': disk_io.write_count, 'read_bytes': disk_io.read_bytes, 'write_bytes': disk_io.write_bytes, 'read_time': disk_io.read_time, 'write_time': disk_io.write_time, 'timestamp': time.time() } last_io = _system_task_state["last_disk_io"] if not last_io: _system_task_state["last_disk_io"] = ret return None diff_t = ret["timestamp"] - last_io.get("timestamp", 0) if diff_t <= 0: return None res = { 'read_count': int((ret["read_count"] - last_io.get("read_count", 0)) / diff_t), 'write_count': int((ret["write_count"] - last_io.get("write_count", 0)) / diff_t), 'read_bytes': int((ret["read_bytes"] - last_io.get("read_bytes", 0)) / diff_t), 'write_bytes': int((ret["write_bytes"] - last_io.get("write_bytes", 0)) / diff_t), 'read_time': int((ret["read_time"] - last_io.get("read_time", 0)) / diff_t), 'write_time': int((ret["write_time"] - last_io.get("write_time", 0)) / diff_t), } _system_task_state["last_disk_io"] = ret return res except Exception: return None _XSS_TRANS = str.maketrans({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }) def xss_encode(s: str) -> str: """XSS""" if not isinstance(s, str): s = str(s) if not any(c in s for c in '&<>"\''): return s return s.translate(_XSS_TRANS) def cut_top_list(process_list, *sort_key, top_num=5) -> list: # """最小堆取前N""" # import heapq # if not process_list or not sort_key: # return [] # try: # if len(sort_key) == 1: # return heapq.nlargest( # top_num, process_list, key=lambda x: x.get(sort_key[0], 0) # ) # else: # return heapq.nlargest( # top_num, process_list, key=lambda x: tuple(x.get(k, 0) for k in sort_key) # ) # except (TypeError, ValueError): # return [] """前N个""" if not process_list or not sort_key: return [] tops = sorted( process_list, key=lambda x: tuple(x.get(k, 0) for k in sort_key), reverse=True ) return tops[:top_num] def get_process_list() -> list: """获取进程列表""" SKIP_NAMES = { 'edac-poller', 'devfreq_wq', 'watchdogd', 'kthrotld', 'acpi_thermal_pm', 'charger_manager', 'kthreadd', 'rcu_gp', 'rcu_par_gp', 'rcu_sched', 'migration/0', 'cpuhp/0', 'kdevtmpfs', 'netns', 'oom_reaper', 'writeback', 'crypto', 'kintegrityd', 'kblockd', 'ata_sff', } try: pids = psutil.pids() current_pid = os.getpid() timer = getattr(time, "monotonic", time.time) cpu_num = psutil.cpu_count() or 1 process_list = [] new_cache = {} inactive_cache = _system_task_state["inactive_process_cache"] for pid in pids: if pid == current_pid: continue if pid < 10: # 核心进程 continue if pid in inactive_cache: inactive_count, last_check = inactive_cache[pid] # 5分钟重新检查一次 if time.time() - last_check < 300 and inactive_count >= 3: inactive_cache[pid] = (inactive_count, time.time()) # 结合后续10分钟清理逻辑 continue try: p = psutil.Process(pid) if p.status() in (psutil.STATUS_ZOMBIE, psutil.STATUS_DEAD): # 休眠/僵尸 continue process_name = None try: process_name = p.name() if process_name in SKIP_NAMES: continue except: pass with p.oneshot(): create_time = p.create_time() if not process_name: try: process_name = p.name() except: process_name = "unknown" last_cache = _system_task_state["last_process_cache"].get(pid) if last_cache and last_cache[0] != create_time: # 进程应该被重启了 last_cache = None if pid in inactive_cache: del inactive_cache[pid] p_cpu_time = p.cpu_times() cpu_time = p_cpu_time.system + p_cpu_time.user io_counters = p.io_counters() memory_info = p.memory_info() current_time = timer() # 刷新缓存 new_cache[pid] = ( create_time, cpu_time, io_counters.read_bytes, io_counters.write_bytes, current_time ) if not last_cache: # 初次处理的进程 continue diff_t = current_time - last_cache[4] if diff_t <= 0: continue cpu_percent = max(round((cpu_time - last_cache[1]) * 100 / diff_t / cpu_num, 2), 0) disk_read = max(0, int((io_counters.read_bytes - last_cache[2]) / diff_t)) disk_write = max(0, int((io_counters.write_bytes - last_cache[3]) / diff_t)) disk_total = disk_read + disk_write if cpu_percent == 0 and disk_total == 0: if pid in inactive_cache: inactive_cache[pid] = (inactive_cache[pid][0] + 1, time.time()) else: inactive_cache[pid] = (1, time.time()) continue if pid in inactive_cache: del inactive_cache[pid] # swap占用 try: swap = p.memory_full_info().swap except: swap = 0 # connect_count = len( # p.net_connections() if hasattr(p, "net_connections") else p.connections() # noqa # ) process_info = { 'pid': pid, 'name': process_name, 'username': p.username(), 'cpu_percent': cpu_percent, 'memory': memory_info.rss, 'swap': swap, 'disk_read': disk_read, 'disk_write': disk_write, 'disk_total': disk_total or 0, 'cmdline': ' '.join(filter(lambda x: x, p.cmdline()))[:500], 'create_time': create_time, 'connect_count': 0, # future 'net_total': 0, # future 'up': 0, # future 'down': 0, # future 'up_package': 0, # future 'down_package': 0, # future } # process_info["net_total"] = process_info["up"] + process_info["down"] process_list.append(process_info) except Exception: continue current_time = time.time() inactive_cache_copy = dict(inactive_cache) for pid, (count, last_check) in inactive_cache_copy.items(): remove_flag = False if current_time - last_check > 600: # 超过 10 分钟 remove_flag = True else: # 进程残留, pid被复用 try: current_create = psutil.Process(pid).create_time() exist_create = None if pid in _system_task_state["last_process_cache"]: exist_create = _system_task_state["last_process_cache"][pid][0] elif pid in new_cache: exist_create = new_cache[pid][0] if exist_create and current_create != exist_create: remove_flag = True # pid被复用 except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): remove_flag = True if remove_flag and pid in inactive_cache: del inactive_cache[pid] _system_task_state["last_process_cache"] = new_cache _system_task_state["inactive_process_cache"] = inactive_cache return process_list except Exception: return [] # 暂无用 # def start_process_net_total(self): # # 进程流量监控,如果文件:/www/server/panel/data/is_net_task.pl 或 /www/server/panel/data/control.conf不存在,则不监控进程流量 # if not (os.path.isfile(self.proc_net_service) and os.path.isfile(self.base_service)): # return # # def process_net_total(): # class_path = '{}/class'.format(BASE_PATH) # if class_path not in sys.path: # sys.path.insert(0, class_path) # import process_task # process_task.process_network_total().start() # # import threading # th = threading.Thread(target=process_net_total, daemon=True) # th.start() def ensure_table(db_file: str) -> None: """表, 字段处理""" if not os.path.isfile(db_file): os.makedirs(os.path.dirname(db_file), exist_ok=True) open(db_file, 'w').close() conn = None cursor = None init_sql = ''' CREATE TABLE IF NOT EXISTS `cpuio` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `pro` INTEGER, `mem` INTEGER, `swap_percent` INTEGER, `swap_total` INTEGER, `swap_used` INTEGER, `swap_free` INTEGER, `addtime` INTEGER ); CREATE TABLE IF NOT EXISTS `network` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `up` INTEGER, `down` INTEGER, `total_up` INTEGER, `total_down` INTEGER, `down_packets` INTEGER, `up_packets` INTEGER, `addtime` INTEGER ); CREATE TABLE IF NOT EXISTS `diskio` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `read_count` INTEGER, `write_count` INTEGER,`read_bytes` INTEGER, `write_bytes` INTEGER, `read_time` INTEGER, `write_time` INTEGER, `addtime` INTEGER ); CREATE TABLE IF NOT EXISTS `load_average` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `pro` REAL, `one` REAL, `five` REAL, `fifteen` REAL, `addtime` INTEGER ); CREATE TABLE IF NOT EXISTS `process_top_list` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `cpu_top` REAL, `memory_top` REAL, `disk_top` REAL, `net_top` REAL, `all_top` REAL, `swap_top` REAL, `addtime` INTEGER ); -- 索引 CREATE INDEX IF NOT EXISTS idx_cpuio_addtime ON cpuio (addtime); CREATE INDEX IF NOT EXISTS idx_network_addtime ON network (addtime); CREATE INDEX IF NOT EXISTS idx_diskio_addtime ON diskio (addtime); CREATE INDEX IF NOT EXISTS idx_load_average_addtime ON load_average (addtime); CREATE INDEX IF NOT EXISTS idx_process_top_list_addtime ON process_top_list (addtime); ''' try: conn = sqlite3.connect(db_file) try: conn.executescript(init_sql) conn.commit() except Exception as e: logger.error("Failed to initialize system.db : {}".format(e)) # 检测cpuio表是否存在swap字段 cursor = conn.cursor() try: cursor.execute( "SELECT swap_total, swap_percent, swap_used, swap_free FROM cpuio ORDER BY id DESC LIMIT 1" ) except sqlite3.OperationalError: # swap字段不存在 alter_sql = ''' ALTER TABLE cpuio ADD COLUMN swap_percent INTEGER DEFAULT 0; ALTER TABLE cpuio ADD COLUMN swap_total INTEGER DEFAULT 0; ALTER TABLE cpuio ADD COLUMN swap_used INTEGER DEFAULT 0; ALTER TABLE cpuio ADD COLUMN swap_free INTEGER DEFAULT 0; ''' try: conn.executescript(alter_sql) conn.commit() except sqlite3.OperationalError: pass # 检测process_top_list表是否存在swap_top字段 try: cursor.execute("SELECT swap_top FROM process_top_list ORDER BY id DESC LIMIT 1") except sqlite3.OperationalError: try: conn.execute("ALTER TABLE process_top_list ADD COLUMN swap_top REAL DEFAULT []") conn.commit() except sqlite3.OperationalError: pass _system_task_state["table_ensure"] = True except Exception as e: logger.error("Failed to connect to system.db : {}".format(e)) _system_task_state["table_ensure"] = False finally: if cursor: cursor.close() if conn: conn.close() def clear_expire_data(conn: sqlite3.Connection) -> None: if not _system_task_state.get("last_clear_time"): return now = int(time.time()) # 检查是否需要清理(每小时一次) if now - _system_task_state.get("last_clear_time", 0) < 3600: return cur = None try: cur = conn.cursor() deltime = now - (keep_days * 86400) cur.execute("DELETE FROM cpuio WHERE addtime < ?", (deltime,)) cur.execute("DELETE FROM network WHERE addtime < ?", (deltime,)) cur.execute("DELETE FROM diskio WHERE addtime < ?", (deltime,)) cur.execute("DELETE FROM load_average WHERE addtime < ?", (deltime,)) cur.execute("DELETE FROM process_top_list WHERE addtime < ?", (deltime,)) conn.commit() _system_task_state["last_clear_time"] = now except: pass finally: if cur: cur.close() control_conf = f"{BASE_PATH}/data/control.conf" db_file = f"{BASE_PATH}/data/system.db" # 表结构检查 if not _system_task_state["table_ensure"]: ensure_table(db_file) # 是否启用监控, 不存在时为不开启监控, 存在时为监控天数 if not os.path.exists(control_conf): return try: keep_days = int(read_file(control_conf) or 30) if keep_days < 1: return except Exception: keep_days = 30 conn = None cursor = None try: conn = sqlite3.connect(db_file, timeout=3) cursor = conn.cursor() cpu_used = get_cpu_percent_smooth() mem_used = get_mem_used_percent() swap_percent, swap_total, swap_used, swap_free = get_swap_used_percent() lpro, one, five, fifteen = get_load_average() network_io = get_network_io() disk_io = get_disk_io() process_list = get_process_list() proc_net_service = f"{BASE_PATH}/data/is_net_task.pl" addtime = int(time.time()) # cpu, swap cursor.execute( "INSERT INTO cpuio (pro, mem, swap_percent, swap_total, swap_used, swap_free, addtime) VALUES (?, ?, ?, ?, ?, ?, ?)", (cpu_used, mem_used, swap_percent, swap_total, swap_used, swap_free, addtime) ) # load cursor.execute( "INSERT INTO load_average (pro, one, five, fifteen, addtime) VALUES (?, ?, ?, ?, ?)", (lpro, one, five, fifteen, addtime) ) # network io if network_io: cursor.execute( "INSERT INTO network (up, down, total_up, total_down, down_packets, up_packets, addtime) VALUES (?, ?, ?, ?, ?, ?, ?)", ( network_io['up'], network_io['down'], network_io['total_up'], network_io['total_down'], json.dumps(network_io['down_packets']), json.dumps(network_io['up_packets']), addtime ) ) # disk io if disk_io: cursor.execute( "INSERT INTO diskio (read_count, write_count, read_bytes, write_bytes, read_time, write_time, addtime) VALUES (?, ?, ?, ?, ?, ?, ?)", ( disk_io['read_count'], disk_io['write_count'], disk_io['read_bytes'], disk_io['write_bytes'], disk_io['read_time'], disk_io['write_time'], addtime ) ) # process top list if process_list: all_top_list = cut_top_list( process_list, 'cpu_percent', 'disk_total', 'memory', 'net_total', top_num=5 ) cpu_top_list = cut_top_list(process_list, 'cpu_percent', top_num=5) disk_top_list = cut_top_list(process_list, 'disk_total', top_num=5) memory_top_list = cut_top_list(process_list, 'memory', top_num=5) swap_top_list = cut_top_list(process_list, 'swap', top_num=5) # net_top_list = all_top_list if os.path.isfile(proc_net_service): # 进程流量监控, 暂无用 # net_top_list = top_lists['net_top'] pass all_top = json.dumps([( p['cpu_percent'], p['disk_read'], p['disk_write'], p['memory'], p['up'], p['down'], p['pid'], xss_encode(p['name']), xss_encode(p['cmdline']), xss_encode(p['username']), p['create_time'] ) for p in all_top_list]) cpu_top = json.dumps([( p['cpu_percent'], p['pid'], xss_encode(p['name']), xss_encode(p['cmdline']), xss_encode(p['username']), p['create_time'] ) for p in cpu_top_list]) disk_top = json.dumps([( p['disk_total'], p['disk_read'], p['disk_write'], p['pid'], xss_encode(p['name']), xss_encode(p['cmdline']), xss_encode(p['username']), p['create_time'] ) for p in disk_top_list]) # net_top = json.dumps([( # p['net_total'], p['up'], p['down'], p['connect_count'], p['up_package'] + p['down_package'], # p['pid'], xss_encode(p['name']), xss_encode(p['cmdline']), xss_encode(p['username']), # p['create_time'] # ) for p in net_top_list]) net_top = None memory_top = json.dumps([( p['memory'], p['pid'], xss_encode(p['name']), xss_encode(p['cmdline']), xss_encode(p['username']), p['create_time'] ) for p in memory_top_list]) swap_top = json.dumps([( p['swap'], p['pid'], xss_encode(p['name']), xss_encode(p['cmdline']), xss_encode(p['username']), p['create_time'] ) for p in swap_top_list]) cursor.execute( "INSERT INTO process_top_list (all_top, cpu_top, disk_top, net_top, memory_top, swap_top, addtime) VALUES (?, ?, ?, ?, ?, ?, ?)", (all_top, cpu_top, disk_top, net_top, memory_top, swap_top, addtime) ) conn.commit() # every 1h check clear old data clear_expire_data(conn) except sqlite3.OperationalError as e: logger.error(f"SQLite OperationalError in systemTask: {e}") ensure_table(db_file) _system_task_state["table_ensure"] = False except Exception: logger.error(f"systemTask error: {traceback.format_exc()}") finally: if cursor: cursor.close() if conn: conn.close() gc.collect() def check502Task(): task_ExecShell("check502task") # 服务守护 def daemon_service(): try: obj = RestartServices() obj.main() del obj finally: gc.collect() # 项目守护 def project_daemon_service(): task_ExecShell("project_daemon_service") # 重启面板服务 def restart_panel(): def service_panel(action='reload'): if not os.path.exists('{}/init.sh'.format(BASE_PATH)): os.system("curl -k https://node.yakpanel.com/install/update_7.x_en.sh|bash &") else: os.system("nohup bash /www/server/panel/init.sh {} > /dev/null 2>&1 &".format(action)) logger.info("Panel Service: {}".format(action)) rtips = '{}/data/restart.pl'.format(BASE_PATH) reload_tips = '{}/data/reload.pl'.format(BASE_PATH) if os.path.exists(rtips): os.remove(rtips) service_panel('restart') if os.path.exists(reload_tips): os.remove(reload_tips) service_panel('reload') # 定时任务去检测邮件信息 def send_mail_time(): if not os.path.exists('/www/server/panel/plugin/mail_sys/mail_sys_main.py') or not os.path.exists('/www/vmail'): return exec_shell("{} /www/server/panel/script/mail_task.py".format(PYTHON_BIN)) # 面板推送消息 def push_msg(): def _read_file(file_path: str) -> Optional[list]: if not os.path.exists(file_path): return None content = read_file(file_path) if not content: return None try: return json.loads(content) except: return [] sender_path = f"{BASE_PATH}/data/mod_push_data/sender.json" task_path = f"{BASE_PATH}/data/mod_push_data/task.json" sender_info = _read_file(sender_path) or [] work = False for s in sender_info: # default sender_type sms data is {} if s.get("sender_type") != "sms" and s.get("data"): work = True break if not work: return if not _read_file(task_path): return task_ExecShell("push_msg") # 检测面板授权 # noinspection PyUnboundLocalVariable def panel_auth(): pro_file = '/www/server/panel/data/panel_pro.pl' update_file = '/www/server/panel/data/now_update_pro.pl' if os.path.exists(pro_file): try: from YakPanel import cache except Exception as e: logger.error("Failed to import cache from YakPanel: {}".format(e)) cache = None if cache: key = 'pro_check_sdfjslk' res = cache.get(key) if os.path.exists(update_file) or res is None: os.system('nohup {} /www/server/panel/script/check_auth.py > /dev/null 2>&1 &'.format(PYTHON_BIN)) if cache: cache.set(key, 'sddsf', 3600) if os.path.exists(update_file): os.remove(update_file) def count_ssh_logs(): task_ExecShell("count_ssh_logs") # 每天提交一次昨天的邮局发送总数 def submit_email_statistics(): task_ExecShell( "submit_email_statistics", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ]) # 每天一次 提交今天之前的统计数据 def submit_module_call_statistics(): task_ExecShell("submit_module_call_statistics") def mailsys_domain_restrictions(): if not os.path.exists('/www/server/panel/plugin/mail_sys/mail_send_bulk.py'): return if not os.path.exists('/www/vmail'): return yesterday = datetime.now() - timedelta(days=1) yesterday = yesterday.strftime('%Y-%m-%d') cloud_yesterday_submit = '{}/data/{}_update_mailsys_domain_restrictions.pl'.format( BASE_PATH, yesterday ) if os.path.exists(cloud_yesterday_submit): return if os.path.exists("/www/server/panel/plugin/mail_sys"): sys.path.insert(1, "/www/server/panel/plugin/mail_sys") # 检查版本 检查是否能查询额度 剩余额度 import public.PluginLoader as plugin_loader bulk = plugin_loader.get_module('{}/plugin/mail_sys/mail_send_bulk.py'.format(BASE_PATH)) SendMailBulk = bulk.SendMailBulk try: SendMailBulk()._get_user_quota() except: logger.error(traceback.format_exc()) return # 添加标记 write_file(cloud_yesterday_submit, '1') # 删除前天标记 before_yesterday = datetime.now() - timedelta(days=2) before_yesterday = before_yesterday.strftime('%Y-%m-%d') cloud_before_yesterday_submit = '{}/data/{}_update_mailsys_domain_restrictions.pl'.format( BASE_PATH, before_yesterday ) if os.path.exists(cloud_before_yesterday_submit): os.remove(cloud_before_yesterday_submit) return def mailsys_domain_blecklisted_alarm(): task_ExecShell( "mailsys_domain_blecklisted_alarm", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ] ) def update_vulnerabilities(): task_ExecShell("update_vulnerabilities") # 邮件域名邮箱使用限额告警 def mailsys_quota_alarm(): try: if not os.path.exists('/www/server/panel/plugin/mail_sys/mail_sys_main.py') or not os.path.exists( '/www/vmail'): return script = '/www/server/panel/plugin/mail_sys/script/check_quota_alerts.py' if not os.path.exists(script): return cmd = f"btpython {script}" exec_shell(cmd) except: pass # 邮局更新域名邮箱使用量 def mailsys_update_usage(): try: if not os.path.exists( '/www/server/panel/plugin/mail_sys/mail_sys_main.py' ) or not os.path.exists('/www/vmail'): return script = '/www/server/panel/plugin/mail_sys/script/update_usage.py' if not os.path.exists(script): return cmd = f"btpython {script}" exec_shell(cmd) except: pass # 邮局自动回复 def auto_reply_tasks(): task_ExecShell( "auto_reply_tasks", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ] ) # 邮局自动扫描异常邮箱 def auto_scan_abnormal_mail(): task_ExecShell( "auto_scan_abnormal_mail", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ] ) # 每6小时aa默认ssl检查 def domain_ssl_service(): # check 6h, inside task_ExecShell("make_suer_ssl_task") # 每隔20分钟更新一次网站报表数据 def update_monitor_requests(): task_ExecShell("update_monitor_requests") # 每隔20分钟更新一次waf报表数据 def update_waf_config(): task_ExecShell("update_waf_config") # 每6小时进行恶意文件扫描 def malicious_file_scanning(): task_ExecShell("malicious_file_scanning") # 多服务守护任务,仅在多服务下执行,每5分钟 300 s 检查一次 def multi_web_server_daemon(): task_ExecShell("multi_web_server_daemon") def parse_soft_name_of_version(name): """ @name 获取软件名称和版本 @param name 软件名称 @return tuple(string, string) 返回软件名称和版本 """ if name.find('Docker') != -1: return 'docker', '1.0' return_default = ('', '') l, r = name.find('['), name.find(']') if l == -1 or r == -1 or l > r: return return_default # 去除括号只保留括号中间的软件名称和版本 if name[l + 1:r].count("-") == 0: return return_default soft_name, soft_version = name[l + 1:r].split('-')[:2] if soft_name == 'php': soft_version = soft_version.replace('.', '') return soft_name, soft_version def check_install_status(name: str): """ @name 检查软件是否安装成功 @param name 软件名称 @return tuple(bool, string) 返回是否安装成功和安装信息 """ return_default = (1, 'Installation successful') try: # 获取安装检查配置 install_config = json.loads(read_file("{}/config/install_check.json".format(BASE_PATH))) except: return return_default try: # 获取软件名称和版本 soft_name, soft_version = parse_soft_name_of_version(name) if not soft_name or not soft_version: return return_default if soft_name not in install_config: return return_default if os.path.exists("{}/install/{}_not_support.pl".format(BASE_PATH, soft_name)): return 0, 'Not compatible with this system! Please click on the details to explain!' if os.path.exists("{}/install/{}_mem_kill.pl".format(BASE_PATH, soft_name)): return 0, 'Insufficient memory installation exception! Please click on the details to explain!' soft_config = install_config[soft_name] # 取计算机名 def get_hostname(): try: import socket return socket.gethostname() except: return 'localhost.localdomain' # 替换soft_config中所有变量 def replace_all(dat: str): if not dat: return dat if dat.find('{') == -1: return dat # 替换安装路径, 替换版本号 dat = dat.replace('{SetupPath}', '/www/server').replace('{Version}', soft_version) # 替换主机名 if dat.find("{Host") != -1: host_name = get_hostname() host = host_name.split('.')[0] dat = dat.replace("{Hostname}", host_name) dat = dat.replace("{Host}", host) return dat # 检查文件是否存在 if 'files_exists' in soft_config: for f_name in soft_config['files_exists']: filename = replace_all(f_name) if not os.path.exists(filename): return 0, 'Installation failed, file does not exist:{}'.format(filename) # 检查pid文件是否有效 if 'pid' in soft_config and soft_config['pid']: pid_file = replace_all(soft_config['pid']) if not os.path.exists(pid_file): return 0, 'Startup failed, PID file does not exist:{}'.format(pid_file) pid = read_file(pid_file) if not pid: return 0, 'Startup failed, PID file does not exist:{}'.format(pid_file) proc_file = '/proc/{}/cmdline'.format(pid.strip()) if not os.path.exists(proc_file): return 0, 'Startup failed, PID file is empty: {}({}) process does not exist'.format(pid_file, pid) # 执行命令检查 if 'cmd' in soft_config: for cmd in soft_config['cmd']: p = subprocess.Popen( replace_all(cmd['exec']), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) p.wait() res = p.stdout.read() + "\n" + p.stderr.read() if res.find(replace_all(cmd['success'])) == -1: return 0, '[{}] Abnormal service startup status'.format(soft_name) except: pass return return_default def soft_task(): # 执行面板soft corn之类的安装执行任务, from task.py -> def startTask(): def ExecShell(cmdstring, cwd=None, shell=True, symbol='&>'): try: import shlex import subprocess import time sub = subprocess.Popen( cmdstring + symbol + exlogPath, cwd=cwd, stdin=subprocess.PIPE, shell=shell, bufsize=4096 ) while sub.poll() is None: time.sleep(0.1) return sub.returncode except: return None # def TaskStart(value: dict, start: int = None, time_out: int = None): # """执行安装任务, 并检测是否已经在执行或超时""" # def is_time_out(pid: int) -> bool: # res = int(time.time()) - start > time_out if time_out else False # if res: # try: # p = psutil.Process(pid) # child = p.children(recursive=True) # for c in child: # try: c.kill() # except: continue # p.kill() # except Exception: pass # return res # # start = int(time.time()) if not start else start # try: # output, _ = exec_shell(f"pgrep -f \"{value['execstr'] + '&>' + exlogPath}\"") # pids = output.strip().split() # if pids: # target_pid = int(pids[0]) # sql.table('tasks').where("id=?", (value['id'],)).save('status', (Running,)) # while not is_time_out(target_pid): # try: # os.kill(target_pid, 0) # time.sleep(1) # 残留阻塞 # except ProcessLookupError: break # except Exception: break # if is_time_out(target_pid): # raise Exception # raise Exception # except Exception: # sql.table('tasks').where("id=?", (value['id'],)).save('status,start', (Running, start)) # ExecShell(value['execstr']) tip_file = "/dev/shm/.panelTask.pl" panel_log_path = "/www/server/panel/logs/installed/" if not os.path.exists(panel_log_path): os.mkdir(panel_log_path) os.chmod(panel_log_path, 0o600) Waitting = "0" # 等待 Running = "-1" # 执行中 Finished = "1" # 完成 try: if os.path.exists(isTask): with db.Sql() as sql: # 检测task表是否存在install_status,message字段 field = 'id,type,execstr,name,install_status,message' check_result = sql.table('tasks').order("id desc").field(field).select() if type(check_result) == str: sql.table('tasks').execute("ALTER TABLE 'tasks' ADD 'install_status' INTEGER DEFAULT 1", ()) sql.table('tasks').execute("ALTER TABLE 'tasks' ADD 'message' TEXT DEFAULT ''", ()) sql.table('tasks').where("status=?", (Running,)).setField('status', Waitting) taskArr = sql.table('tasks').where("status=?", (Waitting,)).field('id,type,execstr,name').order( "id asc" ).select() for value in taskArr: if value['type'] != 'execshell': continue if not sql.table('tasks').where("id=?", (value['id'],)).count(): write_file(tip_file, str(int(time.time()))) continue start = int(time.time()) # TaskStart(value, start=start, time_out=3600) sql.table('tasks').where("id=?", (value['id'],)).save('status,start', (Running, start)) ExecShell(value['execstr']) # 保存安装日志 target_log_file = '{}/task_{}.log'.format(panel_log_path, value['id']) shutil.copy(exlogPath, target_log_file) # 检查软件是否安装成功 end = int(time.time()) try: install_status, install_msg = check_install_status(value['name']) except Exception: install_status = 1 install_msg = "" sql.table('tasks').where("id=?", (value['id'],)).save( 'status,end,install_status,message', (Finished, end, install_status, install_msg) ) if sql.table('tasks').where("status=?", (Waitting,)).count() < 1: if os.path.exists(isTask): os.remove(isTask) write_file(tip_file, str(int(time.time()))) except Exception as e: logger.error(f"start_bt_task error: {e}") # 预安装网站监控报表 def check_site_monitor(): task_ExecShell("check_site_monitor") # 节点监控 def node_monitor(): task_ExecShell("node_monitor") # 节点监控 def node_monitor_check(): task_ExecShell("node_monitor_check") # 检测防爆破计划任务 def breaking_through(): task_ExecShell("breaking_through") # 找site favicons def find_favicons(): task_ExecShell( "find_favicons", paths_exists=[ '/www/server/panel/config/auto_favicon.conf', ] ) # 邮件日志 def maillog_event(): task_ExecShell( "maillog_event", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ] ) # 邮件处理日志聚合 def aggregate_maillogs_task(): task_ExecShell( "aggregate_maillogs_task", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ] ) # 邮件自动化发件任务 def schedule_automations(): task_ExecShell( "schedule_automations", paths_exists=[ "/www/server/panel/plugin/mail_sys/mail_sys_main.py", "/www/vmail", ] ) # 刷新docker app 列表 def refresh_dockerapps(): task_ExecShell("refresh_dockerapps") # 版本更新执行一次性 def task_version_part(): task_ExecShell("task_version_part") # ================================ 这是任务分割线 =============================== TASKS = [ # <核心任务> 面板重启检查, 面板授权检查 {"func": [restart_panel, panel_auth], "interval": 2, "is_core": True}, {"func": soft_task, "interval": 2, "is_core": True}, # 原面板任务 {"func": bt_box_task, "interval": 2, "is_core": True}, # 原面板任务 # <核心任务> 服务守护 {"func": daemon_service, "interval": 10, "is_core": True}, # <核心任务> 原系统监控 {"func": systemTask, "interval": 60, "is_core": True}, # 每1分钟系统监控任务 {"func": project_daemon_service, "interval": 120, "is_core": True}, # 每120秒项目守护 # ================================ 分割线 =============================== # <普通任务> # func 函数, interval 间隔时间秒s, 排队复用线程 (打印请用logger, 日志路径 .../logs/task.log) {"func": push_msg, "interval": 60}, # 每1分钟面板推送消息 {"func": breaking_through, "interval": 60}, # 每分钟防爆破计划任务 {"func": multi_web_server_daemon, "interval": 300}, # 每5分钟多服务守护任务 {"func": check502Task, "interval": 60 * 10}, # 每10分钟 502检查(夹杂若干任务) {"func": check_site_monitor, "interval": 60 * 10}, # 每10分钟检查站点安装监控 {"func": node_monitor, "interval": 60}, # 每1分钟节点监控任务 {"func": node_monitor_check, "interval": 60 * 60 * 24 * 30}, # 每月节点监控检测任务 {"func": update_waf_config, "interval": 60 * 20}, # 每隔20分钟更新一次waf报表数据 {"func": update_monitor_requests, "interval": 60 * 20}, # 每隔20分钟更新一次网站报表数据 {"func": find_favicons, "interval": 43200}, # 每12小时找favicons {"func": domain_ssl_service, "interval": 3600}, # 每6小时进行域名SSL服务(内置时间标记, 可提前检查) {"func": malicious_file_scanning, "interval": 60 * 60 * 6}, # 每每6小时进行恶意文件扫描 {"func": count_ssh_logs, "interval": 3600 * 24}, # 每天统计SSH登录日志 {"func": submit_module_call_statistics, "interval": 3600}, # 每天一次 提交今天之前的统计数据(内置时间标记, 可提前检查) {"func": maillog_event, "interval": 60, "loop": True}, # 邮局日志事件监控 event loop事件, 每60秒一次, 起守护作用 {"func": send_mail_time, "interval": 60 * 3}, # 每3分钟检测邮件信息 {"func": auto_reply_tasks, "interval": 3600}, # 每1小时自动回复邮件 {"func": schedule_automations, "interval": 60}, # 每1分钟邮局自动化任务 {"func": aggregate_maillogs_task, "interval": 60}, # 每1分钟聚合邮局日志 {"func": mailsys_quota_alarm, "interval": 3600 * 2}, # 每2小时邮件域名邮箱使用限额告警 {"func": auto_scan_abnormal_mail, "interval": 3600 * 2}, # 每2小时自动扫描异常邮箱 {"func": mailsys_update_usage, "interval": 3600 * 12}, # 每12小时邮局更新域名邮箱使用量 {"func": submit_email_statistics, "interval": 3600 * 24}, # 每天一次 昨日邮件发送统计 {"func": mailsys_domain_blecklisted_alarm, "interval": 3600 * 24}, # # 每天一次 邮局黑名单检测 {"func": update_vulnerabilities, "interval": 3600 * 24}, # # 每天一次 更新漏洞信息 {"func": refresh_dockerapps, "interval": 3600 * 24}, # # 每天一次 更新docker app 列表 ] def thread_register(brain: SimpleBrain, is_core: bool = True): if not is_core: # delay normal tasks logger.info("Normal Task will be join active after 30s") time.sleep(30) for index, task in enumerate(TASKS): try: if task.get("is_core", False) == is_core: if isinstance(task["func"], list): task_id = "_".join([f.__name__ for f in task["func"]]) else: task_id = task.get("id", task["func"].__name__) # delay normal tasks, 削峰 if not is_core: time.sleep(10) brain.register_task( func=task["func"], task_id=task_id, interval=task.get("interval", 3600), is_core=task.get("is_core", False), loop=task.get("loop", False), ) except Exception: import traceback logger.error(f"Register task {task} failed: {traceback.format_exc()}") continue logger.info( f"All the {'[Core]' if is_core else '[Normal]'} tasks have been registered." ) def main(max_workers: int = None): main_pid = "logs/task.pid" if os.path.exists(main_pid): os.system("kill -9 $(cat {}) &> /dev/null".format(main_pid)) pid = os.fork() if pid: sys.exit(0) os.setsid() _pid = os.fork() if _pid: write_file(main_pid, str(_pid)) sys.exit(0) sys.stdout.flush() sys.stderr.flush() logger.info("Service Up") time.sleep(5) task_version_part() # =================== Start =========================== sb = SimpleBrain(cpu_max=20.0, workers=max_workers) try: # core tasks thread_register(brain=sb, is_core=True) # normal tasks will be delayed threading.Thread( target=thread_register, args=(sb, False), daemon=True ).start() sb.run() except Exception: import traceback logger.error(traceback.format_exc()) sb._shutdown() # =================== End ======================== if __name__ == "__main__": main()