# coding: utf-8 # ------------------------------------------------------------------- # aaPanel # ------------------------------------------------------------------- # Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: hwliang # ------------------------------------------------------------------- # ------------------------------ # node.js模型 # ------------------------------ import os, sys, re, json, shutil, psutil, time, hashlib from time import process_time_ns from urllib.parse import urlparse from projectModelV2.base import projectBase import public from public.validate import Param try: from BTPanel import cache except: pass class main(projectBase): _panel_path = public.get_panel_path() _nodejs_plugin_path = public.get_plugin_path('nodejs') _nodejs_path = '{}/nodejs'.format(public.get_setup_path()) _log_name = 'Project management' _npm_exec_log = '{}/logs/npm-exec.log'.format(_panel_path) _node_pid_path = '{}/vhost/pids'.format(_nodejs_path) _node_logs_path = '{}/vhost/logs'.format(_nodejs_path) _node_run_scripts = '{}/vhost/scripts'.format(_nodejs_path) _pids = None _vhost_path = '{}/vhost'.format(_panel_path) _www_home = '/home/www' _node_logs = '{}/vhost/logs'.format(_nodejs_path) def __init__(self): if not os.path.exists(self._node_run_scripts): os.makedirs(self._node_run_scripts, 493) if not os.path.exists(self._node_pid_path): os.makedirs(self._node_pid_path, 493) if not os.path.exists(self._node_logs_path): os.makedirs(self._node_logs_path, 493) if not os.path.exists(self._www_home): os.makedirs(self._www_home, 493) public.set_own(self._www_home, 'www') def get_exec_logs(self, get): ''' @name 获取执行日志 @author hwliang<2021-08-09> @param get @return string ''' if not os.path.exists(self._npm_exec_log): return public.returnMsg(False, 'NODE_NOT_EXISTS') return public.return_message(0, 0, public.GetNumLines(self._npm_exec_log, 20)) def get_project_list(self, get): ''' @name 获取项目列表(支持全局流量排序) ''' try: get.validate([ Param('search').String(), Param('limit').Integer(), Param('p').Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) p = int(get.get('p', 1)) limit = int(get.get('limit', 20)) search_word = get.get('search', '').strip() re_order = get.get('re_order', '') # 流量排序专用 order_str = get.get('order', 'id desc') where_str = "project_type=?" where_args = ["Node"] if search_word: where_str += " AND (name LIKE ? OR ps LIKE ?)" search_pattern = "%{}%".format(search_word) where_args.extend([search_pattern, search_pattern]) all_data = public.M('sites').where(where_str, tuple(where_args)).order(order_str).select() if not all_data: return public.return_message(0, 0, {'data': [], 'page': ''}) # 获取流量数据 re_data = None if re_order: import data_v2 res = data_v2.data().get_site_request(public.to_dict_obj({'site_type': 'Node'})) if res.get('status') == 0: re_data = res.get('message') for i in range(len(all_data)): all_data[i] = self.get_project_stat(all_data[i]) all_data[i]['re_total'] = 0 if re_data and all_data[i]['name'] in re_data: all_data[i]['re_total'] = re_data[all_data[i]['name']]['total']['request'] # 批量获取备份信息 site_ids = [item['id'] for item in all_data] if site_ids: backup_info_map = { j['pid']: j for j in public.S('backup').where_in('pid', site_ids).where('type=?', ('0',)) .group('pid').field('pid,count(*) as cnt,max(addtime) as last_backup_time').select() } for item in all_data: info = backup_info_map.get(item['id'], {}) item['backup_count'] = info.get('cnt', 0) item['last_backup_time'] = info.get('last_backup_time', '') if re_order: is_reverse = True if re_order == 'desc' else False all_data = sorted(all_data, key=lambda x: x.get('re_total', 0), reverse=is_reverse) count = len(all_data) start = (p - 1) * limit end = start + limit paged_data = all_data[start:end] data = public.get_page(count, p, limit, get.get('callback', '')) data['data'] = paged_data return public.return_message(0, 0, data) def get_ssl_end_date(self, project_name): ''' @name 获取SSL信息 @author hwliang<2021-08-09> @param project_name 项目名称 @return dict ''' import data return data.data().get_site_ssl_info('node_{}'.format(project_name)) def is_install_nodejs(self, get): ''' @name 是否安装nodejs版本管理器 @author hwliang<2021-08-09> @param get 请求数据 @return bool ''' return_message = os.path.exists(self._nodejs_plugin_path) return public.return_message(0, 0, return_message) def _is_install_nodejs(self, get): ''' @name 是否安装nodejs版本管理器 @author hwliang<2021-08-09> @param get 请求数据 @return bool ''' return os.path.exists(self._nodejs_plugin_path) def get_nodejs_version(self, get): ''' @name 获取已安装的nodejs版本 @author hwliang<2021-08-09> @param get 请求数据 @return list ''' nodejs_list = [] if not os.path.exists(self._nodejs_path): return public.return_message(0, 0, nodejs_list) for v in os.listdir(self._nodejs_path): if v[0] != 'v' or v.find('.') == -1: continue node_path = os.path.join(self._nodejs_path, v) node_bin = '{}/bin/node'.format(node_path) if not os.path.exists(node_bin): if os.path.exists(node_path + '/bin'): public.ExecShell('rm -rf {}'.format(node_path)) continue nodejs_list.append(v) return public.return_message(0, 0, nodejs_list) def get_run_list(self, get): ''' @name 获取node项目启动列表 @author hwliang<2021-08-10> @param get{ project_cwd: string<项目目录> } ''' # 校验参数 try: get.validate([ Param('project_cwd').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_cwd = get.project_cwd.strip() if not os.path.exists(project_cwd): return_message = public.return_error(public.lang('The project directory does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_file = '{}/package.json'.format(project_cwd) if not os.path.exists(package_file): return public.return_message(0, 0, {}) package_content = public.readFile(package_file) if not package_content: return public.return_message(0, 0, {}) try: package_info = json.loads(package_content) except json.JSONDecodeError: return public.return_message(0, 0, {}) # package_info = json.loads(public.readFile(package_file)) if not 'scripts' in package_info: return public.return_message(0, 0, {}) if not package_info['scripts']: return public.return_message(0, 0, {}) return public.return_message(0, 0, package_info['scripts']) def get_npm_bin(self, nodejs_version): ''' @name 获取指定node版本的npm路径 @author hwliang<2021-08-10> @param nodejs_version nodejs版本 @return string ''' npm_path = '{}/{}/bin/npm'.format(self._nodejs_path, nodejs_version) if not os.path.exists(npm_path): return False return npm_path def get_yarn_bin(self, nodejs_version): ''' @name 获取指定node版本的yarn路径 @author hwliang<2021-08-28> @param nodejs_version nodejs版本 @return string ''' yarn_path = '{}/{}/bin/yarn'.format(self._nodejs_path, nodejs_version) if not os.path.exists(yarn_path): return False return yarn_path def get_pnpm_bin(self, nodejs_version): ''' @name 获取指定node版本的pnpm路径 @param nodejs_version nodejs版本 @return string ''' pnpm_path = '{}/{}/bin/pnpm'.format(self._nodejs_path, nodejs_version) if not os.path.exists(pnpm_path): return False return pnpm_path def get_node_bin(self, nodejs_version): ''' @name 获取指定node版本的node路径 @author hwliang<2021-08-10> @param nodejs_version nodejs版本 @return string ''' node_path = '{}/{}/bin/node'.format(self._nodejs_path, nodejs_version) if not os.path.exists(node_path): return False return node_path def get_last_env(self, nodejs_version, project_cwd=None): ''' @name 获取前置环境变量 @author hwliang<2021-08-25> @param nodejs_version Node版本 @return string ''' nodejs_bin_path = '{}/{}/bin'.format(self._nodejs_path, nodejs_version) if project_cwd: _bin = '{}/node_modules/.bin'.format(project_cwd) if os.path.exists(_bin): nodejs_bin_path = _bin + ':' + nodejs_bin_path last_env = '''PATH={nodejs_bin_path}:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH '''.format(nodejs_bin_path=nodejs_bin_path) return last_env def install_packages(self, get): ''' @name 安装指定项目的依赖包 @author hwliang<2021-08-10> @param get{ project_name: string<项目名称> } return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) if not os.path.exists(project_find['path']): return_message = public.return_error(public.lang('The project directory does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_file = '{}/package.json'.format(project_find['path']) if not os.path.exists(package_file): return_message = public.return_error( public.lang('The package.json configuration file was not found in the project directory!')) del return_message['status'] return public.return_message(-1, 0, return_message) nodejs_version = project_find['project_config']['nodejs_version'] package_lock_file = '{}/package-lock.json'.format(project_find['path']) node_modules_path = '{}/node_modules'.format(project_find['path']) # 已经安装过的依赖包的情况下,可能存在不同node版本导致的问题,可能需要重新构建依赖包 rebuild = False if os.path.exists(package_lock_file) and os.path.exists(node_modules_path): rebuild = True npm_bin = self.get_npm_bin(nodejs_version) yarn_bin = self.get_yarn_bin(nodejs_version) if not npm_bin and not yarn_bin: return_message = public.return_error(public.lang('The specified nodejs version does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) public.writeFile(self._npm_exec_log, "Installing dependencies...\n") public.writeFile(self._npm_exec_log, "Downloading dependency package, please wait...\n") if yarn_bin: if os.path.exists(package_lock_file): os.remove(package_lock_file) public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} install >> {} 2>&1".format(project_find['path'], yarn_bin, self._npm_exec_log)) else: public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} install >> {} 2>&1".format(project_find['path'], npm_bin, self._npm_exec_log)) public.writeFile(self._npm_exec_log, "|-Successify --- Command executed! ---", 'a+') public.WriteLog(self._log_name, 'Node project: {}, the installation of the dependency package is complete!'.format( project_find['name'])) if rebuild: # 重新构建已安装模块 self.rebuild_project(get.project_name) return_message = public.return_data(True, 'The dependency package is installed successfully!') del return_message['status'] return public.return_message(0, 0, return_message) def update_packages(self, get): ''' @name 更新指定项目的依赖包 @author hwliang<2021-08-10> @param get{ project_name: string<项目名称> } return dict ''' project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) if not os.path.exists(project_find['path']): return_message = public.return_error(public.lang('The project directory does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_file = '{}/package.json'.format(project_find['path']) if not os.path.exists(package_file): return_message = public.return_error( public.lang('The package.json configuration file was not found in the project directory!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_lock_file = '{}/package-lock.json'.format(project_find['path']) if not os.path.exists(package_lock_file): return_message = public.return_error(public.lang('Please install the dependency package first!')) del return_message['status'] return public.return_message(-1, 0, return_message) nodejs_version = project_find['project_config']['nodejs_version'] npm_bin = self.get_npm_bin(nodejs_version) if not npm_bin: return_message = public.return_error(public.lang('The specified nodejs version does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} update &> {}".format(project_find['path'], npm_bin, self._npm_exec_log)) public.WriteLog(self._log_name, 'Project [{}] update all dependent packages'.format(get.project_name)) return_message = public.return_data(True, 'Dependent package updated successfully!') del return_message['status'] return public.return_message(0, 0, return_message) def reinstall_packages(self, get): ''' @name 重新安装指定项目的依赖包 @author hwliang<2021-08-10> @param get{ project_name: string<项目名称> } return dict ''' project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) if not os.path.exists(project_find['path']): return_message = public.return_error(public.lang('The project directory does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_file = '{}/package.json'.format(project_find['path']) if not os.path.exists(package_file): return_message = public.return_error( public.lang('The package.json configuration file was not found in the project directory!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_lock_file = '{}/package-lock.json'.format(project_find['path']) if os.path.exists(package_lock_file): os.remove(package_lock_file) package_path = '{}/node_modules' if os.path.exists(package_path): shutil.rmtree(package_path) nodejs_version = project_find['project_config']['nodejs_version'] npm_bin = self.get_npm_bin(nodejs_version) if not npm_bin: return_message = public.return_error(public.lang('The specified nodejs version does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) public.WriteLog(self._log_name, 'Node project: {}, all dependent packages have been reinstalled') public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} install &> {}".format(project_find['path'], npm_bin, self._npm_exec_log)) return_message = public.return_data(True, 'Dependent package reinstalled successfully!') del return_message['status'] return public.return_message(0, 0, return_message) def get_project_modules(self, get): ''' @name 获取指定项目的依赖包列表 @author hwliang<2021-08-10> @param get{ project_name: string<项目名称> project_cwd: string<项目目录> 可选 } return list ''' # 校验参数 try: get.validate([ Param('project_name').String(), Param('project_cwd').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) if not 'project_cwd' in get: project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) project_cwd = project_find['path'] else: project_cwd = get.project_cwd mod_path = os.path.join(project_cwd, 'node_modules') modules = [] if not os.path.exists(mod_path): return public.return_message(0, 0, modules) for mod_name in os.listdir(mod_path): try: mod_pack_file = os.path.join(mod_path, mod_name, 'package.json') if not os.path.exists(mod_pack_file): continue mod_pack_info = json.loads(public.readFile(mod_pack_file)) pack_info = { "name": mod_name, "version": mod_pack_info['version'], "description": mod_pack_info['description'], "license": mod_pack_info['license'] if 'license' in mod_pack_info else 'NULL', "homepage": mod_pack_info['homepage'] } modules.append(pack_info) except: continue return public.return_message(0, 0, modules) def install_module(self, get): ''' @name 安装指定模块 @author hwliang<2021-08-10> @param get{ project_name: string<项目名称> mod_name: string<模块名称> } @return dict ''' project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) project_cwd = project_find['path'] mod_name = get.mod_name filename = '{}/node_modules/{}/package.json'.format(project_cwd, mod_name) if os.path.exists(filename): return_message = public.return_error(public.lang('The specified module has been installed!')) del return_message['status'] return public.return_message(-1, 0, return_message) nodejs_version = project_find['project_config']['nodejs_version'] npm_bin = self.get_npm_bin(nodejs_version) yarn_bin = self.get_yarn_bin(nodejs_version) if not npm_bin and not yarn_bin: return_message = public.return_error(public.lang('The specified nodejs version does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) if yarn_bin: public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} add {} &> {}".format(project_find['path'], yarn_bin, mod_name, self._npm_exec_log)) else: public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} install {} &> {}".format(project_find['path'], npm_bin, mod_name, self._npm_exec_log)) if not os.path.exists(filename): return_message = public.return_error(public.lang('Failed to install the specified module!')) del return_message['status'] return public.return_message(-1, 0, return_message) public.WriteLog(self._log_name, 'Node project {}, {} module installation is complete!'.format(get.project_name, mod_name)) return_message = public.return_data(True, 'Successful installation!') del return_message['status'] return public.return_message(0, 0, return_message) def uninstall_module(self, get): ''' @name 卸载指定模块 @author hwliang<2021-04-08> @param get{ project_name: string<项目名称> mod_name: string<模块名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), Param('mod_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) project_cwd = project_find['path'] mod_name = get.mod_name filename = '{}/node_modules/{}/package.json'.format(project_cwd, mod_name) if not os.path.exists(filename): return_message = public.return_error(public.lang('The specified module is not installed!')) del return_message['status'] return public.return_message(-1, 0, return_message) nodejs_version = project_find['project_config']['nodejs_version'] npm_bin = self.get_npm_bin(nodejs_version) yarn_bin = self.get_yarn_bin(nodejs_version) if not npm_bin and not yarn_bin: return_message = public.return_error(public.lang('The specified nodejs version does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) if yarn_bin: result = public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} remove {}".format(project_find['path'], yarn_bin, mod_name)) else: result = public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} uninstall {}".format(project_find['path'], npm_bin, mod_name)) if os.path.exists(filename): result = "\n".join(result) if result.find('looking for funding') != -1: return_message = public.return_error( public.lang("This module is dependent on other installed modules and cannot be uninstalled!")) del return_message['status'] return public.return_message(-1, 0, return_message) return_message = public.return_error(public.lang("Unable to uninstall this module!")) del return_message['status'] return public.return_message(-1, 0, return_message) public.WriteLog(self._log_name, 'Node project {}, {} module uninstallation completed!'.format(get.project_name, mod_name)) return_message = public.return_data(True, 'Module unloaded successfully!') del return_message['status'] return public.return_message(0, 0, return_message) def upgrade_module(self, get): ''' @name 更新指定模块 @author hwliang<2021-08-10> @param get{ project_name: string<项目名称> mod_name: string<模块名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), Param('mod_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) project_cwd = project_find['path'] mod_name = get.mod_name filename = '{}/node_modules/{}/package.json'.format(project_cwd, mod_name) if not os.path.exists(filename): return_message = public.return_error(public.lang('The specified module is not installed!')) del return_message['status'] return public.return_message(-1, 0, return_message) nodejs_version = project_find['project_config']['nodejs_version'] npm_bin = self.get_npm_bin(nodejs_version) if not npm_bin: return_message = public.return_error(public.lang('The specified nodejs version does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} update {} &> {}".format(project_find['path'], npm_bin, mod_name, self._npm_exec_log)) public.WriteLog(self._log_name, 'Node project {}, {} module update completed!'.format(get.project_name, mod_name)) return_message = public.return_data(True, 'Module updated successfully!') del return_message['status'] return public.return_message(0, 0, return_message) def _nodejs_error(self, msg): return_message = public.return_error(msg) if 'status' in return_message: del return_message['status'] return public.return_message(-1, 0, return_message) def _parse_domain_list(self, domains): if isinstance(domains, str): domains = domains.strip() if not domains: return [] try: domains = json.loads(domains) except: domains = [i.strip() for i in domains.split(',') if i.strip()] if not isinstance(domains, list): return [] return domains def _format_and_validate_domains(self, bind_extranet, domains, port=0, require_domains=True): try: bind_extranet = int(bind_extranet) except: bind_extranet = 0 if bind_extranet != 1: return [], '' try: port = int(port or 0) except: port = 0 if port == 0: return [], public.lang('Please set the running port for the project first.') domains = self._parse_domain_list(domains) if require_domains and not domains: return [], public.lang('Please add at least one domain name in the [Domain Management] option') format_domains = [] for domain in domains: domain = str(domain).strip() if not domain: return [], public.lang('Domain name cannot be empty') if domain.find(':') == -1: domain += ':80' domain_arr = domain.split(':') if len(domain_arr) < 2 or not domain_arr[1]: domain = '{}:80'.format(domain_arr[0]) domain_arr = domain.split(':') if domain_arr[1] == '443': return [], public.lang('Do not use system ports 443') if public.M('domain').where('name=?', (domain_arr[0],)).count(): return [], 'Domain name already exists: {}'.format(domain) format_domains.append(domain) return format_domains, '' def _normalize_package_manager(self, pkg_manager): pkg_manager = str(pkg_manager or 'npm').strip().lower() if pkg_manager not in ('npm', 'pnpm', 'yarn'): return 'npm' return pkg_manager def _get_package_manager_bin(self, nodejs_version, pkg_manager): pkg_manager = self._normalize_package_manager(pkg_manager) if pkg_manager == 'pnpm': return self.get_pnpm_bin(nodejs_version) if pkg_manager == 'yarn': return self.get_yarn_bin(nodejs_version) return self.get_npm_bin(nodejs_version) def _get_package_manager_run_cmd(self, nodejs_version, pkg_manager, project_script): pkg_manager = self._normalize_package_manager(pkg_manager) pkg_bin = self._get_package_manager_bin(nodejs_version, pkg_manager) if not pkg_bin: return False return '{} run {}'.format(pkg_bin, project_script) def _normalize_project_name(self, name, pid=0): name = str(name or '').strip() if name.startswith('@'): name = name[1:] name = name.replace('/', '_').replace('\\', '_') name = re.sub(r'[^0-9A-Za-z_]+', '_', name) name = re.sub(r'_+', '_', name).strip('_') if not name: name = 'node_project_{}'.format(pid) if pid else 'node_project' return name def _make_unique_project_name(self, name, existing_names=None, pid=0): if existing_names is None: existing_names = set() base_name = self._normalize_project_name(name, pid) project_name = base_name index = 1 while project_name in existing_names or public.M('sites').where('name=?', (project_name,)).count(): project_name = '{}_{}'.format(base_name, index) index += 1 return project_name def _read_proc_cwd(self, pid): try: cwd_link = '/proc/{}/cwd'.format(pid) if os.path.islink(cwd_link): return os.path.realpath(os.readlink(cwd_link)) except: pass return '' def _read_proc_exe(self, pid): try: exe_link = '/proc/{}/exe'.format(pid) if os.path.islink(exe_link): return os.path.realpath(os.readlink(exe_link)) except: pass return '' def _read_proc_cmdline(self, pid): try: cmdline_file = '/proc/{}/cmdline'.format(pid) if not os.path.exists(cmdline_file): return [] with open(cmdline_file, 'rb') as fp: data = fp.read() if not data: return [] return self._clean_cmdline([i.decode('utf-8', 'ignore') for i in data.split(b'\0') if i]) except: return [] def _uid_to_username(self, uid): try: import pwd return pwd.getpwuid(int(uid)).pw_name except: pass return str(uid) if uid not in [None, ''] else 'www' def _read_proc_status(self, pid): status_info = {} try: status_file = '/proc/{}/status'.format(pid) if not os.path.exists(status_file): return status_info with open(status_file, 'r') as fp: for line in fp: if ':' not in line: continue key, value = line.split(':', 1) status_info[key.strip()] = value.strip() except: pass return status_info def _read_process_basic_from_proc(self, pid): try: if not os.path.exists('/proc/{}'.format(pid)): return {} cmdline = self._read_proc_cmdline(pid) if not cmdline: return {} status_info = self._read_proc_status(pid) uid = '' if status_info.get('Uid'): uid = status_info['Uid'].split()[0] return { 'pid': int(pid), 'ppid': int(status_info.get('PPid', '0').split()[0]), 'name': status_info.get('Name', ''), 'cmdline': cmdline, 'cwd': self._read_proc_cwd(pid), 'exe': self._read_proc_exe(pid), 'user': self._uid_to_username(uid) } except: return {} def _read_process_basic(self, pid): try: p = psutil.Process(int(pid)) if p.status() == 'zombie': return {} with p.oneshot(): cmdline = self._clean_cmdline(p.cmdline()) if not cmdline: return {} try: cwd = p.cwd() except: cwd = self._read_proc_cwd(pid) try: exe = p.exe() except: exe = self._read_proc_exe(pid) return { 'pid': int(pid), 'ppid': p.ppid(), 'name': p.name(), 'cmdline': cmdline, 'cwd': os.path.realpath(cwd) if cwd else '', 'exe': os.path.realpath(exe) if exe else '', 'user': p.username() } except: pass return self._read_process_basic_from_proc(pid) def _clean_cmdline(self, cmdline): result = [] for arg in cmdline: arg = str(arg or '').strip() if arg: result.append(arg) return result def _detect_package_manager(self, cmdline): cmdline = self._normalize_cmdline_args(cmdline) manager_map = { 'npm': ('npm', 'npm-cli', 'npm-cli.js', 'npm.cmd'), 'pnpm': ('pnpm', 'pnpm.cjs', 'pnpm.js', 'pnpm.cmd'), 'yarn': ('yarn', 'yarn.js', 'yarnpkg', 'yarnpkg.js', 'yarn.cmd') } for index, arg in enumerate(cmdline): base_name = os.path.basename(str(arg)).lower() for manager, names in manager_map.items(): if base_name in names: return manager, index return '', -1 def _normalize_cmdline_args(self, cmdline): result = [] manager_names = ('npm', 'pnpm', 'yarn', 'node', 'nodejs', 'npm-cli.js', 'pnpm.cjs', 'yarn.js', 'yarnpkg') for arg in cmdline: arg = str(arg or '').strip() if not arg: continue parts = arg.split() if len(parts) > 1 and os.path.basename(parts[0]).lower() in manager_names: result += parts else: result.append(arg) return result def _command_args_after_manager(self, cmdline, manager_index): cmdline = self._normalize_cmdline_args(cmdline) args = [str(i) for i in cmdline[manager_index + 1:]] if '--' in args: args = args[:args.index('--')] options_need_value = ('--prefix', '--userconfig', '--cache', '--registry', '--workspace', '-w', '--dir', '-C') index = 0 while index < len(args): arg = args[index] if not arg.startswith('-'): return args[index:] if arg in options_need_value and index + 1 < len(args): index += 2 continue index += 1 return [] def _parse_package_command(self, cmdline, scripts=None): manager, manager_index = self._detect_package_manager(cmdline) result = { 'manager': manager, 'manager_index': manager_index, 'args': [], 'script': '' } if not manager: return result args = self._command_args_after_manager(cmdline, manager_index) result['args'] = args if not isinstance(scripts, dict) or not scripts or not args: return result command = args[0] if command in ('run', 'run-script') and len(args) > 1: script_name = args[1] result['script'] = script_name if script_name in scripts else '' elif manager in ('pnpm', 'yarn') and command in scripts: result['script'] = command elif manager == 'npm' and command in ('start', 'test', 'restart', 'stop') and command in scripts: result['script'] = command return result def _find_package_root(self, cwd, max_depth=8): if not cwd: return '' path = os.path.realpath(cwd) for _ in range(max_depth + 1): if os.path.exists('{}/package.json'.format(path)): return path parent = os.path.dirname(path) if not parent or parent == path: break path = parent return '' def _read_package_json(self, project_cwd): package_file = '{}/package.json'.format(project_cwd) if not os.path.exists(package_file): return {} package_content = public.readFile(package_file) if not package_content: return {} try: package_info = json.loads(package_content) return package_info if isinstance(package_info, dict) else {} except: return {} def _collect_child_pids(self, pid): try: p = psutil.Process(int(pid)) pids = [int(pid)] for child in p.children(recursive=True): try: if child.status() != 'zombie': pids.append(child.pid) except: continue return sorted(list(set(pids))) except: if os.path.exists('/proc/{}'.format(pid)): return [int(pid)] return [] def _get_listen_info_by_pids(self, pids): listen_info = {} listen_status = getattr(psutil, 'CONN_LISTEN', 'LISTEN') for pid in pids: try: p = psutil.Process(int(pid)) try: connects = p.connections(kind='inet') except TypeError: connects = p.connections() for conn in connects: if conn.status != listen_status and conn.status != 'LISTEN': continue laddr = conn.laddr if not laddr: continue local_addr = getattr(laddr, 'ip', None) local_port = getattr(laddr, 'port', None) if local_addr is None: try: local_addr = laddr[0] local_port = laddr[1] except: continue if not local_port: continue listen_info[int(local_port)] = { 'pid': int(pid), 'local_addr': local_addr, 'port': int(local_port) } except: continue return [listen_info[i] for i in sorted(listen_info.keys())] def _get_primary_listen(self, listen_info): if not listen_info: return {} for item in listen_info: if item.get('local_addr') in ('0.0.0.0', '::', '*'): return item return listen_info[0] def _is_pm2_cmdline(self, cmdline): cmd_text = ' '.join([str(i).lower() for i in cmdline]) return cmd_text.find('pm2') != -1 def _is_docker_process(self, pid): try: cgroup_file = '/proc/{}/cgroup'.format(pid) if not os.path.exists(cgroup_file): return False cgroup = public.readFile(cgroup_file) if not cgroup: return False cgroup = cgroup.lower() for key in ('docker', 'kubepods', 'containerd', 'libpod'): if key in cgroup: return True except: pass return False def _extract_nodejs_version_from_path(self, path): path = str(path or '').replace('\\', '/') match = re.search(r'/nodejs/(v\d+(?:\.\d+){1,3})(?:/|$)', path) return match.group(1) if match else '' def _append_nodejs_version_candidate(self, candidates, source, path, pid=0): version = self._extract_nodejs_version_from_path(path) if not version: return item = { 'source': source, 'version': version, 'path': str(path or '') } try: pid = int(pid or 0) if pid: item['pid'] = pid except: pass for candidate in candidates: if candidate.get('source') == item.get('source') and candidate.get('path') == item.get( 'path') and candidate.get('pid', 0) == item.get('pid', 0): return candidates.append(item) def _detect_nodejs_version_detail(self, process_info, pids=None, listen_info=None): if pids is None: pids = [] if listen_info is None: listen_info = [] pid = 0 cmdline = [] if isinstance(process_info, dict): pid = int(process_info.get('pid', 0) or 0) cmdline = [str(i) for i in process_info.get('cmdline', [])] else: cmdline = [str(i) for i in process_info] listen_pids = [] for item in listen_info: try: item_pid = int(item.get('pid', 0) or 0) if item_pid: listen_pids.append(item_pid) except: continue candidates = [] # If the command uses an absolute version-manager binary, it is the strongest signal. for item in cmdline: self._append_nodejs_version_candidate(candidates, 'cmdline', item, pid) if isinstance(process_info, dict): self._append_nodejs_version_candidate(candidates, 'process_exe', process_info.get('exe', ''), pid) checked_pids = [] for item_pid in [pid] + listen_pids + [int(i) for i in pids if i]: if not item_pid or item_pid in checked_pids: continue checked_pids.append(item_pid) exe = self._read_proc_exe(item_pid) source = 'proc_exe' if item_pid in listen_pids: source = 'listen_proc_exe' elif pid and item_pid != pid: source = 'child_proc_exe' self._append_nodejs_version_candidate(candidates, source, exe, item_pid) try: if pid: env_path = psutil.Process(int(pid)).environ().get('PATH', '') for item in env_path.split(':'): self._append_nodejs_version_candidate(candidates, 'env_path', item, pid) except: pass if candidates: result = candidates[0].copy() versions = [] for item in candidates: if item.get('version') not in versions: versions.append(item.get('version')) result['candidates'] = candidates if len(versions) > 1: result['conflict_versions'] = versions return result default_version = self._get_default_nodejs_version() return { 'source': 'default', 'version': default_version, 'path': '', 'candidates': [] } def _detect_nodejs_version(self, process_info, pids=None, listen_info=None): return self._detect_nodejs_version_detail(process_info, pids, listen_info).get('version', '') def _version_key(self, version): nums = re.findall(r'\d+', str(version)) return tuple([int(i) for i in nums[:4]]) if nums else (0,) def _get_default_nodejs_version(self): try: res = self.get_nodejs_version(public.to_dict_obj({})) nodejs_list = res.get('message', []) if isinstance(res, dict) else [] if nodejs_list: return sorted(nodejs_list, key=self._version_key, reverse=True)[0] except: pass return '' def _get_existing_node_scan_targets(self): existing_names = set() existing_paths = set() try: project_list = public.M('sites').where('project_type=?', ('Node',)).field( 'name,path,project_config').select() or [] for project in project_list: if project.get('name'): existing_names.add(project['name']) if project.get('path'): existing_paths.add(os.path.realpath(project['path'])) try: project_config = json.loads(project.get('project_config', '{}')) if project_config.get('project_cwd'): existing_paths.add(os.path.realpath(project_config['project_cwd'])) except: pass except: pass return existing_paths, existing_names def _inspect_package_manager_process(self, pid, existing_paths=None, existing_names=None, check_existing=True): if existing_paths is None: existing_paths = set() if existing_names is None: existing_names = set() process_info = self._read_process_basic(pid) if not process_info: return None, 'unreadable' package_command = self._parse_package_command(process_info['cmdline']) manager = package_command['manager'] if not manager: return None, 'not_package_manager' if self._is_pm2_cmdline(process_info['cmdline']): return None, 'unsupported' if self._is_docker_process(pid): return None, 'unsupported' project_cwd = self._find_package_root(process_info.get('cwd', '')) if not project_cwd: return None, 'no_package_json' project_cwd = os.path.realpath(project_cwd) if check_existing and project_cwd in existing_paths: return None, 'already_imported' package_info = self._read_package_json(project_cwd) scripts = package_info.get('scripts', {}) if not isinstance(scripts, dict) or not scripts: return None, 'no_script_match' package_command = self._parse_package_command(process_info['cmdline'], scripts) project_script = package_command['script'] if not project_script: return None, 'no_script_match' pids = self._collect_child_pids(pid) listen_info = self._get_listen_info_by_pids(pids) primary_listen = self._get_primary_listen(listen_info) raw_name = package_info.get('name') or os.path.basename(project_cwd) project_name = self._make_unique_project_name(raw_name, existing_names, pid) normalized_name = self._normalize_project_name(raw_name, pid) warnings = [] if project_name != raw_name: warnings.append('Project name normalized from [{}] to [{}].'.format(raw_name, project_name)) if not primary_listen: warnings.append('No listening port was detected, please confirm manually.') scan_source = '{}:{}:{}:{}'.format(pid, project_cwd, manager, project_script) version_detail = self._detect_nodejs_version_detail(process_info, pids, listen_info) candidate = { 'scan_id': hashlib.sha1(scan_source.encode('utf-8')).hexdigest()[:16], 'pid': int(pid), 'pids': pids, 'ppid': process_info.get('ppid', 0), 'process_name': process_info.get('name', ''), 'process_cwd': process_info.get('cwd', ''), 'cmdline': process_info.get('cmdline', []), 'detected_command': ' '.join(process_info.get('cmdline', [])), 'project_cwd': project_cwd, 'project_name': project_name, 'raw_project_name': raw_name, 'normalized_project_name': normalized_name, 'package_manager': manager, 'pkg_manager': manager, 'project_script': project_script, 'start_command': '{} run {}'.format(manager, project_script), 'script_source': 'package.json scripts.{}'.format(project_script), 'port': int(primary_listen.get('port', 0)) if primary_listen else 0, 'ports': [i['port'] for i in listen_info], 'listen_addr': primary_listen.get('local_addr', '') if primary_listen else '', 'listen_info': listen_info, 'run_user': process_info.get('user', 'www') or 'www', 'nodejs_version': version_detail.get('version', ''), 'status': 'importable' if primary_listen else 'need_confirm', # 'confidence': 90 if primary_listen else 75, 'warnings': warnings, 'start_type': 'package_script' } return candidate, '' def scan_import_projects(self, get): ''' @name 扫描可导入的Node项目 @return dict ''' existing_paths, existing_names = self._get_existing_node_scan_targets() skipped = { 'unsupported': 0, 'no_package_json': 0, 'no_script_match': 0, 'already_imported': 0, 'unreadable': 0, 'duplicated': 0 } candidates = [] seen_keys = set() matched_processes = 0 try: pids = psutil.pids() except: pids = [] for pid in pids: candidate, reason = self._inspect_package_manager_process(pid, existing_paths, existing_names, True) if reason == 'not_package_manager': continue if not candidate: if reason in skipped: skipped[reason] += 1 else: skipped['unsupported'] += 1 continue matched_processes += 1 scan_key = '{}|{}|{}'.format(candidate['project_cwd'], candidate['package_manager'], candidate['project_script']) if scan_key in seen_keys: skipped['duplicated'] += 1 continue seen_keys.add(scan_key) existing_names.add(candidate['project_name']) candidates.append(candidate) data = { 'total': len(candidates), 'matched_processes': matched_processes, 'skipped': skipped, 'supported_package_managers': ['npm', 'pnpm', 'yarn'], 'data': candidates } public.set_module_logs('node_site', 'scan_import_projects') return public.return_message(0, 0, data) def create_scan_import_project(self, get): ''' @name 导入扫描到的Node项目 @param get{ pid: int<扫描到的主进程PID> project_cwd: string<项目目录> project_name: string<项目名称> package_manager: string<包管理器 npm/pnpm/yarn> project_script: string<项目脚本> port: int<项目实际监听端口> run_user: string<运行用户> nodejs_version: string bind_extranet: int<是否绑定外网> 1:是 0:否 domains: list<域名列表> ["domain1:80","domain2:80"] // 在bind_extranet=1时,需要填写 } @return dict ''' if not isinstance(get, public.dict_obj): get = public.to_dict_obj(get) if not self._is_install_nodejs(get): return self._nodejs_error(public.lang('Please install nodejs version manager first')) try: pid = int(get.get('pid', 0)) except: pid = 0 if pid <= 0 or not os.path.exists('/proc/{}'.format(pid)): return self._nodejs_error('The scanned process no longer exists, please rescan.') candidate, reason = self._inspect_package_manager_process(pid, set(), set(), False) if not candidate: return self._nodejs_error( 'The scanned process is no longer a supported npm/pnpm/yarn project: {}'.format(reason)) project_cwd = os.path.realpath(str(get.get('project_cwd', candidate['project_cwd'])).strip()) if project_cwd != os.path.realpath(candidate['project_cwd']): return self._nodejs_error( 'The project directory does not match the current running process, please rescan.') package_info = self._read_package_json(project_cwd) scripts = package_info.get('scripts', {}) if not isinstance(scripts, dict) or not scripts: return self._nodejs_error('No scripts were found in package.json.') project_script = str(get.get('project_script', candidate['project_script'])).strip() if not project_script: return self._nodejs_error(public.lang('Start command cannot be empty')) if project_script not in scripts: return self._nodejs_error('The startup script does not exist in package.json: {}'.format(project_script)) if not re.match(r'^[0-9A-Za-z_:\-\.\/]+$', project_script): return self._nodejs_error( 'The startup script name contains unsupported characters: {}'.format(project_script)) pkg_manager = self._normalize_package_manager( get.get('package_manager', get.get('pkg_manager', candidate['package_manager']))) try: port = int(str(get.get('port', candidate.get('port', 0)) or 0).strip()) except: return self._nodejs_error('The project port is invalid: {}'.format(get.get('port', ''))) if port < 0 or port > 65535: return self._nodejs_error('The project port is invalid: {}'.format(port)) if port == 443: return self._nodejs_error(public.lang('Do not use system ports 443.')) if port and self.check_port_is_used(port, True): return self._nodejs_error('This port is already occupied by an aaPanel Node project, port: {}'.format(port)) existing_paths, existing_names = self._get_existing_node_scan_targets() if project_cwd in existing_paths: return self._nodejs_error('The project directory has already been imported: {}'.format(project_cwd)) project_name = self._make_unique_project_name(get.get('project_name', candidate['project_name']), existing_names, pid) run_user = str(get.get('run_user', candidate.get('run_user', 'www')) or 'www').strip() nodejs_version = str(get.get('nodejs_version', candidate.get('nodejs_version', '')) or '').strip() if not nodejs_version: nodejs_version = self._get_default_nodejs_version() if not nodejs_version: return self._nodejs_error('Please install or select a Node.js version first.') if not self.get_node_bin(nodejs_version): return self._nodejs_error(public.lang('The specified nodejs version does not exist!')) try: bind_extranet = int(get.get('bind_extranet', 0) or 0) except: bind_extranet = 0 try: is_power_on = int(get.get('is_power_on', 0) or 0) except: is_power_on = 0 try: max_memory_limit = int(get.get('max_memory_limit', 0) or 0) except: max_memory_limit = 0 format_domains, error_msg = self._format_and_validate_domains(bind_extranet, get.get('domains', []), port) if error_msg: return self._nodejs_error(error_msg) project_config = { 'project_name': project_name, 'project_cwd': project_cwd, 'project_script': project_script, 'project_type': 'nodejs', 'start_type': 'package_script', 'import_source': 'scan', 'pkg_manager': pkg_manager, 'package_manager': pkg_manager, 'bind_extranet': bind_extranet, 'domains': [], 'is_power_on': is_power_on, 'run_user': run_user, 'max_memory_limit': max_memory_limit, 'nodejs_version': nodejs_version, 'port': port, 'scan_pid': pid, 'scan_pids': candidate.get('pids', []), 'scan_time': public.getDate() } pdata = { 'name': project_name, 'path': project_cwd, 'ps': str(get.get('project_ps', get.get('ps', 'Scan-Imported')) or 'Scan-Imported'), 'status': 1, 'type_id': 0, 'project_type': 'Node', 'project_config': json.dumps(project_config), 'addtime': public.getDate() } project_id = public.M('sites').insert(pdata) pid_file = "{}/{}.pid".format(self._node_pid_path, project_name) public.writeFile(pid_file, str(pid)) if bind_extranet: get.project_name = project_name get.domains = format_domains self.project_add_domain(get) public.WriteLog(self._log_name, 'Import scanned Node.js project {}'.format(project_name)) return_message = public.return_data(True, 'Imported project successfully', project_id) return_message['project_name'] = project_name return_message['pid'] = pid if 'status' in return_message: del return_message['status'] public.set_module_logs('node_site', 'create_scan_import_project') return public.return_message(0, 0, return_message) def create_project(self, get): ''' @name 创建新的项目 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> project_cwd: string<项目目录> project_script: string<项目脚本> project_ps: string<项目备注信息> bind_extranet: int<是否绑定外网> 1:是 0:否 domains: list<域名列表> ["domain1:80","domain2:80"] // 在bind_extranet=1时,需要填写 is_power_on: int<是否开机启动> 1:是 0:否 run_user: string<运行用户> max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 nodejs_version: string } @return dict ''' # 校验参数 try: get.validate([ Param('project_cwd').String(), Param('project_name').String(), Param('project_script').String(), Param('port').String(), Param('run_user').String(), Param('nodejs_version').String(), Param('project_ps').String(), # Param('domains').List(), Param('project_env').String(), Param('bind_extranet').Integer(), Param('is_power_on').Integer(), Param('max_memory_limit').Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) public.set_module_logs('node_site_nodejs', 'create_app', 1) public.set_module_logs('node_site', 'create_app', 1) if not isinstance(get, public.dict_obj): return_message = public.return_error(public.lang('The parameter type is wrong, need dict obj object')) del return_message['status'] return public.return_message(-1, 0, return_message) if not self._is_install_nodejs(get): return_message = public.return_error(public.lang('Please install nodejs version manager first')) del return_message['status'] return public.return_message(-1, 0, return_message) project_name = get.project_name.strip() if not re.match(r"^\w+$", project_name): return_message = public.return_error(public.lang( 'The project name format is incorrect and supports letters, numbers, underscores, and expressions: ^[0-9A-Za-z_]$')) del return_message['status'] return public.return_message(-1, 0, return_message) if public.M('sites').where('name=?', (get.project_name,)).count(): return_message = public.return_error( 'The specified project name already exists: {}'.format(get.project_name)) del return_message['status'] return public.return_message(-1, 0, return_message) get.project_cwd = get.project_cwd.strip() if not os.path.exists(get.project_cwd): return_message = public.return_error('The project directory does not exist: {}'.format(get.project_cwd)) del return_message['status'] return public.return_message(-1, 0, return_message) # 端口占用检测 if get.get('port') == '443': return_message = public.return_error(public.lang('Do not use system ports 443.')) del return_message['status'] return public.return_message(-1, 0, return_message) if self.check_port_is_used(get.get('port/port')): return_message = public.return_error( 'This port is already occupied, please modify your project port, port: {}'.format(get.port)) del return_message['status'] return public.return_message(-1, 0, return_message) format_domains, error_msg = self._format_and_validate_domains(get.bind_extranet, get.get('domains', []), get.port) if error_msg: return self._nodejs_error(error_msg) pdata = { 'name': get.project_name, 'path': get.project_cwd, 'ps': get.project_ps, 'status': 1, 'type_id': 0, 'project_type': 'Node', 'project_config': json.dumps( { 'project_name': get.project_name, 'project_cwd': get.project_cwd, 'project_script': get.project_script, 'bind_extranet': int(get.bind_extranet), 'domains': [], 'is_power_on': int(get.get('is_power_on', 0)), 'run_user': get.run_user, 'max_memory_limit': get.max_memory_limit, 'nodejs_version': get.nodejs_version, 'pkg_manager': self._normalize_package_manager( get.get('pkg_manager', get.get('package_manager', 'npm'))), 'port': int(get.port) } ), 'addtime': public.getDate() } project_id = public.M('sites').insert(pdata) if int(get.bind_extranet) == 1: get.domains = format_domains self.project_add_domain(get) self.set_config(get.project_name) public.WriteLog(self._log_name, 'Add Node.js project {}'.format(get.project_name)) self.install_packages(get) self.start_project(get) return_message = public.return_data(True, 'Added project successfully', project_id) # ================ git start ====================== try: if get.get('deploy_type') in ['ssh', 'github']: if get.get('deploy_type') == 'ssh': from git_tools import GitTools git_obj = GitTools() # 已clone项目,使用.git导入 res = git_obj.get_git_directory(public.to_dict_obj({"site_id": project_id})) if res['status'] != 0: return public.return_message(-1, 0, res['message']) res = res['message'] res = git_obj.import_existing_repository( public.to_dict_obj({"site_id": project_id, "repo": res['repo'], "branch": res['branch'], "key_path": res['key_path'], "project_type": 'node'})) if res['status'] != 0: return public.return_message(-1, 0, res['message']) except Exception as e: print(e) # 失败不删除项目 return public.return_message(-1, 0, public.lang("There was an error while configuring Git tools!")) # ================ git end ====================== del return_message['status'] return public.return_message(0, 0, return_message) def modify_project(self, get): ''' @name 修改指定项目 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> project_cwd: string<项目目录> project_script: string<项目脚本> project_ps: string<项目备注信息> is_power_on: int<是否开机启动> 1:是 0:否 run_user: string<运行用户> max_memory_limit: int<最大内存限制> // 超出此值项目将被强制重启 nodejs_version: string } @return dict ''' # 校验参数 try: get.validate([ Param('project_cwd').String(), Param('project_name').String(), Param('project_script').String(), Param('port').String(), Param('run_user').String(), Param('nodejs_version').String(), Param('project_ps').String(), Param('domains').String(), Param('bind_extranet').Integer(), Param('is_power_on').Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) if not isinstance(get, public.dict_obj): return_message = public.return_error(public.lang('The parameter type is wrong, need dict obj')) del return_message['status'] return public.return_message(-1, 0, return_message) if not self._is_install_nodejs(get): return_message = public.return_error( public.lang('Please install nodejs version manager before installing at least one nodejs')) del return_message['status'] return public.return_message(-1, 0, return_message) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error('Item does not exist: {}'.format(get.project_name)) del return_message['status'] return public.return_message(-1, 0, return_message) if not os.path.exists(get.project_cwd): return_message = public.return_error('The project directory does not exist: {}'.format(get.project_cwd)) del return_message['status'] return public.return_message(-1, 0, return_message) rebuild = False if hasattr(get, 'port'): if int(project_find['project_config']['port']) != int(get.port): if self.check_port_is_used(get.get('port/port'), True): return_message = public.return_error( 'The port is already occupied, please modify your port, port: {}'.format(get.port)) del return_message['status'] return public.return_message(-1, 0, return_message) project_find['project_config']['port'] = int(get.port) if hasattr(get, 'project_cwd'): project_find['project_config']['project_cwd'] = get.project_cwd if hasattr(get, 'project_script'): if not get.project_script.strip(): return_message = public.return_error(public.lang('Start command cannot be empty')) del return_message['status'] return public.return_message(-1, 0, return_message) project_find['project_config']['project_script'] = get.project_script.strip() if hasattr(get, 'is_power_on'): project_find['project_config']['is_power_on'] = get.is_power_on if hasattr(get, 'run_user'): project_find['project_config']['run_user'] = get.run_user if hasattr(get, 'max_memory_limit'): project_find['project_config']['max_memory_limit'] = get.max_memory_limit if hasattr(get, 'nodejs_version'): if project_find['project_config']['nodejs_version'] != get.nodejs_version: rebuild = True project_find['project_config']['nodejs_version'] = get.nodejs_version pdata = { 'path': get.project_cwd, 'ps': get.project_ps, 'project_config': json.dumps(project_find['project_config']) } public.M('sites').where('name=?', (get.project_name,)).update(pdata) self.set_config(get.project_name) public.WriteLog(self._log_name, 'Modify Node.js project {}'.format(get.project_name)) if rebuild: self.rebuild_project(get.project_name) return_message = public.return_data(True, 'Modify the project successfully') del return_message['status'] return public.return_message(0, 0, return_message) def rebuild_project(self, project_name): ''' @name 重新构建指定项目 @author hwliang<2021-08-26> @param project_name: string<项目名称> @return bool ''' project_find = self.get_project_find(project_name) if not project_find: return False nodejs_version = project_find['project_config']['nodejs_version'] npm_bin = self.get_npm_bin(nodejs_version) public.ExecShell( self.get_last_env(nodejs_version) + "cd {} && {} rebuild >> {} 2>&1".format(project_find['path'], npm_bin, self._npm_exec_log)) return True def remove_project(self, get): ''' @name 删除指定项目 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error('The specified item does not exist: {}'.format(get.project_name)) del return_message['status'] return public.return_message(-1, 0, return_message) self.stop_project(get) self.clear_config(get.project_name) public.M('domain').where('pid=?', (project_find['id'],)).delete() public.M('sites').where('name=?', (get.project_name,)).delete() pid_file = "{}/{}.pid".format(self._node_pid_path, get.project_name) if os.path.exists(pid_file): os.remove(pid_file) script_file = '{}/{}.sh'.format(self._node_run_scripts, get.project_name) if os.path.exists(script_file): os.remove(script_file) log_file = '{}/{}.log'.format(self._node_logs_path, get.project_name) if os.path.exists(log_file): os.remove(log_file) public.WriteLog(self._log_name, 'Delete Node.js project {}'.format(get.project_name)) return_message = public.return_data(True, 'Successfully deleted item') del return_message['status'] return public.return_message(0, 0, return_message) # 批量操作项目 def batch_operation_project(self, get): ''' @name 批量操作项目 @param project_names [] @return dict ''' try: get.validate([ Param('project_names'), Param('operation_type'), ]) project_names = json.loads(get.get('project_names', [])) operation_type = get.get('operation_type') except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) if not project_names: return public.return_message(-1, 0, public.lang('The project list is empty.')) if operation_type not in ['delete', 'start', 'stop', 'restart']: return public.return_message(-1, 0, public.lang('Operation type is empty!')) project_list = public.S('sites').where_in("name", project_names, 'OR').select() if not project_list: return public.return_message(-1, 0, public.lang('The project list is empty.')) success_count = 0 msg_list = [] from mod.project.nodejs import comMod for project in project_list: try: project['project_config'] = json.loads(project['project_config']) p_type = 'pm2' if project['project_config'].get('pm2_name') else 'nodejs' if operation_type == 'delete': temp_get = public.to_dict_obj({'project_name': project['name'], 'project_type': p_type, 'pm2_name': project['project_config'].get('pm2_name')}) res = comMod.main().delete(temp_get) elif operation_type in ['start', 'stop', 'restart']: res = comMod.main().set_project_status(public.to_dict_obj( {"project_name": project['name'], "project_type": p_type, "status": operation_type, "pm2_name": project['project_config'].get('pm2_name'), "run_user": project['project_config'].get('run_user')})) if res['status'] != 0: msg_list.append({'name': project['name'], 'status': False, 'msg': res['message']['result']}) else: success_count += 1 msg_list.append({'name': project['name'], 'status': True, 'msg': res['message']['result']}) if operation_type == 'delete': # 删除git from git_tools import GitTools GitTools().del_site_git(public.to_dict_obj({'site_id': project['id']})) except Exception as e: msg_list.append({'name': project['name'], 'status': False, 'msg': str(e)}) msg = f"Successfully {success_count} items.Failed on {len(project_names) - success_count} projects." return public.return_message(0, 0, {"msg": msg, "msg_list": msg_list}) def project_get_domain(self, get): ''' @name 获取指定项目的域名列表 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' project_id = public.M('sites').where('name=?', (get.project_name,)).getField('id') if not project_id: return public.return_message(0, 0, []) domains = public.M('domain').where('pid=?', (project_id,)).order('id desc').select() return public.return_message(0, 0, domains) def project_add_domain(self, get): ''' @name 为指定项目添加域名 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> domains: list<域名列表> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), # Param('domains').List(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist')) del return_message['status'] return public.return_message(-1, 0, return_message) project_id = project_find['id'] domains = get.domains success_list = [] error_list = [] for domain in domains: domain = domain.strip() if not domain: return_message = public.return_error(public.lang('Domain name cannot be empty')) del return_message['status'] return public.return_message(-1, 0, return_message) domain_arr = domain.split(':') if len(domain_arr) == 1: domain_arr.append(80) domain += ':80' else: if domain_arr[1] == '443': error_list.append(domain) continue if not public.M('domain').where('name=?', (domain_arr[0],)).count(): public.M('domain').add('name,pid,port,addtime', (domain_arr[0], project_id, domain_arr[1], public.getDate())) if not domain in project_find['project_config']['domains']: project_find['project_config']['domains'].append(domain) public.WriteLog(self._log_name, 'Successfully added the domain [{}] to the project [{}]'.format(domain, get.project_name)) success_list.append(domain) else: public.WriteLog(self._log_name, 'Domain [{}] already exists'.format(domain)) error_list.append(domain) if success_list: public.M('sites').where('id=?', (project_id,)).save('project_config', json.dumps(project_find['project_config'])) self.set_config(get.project_name) return_message = public.return_data(True, "[{}] domain names added successfully, [{}] failed!".format( len(success_list), len(error_list)), error_msg=error_list) del return_message['status'] return public.return_message(0, 0, return_message) return_message = public.return_data(False, "[{}] domain names added successfully, [{}] failed!".format( len(success_list), len(error_list)), error_msg=error_list) del return_message['status'] return public.return_message(-1, 0, return_message) def project_remove_domain(self, get): ''' @name 为指定项目删除域名 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> domain: string<域名> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), Param('domain').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('The specified item does not exist')) del return_message['status'] return public.return_message(-1, 0, return_message) last_domain = get.domain domain_arr = get.domain.split(':') if len(domain_arr) == 1: domain_arr.append(80) project_id = public.M('sites').where('name=?', (get.project_name,)).getField('id') if project_find['project_config']['bind_extranet']: domain_count = public.M('domain').where('pid=?', (project_id,)).count() if domain_count <= 1: return_message = public.return_error( public.lang('At least one domain name is required for the mapped project')) del return_message['status'] return public.return_message(-1, 0, return_message) domain_id = public.M('domain').where('name=? AND pid=?', (domain_arr[0], project_id)).getField('id') if not domain_id: return_message = public.return_error(public.lang('The specified domain name does not exist')) del return_message['status'] return public.return_message(-1, 0, return_message) public.M('domain').where('id=?', (domain_id,)).delete() if get.domain in project_find['project_config']['domains']: project_find['project_config']['domains'].remove(get.domain) if get.domain + ":80" in project_find['project_config']['domains']: project_find['project_config']['domains'].remove(get.domain + ":80") # 处理端口残留 domains = [] for domain in project_find['project_config']['domains']: if get.domain == domain.split(':')[0]: continue domains.append(domain) project_find['project_config']['domains'] = domains public.M('sites').where('id=?', (project_id,)).save('project_config', json.dumps(project_find['project_config'])) public.WriteLog(self._log_name, 'From project: [{}], delete domain name [{}]'.format(get.project_name, get.domain)) self.set_config(get.project_name) return_message = public.return_data(True, 'Domain name deleted successfully') del return_message['status'] return public.return_message(0, 0, return_message) def bind_extranet(self, get): ''' @name 绑定外网 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), # Param('domains').List(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_name = get.project_name.strip() project_find = self.get_project_find(project_name) if not project_find: return_message = public.return_error(public.lang('Item does not exist')) del return_message['status'] return public.return_message(-1, 0, return_message) if not project_find['project_config']['domains']: return_message = public.return_error( public.lang('Please add at least one domain name in the [Domain Management] option')) del return_message['status'] return public.return_message(-1, 0, return_message) if not project_find['project_config'].get('port'): return_message = public.return_error(public.lang('Please set the running port for the project first.')) del return_message['status'] return public.return_message(-1, 0, return_message) project_find['project_config']['bind_extranet'] = 1 public.M('sites').where("id=?", (project_find['id'],)).setField('project_config', json.dumps(project_find['project_config'])) self.set_config(project_name) public.WriteLog(self._log_name, 'Node project{}, enable mapping'.format(project_name)) return_message = public.return_data(True, 'Enable the mapping successfully') del return_message['status'] return public.return_message(0, 0, return_message) def set_config(self, project_name): ''' @name 设置项目配置 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return bool ''' project_find = self.get_project_find(project_name) if not project_find: return False if not project_find['project_config']: return False if not project_find['project_config']['bind_extranet']: return False if not project_find['project_config']['domains']: return False self.set_nginx_config(project_find) self.set_apache_config(project_find) self.set_ols_config(project_find) public.serviceReload() return True def clear_config(self, project_name): ''' @name 清除项目配置 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return bool ''' project_find = self.get_project_find(project_name) if not project_find: return False self.clear_nginx_config(project_find) self.clear_apache_config(project_find) self.clear_ols_config(project_find) public.serviceReload() return True def clear_ols_config(self, project_find): ''' @name 删除OLS配置 @param project_find: dict<项目信息> @return bool ''' project_name = project_find['name'] vhost_dir = public.get_panel_path() + '/vhost/openlitespeed/' detail_file = vhost_dir + 'detail/' + project_name + '.conf' main_conf_file = vhost_dir + project_name + '.conf' listen_dir = vhost_dir + 'listen/' listen_list = [listen_dir + '80.conf', listen_dir + '443.conf'] # 删除虚拟主机配置文件 if os.path.exists(main_conf_file): os.remove(main_conf_file) if os.path.exists(detail_file): os.remove(detail_file) # 清理监听器中的映射 (Map) 关系 if os.path.exists(listen_dir): for conf_name in listen_list: content = public.readFile(conf_name) if content: pattern = r"^\s*map\s+{}\s+.*?\n".format(re.escape(project_name)) if re.search(pattern, content, flags=re.M): new_content = re.sub(pattern, "", content, flags=re.M) public.writeFile(conf_name, new_content) return public.return_message(0, 0, True) def clear_apache_config(self, project_find): ''' @name 清除apache配置 @author hwliang<2021-08-09> @param project_find: dict<项目信息> @return bool ''' project_name = project_find['name'] config_file = "{}/apache/node_{}.conf".format(self._vhost_path, project_name) if os.path.exists(config_file): os.remove(config_file) return True def clear_nginx_config(self, project_find): ''' @name 清除nginx配置 @author hwliang<2021-08-09> @param project_find: dict<项目信息> @return bool ''' project_name = project_find['name'] config_file = "{}/nginx/node_{}.conf".format(self._vhost_path, project_name) if os.path.exists(config_file): os.remove(config_file) rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path=self._panel_path, project_name=project_name) if os.path.exists(rewrite_file): os.remove(rewrite_file) return True def set_nginx_config(self, project_find): ''' @name 设置Nginx配置 @author hwliang<2021-08-09> @param project_find: dict<项目信息> @return bool ''' project_name = project_find['name'] ports = [] domains = [] for d in project_find['project_config']['domains']: domain_tmp = d.split(':') if len(domain_tmp) == 1: domain_tmp.append(80) if not int(domain_tmp[1]) in ports: ports.append(int(domain_tmp[1])) if not domain_tmp[0] in domains: domains.append(domain_tmp[0]) listen_ipv6 = public.listen_ipv6() listen_ports = '' for p in ports: listen_ports += " listen {};\n".format(p) if listen_ipv6: listen_ports += " listen [::]:{};\n".format(p) listen_ports = listen_ports.strip() is_ssl, is_force_ssl = self.exists_nginx_ssl(project_name) ssl_config = '' if is_ssl: http3_header = "" if self.is_nginx_http3(): http3_header = '''\n add_header Alt-Svc 'quic=":443"; h3=":443"; h3-29=":443"; h3-27=":443";h3-25=":443"; h3-T050=":443"; h3-Q050=":443";h3-Q049=":443";h3-Q048=":443"; h3-Q046=":443"; h3-Q043=":443"';''' http3_header += "\n quic_retry on;\n quic_gso on;" if self.ng_ssl_early_data_enabled(): http3_header += "\n ssl_early_data on;" listen_ports += "\n listen 443 ssl;" if listen_ipv6: listen_ports += "\n listen [::]:443 ssl;" ssl_config = '''ssl_certificate {vhost_path}/cert/{priject_name}/fullchain.pem; ssl_certificate_key {vhost_path}/cert/{priject_name}/privkey.pem; ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=31536000";{http3_header} error_page 497 https://$host$request_uri;'''.format(vhost_path=self._vhost_path, priject_name=project_name, http3_header=http3_header) if is_force_ssl: ssl_config += ''' #HTTP_TO_HTTPS_START if ($server_port !~ 443){ rewrite ^(/.*)$ https://$host$1 permanent; } #HTTP_TO_HTTPS_END''' config_file = "{}/nginx/node_{}.conf".format(self._vhost_path, project_name) template_file = "{}/template/nginx/node_http.conf".format(self._vhost_path) config_body = public.readFile(template_file) config_body = config_body.format( site_path=project_find['path'], domains=' '.join(domains), project_name=project_name, panel_path=self._panel_path, log_path=public.get_logs_path(), url='http://127.0.0.1:{}'.format(project_find['project_config']['port']), host='$host', listen_ports=listen_ports, ssl_config=ssl_config ) # # 恢复旧的SSL配置 # ssl_config = self.get_nginx_ssl_config(project_name) # if ssl_config: # config_body.replace('#error_page 404/404.html;',ssl_config) rewrite_file = "{panel_path}/vhost/rewrite/node_{project_name}.conf".format(panel_path=self._panel_path, project_name=project_name) if not os.path.exists(rewrite_file): public.writeFile(rewrite_file, '# Please fill in the URLrewrite or custom NGINX config here\n') public.writeFile(config_file, config_body) return True def get_nginx_ssl_config(self, project_name): ''' @name 获取项目Nginx SSL配置 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return string ''' result = '' config_file = "{}/nginx/node_{}".format(self._vhost_path, project_name) if not os.path.exists(config_file): return result config_body = public.readFile(config_file) if not config_body: return result if config_body.find('ssl_certificate') == -1: return result ssl_body = re.search("#SSL-START(.|\n)+#SSL-END", config_body) if not ssl_body: return result result = ssl_body.group() return result def exists_nginx_ssl(self, project_name): ''' @name 判断项目是否配置Nginx SSL配置 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return tuple ''' config_file = "{}/nginx/node_{}.conf".format(public.get_vhost_path(), project_name) if not os.path.exists(config_file): return False, False config_body = public.readFile(config_file) if not config_body: return False, False is_ssl, is_force_ssl = False, False if config_body.find('ssl_certificate') != -1: is_ssl = True if config_body.find('HTTP_TO_HTTPS_START') != -1: is_force_ssl = True return is_ssl, is_force_ssl def exists_apache_ssl(self, project_name): ''' @name 判断项目是否配置Apache SSL配置 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return bool ''' config_file = "{}/apache/node_{}.conf".format(public.get_vhost_path(), project_name) if not os.path.exists(config_file): return False, False config_body = public.readFile(config_file) if not config_body: return False, False is_ssl, is_force_ssl = False, False if config_body.find('SSLCertificateFile') != -1: is_ssl = True if config_body.find('HTTP_TO_HTTPS_START') != -1: is_force_ssl = True return is_ssl, is_force_ssl def set_apache_config(self, project_find): ''' @name 设置Apache配置 @author hwliang<2021-08-09> @param project_find: dict<项目信息> @return bool ''' project_name = project_find['name'] # 处理域名和端口 ports = [] domains = [] for d in project_find['project_config']['domains']: domain_tmp = d.split(':') if len(domain_tmp) == 1: domain_tmp.append(80) if not int(domain_tmp[1]) in ports: ports.append(int(domain_tmp[1])) if not domain_tmp[0] in domains: domains.append(domain_tmp[0]) config_file = "{}/apache/node_{}.conf".format(self._vhost_path, project_name) template_file = "{}/template/apache/node_http.conf".format(self._vhost_path) config_body = public.readFile(template_file) apache_config_body = '' # 旧的配置文件是否配置SSL is_ssl, is_force_ssl = self.exists_apache_ssl(project_name) if is_ssl: if not 443 in ports: ports.append(443) from panelSite import panelSite s = panelSite() # 根据端口列表生成配置 for p in ports: # 生成SSL配置 ssl_config = '' if p == 443 and is_ssl: ssl_key_file = "{vhost_path}/cert/{project_name}/privkey.pem".format(project_name=project_name, vhost_path=public.get_vhost_path()) if not os.path.exists(ssl_key_file): continue # 不存在证书文件则跳过 ssl_config = '''#SSL SSLEngine On SSLCertificateFile {vhost_path}/cert/{project_name}/fullchain.pem SSLCertificateKeyFile {vhost_path}/cert/{project_name}/privkey.pem SSLCipherSuite EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5 SSLProtocol All -SSLv2 -SSLv3 -TLSv1 SSLHonorCipherOrder On'''.format(project_name=project_name, vhost_path=public.get_vhost_path()) else: if is_force_ssl: ssl_config = '''#HTTP_TO_HTTPS_START RewriteEngine on RewriteCond %{SERVER_PORT} !^443$ RewriteRule (.*) https://%{SERVER_NAME}$1 [L,R=301] #HTTP_TO_HTTPS_END''' # 多服务下切换端口 if public.get_multi_webservice_status() and str(p) in ['80', '443']: if str(p) == '80': p = 8288 else: p = 8290 # 生成vhost主体配置 apache_config_body += config_body.format( site_path=project_find['path'], server_name='{}.{}'.format(p, project_name), domains=' '.join(domains), log_path=public.get_logs_path(), server_admin='admin@{}'.format(project_name), url='http://127.0.0.1:{}'.format(project_find['project_config']['port']), port=p, ssl_config=ssl_config, project_name=project_name ) apache_config_body += "\n" # 添加端口到主配置文件 if not p in [80] and not public.get_multi_webservice_status(): s.apacheAddPort(p) # 写.htaccess rewrite_file = "{}/.htaccess".format(project_find['path']) if not os.path.exists(rewrite_file): public.writeFile(rewrite_file, '# Please fill in the URLrewrite rules or custom Apache config here\n') # 写配置文件 public.writeFile(config_file, apache_config_body) return True def set_ols_config(self, project_find): project_name = project_find['name'] project_port = project_find['project_config']['port'] domains = project_find['project_config']['domains'] # 路径与基础检查 vhost_dir = public.get_panel_path() + '/vhost/openlitespeed/' detail_dir = vhost_dir + 'detail/' listen_dir = vhost_dir + 'listen/' for path in [detail_dir, listen_dir]: if not os.path.exists(path): os.makedirs(path) # 生成虚拟主机 vhost_main_file = vhost_dir + project_name + '.conf' vhost_main_body = """virtualhost {name} {{ vhRoot {path} configFile {detail_dir}{name}.conf allowSymbolLink 1 enableScript 1 restrained 1 setUIDMode 0 }}""".format(name=project_name, path=project_find['path'], detail_dir=detail_dir) public.writeFile(vhost_main_file, vhost_main_body) # 生成Host detail_file = detail_dir + project_name + '.conf' conf_body = '''docRoot $VH_ROOT vhDomain $VH_NAME adminEmails admin@{name} enableGzip 1 errorlog /www/wwwlogs/{name}_ols.error_log {{ useServer 0 logLevel ERROR rollingSize 10M }} accesslog /www/wwwlogs/{name}_ols.access_log {{ useServer 0 logFormat '%{{X-Forwarded-For}}i %h %l %u %t "%r" %>s %b "%{{Referer}}i" "%{{User-Agent}}i"' logHeaders 5 rollingSize 10M keepDays 10 }} extprocessor {name}_proxy {{ type proxy address 127.0.0.1:{node_port} maxConns 100 pcKeepAliveTimeout 60 initTimeout 60 retryTimeout 0 respBuffer 0 }} context / {{ type proxy handler {name}_proxy addDefaultCharset off }} rewrite {{ enable 1 autoLoadHtaccess 1 }} '''.format(name=project_name, node_port=project_port) public.writeFile(detail_file, conf_body) # 处理监听器映射 # 提取端口和对应的域名 port_map = {} for d in domains: tmp = d.split(':') p = tmp[1] if len(tmp) > 1 else "80" if p not in port_map: port_map[p] = [] port_map[p].append(tmp[0]) # 检查 SSL 状态 (参考原 Apache 逻辑) is_ssl, _ = self.exists_apache_ssl(project_name) if is_ssl and "443" not in port_map: # 如果有证书但域名列表没写443,默认把所有域名映射到443 port_map["443"] = [d.split(':')[0] for d in domains] for p, ds in port_map.items(): listen_file = "{}{}.conf".format(listen_dir, p) if not os.path.exists(listen_file): continue content = public.readFile(listen_file) # 构造映射行:map 项目名 域名1,域名2 domain_str = ",".join(ds) map_line = "\tmap\t{}\t{}\n".format(project_name, domain_str) # 如果该项目已在 map 中,先正则替换或跳过,这里采用简单判断 if "map\t{}\t".format(project_name) in content: import re content = re.sub(r"map\t{}\t.*?\n".format(project_name), map_line, content) else: # 插入到最后一个花括号之前 last_brace_index = content.rfind('}') if last_brace_index != -1: content = content[:last_brace_index] + map_line + content[last_brace_index:] public.writeFile(listen_file, content) return public.return_message(0, 0, True) def unbind_extranet(self, get): ''' @name 解绑外网 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_name = get.project_name.strip() self.clear_config(project_name) public.serviceReload() project_find = self.get_project_find(project_name) project_find['project_config']['bind_extranet'] = 0 public.M('sites').where("id=?", (project_find['id'],)).setField('project_config', json.dumps(project_find['project_config'])) public.WriteLog(self._log_name, 'Node project {}, disable the mapping'.format(project_name)) return_message = public.return_data(True, 'Disabled successfully') del return_message['status'] return public.return_message(0, 0, return_message) def get_project_pids(self, get=None, pid=None, without_request=False): ''' @name 获取项目进程pid列表 @author hwliang<2021-08-10> @param pid: string<项目pid> @return list ''' if get: pid = int(get.pid) if not self._pids: self._pids = psutil.pids() project_pids = [] for i in self._pids: try: p = psutil.Process(i) if p.status() == "zombie": continue if p.ppid() == pid: if i in project_pids: continue project_pids.append(i) except: continue other_pids = [] for i in project_pids: other_pids += self.get_project_pids(pid=i)['message'] if os.path.exists('/proc/{}'.format(pid)): project_pids.append(pid) all_pids = list(set(project_pids + other_pids)) if not all_pids: all_pids = self.get_other_pids(pid)['message'] # public.print_log("all_pids -- {}".format(all_pids)) def convert_to_int(item): try: return int(item) except ValueError: return item # 将所有元素转换为整数后进行排序 sorted_pids = sorted(all_pids, key=convert_to_int) if without_request: return sorted_pids return public.return_message(0, 0, sorted_pids) def get_other_pids(self, pid): ''' @name 获取其他进程pid列表 @author hwliang<2021-08-10> @param pid: string<项目pid> @return list ''' project_name = None for pid_name in os.listdir(self._node_pid_path): pid_file = '{}/{}'.format(self._node_pid_path, pid_name) # s_pid = int(public.readFile(pid_file)) data = public.readFile(pid_file) if isinstance(data, str) and data: data = data.strip() if not data.isdigit(): return public.return_message(0, 0, []) s_pid = int(data) else: return public.return_message(0, 0, []) if pid == s_pid: project_name = pid_name[:-4] break project_find = self.get_project_find(project_name) if not project_find: return public.return_message(0, 0, []) if not self._pids: self._pids = psutil.pids() all_pids = [] for i in self._pids: try: p = psutil.Process(i) if p.cwd() == project_find['path']: pname = p.name() if pname in ['node', 'npm', 'pm2', 'yarn'] or pname.find('node ') == 0: cmdline = ','.join(p.cmdline()) if cmdline.find('God Daemon') != -1: continue env_list = p.environ() if 'name' in env_list: if not env_list['name'] == project_name: continue if 'NODE_PROJECT_NAME' in env_list: if not env_list['NODE_PROJECT_NAME'] == project_name: continue all_pids.append(i) except: continue return public.return_message(0, 0, all_pids) def get_project_state_by_cwd(self, project_name): ''' @name 通过cwd获取项目状态 @author hwliang<2022-01-17> @param project_name 项目名称 @return bool or list ''' project_find = self.get_project_find(project_name) self._pids = psutil.pids() if not project_find: return [] all_pids = [] for i in self._pids: try: p = psutil.Process(i) if p.cwd() == project_find['path']: pname = p.name() if pname in ['node', 'npm', 'pm2', 'yarn'] or pname.find('node ') == 0: cmdline = ','.join(p.cmdline()) if cmdline.find('God Daemon') != -1: continue env_list = p.environ() if 'name' in env_list: if not env_list['name'] == project_name: continue if 'NODE_PROJECT_NAME' in env_list: if not env_list['NODE_PROJECT_NAME'] == project_name: continue all_pids.append(i) except: continue if all_pids: pid_file = "{}/{}.pid".format(self._node_pid_path, project_name) public.writeFile(pid_file, str(all_pids[0])) return all_pids return False def kill_pids(self, get=None, pids=None): ''' @name 结束进程列表 @author hwliang<2021-08-10> @param pids: string<进程pid列表> @return dict ''' if get: pids = get.pids if not pids: return_message = public.return_data(True, 'No process') del return_message['status'] return public.return_message(0, 0, return_message) pids = sorted(pids, reverse=True) for i in pids: try: p = psutil.Process(i) p.kill() except: pass return_message = public.return_data(True, 'The process has all ended') del return_message['status'] return public.return_message(0, 0, return_message) def start_project(self, get): ''' @name 启动项目 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) pid_file = "{}/{}.pid".format(self._node_pid_path, get.project_name) if os.path.exists(pid_file): self.stop_project(get) project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('Item does not exist')) del return_message['status'] return public.return_message(-1, 0, return_message) if not os.path.exists(project_find['path']): error_msg = 'Startup failed, Nodejs project {}, running directory {} does not exist!'.format( get.project_name, project_find['path']) public.WriteLog(self._log_name, error_msg) return_message = public.return_error(error_msg) del return_message['status'] return public.return_message(-1, 0, return_message) # 是否安装依赖模块? package_file = "{}/package.json".format(project_find['path']) package_info = {} if os.path.exists(package_file): node_modules_path = "{}/node_modules".format(project_find['path']) if not os.path.exists(node_modules_path): return_message = public.return_error(public.lang( 'Please go to the [Module] and click [One-key install] to install the module dependencies!')) del return_message['status'] return public.return_message(-1, 0, return_message) package_info = json.loads(public.readFile(package_file)) if not package_info: package_info['scripts'] = {} if 'scripts' not in package_info: package_info['scripts'] = {} try: scripts_keys = package_info['scripts'].keys() except: scripts_keys = [] # 前置准备 nodejs_version = project_find['project_config']['nodejs_version'] node_bin = self.get_node_bin(nodejs_version) pkg_manager = self._normalize_package_manager(project_find['project_config'].get('pkg_manager', project_find[ 'project_config'].get('package_manager', 'npm'))) project_script = project_find['project_config']['project_script'].strip().replace(' ', ' ') if project_script[:3] == 'pm2': # PM2启动方式处理 project_script = project_script.replace('pm2 ', 'pm2 -u {} -n {} '.format( project_find['project_config']['run_user'], get.project_name)) project_find['project_config']['run_user'] = 'root' log_file = "{}/{}.log".format(self._node_logs_path, get.project_name) if not project_script: return_message = public.return_error(public.lang('No startup script configured')) del return_message['status'] return public.return_message(-1, 0, return_message) last_env = self.get_last_env(nodejs_version, project_find['path']) project_port = project_find['project_config'].get('port', '') port_export = 'export PORT="{}"\n'.format(project_port) if project_port else '' # 生成启动脚本 if os.path.exists(project_script): start_cmd = '''{last_env} export NODE_PROJECT_NAME="{project_name}" {port_export}cd {project_cwd} nohup {node_bin} {project_script} >> {log_file} 2>&1 & echo $! > {pid_file} '''.format( project_cwd=project_find['path'], node_bin=node_bin, project_script=project_script, log_file=log_file, pid_file=pid_file, last_env=last_env, project_name=get.project_name, port_export=port_export ) elif project_script in scripts_keys: package_run_cmd = self._get_package_manager_run_cmd(nodejs_version, pkg_manager, project_script) if not package_run_cmd: return_message = public.return_error( 'The specified package manager is not available: {}'.format(pkg_manager)) del return_message['status'] return public.return_message(-1, 0, return_message) start_cmd = '''{last_env} export NODE_PROJECT_NAME="{project_name}" {port_export}cd {project_cwd} nohup {package_run_cmd} >> {log_file} 2>&1 & echo $! > {pid_file} '''.format( project_cwd=project_find['path'], package_run_cmd=package_run_cmd, pid_file=pid_file, log_file=log_file, last_env=last_env, project_name=get.project_name, port_export=port_export ) else: start_cmd = '''{last_env} export NODE_PROJECT_NAME="{project_name}" {port_export}cd {project_cwd} nohup {project_script} >> {log_file} 2>&1 & echo $! > {pid_file} '''.format( project_cwd=project_find['path'], project_script=project_script, pid_file=pid_file, log_file=log_file, last_env=last_env, project_name=get.project_name, port_export=port_export ) script_file = "{}/{}.sh".format(self._node_run_scripts, get.project_name) # 写入启动脚本 public.writeFile(script_file, start_cmd) if os.path.exists(pid_file): os.remove(pid_file) # 处理前置权限 public.ExecShell("chown -R {user}:{user} {project_cwd}".format(user=project_find['project_config']['run_user'], project_cwd=project_find['path'])) public.ExecShell("chown -R www:www {}/vhost".format(self._nodejs_path)) public.ExecShell("chmod 755 {} {} {}".format(self._nodejs_path, public.get_setup_path(), '/www')) public.set_own(script_file, project_find['project_config']['run_user'], project_find['project_config']['run_user']) public.set_mode(script_file, 755) p = public.ExecShell("bash {}".format(script_file), user=project_find['project_config']['run_user']) time.sleep(1) n = 0 while n < 5: if self.get_project_state_by_cwd(get.project_name): break n += 1 if not os.path.exists(pid_file): p = '\n'.join(p) public.writeFile(log_file, p, "a+") if p.find('[Errno 0]') != -1: if os.path.exists('{}/bt_security'.format(public.get_plugin_path())): return_message = public.return_error( 'The start command was intercepted by [Fort Tower Defense Privilege], please turn off {} user protection'.format( project_find['project_config']['run_user'])) del return_message['status'] return public.return_message(-1, 0, return_message) return_message = public.return_error(public.lang( 'The startup command was intercepted by unknown security software, please check the installation software log')) del return_message['status'] return public.return_message(-1, 0, return_message) return_message = public.return_error('failed to activate
{}
'.format(p)) del return_message['status'] return public.return_message(-1, 0, return_message) # 获取PID try: pid = int(public.readFile(pid_file)) except: return public.return_error('Startup failed
{}'.format(public.GetNumLines(log_file, 20))) pids = self.get_project_pids(pid=pid, without_request=True) if not pids: if os.path.exists(pid_file): os.remove(pid_file) return_message = public.return_error('failed to activate
{}'.format(public.GetNumLines(log_file, 20))) del return_message['status'] return public.return_message(-1, 0, return_message) return_message = public.return_data(True, 'Successfully started', pids) del return_message['status'] return public.return_message(0, 0, return_message) def stop_project(self, get): ''' @name 停止项目 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> is_power_on: boolean<> #标记是否自动关闭自动重启 } @return dict ''' project_find = self.get_project_find(get.project_name) if not project_find: return_message = public.return_error(public.lang('Project does not exist')) del return_message['status'] return public.return_message(-1, 0, return_message) project_script = project_find['project_config']['project_script'].strip().replace(' ', ' ') pid_file = "{}/{}.pid".format(self._node_pid_path, get.project_name) if project_script.find('pm2 start') != -1: # 处理PM2启动的项目 nodejs_version = project_find['project_config']['nodejs_version'] last_env = self.get_last_env(nodejs_version, project_find['path']) project_script = project_script.replace('pm2 start', 'pm2 stop') public.ExecShell('''{} cd {} {}'''.format(last_env, project_find['path'], project_script)) else: pid_file = "{}/{}.pid".format(self._node_pid_path, get.project_name) if not os.path.exists(pid_file): return_message = public.return_error(public.lang('Project did not start')) del return_message['status'] return public.return_message(-1, 0, return_message) data = public.readFile(pid_file) if isinstance(data, str) and data: pid = int(data) pids = self.get_project_pids(pid=pid, without_request=True) else: return_message = public.return_error(public.lang('Project did not start')) del return_message['status'] return public.return_message(-1, 0, return_message) if not pids: return_message = public.return_error(public.lang('Project did not start')) del return_message['status'] return public.return_message(-1, 0, return_message) self.kill_pids(pids=pids) if os.path.exists(pid_file): os.remove(pid_file) time.sleep(0.5) pids = self.get_project_state_by_cwd(get.project_name) if pids: self.kill_pids(pids=pids) # 停用项目自启 if get.get('is_power_on') in ['True', 'true', 1, '1']: data = project_find['project_config'] data = { "project_cwd": data['project_cwd'], "project_name": data['project_name'], "project_script": data['project_script'], "port": str(data['port']), "run_user": data['run_user'], "nodejs_version": data['nodejs_version'], "project_ps": project_find['ps'], "is_power_on": 0 } self.modify_project(public.to_dict_obj(data)) return_message = public.return_data(True, 'Stopped successfully') del return_message['status'] return public.return_message(0, 0, return_message) def restart_project(self, get): ''' @name 重启项目 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) res = self.stop_project(get) if res['status'] == -1: return res res = self.start_project(get) if res['status'] == -1: return res return_message = public.return_data(True, 'Successful restart') del return_message['status'] return public.return_message(0, 0, return_message) # xss 防御 def xsssec(self, text): return text.replace('<', '<').replace('>', '>') def get_project_log(self, get): ''' @name 获取项目日志 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_find = self.get_project_find(get.project_name) if not project_find: return public.returnMsg(False, '项目不存在') if "log_path" not in project_find['project_config']: log_file = "{}/{}.log".format(self._node_logs, project_find["name"]) else: log_file = "{}/{}.log".format(project_find['project_config']["log_path"], project_find["name"]) if not os.path.exists(log_file): return public.return_message(-1, 0, public.lang("The log file does not exist.")) return public.return_message(0, 0, self.xsssec(public.GetNumLines(log_file, 200))) def get_project_load_info(self, get=None, project_name=None): ''' @name 获取项目负载信息 @author hwliang<2021-08-12> @param get{ project_name: string<项目名称> } @return dict ''' if get: project_name = get.project_name.strip() load_info = {} pid_file = "{}/{}.pid".format(self._node_pid_path, project_name) if not os.path.exists(pid_file): return public.return_message(0, 0, load_info) data = public.readFile(pid_file) if isinstance(data, str) and data: pid = int(data) pids = self.get_project_pids(pid=pid)['message'] else: return public.return_message(0, 0, load_info) if not pids: return public.return_message(0, 0, load_info) for i in pids: process_info = self.get_process_info_by_pid(i) if process_info: load_info[i] = process_info return public.return_message(0, 0, load_info) def object_to_dict(self, obj): ''' @name 将对象转换为字典 @author hwliang<2021-08-09> @param obj @return dict ''' result = {} for name in dir(obj): value = getattr(obj, name) if not name.startswith('__') and not callable(value) and not name.startswith('_'): result[name] = value return result def list_to_dict(self, data): ''' @name 将列表转换为字典 @author hwliang<2021-08-09> @param data @return dict ''' result = [] for s in data: result.append(self.object_to_dict(s)) return result def get_connects(self, pid): ''' @name 获取进程连接信息 @author hwliang<2021-08-09> @param pid @return dict ''' connects = 0 try: if pid == 1: return connects tp = '/proc/' + str(pid) + '/fd/' if not os.path.exists(tp): return connects for d in os.listdir(tp): fname = tp + d if os.path.islink(fname): l = os.readlink(fname) if l.find('socket:') != -1: connects += 1 except: pass return connects def format_connections(self, connects): ''' @name 获取进程网络连接信息 @author hwliang<2021-08-09> @param connects @return list ''' result = [] for i in connects: raddr = i.raddr if not i.raddr: raddr = ('', 0) laddr = i.laddr if not i.laddr: laddr = ('', 0) result.append({ "fd": i.fd, "family": i.family, "local_addr": laddr[0], "local_port": laddr[1], "client_addr": raddr[0], "client_rport": raddr[1], "status": i.status }) return result def get_process_info_by_pid(self, pid): ''' @name 获取进程信息 @author hwliang<2021-08-12> @param pid: int<进程id> @return dict ''' process_info = {} try: if not os.path.exists('/proc/{}'.format(pid)): return process_info p = psutil.Process(pid) status_ps = {'sleeping': 'Sleeping', 'running': 'Running'} with p.oneshot(): p_mem = p.memory_full_info() if p_mem.uss + p_mem.rss + p_mem.pss + p_mem.data == 0: return process_info p_state = p.status() if p_state in status_ps: p_state = status_ps[p_state] # process_info['exe'] = p.exe() process_info['name'] = p.name() process_info['pid'] = pid process_info['ppid'] = p.ppid() process_info['create_time'] = int(p.create_time()) process_info['status'] = p_state process_info['user'] = p.username() process_info['memory_used'] = p_mem.uss process_info['cpu_percent'] = self.get_cpu_precent(p) process_info['io_write_bytes'], process_info['io_read_bytes'] = self.get_io_speed(p) process_info['connections'] = self.format_connections(p.connections()) process_info['connects'] = self.get_connects(pid) process_info['open_files'] = self.list_to_dict(p.open_files()) process_info['threads'] = p.num_threads() process_info['exe'] = ' '.join(p.cmdline()) return process_info except: return process_info def get_io_speed(self, p): ''' @name 获取磁盘IO速度 @author hwliang<2021-08-12> @param p: Process<进程对像> @return list ''' skey = "io_speed_{}".format(p.pid) old_pio = cache.get(skey) if not hasattr(p, 'io_counters'): return 0, 0 pio = p.io_counters() if not old_pio: cache.set(skey, [pio, time.time()], 3600) # time.sleep(0.1) old_pio = cache.get(skey) pio = p.io_counters() old_write_bytes = old_pio[0].write_bytes old_read_bytes = old_pio[0].read_bytes old_time = old_pio[1] new_time = time.time() write_bytes = pio.write_bytes read_bytes = pio.read_bytes cache.set(skey, [pio, new_time], 3600) write_speed = int((write_bytes - old_write_bytes) / (new_time - old_time)) read_speed = int((read_bytes - old_read_bytes) / (new_time - old_time)) return write_speed, read_speed def get_cpu_precent(self, p): ''' @name 获取进程cpu使用率 @author hwliang<2021-08-09> @param p: Process<进程对像> @return dict ''' skey = "cpu_pre_{}".format(p.pid) old_cpu_times = cache.get(skey) process_cpu_time = self.get_process_cpu_time(p.cpu_times()) if not old_cpu_times: cache.set(skey, [process_cpu_time, time.time()], 3600) # time.sleep(0.1) old_cpu_times = cache.get(skey) process_cpu_time = self.get_process_cpu_time(p.cpu_times()) old_process_cpu_time = old_cpu_times[0] old_time = old_cpu_times[1] new_time = time.time() cache.set(skey, [process_cpu_time, new_time], 3600) percent = round(100.00 * (process_cpu_time - old_process_cpu_time) / (new_time - old_time) / psutil.cpu_count(), 2) return percent def get_process_cpu_time(self, cpu_times): cpu_time = 0.00 for s in cpu_times: cpu_time += s return cpu_time def get_project_run_state(self, get=None, project_name=None): ''' @name 获取项目运行状态 @author hwliang<2021-08-12> @param get{ project_name: string<项目名称> } @param project_name 项目名称 @return bool ''' if get: project_name = get.project_name.strip() pid_file = "{}/{}.pid".format(self._node_pid_path, project_name) if not os.path.exists(pid_file): return public.return_message(0, 0, False) data = public.readFile(pid_file) if isinstance(data, str) and data: if not data.isdigit(): return public.return_message(0, 0, False) pid = int(data) pids = self.get_project_pids(pid=pid, without_request=True) else: return public.return_message(0, 0, self.get_project_state_by_cwd(project_name)) if not pids: return self.get_project_state_by_cwd(project_name) return public.return_message(0, 0, True) def _get_project_run_state(self, get=None, project_name=None): ''' @name 获取项目运行状态 @author hwliang<2021-08-12> @param get{ project_name: string<项目名称> } @param project_name 项目名称 @return bool ''' if get: project_name = get.project_name.strip() pid_file = "{}/{}.pid".format(self._node_pid_path, project_name) if not os.path.exists(pid_file): return False data = public.readFile(pid_file) if isinstance(data, str) and data: pid = int(data) pids = self.get_project_pids(pid=pid)['message'] else: return self.get_project_state_by_cwd(project_name) if not pids: return self.get_project_state_by_cwd(project_name) return True def get_project_find(self, project_name): ''' @name 获取指定项目配置 @author hwliang<2021-08-09> @param project_name 项目名称 @return dict ''' project_info = public.M('sites').where('project_type=? AND name=?', ('Node', project_name)).find() if not project_info: return False project_info['project_config'] = json.loads(project_info['project_config']) return project_info def get_project_info(self, get): ''' @name 获取指定项目信息 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> } @return dict ''' # 校验参数 try: get.validate([ Param('project_name').String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_info = public.M('sites').where('project_type=? AND name=?', ('Node', get.project_name)).find() if not project_info: return_message = public.return_error(public.lang('The specified item does not exist!')) del return_message['status'] return public.return_message(-1, 0, return_message) project_info = self.get_project_stat(project_info) return public.return_message(0, 0, project_info) def get_project_stat(self, project_info): ''' @name 获取项目状态信息 @author hwliang<2021-08-09> @param project_info 项目信息 @return list ''' project_info['project_config'] = json.loads(project_info['project_config']) project_info['load_info'] = {} if ("pm2_name" in project_info['project_config'] and "project_type" in project_info['project_config'] and project_info['project_config']["project_type"] == "pm2") or "pm2" in project_info['project_config'][ 'project_script']: from mod.project.nodejs import pm2Mod pm2_list = pm2Mod.main().get_jlist() project_info['run'] = False pm2_name = project_info['project_config'].get("pm2_name", project_info['name']) pm2_pids = [] project_info['project_config']['watch'] = True if project_info['project_config'].get('watch', False) in [ 'True', 'true', True] else False for pm2_info in pm2_list: if pm2_info.get("name") in ("pm2-sysmonit", "pm2-logrotate"): continue env_projectname = "" if "pm2_env" in pm2_info and "NODE_PROJECT_NAME" in pm2_info["pm2_env"]: env_projectname = pm2_info["pm2_env"]["NODE_PROJECT_NAME"] if pm2_name != pm2_info.get("name") and pm2_name != env_projectname: continue project_info['run'] = pm2_info.get("pm2_env").get("status") == "online" if project_info['run']: pm2_pids.append(pm2_info.get("pid")) break project_info['load_info'] = self.get_pm2_load_info_new(pm2_pids) else: project_info['run'] = self._get_project_run_state(project_name=project_info['name']) if project_info['run']: project_info['load_info'] = self.get_project_load_info(project_name=project_info['name'])['message'] project_info['ssl'] = self.get_ssl_end_date(project_name=project_info['name']) project_info['listen'] = [] project_info['listen_ok'] = True if project_info['load_info']: for pid in project_info['load_info'].keys(): if not pid: continue if not 'connections' in project_info['load_info'][pid]: project_info['load_info'][pid]['connections'] = [] for conn in project_info['load_info'][pid]['connections']: if not conn['status'] == 'LISTEN': continue if not conn['local_port'] in project_info['listen']: project_info['listen'].append(conn['local_port']) if project_info['listen']: if project_info['project_config']['port'] is None: # 端口回填 project_info['project_config']['port'] = project_info['listen'][0] try: update_data = { 'project_config': json.dumps(project_info['project_config']) } public.M('sites').where('name=?', (project_info['name'],)).update(update_data) except: pass project_info['listen_ok'] = project_info['project_config']['port'] in project_info['listen'] return project_info def get_pm2_load_info_new(self, pids: list): ''' @name 获取项目负载信息 @author hwliang<2021-08-12> @param get{ project_name: string<项目名称> } @return dict ''' load_info = {} for i in pids: process_info = self.get_process_info_by_pid(i) if process_info: load_info[i] = process_info return load_info # 获取项目状态,支持PM2/node def get_project_run(self, project_name, project_type): try: if project_type == 'node': return self._get_project_run_state(project_name=project_name) else: from mod.project.nodejs import pm2Mod pm2_list = pm2Mod.main().get_jlist() pm2_name = project_name for pm2_info in pm2_list: if pm2_info.get("name") in ("pm2-sysmonit", "pm2-logrotate"): continue env_projectname = "" if "pm2_env" in pm2_info and "NODE_PROJECT_NAME" in pm2_info["pm2_env"]: env_projectname = pm2_info["pm2_env"]["NODE_PROJECT_NAME"] if pm2_name != pm2_info.get("name") and pm2_name != env_projectname: continue return pm2_info.get("pm2_env").get("status") == "online" return False except Exception as e: return False def get_project_state(self, project_name): ''' @name 获取项目状态 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return dict ''' project_info = public.M('sites').where('project_type=? AND name=?', ('Node', project_name)).find() if not project_info: return False return project_info['status'] def get_project_listen(self, project_name): ''' @name 获取项目监听端口 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return dict ''' project_config = json.loads(public.M('sites').where('name=?', project_name).getField('project_config')) if 'listen_port' in project_config: return project_config['listen_port'] return False def set_project_listen(self, get): ''' @name 设置项目监听端口(请设置与实际端口相符的,仅在自动获取不正确时使用) @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> port: int<端口> } @return dict ''' project_config = json.loads(public.M('sites').where('name=?', get.project_name).getField('project_config')) project_config['listen_port'] = get.port public.M('sites').where('name=?', get.project_name).save('project_config', json.dumps(project_config)) public.WriteLog(self._log_name, 'Modify the port of the project [' + get.project_name + '] to [' + get.port + ']') return_message = public.return_data(True, 'Set successfully') del return_message['status'] return public.return_message(0, 0, return_message) def set_project_nodejs_version(self, get): ''' @name 设置nodejs版本 @author hwliang<2021-08-09> @param get{ project_name: string<项目名称> nodejs_version: string } @return dict ''' project_config = json.loads(public.M('sites').where('name=?', get.project_name).getField('project_config')) project_config['nodejs_version'] = get.nodejs_version public.M('sites').where('name=?', get.project_name).save('project_config', json.dumps(project_config)) public.WriteLog(self._log_name, 'Modify the nodejs version of the project [' + get.project_name + '] to [' + get.nodejs_version + ']') return_message = public.return_data(True, 'Set successfully') del return_message['status'] return public.return_message(0, 0, return_message) def get_project_nodejs_version(self, project_name): ''' @name 获取nodejs版本 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return string ''' project_config = json.loads(public.M('sites').where('name=?', project_name).getField('project_config')) if 'nodejs_version' in project_config: return project_config['nodejs_version'] return False def check_port_is_used(self, port, sock=False): ''' @name 检查端口是否被占用 @author hwliang<2021-08-09> @param port: int<端口> @return bool ''' if not isinstance(port, int): port = int(port) if port == 0: return False project_list = public.M('sites').where('status=? AND project_type=?', (1, 'Node')).field( 'name,path,project_config').select() for project_find in project_list: project_config = json.loads(project_find['project_config']) if not 'port' in project_config: continue try: if int(project_config['port']) == port: return True except: pass if sock: return False return public.check_tcp('127.0.0.1', port) def get_project_run_state_byaotu(self, project_name): ''' @name 获取项目运行状态 @author hwliang<2021-08-09> @param project_name: string<项目名称> @return dict ''' pid_file = "{}/{}.pid".format(self._node_pid_path, project_name) if not os.path.exists(pid_file): return False pid = public.readFile(pid_file) pids = self.get_project_pids(pid=pid, without_request=True) if not pids: return False return True # 废弃,重定向于script/project_daemon.py def auto_run(self): ''' @name 自动启动所有项目 @author hwliang<2021-08-09> @return bool ''' project_list = public.M('sites').where('project_type=?', ('Node',)).field('name,path,project_config').select() get = public.dict_obj() success_count = 0 error_count = 0 for project_find in project_list: try: project_config = json.loads(project_find['project_config']) if project_config['is_power_on'] in [0, False, '0', None]: continue project_name = project_find['name'] project_state = self._get_project_run_state(project_name=project_name) if not project_state: get.project_name = project_name result = self.start_project(get)['message'] if result['status'] == -1: error_count += 1 error_msg = 'Automatically start Nodej project [' + project_name + '] failed!' public.WriteLog(self._log_name, error_msg) public.print_log(error_msg + ", " + result['error_msg'], 'ERROR') else: success_count += 1 success_msg = 'Automatically start the Nodej project [' + project_name + '] successfully!' public.WriteLog(self._log_name, success_msg) public.print_log(success_msg, 'INFO') except: error_count += 1 public.print_log(public.get_error_info(), 'ERROR') if (success_count + error_count) < 1: return False dene_msg = 'A total of {} Nodejs projects need to be started, {} successfully and {} failed'.format( success_count + error_count, success_count, error_count) public.WriteLog(self._log_name, dene_msg) public.print_log(dene_msg, 'INFO') return True