#!/usr/bin/env python3 """ 工具函数模块 """ import os import json import subprocess import re import signal import shutil from datetime import datetime from config import LOG_FILE, PROJECTS_FILE, CRON_BACKUP_DIR, ALERT_CONFIG_FILE # 进程状态缓存 process_cache = {} def log_message(msg): """写入日志""" timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') log_line = f"[{timestamp}] {msg}\n" print(log_line.strip()) try: os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) with open(LOG_FILE, 'a') as f: f.write(log_line) except: pass def load_projects(): """加载项目配置""" try: with open(PROJECTS_FILE, 'r', encoding='utf-8') as f: data = json.load(f) return data.get('projects', []) except: return [] def load_projects_full(): """加载完整项目配置(包含server和external_services)""" try: with open(PROJECTS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except: return {'server': {}, 'projects': [], 'external_services': []} def save_projects(projects): """保存项目配置(保留 server 和 external_services)""" full_data = load_projects_full() full_data['projects'] = projects with open(PROJECTS_FILE, 'w', encoding='utf-8') as f: json.dump(full_data, f, ensure_ascii=False, indent=2) def check_port(port): """检查端口是否有进程监听""" try: import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) result = sock.connect_ex(('localhost', port)) sock.close() return result == 0 except: return False def stop_process(port): """停止占用指定端口的进程""" try: # 使用 ss 找到占用端口的进程 result = subprocess.run(f"ss -tlnp | grep :{port}", shell=True, capture_output=True, text=True) if result.stdout: # 解析 pid import re match = re.search(r'pid=(\d+)', result.stdout) if match: pid = int(match.group(1)) os.kill(pid, signal.SIGTERM) log_message(f"已停止进程 {pid} (端口 {port})") return 1 except Exception as e: log_message(f"停止进程失败: {e}") return 0 def run_command(cmd, cwd, project_id, action): """运行命令""" log_message(f"[{project_id}] 执行 {action}: {cmd}") try: subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return True except Exception as e: log_message(f"命令执行失败: {e}") return False def backup_crontab(): """备份当前 crontab""" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_file = os.path.join(CRON_BACKUP_DIR, f'crontab_{timestamp}.txt') try: os.makedirs(CRON_BACKUP_DIR, exist_ok=True) result = subprocess.run("crontab -l 2>/dev/null", shell=True, capture_output=True, text=True) with open(backup_file, 'w') as f: f.write(result.stdout) log_message(f"crontab 已备份到 {backup_file}") except Exception as e: log_message(f"备份 crontab 失败: {e}") def get_system_crons(): """获取系统所有 cron 任务""" crons = [] try: result = subprocess.run("crontab -l 2>/dev/null", shell=True, capture_output=True, text=True, timeout=5) for line in result.stdout.strip().split('\n'): line = line.strip() if line and not line.startswith('#'): parts = line.split() if len(parts) >= 6: crons.append({'source': 'user', 'expression': ' '.join(parts[:5]), 'command': ' '.join(parts[5:]), 'line': line}) except: pass try: if os.path.exists('/etc/crontab'): with open('/etc/crontab', 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): parts = line.split() if len(parts) >= 7: crons.append({'source': 'system', 'expression': ' '.join(parts[:5]), 'user': parts[5], 'command': ' '.join(parts[6:]), 'line': line}) except: pass try: if os.path.exists('/etc/cron.d'): for filename in os.listdir('/etc/cron.d'): filepath = os.path.join('/etc/cron.d', filename) if os.path.isfile(filepath): with open(filepath, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): parts = line.split() if len(parts) >= 7: crons.append({'source': 'cron.d', 'file': filename, 'expression': ' '.join(parts[:5]), 'user': parts[5], 'command': ' '.join(parts[6:]), 'line': line}) except: pass return crons def parse_cron_expression(expr): """解析 cron 表达式为人类可读格式""" parts = expr.split() if len(parts) != 5: return expr minute, hour, day, month, weekday = parts desc = [] if minute == '*': desc.append('每分钟') elif minute.startswith('*/'): desc.append(f'每{minute[2:]}分钟') else: desc.append(f'{minute}分') if hour != '*': if hour.startswith('*/'): desc.append(f'每{hour[2:]}小时') else: desc.append(f'{hour}时') if day != '*' and not day.startswith('*'): desc.append(f'{day}日') if month != '*' and not month.startswith('*'): desc.append(f'{month}月') if weekday != '*' and not weekday.startswith('*'): weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] try: desc.append(weekdays[int(weekday)]) except: pass return ' '.join(desc) if desc else expr def cron_expression_to_human(expr): """更友好的人类可读格式""" parts = expr.split() if len(parts) != 5: return expr minute, hour, day, month, weekday = parts # 特殊模式 patterns = [ ('0 4 * * *', '每天凌晨4点'), ('0 0 * * *', '每天午夜0点'), ('*/5 * * * *', '每5分钟'), ('*/10 * * * *', '每10分钟'), ('*/20 * * * *', '每20分钟'), ('*/30 * * * *', '每30分钟'), ('0 */1 * * *', '每小时'), ('0 */2 * * *', '每2小时'), ('0 9 * * 1', '每周一9点'), ('0 9 * * 1-5', '每周一到五9点'), ] for pattern, desc in patterns: if expr == pattern: return desc # 通用解析 return parse_cron_expression(expr) def guess_cron_name(command): """从命令推断任务名称""" if 'openclaw' in command or 'agent' in command: return 'OpenClaw Agent' elif 'backup' in command: return '备份任务' elif 'sync' in command: return '同步任务' elif 'clean' in command or 'cleanup' in command: return '清理任务' elif 'report' in command: return '报告任务' elif 'monitor' in command or 'check' in command: return '监控任务' elif 'git' in command: return 'Git 任务' return '自定义任务' def guess_cron_category(command): """从命令推断分类""" if 'openclaw' in command: return 'agent' elif 'backup' in command: return 'backup' elif 'sync' in command: return 'sync' elif 'clean' in command or 'cleanup' in command: return 'cleanup' elif 'report' in command: return 'report' elif 'git' in command: return 'git' return 'other' def sync_crontab_from_db(db): """从数据库同步到系统 crontab""" c = db.cursor() c.execute("SELECT expression, command, enabled FROM cron_tasks WHERE enabled = 1") tasks = c.fetchall() lines = [] for task in tasks: lines.append(f"{task['expression']} {task['command']}") # 写入临时文件 tmp_file = '/tmp/crontab_sync.tmp' with open(tmp_file, 'w') as f: f.write('\n'.join(lines) + '\n') # 安装 crontab subprocess.run(f"crontab {tmp_file}", shell=True) os.remove(tmp_file) # 备份 backup_crontab() def get_next_run_time(expression): """获取下次执行时间""" try: from croniter import croniter base = datetime.now() cron = croniter(expression, base) next_time = cron.get_next(datetime) return next_time.strftime('%Y-%m-%d %H:%M:%S') except: return '无法计算' def get_cpu_temperatures(): """获取 CPU Package 温度""" temps = {'package_0': None, 'package_1': None} try: result = subprocess.run(['sensors'], capture_output=True, text=True, timeout=5) output = result.stdout # 解析 Package id 0 和 Package id 1 for line in output.split('\n'): if 'Package id 0:' in line: match = re.search(r'Package id 0:\s*([+\d.]+)°C', line) if match: temps['package_0'] = float(match.group(1).replace('+', '')) elif 'Package id 1:' in line: match = re.search(r'Package id 1:\s*([+\d.]+)°C', line) if match: temps['package_1'] = float(match.group(1).replace('+', '')) except Exception as e: log_message(f"获取CPU温度失败: {e}") return temps def load_alert_config(): """加载预警配置""" try: with open(ALERT_CONFIG_FILE, 'r', encoding='utf-8') as f: return json.load(f) except: return {} def save_alert_config(config): """保存预警配置""" with open(ALERT_CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2)