#!/www/server/panel/pyenv/bin/python #coding: utf-8 # +------------------------------------------------------------------- # | YakPanel # +------------------------------------------------------------------- # | Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved. # +------------------------------------------------------------------- # | Author: hwliang # +------------------------------------------------------------------- from gevent import monkey monkey.patch_all() import os import sys import ssl import time import logging import psutil _PATH = '/www/server/panel' os.chdir(_PATH) # upgrade_file = 'script/upgrade_flask.sh' # if os.path.exists(upgrade_file): # os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) # # upgrade_file = 'script/upgrade_gevent.sh' # if os.path.exists(upgrade_file): # os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) upgrade_file = 'script/upgrade_telegram.sh' if os.path.exists(upgrade_file): os.system("nohup bash {} &>/dev/null &".format(upgrade_file)) if os.path.exists('class/flask'): os.system('rm -rf class/flask') if not 'class/' in sys.path: sys.path.insert(0,'class/') from YakPanel import app,sys,public is_debug = os.path.exists('data/debug.pl') # 检查加载器 def check_plugin_loader(): plugin_loader_file = 'class/PluginLoader.so' machine = 'x86_64' try: machine = os.uname().machine except: pass plugin_loader_src_file = "class/PluginLoader.{}.Python3.12.so".format(machine) if machine == 'x86_64': glibc_version = public.get_glibc_version() if glibc_version in ['2.14','2.13','2.12','2.11','2.10']: plugin_loader_src_file = "class/PluginLoader.{}.glibc214.Python3.12.so".format(machine) if os.path.exists(plugin_loader_src_file): os.system(r"\cp -f {} {}".format(plugin_loader_src_file, plugin_loader_file)) check_plugin_loader() if is_debug: import pyinotify,time,logging,re logging.basicConfig(level=logging.DEBUG,format="[%(asctime)s][%(levelname)s] - %(message)s") logger = logging.getLogger() class PanelEventHandler(pyinotify.ProcessEvent): _exts = ['py','html','Yak-Panel','so'] _explude_patts = [ re.compile('{}/plugin/.+'.format(_PATH)), re.compile('{}/(tmp|temp)/.+'.format(_PATH)), re.compile('{}/pyenv/.+'.format(_PATH)), re.compile('{}/class/projectModel/.+'.format(_PATH)), re.compile('{}/class/databaseModel/.+'.format(_PATH)), re.compile('{}/panel/data/mail/in_bulk/content/.+'.format(_PATH)) ] _lsat_time = 0 def is_ext(self,filename): fname = os.path.basename(filename) result = fname.split('.')[-1] in self._exts if not result: return False for e in self._explude_patts: if e.match(filename): return False return True def panel_reload(self,filename,in_type): stime = time.time() if stime - self._lsat_time < 2: return self._lsat_time = stime logger.debug('File detected: {} -> {}'.format(filename,in_type)) fname = os.path.basename(filename) os.chmod(_PATH + "/Yak-Panel", 700) os.chmod(_PATH + "/Yak-Task", 700) if fname in ['task.py','Yak-Task']: logger.debug('Background task...') public.ExecShell("{} {}/Yak-Task".format(public.get_python_bin(),_PATH)) logger.debug('Background task started!') else: logger.debug('Restarting panel...') public.ExecShell("bash {}/init.sh reload &>/dev/null &".format(_PATH)) def process_IN_CREATE(self, event): if not self.is_ext(event.pathname): return self.panel_reload(event.pathname,'[Create]') def process_IN_DELETE(self,event): if not self.is_ext(event.pathname): return self.panel_reload(event.pathname,'[Delete]') def process_IN_MODIFY(self,event): if not self.is_ext(event.pathname): return self.panel_reload(event.pathname,'[Modify]') def process_IN_MOVED_TO(self,event): if not self.is_ext(event.pathname): return self.panel_reload(event.pathname,'[Cover]') def debug_event(): logger.debug('Launch the panel in debug mode') logger.debug('Listening port:0.0.0.0:{}'.format(public.readFile('data/port.pl'))) event = PanelEventHandler() watchManager = pyinotify.WatchManager() mode = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY | pyinotify.IN_MOVED_TO watchManager.add_watch(_PATH, mode, auto_add=True, rec=True) notifier = pyinotify.Notifier(watchManager, event) notifier.loop() def run_task(): public.ExecShell("chmod 700 {}/Yak-Task".format(_PATH)) public.ExecShell("{}/Yak-Task".format(_PATH)) def daemon_task(): cycle = 60 task_pid_file = "{}/logs/task.pid".format(_PATH) while 1: time.sleep(cycle) # 检查pid文件是否存在 if not os.path.exists(task_pid_file): continue # 读取pid文件 task_pid = public.readFile(task_pid_file) if not task_pid: run_task() continue # 检查进程是否存在 comm_file = "/proc/{}/comm".format(task_pid) if not os.path.exists(comm_file): run_task() continue # 是否为面板进程 comm = public.readFile(comm_file) if comm.find('Yak-Task') == -1: run_task() continue def get_process_count(): ''' @name 获取进程数量 @return int ''' # 如果存在用户配置,则直接返回用户配置的进程数量 process_count_file = "{}/data/process_count.pl".format(_PATH) if os.path.exists(process_count_file): str_count = public.readFile(process_count_file).strip() try: if str_count: return int(str_count) except: pass # 否则根据内存和CPU核心数来决定启动进程数量 memory = psutil.virtual_memory().total / 1024 / 1024 cpu_count = psutil.cpu_count() if memory < 4000 or cpu_count < 4: return 1 # 内存小于4G或CPU核心小于4核,则只启动1个进程 if memory < 8000 and cpu_count > 3: return 2 # 内存大于4G且小于8G,且CPU核心大于3核,则启动2个进程 if memory > 14000 and cpu_count > 7: return 3 # 内存大于8G且14G,且CPU核心大于7核,则启动3个进程 if memory > 30000 and cpu_count > 15: return 4 # 内存大于30G且CPU核心大于15核,则启动4个进程 return 1 def check_system_restarted(): ''' @name 检测系统是否重启 1. 若与上次记录时间差异 > 3分钟 → 视为真实重启,标记 status=0(未读) 2. 若差异 ≤ 3分钟 → 视为时间微调,更新 last_reboot_time,保持/设置 status=1(已读) 3. 首次运行则初始化记录 ''' try: import json status_file = '{}/data/reboot_notification.json'.format(_PATH) current_boot_time = int(psutil.boot_time()) if os.path.exists(status_file): try: with open(status_file, 'r') as f: data = json.load(f) last_recorded_time = data.get("last_reboot_time", 0) last_status = data.get("status", 1) if last_recorded_time > 0: diff = abs(current_boot_time - last_recorded_time) if diff > 60 * 3: new_data = { "last_reboot_time": current_boot_time, "status": 0 } with open(status_file, 'w') as f: json.dump(new_data, f, indent=2) return True elif diff > 0: # 有变化才写入 new_data = { "last_reboot_time": current_boot_time, "status": last_status if last_status == 0 else 1 } try: with open(status_file, 'w') as f: json.dump(new_data, f, indent=2) except Exception as e: print(f"Failed to update boot time (drift): {e}") except Exception as e: print(f"Error reading/parsing status file: {e}") else: # 首次运行,保存当前启动时间 with open(status_file, 'w') as f: json.dump({ "last_reboot_time": current_boot_time, "status": 1 }, f, indent=2) except Exception as e: pass return False # 默认没有重启 if __name__ == '__main__': pid_file = "{}/logs/panel.pid".format(_PATH) if os.path.exists(pid_file): public.ExecShell("kill -9 {}".format(public.readFile(pid_file))) # 重启面板前检查系统是否重启 check_system_restarted() pid = os.fork() if pid: sys.exit(0) os.setsid() _pid = os.fork() if _pid: public.writeFile(pid_file,str(_pid)) sys.exit(0) sys.stdout.flush() sys.stderr.flush() # 面板启动任务初始化 os.system("nohup ./pyenv/bin/python3 class/jobs.py &>/dev/null &") try: f = open('data/port.pl') PORT = int(f.read()) f.close() if not PORT: PORT = 7800 except: PORT = 7800 HOST = '0.0.0.0' if os.path.exists('data/ipv6.pl'): HOST = "0:0:0:0:0:0:0:0" keyfile = 'ssl/privateKey.pem' certfile = 'ssl/certificate.pem' is_ssl = False if os.path.exists('data/ssl.pl') and os.path.exists(keyfile) and os.path.exists(certfile): is_ssl = True if not is_ssl or is_debug: try: err_f = open('logs/error.log','a+') os.dup2(err_f.fileno(),sys.stderr.fileno()) err_f.close() except Exception as ex: print(ex) import threading task_thread = threading.Thread(target=daemon_task, daemon=True) task_thread.start() if is_ssl: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(certfile=certfile,keyfile=keyfile) if hasattr(ssl_context, "minimum_version"): ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 else: ssl_context.options = (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1) ssl_context.set_ciphers("ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE") is_ssl_verify = os.path.exists('/www/server/panel/data/ssl_verify_data.pl') if is_ssl_verify: crlfile = '/www/server/panel/ssl/crl.pem' rootcafile = '/www/server/panel/ssl/ca.pem' #注销列表 # ssl_context.load_verify_locations(crlfile) # ssl_context.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN #加载证书 ssl_context.load_verify_locations(rootcafile) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.set_default_verify_paths() # 设置日志格式 _level = logging.WARNING if is_debug: _level = logging.NOTSET logging.basicConfig(level=_level,format="[%(asctime)s][%(levelname)s] - %(message)s") logger = logging.getLogger() app.logger = logger from gevent.pywsgi import WSGIServer import webserver class BtWSGIServer(WSGIServer): def wrap_socket_and_handle(self, client_socket, address): try: return super(BtWSGIServer, self).wrap_socket_and_handle(client_socket, address) except OSError as e: pass # public.print_exc_stack(e) def do_read(self): try: return super(BtWSGIServer, self).do_read() except OSError as e: pass # public.print_exc_stack(e) webserver_obj = webserver.webserver() is_webserver = webserver_obj.run_webserver() # is_webserver = False if is_webserver: from gevent import socket listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) unix_socket = '/tmp/panel.sock' if os.path.exists(unix_socket): os.remove(unix_socket) listener.bind(unix_socket) listener.listen(500) os.chmod(unix_socket, 0o777) try: import flask_sock http_server = BtWSGIServer(listener, app,log=app.logger) except: from geventwebsocket.handler import WebSocketHandler http_server = BtWSGIServer(listener, app,handler_class=WebSocketHandler,log=app.logger) else: if is_ssl: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) if hasattr(ssl_context, "minimum_version"): ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 else: ssl_context.options = (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1) ssl_context.set_ciphers("ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE") is_ssl_verify = os.path.exists('/www/server/panel/data/ssl_verify_data.pl') if is_ssl_verify: crlfile = '/www/server/panel/ssl/crl.pem' rootcafile = '/www/server/panel/ssl/ca.pem' #注销列表 # ssl_context.load_verify_locations(crlfile) # ssl_context.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN #加载证书 ssl_context.load_verify_locations(rootcafile) ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.set_default_verify_paths() try: import flask_sock if is_ssl: http_server = BtWSGIServer((HOST, PORT), app,ssl_context = ssl_context,log=app.logger) else: http_server = BtWSGIServer((HOST, PORT), app,log=app.logger) except: from geventwebsocket.handler import WebSocketHandler if is_ssl: http_server = BtWSGIServer((HOST, PORT), app,ssl_context = ssl_context,handler_class=WebSocketHandler,log=app.logger) else: http_server = BtWSGIServer((HOST, PORT), app,handler_class=WebSocketHandler,log=app.logger) if is_debug: try: dev = threading.Thread(target=debug_event) dev.start() except: pass is_process = os.path.exists('data/is_process.pl') if not is_process: try: http_server.serve_forever() except: from traceback import format_exc public.print_log(format_exc()) app.run(host=HOST, port=PORT, threaded=True) else: http_server.start() from multiprocessing import Process def serve_forever(): http_server.start_accepting() http_server._stop_event.wait() # 获取最大进程数量,最小为2个 process_count = get_process_count() if process_count < 2: process_count = 2 # 启动主进程 main_p = Process(target=serve_forever) main_p.daemon = True main_p.start() main_psutil = psutil.Process(main_p.pid) # 动态按需调整子进程数量 process_dict = {} while 1: t = time.time() # 当主进程CPU占用率超过90%时,尝试启动新的子进程协同处理 cpu_percent = main_psutil.cpu_percent(interval=1) if cpu_percent > 90: is_alive = 0 process_num = 0 # 检查是否存在空闲的子进程 for i in process_dict.keys(): process_num += 1 if process_dict[i][2].cpu_percent(interval=1) > 0: is_alive += 1 # 如果没有空闲的子进程,且当前子进程数量小于最大进程数量,则启动新的子进程 if process_num == is_alive and process_num < process_count: p = Process(target=serve_forever) p.daemon = True p.start() process_dict[p.pid] = [p, t, psutil.Process(p.pid)] # 结束创建时间超过60秒,且连续空闲5秒钟以上的子进程 keys = list(process_dict.keys()) for i in keys: if t - process_dict[i][1] < 60: continue if process_dict[i][2].cpu_percent(interval=5) > 0: continue process_dict[i][0].kill() process_dict.pop(i) time.sleep(1)