Files
project-panel/utils.py

334 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)