Files
SMANAGE/backup/_flask_backup/TempLateModule/Tasks/Pool.py
T

421 lines
16 KiB
Python

import sys
import time
import subprocess
import uuid
import select
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
import threading
from dataclasses import dataclass, field
from typing import Dict, Optional, Callable, Any
# ========== 1. 定义常量和类型别名 ==========
DEFAULT_MAX_WORKERS = 5
TaskCallback = Optional[Callable[[str, Dict[str, Any]], None]] # (TaskID, 结果字典) -> None
# 适配不同系统的select支持
if sys.platform == 'win32':
import msvcrt
def _win32_readline(process):
"""Windows下非阻塞读取子进程输出"""
lines = []
while True:
if msvcrt.kbhit():
break
try:
line = process.stdout.readline()
if not line:
break
lines.append(line)
except:
break
return lines
else:
def _unix_readline(process):
"""Unix/Linux下非阻塞读取子进程输出"""
lines = []
ready, _, _ = select.select([process.stdout], [], [], 0.1)
if ready:
for line in iter(process.stdout.readline, ''):
if line:
lines.append(line)
else:
break
return lines
# ========== 2. 任务类(记录任务状态) ==========
@dataclass
class Task:
TaskID: str # 任务唯一ID(帕斯卡命名)
Command: str # 要执行的命令
Process: Optional[subprocess.Popen] = None # 命令进程
IsRunning: bool = False # 任务运行状态
IsStarted: bool = False # 任务是否已启动过(用于重复启动)
StartTime: Optional[float] = None # 启动时间
Callback: TaskCallback = None # 任务结束回调函数
ExitCode: Optional[int] = None # 任务退出码
ErrorMsg: Optional[str] = None # 错误信息
Lock: threading.Lock = field(default_factory=Lock) # 单个任务的锁
StopFlag: bool = False # 任务停止标记
# ========== 3. 任务池核心类 ==========
class CommandTaskPool:
def __init__(self, maxWorkers: int = DEFAULT_MAX_WORKERS):
"""
初始化命令任务池
:param maxWorkers: 最大并发工作线程数
"""
# 线程池(maxWorkers限制并发任务数)
self.Executor = ThreadPoolExecutor(max_workers=maxWorkers)
# 任务字典:key=TaskID, value=Task对象
self.Tasks: Dict[str, Task] = {}
# 全局锁(防止多线程修改任务字典冲突)
self.GlobalLock = Lock()
# 日志前缀(区分不同任务的输出)
self.LogPrefix = "[TaskPool] "
def _Log(self, msg: str, taskID: str = "") -> None:
"""
日志输出(带时间戳和任务ID)
:param msg: 日志信息
:param taskID: 任务ID
"""
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
taskTag = f"[{taskID}] " if taskID else ""
logMsg = f"{self.LogPrefix}{timestamp} {taskTag}{msg}\n"
sys.stdout.write(logMsg)
sys.stdout.flush() # 强制刷新,避免日志缓存
def _ExecuteCallback(self, task: Task) -> None:
"""
执行任务回调函数
:param task: 任务对象
"""
if task.Callback:
try:
result = {
"Command": task.Command,
"IsRunning": task.IsRunning,
"ExitCode": task.ExitCode,
"ErrorMsg": task.ErrorMsg,
"RuntimeSeconds": round(time.time() - task.StartTime, 2) if task.StartTime else 0
}
task.Callback(task.TaskID, result)
except Exception as e:
self._Log(f"回调函数执行失败: {str(e)}", task.TaskID)
def _RunCommand(self, task: Task) -> None:
"""
执行命令核心逻辑(实时输出日志,修复卡死问题)
:param task: 任务对象
"""
with task.Lock:
if task.IsRunning:
self._Log("任务已在运行中,跳过重复执行", task.TaskID)
return
task.IsRunning = True
task.IsStarted = True
task.StartTime = time.time()
task.ExitCode = None
task.ErrorMsg = None
task.StopFlag = False
try:
self._Log(f"启动命令: {task.Command}", task.TaskID)
# 启动子进程执行命令(修改为非阻塞模式)
task.Process = subprocess.Popen(
task.Command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # 将stderr合并到stdout
text=True, # 文本模式读取
bufsize=0, # 无缓冲,避免数据堆积
universal_newlines=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == 'win32' else 0
)
# 非阻塞读取输出(核心修复:避免死锁)
while True:
# 检查是否需要停止
with task.Lock:
if task.StopFlag:
break
# 检查进程是否已退出
if task.Process.poll() is not None:
break
# 根据系统选择读取方式
try:
if sys.platform == 'win32':
lines = _win32_readline(task.Process)
else:
lines = _unix_readline(task.Process)
for line in lines:
if line:
sys.stdout.write(f"[{task.TaskID}] {line}")
sys.stdout.flush()
except Exception as e:
self._Log(f"读取输出异常: {str(e)}", task.TaskID)
break
time.sleep(0.01) # 降低CPU占用
# 等待命令结束并获取退出码
task.ExitCode = task.Process.wait(timeout=2) if task.Process.poll() is None else task.Process.poll()
with task.Lock:
task.IsRunning = False
if task.ExitCode == 0:
self._Log(f"命令正常结束,退出码: {task.ExitCode}", task.TaskID)
else:
self._Log(f"命令异常结束,退出码: {task.ExitCode}", task.TaskID)
except subprocess.TimeoutExpired:
with task.Lock:
task.IsRunning = False
task.ExitCode = -2
self._Log(f"任务超时终止", task.TaskID)
except Exception as e:
errorMsg = str(e)
with task.Lock:
task.IsRunning = False
task.ErrorMsg = errorMsg
task.ExitCode = -1
self._Log(f"命令执行失败: {errorMsg}", task.TaskID)
finally:
# 确保进程被终止
if task.Process and task.Process.poll() is None:
try:
task.Process.kill()
except:
pass
# 执行回调函数
self._ExecuteCallback(task)
def GenerateTaskID(self, prefix: str = "TASK") -> str:
"""
生成唯一任务ID(前缀+UUID)
:param prefix: 任务ID前缀
:return: 唯一任务ID
"""
return f"{prefix}_{uuid.uuid4().hex[:8].upper()}"
def AddTask(self, command: str, taskID: Optional[str] = None,
callback: TaskCallback = None, autoStart: bool = True) -> str:
"""
添加任务到任务池
:param command: 要执行的命令
:param taskID: 自定义任务ID(为空则自动生成)
:param callback: 任务结束回调函数
:param autoStart: 是否添加后自动启动
:return: 任务ID(添加失败返回空字符串)
"""
with self.GlobalLock:
# 自动生成任务ID
taskID = taskID or self.GenerateTaskID()
# 检查任务ID是否已存在
if taskID in self.Tasks:
self._Log(f"任务ID已存在: {taskID}")
return ""
# 创建任务对象
task = Task(
TaskID=taskID,
Command=command,
Callback=callback
)
self.Tasks[taskID] = task
# 自动启动任务
if autoStart:
self.StartTask(taskID)
self._Log(f"任务已添加 {'(自动启动)' if autoStart else '(未启动)'}", taskID)
return taskID
def StartTask(self, taskID: str) -> bool:
"""
手动启动任务(支持重复启动已停止的任务)
:param taskID: 任务ID
:return: 启动成功/失败
"""
with self.GlobalLock:
if taskID not in self.Tasks:
self._Log(f"任务不存在: {taskID}")
return False
task = self.Tasks[taskID]
with task.Lock:
if task.IsRunning:
self._Log(f"任务已在运行: {taskID}")
return False
task.StopFlag = False # 重置停止标记
# 提交任务到线程池
self.Executor.submit(self._RunCommand, task)
self._Log(f"任务启动成功", taskID)
return True
def StopTask(self, taskID: str) -> bool:
"""
停止指定任务(修复卡死问题)
:param taskID: 任务ID
:return: 停止成功/失败
"""
with self.GlobalLock:
if taskID not in self.Tasks:
self._Log(f"任务不存在: {taskID}")
return False
task = self.Tasks[taskID]
with task.Lock:
if not task.IsRunning or task.Process is None:
self._Log(f"任务未运行: {taskID}")
return False
# 设置停止标记
task.StopFlag = True
# 终止子进程
try:
if sys.platform == 'win32':
# Windows下终止进程组
subprocess.call(['taskkill', '/F', '/T', '/PID', str(task.Process.pid)])
else:
task.Process.terminate()
task.IsRunning = False
task.ExitCode = -1 # 标记为手动终止
self._Log(f"任务已终止", taskID)
return True
except Exception as e:
self._Log(f"终止任务失败: {str(e)}", taskID)
return False
def ListTasks(self) -> Dict[str, dict]:
"""
查看所有任务状态
:return: 任务状态字典
"""
with self.GlobalLock:
taskStatus = {}
currentTime = time.time()
for taskID, task in self.Tasks.items():
with task.Lock:
runtime = round(currentTime - task.StartTime, 2) if task.StartTime else 0
taskStatus[taskID] = {
"Command": task.Command,
"IsRunning": task.IsRunning,
"IsStarted": task.IsStarted,
"RuntimeSeconds": runtime,
"ExitCode": task.ExitCode,
"ErrorMsg": task.ErrorMsg
}
return taskStatus
def RemoveTask(self, taskID: str) -> bool:
"""
移除任务(需先停止)
:param taskID: 任务ID
:return: 移除成功/失败
"""
with self.GlobalLock:
if taskID not in self.Tasks:
self._Log(f"任务不存在: {taskID}")
return False
task = self.Tasks[taskID]
with task.Lock:
if task.IsRunning:
self._Log(f"任务正在运行,无法移除: {taskID}")
return False
del self.Tasks[taskID]
self._Log(f"任务已移除", taskID)
return True
def Shutdown(self, wait: bool = False) -> None:
"""
关闭任务池,停止所有任务(修改wait默认值为False避免卡死)
:param wait: 是否等待所有任务结束
"""
self._Log("开始关闭任务池...")
# 停止所有运行中的任务
with self.GlobalLock:
for taskID in list(self.Tasks.keys()):
self.StopTask(taskID)
# 关闭线程池(非阻塞模式)
self.Executor.shutdown(wait=wait, cancel_futures=True)
self._Log("任务池已关闭")
# ========== 4. 测试示例 ==========
def TaskCallbackExample(taskID: str, result: dict) -> None:
"""示例回调函数"""
print(f"\n=== 任务 {taskID} 回调结果 ===")
print(f"命令: {result['Command']}")
print(f"运行状态: {result['IsRunning']}")
print(f"退出码: {result['ExitCode']}")
print(f"运行时长: {result['RuntimeSeconds']}秒")
if result['ErrorMsg']:
print(f"错误信息: {result['ErrorMsg']}")
if __name__ == "__main__":
# 创建任务池(最多并发5个任务)
taskPool = CommandTaskPool(maxWorkers=5)
# 示例1:添加任务(自动启动,带回调)
pingTaskID = taskPool.AddTask(
command="ping -t 127.0.0.1", # Windows
# command="ping 127.0.0.1 -c 100", # Linux/Mac
callback=TaskCallbackExample
)
print(f"添加Ping任务,ID: {pingTaskID}")
# 示例2:添加任务(不自动启动)
frpTaskID = taskPool.AddTask(
command="frps -c /etc/frp/frps.ini",
taskID="FRPS_SERVICE", # 自定义任务ID
autoStart=False # 不自动启动
)
print(f"添加FRP任务,ID: {frpTaskID}")
# 查看初始任务状态
time.sleep(1)
print("\n=== 初始任务状态 ===")
for taskID, status in taskPool.ListTasks().items():
print(f"任务ID: {taskID}")
print(f" 命令: {status['Command']}")
print(f" 运行中: {status['IsRunning']}")
print(f" 已启动: {status['IsStarted']}")
# 手动启动FRP任务
print("\n=== 手动启动FRP任务 ===")
taskPool.StartTask(frpTaskID)
time.sleep(1)
frpStatus = taskPool.ListTasks().get(frpTaskID, {})
print(f"FRP任务状态: {frpStatus.get('IsRunning', False)}")
# 模拟运行3秒后停止Ping任务
time.sleep(3)
print("\n=== 停止Ping任务 ===")
taskPool.StopTask(pingTaskID)
# 重新启动Ping任务(支持重复启动)
print("\n=== 重新启动Ping任务 ===")
taskPool.StartTask(pingTaskID)
time.sleep(2)
# 查看最终任务状态
print("\n=== 最终任务状态 ===")
for taskID, status in taskPool.ListTasks().items():
print(f"任务ID: {taskID}")
print(f" 运行中: {status['IsRunning']}")
print(f" 运行时长: {status['RuntimeSeconds']}秒")
print(f" 退出码: {status['ExitCode']}")
# 关闭任务池(非阻塞模式)
taskPool.Shutdown(wait=False)
print("\n任务池已关闭,程序正常退出")