├── .env.example ├── .github └── workflows │ └── workflow.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo.py ├── examples ├── example.py ├── scripts │ ├── archive_data.sh │ ├── data_preprocess.py │ ├── evaluate_model.py │ └── train_model.py └── tasks.yaml ├── images └── demo_screenshot.png ├── multitaskflow ├── __init__.py ├── process_monitor.py └── task_flow.py ├── pyproject.toml ├── requirements.txt └── taskflowPro.sh /.env.example: -------------------------------------------------------------------------------- 1 | # PushPlus Token - 用于微信消息推送 2 | # 请访问 https://www.pushplus.plus/ 获取您的token 3 | MSG_PUSH_TOKEN=your_pushplus_token_here 4 | 5 | # 其他配置项可以根据需要添加 -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-F/MultiTaskFlow/84e933154fafe32b6f3d38ebdfba1771196c607c/.github/workflows/workflow.yaml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ## self 3 | demo/ 4 | local_test.py 5 | .env 6 | 7 | # Python 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | *.so 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # Virtual Environment 31 | venv/ 32 | env/ 33 | ENV/ 34 | .env 35 | 36 | # IDE 37 | .idea/ 38 | .vscode/ 39 | *.swp 40 | *.swo 41 | 42 | # Logs 43 | *.log 44 | logs 45 | logs/ 46 | 47 | # Local development settings 48 | .env.local 49 | .env.development.local 50 | .env.test.local 51 | .env.production.local 52 | 53 | # OS generated files 54 | .DS_Store 55 | .DS_Store? 56 | ._* 57 | .Spotlight-V100 58 | .Trashes 59 | ehthumbs.db 60 | Thumbs.db 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | 4 | 5 | ## [0.1.3] - 2025年5月13日 6 | ### 新功能 7 | - 增加环境变量静默模式,通过设置MTF_SILENT_MODE环境变量控制是否发送消息通知,适用于无网络或者不想要接收消息场景 8 | ### 优化 9 | - 优化日志输出,提供更清晰的任务状态信息 10 | 11 | ## [0.1.1] - 2025年4月30日 12 | 13 | 14 | 15 | ### 修复 16 | - MSG_PUSH_TOKEN环境变量读取问题 17 | 18 | ## 2025年5月12日 TODO 19 | 20 | 1. CLI 命令增加一个 监听单个任务的功能: 21 | 输入 taskflow monitor 'XXX', 其中'XXX'表示需要监听的任务的进程名称,针对已经有一个且只有一个任务已经在跑的场景,通过 monitor 在任务结束发送消息。 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Polaris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiTaskFlow 多任务流管理工具 2 | 3 | MultiTaskFlow 是一个轻量级的多任务流管理工具,用于按顺序执行和监控一系列任务。它可以帮助您管理数据处理、模型训练、评估等一系列需要顺序执行的任务,并提供实时状态更新和执行结果跟踪。 4 | 5 | ## 功能特点 6 | 7 | - 基于YAML配置文件定义任务流 8 | - 支持Python脚本和Shell命令的执行 9 | - 提供任务状态实时监控 10 | - 自动执行失败任务的重试逻辑 11 | - 支持任务之间的依赖关系 12 | - 完整的日志记录和任务执行历史 13 | - 进程PID跟踪与管理 14 | - 优雅的信号处理和任务终止 15 | - 支持静默模式,可跳过消息通知 16 | 17 | ## 安装方法 18 | 19 | ### 要求 20 | 21 | - Python 3.7+ 22 | - PyYAML 23 | - 其他依赖库(如有) 24 | 25 | ### 配置消息推送令牌和静默模式 26 | 27 | #### 消息推送令牌 28 | 29 | 在使用消息推送功能前,需要配置 MSG_PUSH_TOKEN 环境变量。以下是配置方法: 30 | 31 | ##### 1. 永久配置(推荐) 32 | 33 | 在 `~/.bashrc` 或 `~/.zshrc` 文件中添加: 34 | 35 | ```bash 36 | # MultiTaskFlow 消息推送配置 37 | export MSG_PUSH_TOKEN="your_pushplus_token_here" 38 | ``` 39 | 40 | 然后重新加载配置: 41 | ```bash 42 | source ~/.bashrc # 或 source ~/.zshrc 43 | ``` 44 | 45 | ##### 2. 临时配置 46 | 47 | 在运行命令前设置: 48 | ```bash 49 | MSG_PUSH_TOKEN=your_token python your_script.py 50 | ``` 51 | 52 | ##### 3. 开发模式配置 53 | 54 | 在项目根目录创建 `.env` 文件: 55 | ```bash 56 | echo "MSG_PUSH_TOKEN=your_token" > .env 57 | ``` 58 | 59 | #### 静默模式配置 60 | 61 | 如果您不希望收到消息通知,可以启用静默模式: 62 | 63 | ##### 1. 永久配置静默模式 64 | 65 | 在 `~/.bashrc` 或 `~/.zshrc` 文件中添加: 66 | 67 | ```bash 68 | # MultiTaskFlow 静默模式配置 69 | export MTF_SILENT_MODE=true 70 | ``` 71 | 72 | 然后重新加载配置: 73 | ```bash 74 | source ~/.bashrc # 或 source ~/.zshrc 75 | ``` 76 | 77 | ##### 2. 临时配置静默模式 78 | 79 | 在运行命令前设置: 80 | ```bash 81 | MTF_SILENT_MODE=true taskflow your_tasks.yaml 82 | ``` 83 | 84 | ##### 3. 开发模式配置静默模式 85 | 86 | 在项目根目录创建或编辑 `.env` 文件: 87 | ```bash 88 | echo "MTF_SILENT_MODE=true" >> .env 89 | ``` 90 | 91 | #### 获取 Token 92 | 93 | 1. 访问 [PushPlus 官网](https://www.pushplus.plus/) 94 | 2. 注册并登录 95 | 3. 在个人中心获取您的 token 96 | 97 | ### (方法1)从PyPI安装 98 | 99 | ```bash 100 | # 使用pip直接安装 101 | pip install multitaskflow 102 | ``` 103 | 104 | ### (方法2)从源码安装 105 | 106 | ```bash 107 | # 克隆仓库 108 | git clone https://github.com/Polaris-F/MultiTaskFlow.git 109 | cd MultiTaskFlow 110 | 111 | # 方法1: 使用pip直接安装 112 | pip install . 113 | 114 | # 方法2: 开发模式安装 115 | pip install -e . 116 | ``` 117 | 118 | ### (方法3)构建离线包方法 119 | 120 | 如果您想构建wheel包或源码分发包,可以使用以下命令: 121 | 122 | ```bash 123 | # 安装构建工具 124 | pip install build 125 | 126 | # 构建分发包 127 | python -m build 128 | 129 | # 构建的包会在dist/目录下生成 130 | ``` 131 | 132 | ## 使用方法 133 | 134 | **如果需要使用 消息接收功能,请访问 https://www.pushplus.plus/ 获取您的token** 135 | 136 | ### 1. 创建任务配置文件 137 | 138 | 创建一个YAML格式的任务配置文件,定义您要执行的任务序列: 139 | 140 | ```yaml 141 | # tasks.yaml 示例 142 | - name: "任务1-数据准备" 143 | command: "python scripts/prepare_data.py --input data/raw --output data/processed" 144 | status: "pending" 145 | 146 | - name: "任务2-模型训练" 147 | command: "python scripts/train_model.py --data data/processed --epochs 10" 148 | status: "pending" 149 | 150 | - name: "任务3-结果评估" 151 | command: "python scripts/evaluate.py --model-path models/latest.pt" 152 | status: "pending" 153 | ``` 154 | 155 | ### 2. (方法一)使用Python API (推荐使用方法二、三) 156 | 157 | 在您的Python代码中使用MultiTaskFlow: 158 | 159 | ```python 160 | from multitaskflow import TaskFlow 161 | 162 | # 创建任务流管理器 163 | task_manager = TaskFlow("path/to/your/tasks.yaml") 164 | 165 | # 启动任务执行 166 | task_manager.run() 167 | 168 | # 您也可以动态添加任务 169 | task_manager.add_task_by_config( 170 | name="额外任务", 171 | command="echo '这是一个动态添加的任务'" 172 | ) 173 | ``` 174 | 175 | ### 2. (方法二)使用命令行工具【使用场景:不需要后台运行,可实时查看输出】 176 | 177 | 安装后,您可以直接使用`taskflow`命令行工具: 178 | 179 | ```bash 180 | # 使用配置文件运行任务流 181 | taskflow path/to/your/tasks.yaml 182 | 183 | # 使用默认配置 184 | # 如果不提供配置文件路径,将在examples/tasks.yaml创建示例配置 185 | taskflow 186 | 187 | # 查看帮助 188 | taskflow --help 189 | ``` 190 | ### 2. (方法三)使用sh脚本工具【使用场景:需要后台运行,通过log查看输出】 191 | 首先```taskflowPro.sh```修改脚本中 ```TASK_CONFIG```为任务流yaml路径 192 | ```bash 193 | chmod +x taskflowPro.sh 194 | ./taskflowPro.sh start # 开始运行 195 | ./taskflowPro.sh stop # 结束运行 196 | ``` 197 | 198 | ## 效果展示 199 | 200 | 您可以运行我们提供的演示脚本,查看任务管理和消息接收的实际效果。演示脚本模拟了一个完整的深度学习工作流,包括数据预处理、模型训练、模型评估和数据归档等步骤。 201 | 202 | ### 运行演示脚本 203 | 204 | ```bash 205 | # 安装完成后,直接运行示例脚本 206 | python -m multitaskflow.examples.demo 207 | 208 | # 或使用命令行工具 209 | taskflow examples/tasks.yaml 210 | ``` 211 | 212 | ### 演示内容 213 | 214 | 演示脚本将依次执行以下任务: 215 | 216 | 1. **数据预处理** - 模拟数据集加载、清洗和处理过程 217 | 2. **模型训练-阶段1** - 模拟第一阶段模型训练过程 218 | 3. **模型评估-阶段1** - 模拟对第一阶段训练模型的评估 219 | 4. **模型训练-阶段2** - 模拟基于第一阶段模型继续训练 220 | 5. **模型评估-阶段2** - 模拟对第二阶段训练模型的评估 221 | 6. **数据归档** - 模拟模型和结果数据的归档过程 222 | 223 | 每个任务都会显示详细的执行进度和模拟输出,让您直观了解MultiTaskFlow的任务管理能力。所有演示任务都是模拟执行,不会创建实际文件或占用大量资源。 224 | 225 | ### 期望效果 226 | 227 | 运行示例后,您将看到: 228 | 229 | - 任务管理器启动和初始化过程 230 | - 任务状态的实时更新(等待中→执行中→完成/失败) 231 | - 每个任务的详细输出和进度信息 232 | - 任务完成后的状态汇总 233 | 234 | 通过观察演示效果,您可以了解MultiTaskFlow如何帮助管理复杂的多步骤工作流程,以及它如何提供清晰的任务执行状态和结果反馈。 235 | 236 | ### 运行效果截图 237 | 238 | ![任务管理和执行效果](https://raw.githubusercontent.com/Polaris-F/MultiTaskFlow/main/images/demo_screenshot.png) 239 | 240 | *实际运行时在控制台中会看到详细的输出,显示任务状态和进度信息* 241 | 242 | ## 高级功能(TODO) 243 | 244 | ### 任务配置选项 245 | 246 | 任务配置文件支持以下选项: 247 | 248 | ```yaml 249 | - name: "示例任务" 250 | command: "python script.py" 251 | status: "pending" # pending, running, completed, failed 252 | retry: 3 # 失败后重试次数 (TODO) 253 | timeout: 3600 # 任务超时时间(秒)(TODO) 254 | depends_on: ["前置任务名称"] # 依赖的任务 (TODO) 255 | ``` 256 | 257 | ### 静默模式 258 | 259 | MultiTaskFlow 支持静默模式,在此模式下不会发送任何消息通知。这对于以下场景非常有用: 260 | 261 | - **生产环境部署**:在生产环境中运行时,可能不需要消息通知 262 | - **调试阶段**:开发和调试过程中避免频繁接收通知 263 | - **批量任务**:执行大量批处理任务时,只关注最终结果而非每个任务 264 | - **CI/CD 流程**:在自动化构建流水线中使用,避免触发过多通知 265 | 266 | #### 启用静默模式 267 | 268 | 静默模式通过环境变量 `MTF_SILENT_MODE` 控制: 269 | 270 | ```bash 271 | # 启用静默模式 272 | export MTF_SILENT_MODE=true 273 | 274 | # 临时启用 275 | MTF_SILENT_MODE=true taskflow tasks.yaml 276 | ``` 277 | 278 | 支持的值: 279 | - 设为 `true`, `1`, `yes`, `on` 表示启用静默模式 280 | - 不设置或设为其他值表示禁用静默模式 281 | 282 | #### 静默模式的工作原理 283 | 284 | 当启用静默模式时: 285 | 1. 所有任务执行完成后不会发送消息通知 286 | 2. 任务管理器完成时不会发送总结报告 287 | 3. 所有操作和结果仍会记录在日志文件中 288 | 4. 控制台输出不受影响,仍然会显示所有信息 289 | 290 | 注意,静默模式只影响消息通知行为,不会改变任务的实际执行过程。 291 | 292 | ### 自定义通知 293 | 294 | 您可以配置系统在任务状态变更时发送通知: 295 | 296 | ```python 297 | from multitaskflow import TaskFlow, Msg_push 298 | 299 | # 创建消息推送实例 300 | notifier = Msg_push( 301 | webhook_url="your_webhook_url", 302 | channel="your_channel" 303 | ) 304 | 305 | # 创建带通知功能的任务流管理器 306 | task_manager = TaskFlow( 307 | "tasks.yaml", 308 | msg_push=notifier 309 | ) 310 | ``` 311 | 312 | ## 自定义与扩展 313 | 314 | MultiTaskFlow设计为可扩展的,您可以: 315 | 316 | - 自定义任务状态处理逻辑 317 | - 添加新的任务类型 318 | - 扩展监控和报告功能 319 | 320 | ### 自定义任务处理器示例 321 | 322 | ```python 323 | from multitaskflow import TaskFlow 324 | 325 | class CustomTaskFlow(TaskFlow): 326 | def process_task_output(self, task, output): 327 | # 自定义输出处理逻辑 328 | print(f"处理任务 {task.name} 的输出: {output}") 329 | # 继续处理... 330 | super().process_task_output(task, output) 331 | ``` 332 | 333 | ## 常见问题(FAQ) 334 | 335 | **Q: XXXX?** 336 | 337 | ## 贡献指南 338 | 339 | 欢迎贡献代码、报告问题或提出新功能建议! 340 | 341 | 1. Fork 这个仓库 342 | 2. 创建您的特性分支 (`git checkout -b feature/amazing-feature`) 343 | 3. 提交您的更改 (`git commit -m 'Add some amazing feature'`) 344 | 4. 推送到分支 (`git push origin feature/amazing-feature`) 345 | 5. 打开一个 Pull Request 346 | 347 | ## 版本历史 348 | 349 | - **1.0.0** - 2024-03-15 350 | - 首次发布 351 | - 基本任务管理功能 352 | - 命令行工具支持 353 | 354 | ## 许可证 355 | 356 | 本项目采用MIT许可证 - 详情请查看 [LICENSE](LICENSE) 文件 357 | 358 | ## 作者与致谢 359 | 360 | - **主要开发者**: [Polaris](https://github.com/Polaris-F) 361 | - 感谢所有贡献者和使用者的宝贵反馈 -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from multitaskflow import TaskFlow 2 | 3 | # 创建任务流管理器 4 | manager = TaskFlow("examples/tasks.yaml") 5 | 6 | # 启动任务流管理器 7 | manager.run() -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | from multitaskflow import TaskFlow 2 | 3 | # 创建任务流管理器 4 | manager = TaskFlow("examples/tasks.yaml") 5 | 6 | # 启动任务流管理器 7 | manager.run() -------------------------------------------------------------------------------- /examples/scripts/archive_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 数据归档脚本示例 4 | # 此脚本用于演示MultiTaskFlow对非Python任务的支持 5 | # 用法: ./archive_data.sh <模型文件> <结果目录> 6 | 7 | # 显示分隔符 8 | echo "==================================================" 9 | echo "模拟开始数据归档任务" 10 | echo "==================================================" 11 | 12 | # 检查参数 13 | if [ $# -lt 2 ]; then 14 | echo "错误: 缺少必要参数" 15 | echo "用法: $0 <模型文件> <结果目录>" 16 | exit 1 17 | fi 18 | 19 | MODEL_PATH=$1 20 | RESULTS_DIR=$2 21 | 22 | # 显示模拟归档信息 23 | echo "模拟归档操作信息:" 24 | echo " - 模型文件: $MODEL_PATH" 25 | echo " - 结果目录: $RESULTS_DIR" 26 | echo " - 归档文件: archives/model_results.tar.gz (模拟)" 27 | echo "" 28 | 29 | # 模拟环境准备 30 | echo "步骤 1/8: 模拟检查环境..." 31 | sleep 3 32 | echo " - 检查磁盘空间: 充足" 33 | echo " - 检查权限: 正常" 34 | echo "" 35 | 36 | # 模拟扫描文件 37 | echo "步骤 2/8: 模拟扫描文件..." 38 | sleep 3 39 | echo " - 发现模型文件: $MODEL_PATH (235MB)" 40 | echo " - 发现结果文件: 15个文件 (约120MB)" 41 | echo "" 42 | 43 | # 模拟整理文件 44 | echo "步骤 3/8: 模拟整理文件..." 45 | sleep 3 46 | echo " - 创建临时目录: /tmp/archive_temp" 47 | echo " - 按类型排序文件" 48 | echo " - 准备元数据文件" 49 | echo "" 50 | 51 | # 模拟压缩数据 52 | echo "步骤 4/8: 模拟压缩模型文件..." 53 | sleep 3 54 | echo " - 使用算法: LZMA" 55 | echo " - 压缩级别: 9" 56 | echo " - 进度: 100%" 57 | echo "" 58 | 59 | # 模拟压缩结果 60 | echo "步骤 5/8: 模拟压缩结果文件..." 61 | for i in {1..5}; do 62 | progress=$((i*20)) 63 | echo " - 压缩进度: ${progress}%" 64 | sleep 1 65 | done 66 | echo "" 67 | 68 | # 模拟合并归档 69 | echo "步骤 6/8: 模拟合并归档..." 70 | sleep 2 71 | echo " - 合并模型和结果数据" 72 | echo " - 添加压缩算法信息" 73 | echo "" 74 | 75 | # 模拟计算校验和 76 | echo "步骤 7/8: 模拟计算校验和..." 77 | sleep 2 78 | echo " - 使用算法: SHA-256" 79 | echo " - 校验和: 8a7b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t (模拟值)" 80 | echo "" 81 | 82 | # 模拟验证归档 83 | echo "步骤 8/8: 模拟验证归档..." 84 | sleep 2 85 | echo " - 验证文件完整性" 86 | echo " - 验证数据一致性" 87 | echo "" 88 | 89 | # 模拟完成信息 90 | echo "模拟归档完成!" 91 | echo " - 归档文件: archives/model_results.tar.gz" 92 | echo " - 原始数据: 355MB" 93 | echo " - 压缩后: 220MB" 94 | echo " - 压缩率: 38%" 95 | echo " - 校验和文件: archives/model_results.tar.gz.sha256" 96 | echo "==================================================" 97 | 98 | exit 0 -------------------------------------------------------------------------------- /examples/scripts/data_preprocess.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 模拟数据预处理脚本 6 | 此脚本用于演示MultiTaskFlow的用法,只是模拟一个数据预处理过程 7 | """ 8 | 9 | import argparse 10 | import time 11 | import random 12 | 13 | def parse_args(): 14 | parser = argparse.ArgumentParser(description="数据预处理示例脚本") 15 | parser.add_argument('--dataset', type=str, default='coco', help='数据集名称') 16 | parser.add_argument('--output-dir', type=str, default='data/processed', help='输出目录') 17 | return parser.parse_args() 18 | 19 | def simulate_preprocessing(args): 20 | print(f"\n{'='*50}") 21 | print(f"模拟开始预处理数据集: {args.dataset}") 22 | print(f"输出目录: {args.output_dir}") 23 | print(f"{'='*50}\n") 24 | 25 | # 模拟环境初始化 26 | print("初始化预处理环境...") 27 | time.sleep(3) 28 | 29 | # 模拟数据预处理步骤 30 | steps = [ 31 | "加载原始数据集", 32 | "验证数据集完整性", 33 | "标注格式转换", 34 | "清洗标注数据", 35 | "图像检查与修复", 36 | "图像缩放和归一化", 37 | "数据增强处理", 38 | "创建训练/验证/测试集", 39 | "生成数据统计信息", 40 | "保存处理后的数据" 41 | ] 42 | 43 | total_files = random.randint(5000, 10000) 44 | 45 | for i, step in enumerate(steps): 46 | print(f"步骤 {i+1}/{len(steps)}: {step}") 47 | 48 | # 根据步骤不同模拟不同的处理时间和显示 49 | if i == 0: # 加载原始数据集 50 | print(f" 扫描数据集目录...") 51 | time.sleep(1) 52 | print(f" 找到 {total_files} 个文件") 53 | time.sleep(1) 54 | 55 | elif i == 1: # 验证数据集完整性 56 | print(f" 检查文件完整性...") 57 | time.sleep(1) 58 | corrupt_files = random.randint(0, 5) 59 | if corrupt_files > 0: 60 | print(f" 发现 {corrupt_files} 个损坏文件,已标记为跳过") 61 | time.sleep(0.5) 62 | 63 | elif i in [2, 3, 4]: # 标注和清洗相关 64 | file_count = total_files 65 | display_steps = 4 66 | 67 | for j in range(display_steps): 68 | progress = (j + 1) / display_steps 69 | processed = int(file_count * progress) 70 | time.sleep(0.5) # 每次显示进度耗时0.5秒 71 | print(f" 处理中: {processed}/{file_count} 文件 ({progress*100:.1f}%)") 72 | 73 | # 随机添加一些处理细节 74 | if random.random() > 0.7: 75 | issue_types = ["标注缺失", "标注重叠", "无效边界框", "类别错误"] 76 | issue = random.choice(issue_types) 77 | issue_count = random.randint(1, 10) 78 | print(f" 修复了 {issue_count} 个{issue}问题") 79 | 80 | elif i in [5, 6]: # 图像处理相关 81 | file_count = total_files 82 | # 这些步骤需要更多时间 83 | display_steps = 5 84 | 85 | for j in range(display_steps): 86 | progress = (j + 1) / display_steps 87 | processed = int(file_count * progress) 88 | time.sleep(0.8) # 每次显示进度耗时0.8秒 89 | print(f" 处理中: {processed}/{file_count} 文件 ({progress*100:.1f}%)") 90 | 91 | # 随机添加一些图像处理细节 92 | if j == 2 or random.random() > 0.7: 93 | detail = random.choice([ 94 | "调整图像尺寸到 512x512", 95 | "应用数据增强: 水平翻转", 96 | "应用数据增强: 随机裁剪", 97 | "应用数据增强: 色彩抖动", 98 | "应用数据增强: 亮度调整" 99 | ]) 100 | print(f" {detail}") 101 | 102 | elif i == 7: # 创建数据集划分 103 | print(f" 计算数据集划分...") 104 | time.sleep(1) 105 | train_count = int(total_files * 0.8) 106 | val_count = int(total_files * 0.1) 107 | test_count = total_files - train_count - val_count 108 | print(f" 训练集: {train_count} 文件") 109 | print(f" 验证集: {val_count} 文件") 110 | print(f" 测试集: {test_count} 文件") 111 | time.sleep(1) 112 | 113 | elif i == 8: # 生成统计信息 114 | print(f" 计算数据统计...") 115 | time.sleep(2) 116 | classes = ["人", "车", "自行车", "动物", "建筑物"] 117 | print(f" 类别分布:") 118 | for cls in classes: 119 | count = random.randint(500, 5000) 120 | print(f" - {cls}: {count} 个实例") 121 | time.sleep(1) 122 | 123 | else: # 保存数据 124 | print(f" 写入处理后的数据...") 125 | time.sleep(2) 126 | print(f" 压缩数据文件...") 127 | time.sleep(1) 128 | 129 | print(f" 完成: {step}") 130 | 131 | print(f"\n模拟预处理完成!") 132 | print(f"模拟处理了总计 {total_files} 个文件") 133 | print(f"模拟数据已准备就绪,可以开始训练") 134 | print(f"{'='*50}\n") 135 | 136 | if __name__ == "__main__": 137 | args = parse_args() 138 | simulate_preprocessing(args) -------------------------------------------------------------------------------- /examples/scripts/evaluate_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 模拟模型评估脚本 6 | 此脚本用于演示MultiTaskFlow的用法,只是模拟一个模型评估过程 7 | """ 8 | 9 | import argparse 10 | import time 11 | import random 12 | from datetime import datetime 13 | 14 | def parse_args(): 15 | parser = argparse.ArgumentParser(description="模型评估示例脚本") 16 | parser.add_argument('--model-path', type=str, required=True, help='模型文件路径') 17 | parser.add_argument('--dataset', type=str, default='data/val', help='验证数据集路径') 18 | return parser.parse_args() 19 | 20 | def simulate_evaluation(args): 21 | print(f"\n{'='*50}") 22 | print(f"模拟开始评估模型: {args.model_path}") 23 | print(f"评估数据集: {args.dataset}") 24 | print(f"{'='*50}\n") 25 | 26 | # 模拟初始化评估环境 27 | print("模拟初始化评估环境...") 28 | time.sleep(3) 29 | 30 | # 模拟加载模型 31 | print("模拟加载模型中...") 32 | time.sleep(3) 33 | 34 | # 模拟加载数据集 35 | print("模拟加载数据集中...") 36 | time.sleep(3) 37 | 38 | # 从模型路径中提取信息 39 | model_name = args.model_path.split('/')[-1].split('_')[0] if '/' in args.model_path else args.model_path 40 | 41 | # 随机生成一些评估指标 42 | metrics = { 43 | "precision": random.uniform(0.75, 0.95), 44 | "recall": random.uniform(0.70, 0.90), 45 | "mAP": random.uniform(0.72, 0.92), 46 | "inference_time": random.uniform(10, 30), # ms 47 | } 48 | 49 | # 模拟评估过程 50 | print("模拟评估进行中...") 51 | total_samples = 2000 # 模拟评估的样本数量 52 | steps = 10 # 显示10次进度更新 53 | 54 | for i in range(steps): 55 | progress = (i + 1) / steps 56 | current_samples = int(total_samples * progress) 57 | 58 | # 增加每步的等待时间 59 | time.sleep(1) # 每个进度更新需要1秒 60 | 61 | print(f" 进度: {progress*100:.1f}% ({current_samples}/{total_samples} 样本)") 62 | 63 | # 每步都更新一下评估指标以模拟真实情况 64 | current_precision = metrics["precision"] * (0.9 + 0.1 * progress) 65 | current_recall = metrics["recall"] * (0.9 + 0.1 * progress) 66 | 67 | # 随机显示一些测试样本的结果 68 | if random.random() > 0.7: 69 | sample_id = random.randint(1, total_samples) 70 | print(f" 样本 #{sample_id}: 分类正确" if random.random() > 0.2 else f" 样本 #{sample_id}: 分类错误") 71 | 72 | # 计算F1分数 73 | f1_score = 2 * (metrics["precision"] * metrics["recall"]) / (metrics["precision"] + metrics["recall"]) 74 | metrics["f1_score"] = f1_score 75 | 76 | # 模拟分析评估结果 77 | print("\n分析评估结果中...") 78 | time.sleep(2) 79 | 80 | # 打印评估结果 81 | print("\n评估结果:") 82 | print(f" 精确率 (Precision): {metrics['precision']:.4f}") 83 | print(f" 召回率 (Recall): {metrics['recall']:.4f}") 84 | print(f" F1分数: {metrics['f1_score']:.4f}") 85 | print(f" mAP: {metrics['mAP']:.4f}") 86 | print(f" 推理时间: {metrics['inference_time']:.2f} ms/图") 87 | 88 | # 模拟生成评估可视化 89 | print("\n模拟生成评估可视化...") 90 | time.sleep(2) 91 | 92 | # 模拟保存评估结果 93 | print(f"\n模拟保存评估报告到: results/{model_name}_evaluation.txt") 94 | time.sleep(1) 95 | 96 | print(f"\n评估完成!") 97 | print(f"{'='*50}\n") 98 | 99 | return True 100 | 101 | if __name__ == "__main__": 102 | args = parse_args() 103 | simulate_evaluation(args) -------------------------------------------------------------------------------- /examples/scripts/train_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 模拟深度学习模型训练脚本 6 | 此脚本用于演示MultiTaskFlow的用法,只是模拟一个训练过程 7 | """ 8 | 9 | import argparse 10 | import time 11 | import random 12 | from datetime import datetime 13 | 14 | def parse_args(): 15 | parser = argparse.ArgumentParser(description="模型训练示例脚本") 16 | parser.add_argument('--model', type=str, default='yolov8', help='模型名称') 17 | parser.add_argument('--epochs', type=int, default=10, help='训练轮数') 18 | parser.add_argument('--batch-size', type=int, default=32, help='批次大小') 19 | parser.add_argument('--dataset', type=str, default='data/processed', help='数据集路径') 20 | parser.add_argument('--resume', type=str, default=None, help='恢复训练的检查点路径') 21 | return parser.parse_args() 22 | 23 | def simulate_training(args): 24 | print(f"\n{'='*50}") 25 | print(f"模拟开始训练模型: {args.model}") 26 | print(f"训练配置:") 27 | print(f" - 模型: {args.model}") 28 | print(f" - 训练轮数: {args.epochs}") 29 | print(f" - 批次大小: {args.batch_size}") 30 | print(f" - 数据集: {args.dataset}") 31 | print(f" - 恢复训练: {'是 - ' + args.resume if args.resume else '否'}") 32 | print(f"{'='*50}\n") 33 | 34 | # 初始化模拟 35 | print("正在初始化训练环境...") 36 | time.sleep(3) # 增加初始化时间 37 | 38 | # 如果有恢复训练参数,模拟检查点加载 39 | if args.resume: 40 | print(f"模拟加载检查点: {args.resume}") 41 | time.sleep(2) # 增加加载检查点时间 42 | 43 | # 模拟数据加载 44 | print("模拟加载数据集...") 45 | time.sleep(3) # 增加数据加载时间 46 | 47 | # 模拟训练过程 48 | for epoch in range(1, args.epochs + 1): 49 | # 模拟准确率和损失 50 | accuracy = min(0.99, 0.5 + (epoch / args.epochs) * 0.4) 51 | loss = max(0.1, 2.0 - (epoch / args.epochs) * 1.9) 52 | 53 | # 随机波动 54 | accuracy += random.uniform(-0.02, 0.02) 55 | loss += random.uniform(-0.1, 0.1) 56 | 57 | print(f"\n开始训练第 {epoch}/{args.epochs} 轮...") 58 | 59 | # 模拟每轮中的批次训练 60 | total_batches = 100 # 假设有100个批次 61 | batch_steps = 5 # 显示5次进度更新 62 | for i in range(batch_steps): 63 | batch_progress = (i + 1) / batch_steps 64 | current_batch = int(total_batches * batch_progress) 65 | # 每个批次更新消耗更多时间 66 | time.sleep(0.8) # 每次进度更新消耗0.8秒 67 | print(f" 批次进度: {current_batch}/{total_batches} ({batch_progress*100:.1f}%)") 68 | 69 | # 打印训练进度 70 | print(f"轮次 {epoch}/{args.epochs} - 损失: {loss:.4f}, 准确率: {accuracy:.4f}") 71 | 72 | # 每轮结束模拟验证 73 | print("模拟验证集评估中...") 74 | time.sleep(1) # 增加验证时间 75 | val_accuracy = accuracy - random.uniform(-0.02, 0.03) 76 | val_loss = loss + random.uniform(-0.05, 0.1) 77 | print(f" 验证集 - 损失: {val_loss:.4f}, 准确率: {val_accuracy:.4f}") 78 | 79 | # 每轮或最后一轮模拟保存检查点 80 | if epoch % 2 == 0 or epoch == args.epochs: 81 | checkpoint_path = f"checkpoints/{args.model}_epoch_{epoch}.pt" 82 | print(f"模拟保存检查点: {checkpoint_path}") 83 | time.sleep(1) # 增加保存检查点时间 84 | 85 | print(f"\n训练完成! 最终模型已保存") 86 | print(f"模拟性能指标 - 损失: {loss:.4f}, 准确率: {accuracy:.4f}") 87 | print(f"{'='*50}\n") 88 | 89 | if __name__ == "__main__": 90 | args = parse_args() 91 | simulate_training(args) -------------------------------------------------------------------------------- /examples/tasks.yaml: -------------------------------------------------------------------------------- 1 | # MultiTaskFlow 任务流配置示例 2 | # 此文件定义了一系列用于演示的模拟任务 3 | # 任务将依次执行,每个任务完成后才会开始下一个 4 | 5 | # 模拟深度学习训练任务 6 | - name: "数据预处理" 7 | command: "python examples/scripts/data_preprocess.py --dataset coco --output-dir data/processed" 8 | status: "pending" 9 | # silent: false # 默认会发送消息通知 10 | 11 | - name: "模型训练-阶段1" 12 | command: "python examples/scripts/train_model.py --model yolov8 --epochs 5 --batch-size 32 --dataset data/processed" 13 | status: "pending" 14 | silent: true # 静默模式,不发送消息通知 15 | 16 | - name: "模型评估-阶段1" 17 | command: "python examples/scripts/evaluate_model.py --model-path checkpoints/yolov8_epoch_5.pt --dataset data/val" 18 | status: "pending" 19 | # silent: false # 默认会发送消息通知 20 | 21 | - name: "模型训练-阶段2" 22 | command: "python examples/scripts/train_model.py --model yolov8 --epochs 5 --batch-size 16 --resume checkpoints/yolov8_epoch_5.pt" 23 | status: "pending" 24 | silent: true # 静默模式,不发送消息通知 25 | 26 | - name: "模型评估-阶段2" 27 | command: "python examples/scripts/evaluate_model.py --model-path checkpoints/yolov8_epoch_5.pt --dataset data/val" 28 | status: "pending" 29 | # silent: false # 默认会发送消息通知 30 | 31 | # 非Python任务示例 32 | - name: "数据归档" 33 | command: "bash examples/scripts/archive_data.sh checkpoints/yolov8_epoch_5.pt results/" 34 | status: "pending" 35 | # silent: false # 默认会发送消息通知 -------------------------------------------------------------------------------- /images/demo_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-F/MultiTaskFlow/84e933154fafe32b6f3d38ebdfba1771196c607c/images/demo_screenshot.png -------------------------------------------------------------------------------- /multitaskflow/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MultiTaskFlow - 多任务流管理工具 3 | 4 | 此工具用于管理和监控多个连续任务的执行,特别适用于深度学习训练场景。 5 | 支持任务状态监控、执行时间统计和消息推送通知功能。 6 | 7 | 主要模块: 8 | - task_flow: 任务流管理器,用于按顺序执行多个任务 9 | - process_monitor: 进程监控工具,用于监控任务执行并发送通知 10 | 11 | 作者: LHF 12 | 许可证: MIT 13 | 版本: 0.1.0 14 | """ 15 | 16 | from .task_flow import TaskFlow, Task 17 | from .process_monitor import ProcessMonitor, Msg_push 18 | 19 | __version__ = '0.1.0' 20 | __author__ = 'LHF' 21 | 22 | __all__ = [ 23 | 'TaskFlow', 24 | 'Task', 25 | 'ProcessMonitor', 26 | 'Msg_push' 27 | ] -------------------------------------------------------------------------------- /multitaskflow/process_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 进程监控模块 (Process Monitor Module) 6 | 7 | 此模块提供了进程监控和消息推送功能,主要用于: 8 | 1. 监控长时间运行的进程状态 9 | 2. 在进程完成或失败时发送通知 10 | 3. 提供简单的消息推送功能 11 | 12 | 主要组件: 13 | - ProcessMonitor: 进程监控线程类,用于实时监控指定进程的运行状态 14 | - Msg_push: 消息推送函数,用于发送通知到微信等平台 15 | - setup_logger: 日志设置辅助函数 16 | 17 | 使用示例见文件末尾的 __main__ 部分 18 | """ 19 | 20 | import logging 21 | import os 22 | import subprocess 23 | import time 24 | import psutil 25 | import requests 26 | from threading import Thread 27 | from datetime import datetime 28 | from dotenv import load_dotenv 29 | from typing import Dict, Optional, Union, List 30 | 31 | def Msg_push(title: str, content: str, logger: Optional[logging.Logger] = None) -> bool: 32 | """ 33 | 发送消息到PushPlus平台,可用于向微信推送通知 34 | 35 | Args: 36 | title: 消息标题,显示在通知顶部 37 | content: 消息内容,支持HTML/文本格式 38 | logger: 可选的日志记录器,如不提供则创建一个新的 39 | 40 | Returns: 41 | bool: 发送是否成功 42 | 43 | 示例: 44 | >>> Msg_push("任务完成", "训练已完成,准确率达到95%") 45 | >>> Msg_push("错误警告", "服务器CPU使用率超过90%,请检查", my_logger) 46 | 47 | 注意: 48 | 需要设置 MSG_PUSH_TOKEN 环境变量,可以通过以下方式设置: 49 | 1. 在 ~/.bashrc 或 ~/.zshrc 中添加: export MSG_PUSH_TOKEN=your_token 50 | 2. 在运行前临时设置: MSG_PUSH_TOKEN=your_token python your_script.py 51 | 3. 在 .env 文件中设置(仅开发模式) 52 | """ 53 | # 如果没有提供logger,创建一个简单的logger 54 | if logger is None: 55 | logger = logging.getLogger('Msg_push') 56 | handler = logging.StreamHandler() 57 | handler.setFormatter(logging.Formatter( 58 | '%(asctime)s - %(levelname)s - %(message)s', 59 | datefmt='%Y-%m-%d %H:%M:%S' 60 | )) 61 | logger.addHandler(handler) 62 | logger.setLevel(logging.INFO) 63 | 64 | # 加载token 65 | load_dotenv() 66 | token = os.getenv('MSG_PUSH_TOKEN') 67 | 68 | # 详细的token检查和处理 69 | if not token: 70 | error_msg = """ 71 | 未找到 MSG_PUSH_TOKEN 环境变量! 72 | 73 | 请通过以下方式之一设置 MSG_PUSH_TOKEN: 74 | 1. 在 ~/.bashrc 或 ~/.zshrc 中添加: 75 | export MSG_PUSH_TOKEN=your_token 76 | 77 | 2. 在运行前临时设置: 78 | MSG_PUSH_TOKEN=your_token python your_script.py 79 | 80 | 3. 在 .env 文件中设置(仅开发模式) 81 | 82 | 获取 token 的方法: 83 | 1. 访问 https://www.pushplus.plus/ 84 | 2. 登录并获取您的 token 85 | 3. 将 token 添加到上述配置中 86 | """ 87 | logger.error(error_msg) 88 | return False 89 | 90 | # 检查token格式 91 | if not isinstance(token, str) or len(token.strip()) == 0: 92 | logger.error("MSG_PUSH_TOKEN 格式无效:应为非空字符串") 93 | return False 94 | 95 | data = { 96 | "token": token, 97 | "title": title, 98 | "content": content 99 | } 100 | 101 | # 增加重试次数和等待时间以应对频率限制 102 | max_retries = 5 103 | base_wait_time = 3 # 初始等待时间,秒 104 | 105 | for attempt in range(max_retries): 106 | try: 107 | # 指数退避策略 108 | wait_time = base_wait_time * (2 ** attempt) 109 | 110 | if attempt > 0: 111 | logger.info(f"尝试第 {attempt+1}/{max_retries} 次发送消息,等待 {wait_time} 秒...") 112 | time.sleep(wait_time) 113 | 114 | response = requests.post( 115 | 'https://www.pushplus.plus/send', 116 | json=data, 117 | headers={'Content-Type': 'application/json'}, 118 | timeout=15 # 增加超时时间 119 | ) 120 | 121 | result = response.json() 122 | if response.status_code == 200 and result.get('code') == 200: 123 | logger.info("消息发送成功") 124 | return True 125 | elif result.get('code') == 429: # 频率限制错误码 126 | logger.warning(f"消息发送受到频率限制,将在 {wait_time} 秒后重试...") 127 | time.sleep(wait_time) # 遇到频率限制时额外等待 128 | continue 129 | else: 130 | error_msg = f"消息发送失败,状态码: {response.status_code}, 返回: {response.text}" 131 | logger.warning(error_msg) 132 | # 如果是token相关错误,提供更详细的提示 133 | if result.get('code') in [401, 403]: 134 | logger.error(""" 135 | Token 验证失败,请检查: 136 | 1. MSG_PUSH_TOKEN 是否正确设置 137 | 2. Token 是否已过期 138 | 3. 是否在 pushplus.plus 平台正确配置 139 | """) 140 | except requests.exceptions.RequestException as e: 141 | logger.error(f"网络请求失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}") 142 | except Exception as e: 143 | logger.error(f"发送通知时发生未知错误 (尝试 {attempt + 1}/{max_retries}): {str(e)}") 144 | 145 | # 如果不是最后一次尝试,等待一段时间后继续 146 | if attempt < max_retries - 1: 147 | time.sleep(wait_time) 148 | 149 | logger.error("多次尝试后仍无法发送消息") 150 | return False 151 | 152 | class ProcessMonitor(Thread): 153 | """ 154 | 进程监控器类,用于监控特定进程并在进程结束时发送通知 155 | 156 | 此类创建一个后台线程,定期检查指定进程是否仍在运行, 157 | 当进程结束时发送通知消息。适用于监控长时间运行的训练任务等。 158 | 159 | Attributes: 160 | process_name: 进程名称,用于显示和日志 161 | process_cmd: 进程命令,用于查找进程 162 | pid: 进程ID 163 | start_time: 进程开始时间 164 | 165 | Methods: 166 | set_result: 设置任务执行结果和错误信息 167 | check_process: 检查进程是否仍在运行 168 | get_duration: 获取进程运行时长的格式化字符串 169 | send_notification: 发送进程结束通知 170 | """ 171 | 172 | # 固定的消息模板 173 | MESSAGE_TEMPLATE = { 174 | "title": "任务执行通知", 175 | "content": """ 176 | 任务名称: {process_name} 177 | 执行状态: {status} 178 | PID: {pid} 179 | 运行时长: {duration} 180 | 结束时间: {end_time} 181 | {error_msg} 182 | """ 183 | } 184 | 185 | def __init__(self, process_name: str, process_cmd: str, logger: logging.Logger, start_time: datetime = None): 186 | """ 187 | 初始化进程监控器 188 | 189 | Args: 190 | process_name: 进程名称(用于显示和通知) 191 | process_cmd: 进程命令(用于查找进程) 192 | logger: 日志记录器 193 | start_time: 开始时间,默认为当前时间 194 | """ 195 | super().__init__() 196 | self.process_name = process_name 197 | self.process_cmd = process_cmd 198 | self.logger = logger 199 | self.start_time = start_time or datetime.now() 200 | self.daemon = True # 设置为守护线程,随主线程退出 201 | self.return_code = None 202 | self.error_message = None 203 | load_dotenv() 204 | self.pid = self._find_process_pid() 205 | 206 | def _find_process_pid(self) -> Optional[int]: 207 | """ 208 | 查找进程ID 209 | 210 | 通过命令行参数匹配来查找进程,返回匹配的第一个PID 211 | 212 | Returns: 213 | Optional[int]: 如果找到则返回进程ID,否则返回None 214 | """ 215 | pids = [] 216 | self.logger.info(f"开始查找进程: {self.process_name}") 217 | 218 | for proc in psutil.process_iter(['pid', 'name', 'cmdline']): 219 | try: 220 | cmdline = " ".join(proc.info['cmdline'] or []) 221 | if self.process_cmd in cmdline: 222 | pids.append(proc.info['pid']) 223 | self.logger.debug(f"找到匹配进程: PID={proc.info['pid']}, cmdline={cmdline}") 224 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 225 | continue 226 | 227 | if not pids: 228 | self.logger.info(f"未找到匹配的进程: {self.process_name}") 229 | return None 230 | 231 | self.logger.info(f"进程 {self.process_name} 的PID列表: {pids},使用第一个PID: {pids[0]}") 232 | return pids[0] 233 | 234 | def check_process(self) -> bool: 235 | """ 236 | 检查进程是否仍在运行 237 | 238 | Returns: 239 | bool: 进程是否仍在运行 240 | """ 241 | if not self.pid: 242 | return False 243 | try: 244 | p = psutil.Process(self.pid) 245 | status = p.status() 246 | return p.is_running() and status != psutil.STATUS_ZOMBIE 247 | except psutil.NoSuchProcess: 248 | return False 249 | 250 | def get_duration(self) -> str: 251 | """ 252 | 获取进程运行时长的格式化字符串 253 | 254 | Returns: 255 | str: 格式化的时间字符串,如 "2h15m30s" 256 | """ 257 | duration = datetime.now() - self.start_time 258 | total_seconds = int(duration.total_seconds()) 259 | hours = total_seconds // 3600 260 | minutes = (total_seconds % 3600) // 60 261 | seconds = total_seconds % 60 262 | if hours > 0: 263 | return f"{hours}h{minutes}m{seconds}s" 264 | elif minutes > 0: 265 | return f"{minutes}m{seconds}s" 266 | else: 267 | return f"{seconds}s" 268 | 269 | def set_result(self, return_code: int, error_message: str = None): 270 | """ 271 | 设置任务执行结果 272 | 273 | Args: 274 | return_code: 返回码,0表示成功 275 | error_message: 错误信息,失败时提供 276 | """ 277 | self.return_code = return_code 278 | self.error_message = error_message 279 | 280 | def run(self): 281 | """ 282 | 线程运行的主方法,定期检查进程状态 283 | """ 284 | if not self.pid: 285 | self.logger.error(f"未找到进程 {self.process_name}") 286 | return 287 | if not self.check_process(): 288 | self.logger.error(f"进程 {self.process_name} (PID: {self.pid}) 未运行") 289 | return 290 | 291 | was_running = True 292 | while True: 293 | is_running = self.check_process() 294 | if was_running and not is_running: 295 | self.logger.info(f"进程 {self.process_name} (PID: {self.pid}) 已结束") 296 | self.send_notification() 297 | break 298 | time.sleep(10) # 每10秒检查一次 299 | was_running = is_running 300 | 301 | def send_notification(self): 302 | """ 303 | 发送进程结束通知 304 | 305 | 如果设置了环境变量MTF_SILENT_MODE=true,将跳过消息发送,只记录日志 306 | 307 | Returns: 308 | bool: 通知是否发送成功 309 | """ 310 | # 首先检查环境变量,实现基于环境变量的静默模式 311 | if os.getenv('MTF_SILENT_MODE', '').lower() in ('true', '1', 'yes', 'on'): 312 | self.logger.info(f"环境变量MTF_SILENT_MODE已设置为{os.getenv('MTF_SILENT_MODE')},跳过消息发送") 313 | return True 314 | 315 | status = "成功完成" if self.return_code == 0 else "执行失败" 316 | error_msg = f"错误信息: {self.error_message}" if self.error_message else "" 317 | 318 | content = self.MESSAGE_TEMPLATE["content"].format( 319 | process_name=self.process_name, 320 | status=status, 321 | pid=self.pid, 322 | duration=self.get_duration(), 323 | end_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 324 | error_msg=error_msg 325 | ) 326 | 327 | # 记录任务完成日志 328 | self.logger.info(f"任务 {self.process_name} 已{status},运行时长: {self.get_duration()}") 329 | 330 | return Msg_push(self.MESSAGE_TEMPLATE["title"], content, self.logger) 331 | 332 | def setup_logger(name: str, log_dir: str = "logs") -> logging.Logger: 333 | """ 334 | 设置日志记录器 335 | 336 | Args: 337 | name: 日志记录器名称 338 | log_dir: 日志目录,默认为"logs" 339 | 340 | Returns: 341 | logging.Logger: 配置好的日志记录器 342 | 343 | 示例: 344 | >>> logger = setup_logger("my_app") 345 | >>> logger.info("应用启动") 346 | """ 347 | logger = logging.getLogger(name) 348 | logger.setLevel(logging.INFO) 349 | 350 | if not os.path.exists(log_dir): 351 | os.makedirs(log_dir) 352 | 353 | fh = logging.FileHandler( 354 | f"{log_dir}/{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log", 355 | encoding='utf-8' 356 | ) 357 | fh.setLevel(logging.INFO) 358 | 359 | ch = logging.StreamHandler() 360 | ch.setLevel(logging.INFO) 361 | 362 | formatter = logging.Formatter( 363 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 364 | datefmt='%Y-%m-%d %H:%M:%S' 365 | ) 366 | fh.setFormatter(formatter) 367 | ch.setFormatter(formatter) 368 | 369 | logger.addHandler(fh) 370 | logger.addHandler(ch) 371 | 372 | return logger 373 | 374 | if __name__ == "__main__": 375 | """ 376 | 模块使用示例 377 | """ 378 | 379 | # ==================示例1:直接调用Msg_push发送自定义消息================= 380 | # 设置日志记录器 381 | logger = setup_logger("MessageDemo") 382 | 383 | # 自定义消息示例 384 | title = "训练任务完成" 385 | content = f""" 386 | 【训练结果报告】 387 | 模型:YOLOv8 388 | 数据集:COCO 389 | 准确率:98.5% 390 | 训练轮数:100 391 | 完成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 392 | 备注:模型性能超出预期 393 | """ 394 | 395 | # 发送自定义消息 396 | logger.info("发送自定义消息示例...") 397 | Msg_push( 398 | title, 399 | content, 400 | logger 401 | ) 402 | 403 | # 等待消息发送完成 404 | time.sleep(2) 405 | 406 | # ==================示例2:使用本部分程序监控进程=================== 407 | # 设置另一个日志记录器 408 | logger = setup_logger("ProcessMonitorDemo") 409 | 410 | # 模拟一个长时间运行的进程 411 | logger.info("启动一个模拟进程...") 412 | 413 | # 在实际使用中,这可能是一个训练脚本或其他长时间运行的任务 414 | # 这里我们使用sleep来模拟 415 | cmd = "python -c 'import time; print(\"模拟进程开始运行...\"); time.sleep(15); print(\"模拟进程完成\")'" 416 | process = subprocess.Popen(cmd, shell=True) 417 | 418 | # 创建并启动进程监控器 419 | logger.info("创建进程监控器...") 420 | monitor = ProcessMonitor( 421 | process_name="示例进程", 422 | process_cmd="python -c", 423 | logger=logger 424 | ) 425 | monitor.start() 426 | 427 | # 等待进程和监控器完成 428 | process.wait() 429 | monitor.join(timeout=30) 430 | 431 | logger.info("示例完成") -------------------------------------------------------------------------------- /multitaskflow/task_flow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 任务流管理模块 (Task Flow Manager) 6 | 7 | 此模块提供了任务队列管理和执行功能,主要用于: 8 | 1. 管理多个连续任务的执行 9 | 2. 追踪任务状态和执行时间 10 | 3. 在任务完成或失败时发送通知 11 | 4. 支持动态添加新任务 12 | 13 | 主要组件: 14 | - Task: 任务类,表示一个可执行的任务 15 | - TaskFlow: 任务流管理器,负责任务的调度和执行 16 | 17 | 典型用例: 18 | - 深度学习模型的连续训练任务 19 | - 数据处理批处理任务 20 | - 需要顺序执行的脚本集合 21 | 22 | 使用示例见文件末尾的 __main__ 部分 23 | """ 24 | 25 | import logging 26 | import os 27 | import time 28 | import subprocess 29 | import psutil 30 | import json 31 | import requests 32 | import sys 33 | from typing import List, Dict, Any, Optional, Union 34 | from datetime import datetime, timedelta 35 | import yaml 36 | from threading import Thread, Event, Lock 37 | from dotenv import load_dotenv 38 | from queue import Queue 39 | import queue 40 | import signal 41 | import importlib.util 42 | import inspect 43 | 44 | # 检查process_monitor模块是否可导入 45 | if importlib.util.find_spec("multitaskflow.process_monitor") is not None: 46 | from multitaskflow.process_monitor import ProcessMonitor, Msg_push 47 | else: 48 | # 尝试从当前目录导入 49 | try: 50 | from process_monitor import ProcessMonitor, Msg_push 51 | except ImportError: 52 | raise ImportError("无法导入ProcessMonitor模块,请确保process_monitor.py在正确的路径下") 53 | 54 | class Task: 55 | """ 56 | 任务类,表示一个可执行的任务 57 | 58 | 属性: 59 | name: 任务名称 60 | command: 要执行的命令 61 | status: 任务状态 62 | start_time: 开始时间 63 | end_time: 结束时间 64 | return_code: 命令返回值 65 | duration: 执行时长 66 | """ 67 | 68 | STATUS_PENDING = "pending" 69 | STATUS_RUNNING = "running" 70 | STATUS_COMPLETED = "completed" 71 | STATUS_FAILED = "failed" 72 | 73 | def __init__(self, name: str, command: str, status: str = STATUS_PENDING): 74 | """ 75 | 初始化任务实例 76 | 77 | Args: 78 | name: 任务名称 79 | command: 要执行的命令行字符串 80 | status: 初始状态,默认为"pending" 81 | """ 82 | self.name = name 83 | self.command = command 84 | self.status = status 85 | self.start_time = None 86 | self.end_time = None 87 | self.return_code = None 88 | self.process = None 89 | self.error_message = None 90 | self.duration = None 91 | self.monitor = None 92 | 93 | def to_dict(self) -> Dict[str, Any]: 94 | """ 95 | 将任务转换为字典格式,用于保存到配置文件和生成报告 96 | 97 | Returns: 98 | Dict[str, Any]: 任务的字典表示 99 | """ 100 | return { 101 | "name": self.name, 102 | "command": self.command, 103 | "status": self.status, 104 | "start_time": self.start_time.strftime('%Y-%m-%d %H:%M:%S') if self.start_time else None, 105 | "end_time": self.end_time.strftime('%Y-%m-%d %H:%M:%S') if self.end_time else None, 106 | "duration": str(self.duration) if self.duration else "未完成", 107 | "return_code": self.return_code, 108 | "error_message": self.error_message 109 | } 110 | 111 | def update_duration(self): 112 | """ 113 | 更新任务运行时长 114 | """ 115 | if self.start_time and self.end_time: 116 | self.duration = self.end_time - self.start_time 117 | 118 | def start(self): 119 | """ 120 | 开始任务 121 | """ 122 | self.status = self.STATUS_RUNNING 123 | self.start_time = datetime.now() 124 | 125 | def complete(self, return_code: int, error_message: str = None): 126 | """ 127 | 完成任务 128 | 129 | Args: 130 | return_code: 返回码,0表示成功 131 | error_message: 错误信息,失败时提供 132 | """ 133 | self.end_time = datetime.now() 134 | self.return_code = return_code 135 | self.error_message = error_message 136 | self.status = self.STATUS_COMPLETED if return_code == 0 else self.STATUS_FAILED 137 | self.update_duration() 138 | 139 | class TaskFlow: 140 | """ 141 | 任务流管理器,负责任务的调度和执行 142 | 143 | 任务流管理器可以从配置文件加载任务,按顺序执行任务, 144 | 并在任务完成时发送通知。支持动态添加新任务和中断处理。 145 | 146 | 属性: 147 | tasks: 任务列表 148 | total_tasks: 任务总数 149 | completed_tasks: 已完成任务数 150 | failed_tasks: 失败任务数 151 | pending_tasks: 等待任务数 152 | """ 153 | 154 | TASK_DIVIDER = "=" * 50 155 | 156 | def __init__(self, config_path: str): 157 | """ 158 | 初始化任务流管理器 159 | 160 | Args: 161 | config_path: 任务配置文件路径 162 | """ 163 | self.config_path = config_path 164 | self.tasks: List[Task] = [] 165 | self.logger = self._setup_logger() 166 | self.task_queue = Queue() 167 | self.running = False 168 | self.task_lock = Lock() 169 | self.stop_event = Event() 170 | self.start_time = None 171 | self.end_time = None 172 | 173 | # 初始化任务计数器 174 | self._reset_task_counters() 175 | 176 | self.logger.info(self.TASK_DIVIDER) 177 | self.logger.info("任务流管理器初始化...") 178 | self.load_tasks() 179 | self.logger.info(self.TASK_DIVIDER) 180 | 181 | def _reset_task_counters(self): 182 | """重置任务计数器""" 183 | self.total_tasks = 0 184 | self.completed_tasks = 0 185 | self.failed_tasks = 0 186 | self.pending_tasks = 0 187 | 188 | def _update_task_counters(self): 189 | """更新任务计数器""" 190 | self._reset_task_counters() 191 | self.total_tasks = len(self.tasks) 192 | for task in self.tasks: 193 | if task.status == Task.STATUS_COMPLETED: 194 | self.completed_tasks += 1 195 | elif task.status == Task.STATUS_FAILED: 196 | self.failed_tasks += 1 197 | elif task.status == Task.STATUS_PENDING: 198 | self.pending_tasks += 1 199 | 200 | def _setup_logger(self) -> logging.Logger: 201 | """ 202 | 设置日志记录器 203 | 204 | Returns: 205 | logging.Logger: 配置好的日志记录器 206 | """ 207 | logger = logging.getLogger("TaskFlow") 208 | logger.setLevel(logging.INFO) 209 | 210 | # 确保日志目录存在 211 | if not os.path.exists("logs"): 212 | os.makedirs("logs") 213 | 214 | # 文件处理器 215 | fh = logging.FileHandler( 216 | f"logs/taskflow_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log", 217 | encoding='utf-8' 218 | ) 219 | fh.setLevel(logging.INFO) 220 | 221 | # 控制台处理器 222 | ch = logging.StreamHandler(sys.stdout) # 明确指定输出到stdout 223 | ch.setLevel(logging.INFO) 224 | 225 | # 创建格式器 226 | formatter = logging.Formatter( 227 | '%(asctime)s - %(levelname)s - %(message)s', 228 | datefmt='%Y-%m-%d %H:%M:%S' 229 | ) 230 | fh.setFormatter(formatter) 231 | ch.setFormatter(formatter) 232 | 233 | # 添加处理器 234 | logger.addHandler(fh) 235 | logger.addHandler(ch) 236 | 237 | return logger 238 | 239 | def load_tasks(self): 240 | """ 241 | 从配置文件加载初始任务 242 | 243 | 配置文件应为YAML格式,包含任务列表,每个任务需指定名称和命令 244 | 任务可以包含以下参数: 245 | - name: 任务名称(必需) 246 | - command: 要执行的命令(必需) 247 | - status: 任务状态(可选,默认为"pending") 248 | """ 249 | try: 250 | with open(self.config_path, 'r', encoding='utf-8') as f: 251 | task_list = yaml.safe_load(f) 252 | 253 | if not isinstance(task_list, list): 254 | raise ValueError("配置文件格式错误:应该是任务列表") 255 | 256 | for task_config in task_list: 257 | task = Task( 258 | name=task_config['name'], 259 | command=task_config['command'], 260 | status=task_config.get('status', 'pending') 261 | ) 262 | self.add_task(task) 263 | 264 | self.logger.info(f"已加载 {len(self.tasks)} 个初始任务") 265 | except Exception as e: 266 | self.logger.error(f"加载任务配置失败: {str(e)}") 267 | raise 268 | 269 | def add_task(self, task: Task): 270 | """ 271 | 添加新任务到队列 272 | 273 | Args: 274 | task: 要添加的任务实例 275 | """ 276 | with self.task_lock: 277 | self.tasks.append(task) 278 | self.task_queue.put(task) 279 | self.total_tasks += 1 280 | self.logger.info(f"新任务已添加: {task.name}") 281 | 282 | def add_task_by_config(self, name: str, command: str): 283 | """ 284 | 通过参数添加新任务 285 | 286 | Args: 287 | name: 任务名称 288 | command: 要执行的命令 289 | 290 | Returns: 291 | Task: 新添加的任务实例 292 | """ 293 | task = Task(name=name, command=command) 294 | self.add_task(task) 295 | return task 296 | 297 | def format_duration(self, duration: timedelta) -> str: 298 | """ 299 | 将时间间隔转换为中文格式的天时分秒 300 | 301 | Args: 302 | duration: 时间间隔 303 | 304 | Returns: 305 | str: 格式化的时间字符串 306 | """ 307 | total_seconds = int(duration.total_seconds()) 308 | days = total_seconds // (24 * 3600) 309 | remaining_seconds = total_seconds % (24 * 3600) 310 | hours = remaining_seconds // 3600 311 | remaining_seconds %= 3600 312 | minutes = remaining_seconds // 60 313 | seconds = remaining_seconds % 60 314 | 315 | parts = [] 316 | if days > 0: 317 | parts.append(f"{days}天") 318 | if hours > 0 or days > 0: 319 | parts.append(f"{hours}时") 320 | if minutes > 0 or hours > 0 or days > 0: 321 | parts.append(f"{minutes}分") 322 | parts.append(f"{seconds}秒") 323 | 324 | return "".join(parts) 325 | 326 | def get_duration(self) -> str: 327 | """ 328 | 获取总运行时长 329 | 330 | Returns: 331 | str: 格式化的时间字符串 332 | """ 333 | if not self.start_time: 334 | return "0秒" 335 | duration = datetime.now() - self.start_time 336 | return self.format_duration(duration) 337 | 338 | def generate_summary(self) -> str: 339 | """ 340 | 生成详细的任务执行报告 341 | 342 | Returns: 343 | str: 任务执行报告文本 344 | """ 345 | if self.start_time: 346 | self.end_time = datetime.now() 347 | total_duration = self.end_time - self.start_time 348 | else: 349 | total_duration = timedelta(0) 350 | 351 | summary = f""" 352 | 【任务流管理器执行报告】 353 | ==================== 354 | 执行开始时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S') if self.start_time else '未开始'} 355 | 执行结束时间: {self.end_time.strftime('%Y-%m-%d %H:%M:%S') if self.end_time else '未结束'} 356 | 总运行时长: {self.format_duration(total_duration)} 357 | 任务统计: {self.total_tasks}个任务 (总数) 358 | 状态分布: 成功 {self.completed_tasks}个 | 失败 {self.failed_tasks}个 | 等待 {self.pending_tasks}个 359 | """ 360 | # 只有当有失败任务时才显示失败任务列表 361 | failed_tasks = [task for task in self.tasks if task.status == "failed"] 362 | if failed_tasks: 363 | summary += "\n失败任务列表:\n" 364 | for task in failed_tasks: 365 | summary += f""" 366 | {'-' * 40} 367 | 任务名称: {task.name} 368 | 执行命令: {task.command} 369 | 执行时长: {self.format_duration(task.duration) if task.duration else '未完成'} 370 | 错误信息: {task.error_message or '未知错误'} 371 | """ 372 | return summary 373 | 374 | def execute_task(self, task: Task) -> bool: 375 | """ 376 | 执行单个任务 377 | 378 | Args: 379 | task: 要执行的任务 380 | 381 | Returns: 382 | bool: 任务是否成功执行 383 | """ 384 | self.logger.info(self.TASK_DIVIDER) 385 | self.logger.info(f"开始执行任务: {task.name}") 386 | self.logger.info(f"执行命令: {task.command}") 387 | 388 | task.start() 389 | 390 | try: 391 | # 启动进程,保持原始输出到终端 392 | task.process = subprocess.Popen( 393 | task.command, 394 | shell=True, 395 | bufsize=1, 396 | universal_newlines=True, 397 | stdout=None, # 保持原始输出到终端 398 | stderr=None # 保持原始输出到终端 399 | ) 400 | 401 | # 启动进程监控 402 | task.monitor = ProcessMonitor( 403 | process_name=task.name, 404 | process_cmd=task.command, 405 | logger=self.logger, 406 | start_time=task.start_time 407 | ) 408 | task.monitor.start() 409 | 410 | # 等待进程完成 411 | return_code = task.process.wait() # 等待进程完成 412 | 413 | # 更新任务状态 414 | task.complete(return_code) 415 | 416 | # 更新监控器状态(消息发送由monitor自己处理) 417 | if task.monitor: 418 | task.monitor.set_result( 419 | return_code, 420 | "执行失败" if return_code != 0 else None 421 | ) 422 | 423 | if task.status == Task.STATUS_COMPLETED: 424 | self.logger.info(f"任务执行完成: {task.name}") 425 | return True 426 | else: 427 | self.logger.error(f"任务执行失败: {task.name}") 428 | self.logger.error(f"返回值: {return_code}") 429 | return False 430 | 431 | except Exception as e: 432 | error_msg = str(e) 433 | task.complete(-1, error_msg) 434 | 435 | # 更新监控器状态(消息发送由monitor自己处理) 436 | if task.monitor: 437 | task.monitor.set_result(-1, error_msg) 438 | 439 | self.logger.error(f"任务执行异常: {task.name}") 440 | self.logger.error(f"异常信息: {error_msg}") 441 | return False 442 | finally: 443 | self._update_task_counters() 444 | self.logger.info(self.TASK_DIVIDER) 445 | 446 | def check_new_tasks(self): 447 | """ 448 | 检查配置文件中是否有新任务 449 | """ 450 | try: 451 | with open(self.config_path, 'r', encoding='utf-8') as f: 452 | task_list = yaml.safe_load(f) 453 | 454 | if not isinstance(task_list, list): 455 | return 456 | 457 | existing_tasks = {task.name for task in self.tasks} 458 | 459 | for task_config in task_list: 460 | task_name = task_config['name'] 461 | if task_name not in existing_tasks: 462 | self.logger.info(f"发现新任务: {task_name}") 463 | task = Task( 464 | name=task_config['name'], 465 | command=task_config['command'], 466 | status=task_config.get('status', 'pending') 467 | ) 468 | self.add_task(task) 469 | 470 | except Exception as e: 471 | self.logger.error(f"检查新任务时出错: {str(e)}") 472 | 473 | def run(self): 474 | """ 475 | 运行任务流管理器,开始执行任务队列 476 | """ 477 | self.running = True 478 | self.start_time = datetime.now() 479 | self.logger.info("任务流管理器启动") 480 | 481 | last_task_time = datetime.now() 482 | 483 | # 添加处理中断的代码 484 | try: 485 | while not self.stop_event.is_set(): 486 | try: 487 | self.check_new_tasks() 488 | task = self.task_queue.get(timeout=1) 489 | last_task_time = datetime.now() 490 | self.execute_task(task) 491 | except queue.Empty: 492 | # 计算已等待时间 493 | wait_time = (datetime.now() - last_task_time).total_seconds() 494 | 495 | # 每5秒打印一次等待信息 496 | if wait_time % 5 < 1: 497 | remaining = 60 - wait_time 498 | if remaining > 0: 499 | self.logger.info(f"等待新任务中... 还有 {int(remaining)} 秒后将检查新任务") 500 | 501 | # 如果已标记停止,立即退出循环 502 | if self.stop_event.is_set(): 503 | break 504 | 505 | if wait_time > 60: 506 | self.check_new_tasks() 507 | if self.task_queue.empty(): 508 | self.logger.info("1分钟内无新任务,准备停止任务流管理器") 509 | self.stop() 510 | continue 511 | except Exception as e: 512 | self.logger.error(f"执行任务时出现异常: {str(e)}") 513 | continue 514 | except KeyboardInterrupt: 515 | # 捕获键盘中断 516 | self.logger.info("接收到键盘中断,立即终止所有任务") 517 | self.stop() 518 | 519 | self.logger.info("任务流管理器已停止") 520 | self.running = False 521 | 522 | def stop(self): 523 | """ 524 | 停止任务流管理器并发送总结报告 525 | 526 | 如果设置了环境变量MTF_SILENT_MODE=true,将跳过消息发送,只记录日志 527 | """ 528 | self.stop_event.set() 529 | self.logger.info("正在停止任务流管理器...") 530 | 531 | # 等待当前任务完成 532 | if self.running: 533 | self.end_time = datetime.now() 534 | summary = self.generate_summary() 535 | self.logger.info("任务总结报告:") 536 | self.logger.info(summary) # 在日志中记录摘要 537 | 538 | # 检查环境变量MTF_SILENT_MODE 539 | if os.getenv('MTF_SILENT_MODE', '').lower() in ('true', '1', 'yes', 'on'): 540 | self.logger.info(f"环境变量MTF_SILENT_MODE已设置为{os.getenv('MTF_SILENT_MODE')},跳过发送总结报告") 541 | return 542 | 543 | # 发送总结报告 544 | self.logger.info("发送任务总结报告...") 545 | Msg_push( 546 | title="任务流管理器执行报告", 547 | content=summary, 548 | logger=self.logger 549 | ) 550 | 551 | def is_running(self) -> bool: 552 | """ 553 | 返回任务流管理器是否正在运行 554 | 555 | Returns: 556 | bool: 是否正在运行 557 | """ 558 | return self.running 559 | 560 | def main(): 561 | """ 562 | 任务流管理器的命令行入口点 563 | 564 | 用法: 565 | taskflow [config_file_path] 566 | 567 | 参数: 568 | config_file_path: 任务配置文件路径,默认为 examples/tasks.yaml 569 | """ 570 | import sys 571 | import signal 572 | from threading import Thread 573 | 574 | def signal_handler(signum, frame): 575 | """处理终止信号""" 576 | print("\n接收到终止信号,正在终止所有进程...") 577 | 578 | # 1. 尝试获取manager实例 - 只获取一次 579 | manager_instance = None 580 | try: 581 | # 从globals获取 582 | if 'manager' in globals(): 583 | manager_instance = globals()['manager'] 584 | print("通过globals获取到manager实例") 585 | 586 | # 如果未找到,从frame获取 587 | if manager_instance is None: 588 | for frame_info in inspect.getouterframes(frame): 589 | if 'manager' in frame_info.frame.f_locals: 590 | manager_instance = frame_info.frame.f_locals['manager'] 591 | print("通过栈帧获取到manager实例") 592 | break 593 | except Exception as e: 594 | print(f"获取manager实例时出错: {e}") 595 | 596 | # 2. 打印任务摘要 597 | print("\n--- 任务执行摘要 ---") 598 | if manager_instance is not None: 599 | try: 600 | print(f"任务总数: {len(manager_instance.tasks)}") 601 | running = 0 602 | pending = 0 603 | completed = 0 604 | failed = 0 605 | 606 | # 统计各状态任务数量 607 | for task in manager_instance.tasks: 608 | if task.status == "running": 609 | running += 1 610 | elif task.status == "pending": 611 | pending += 1 612 | elif task.status == "completed": 613 | completed += 1 614 | elif task.status == "failed": 615 | failed += 1 616 | 617 | print(f"已完成: {completed} | 运行中: {running} | 等待中: {pending} | 失败: {failed}") 618 | 619 | # 显示正在运行的任务 620 | if running > 0: 621 | print("\n当前运行的任务:") 622 | for task in manager_instance.tasks: 623 | if task.status == "running": 624 | start_time = task.start_time.strftime('%H:%M:%S') if task.start_time else "未知" 625 | print(f" - {task.name} (开始于 {start_time})") 626 | 627 | # 显示等待执行的任务 628 | if pending > 0: 629 | print("\n等待执行的任务:") 630 | shown = 0 631 | for task in manager_instance.tasks: 632 | if task.status == "pending": 633 | print(f" - {task.name}") 634 | shown += 1 635 | if shown >= 3: # 只显示前3个 636 | if pending > 3: 637 | print(f" ... 还有 {pending-3} 个任务") 638 | break 639 | except Exception as e: 640 | print(f"生成任务摘要时出错: {e}") 641 | else: 642 | print("无法获取任务信息") 643 | 644 | print("------------------------\n") 645 | 646 | # 3. 取消未执行任务并终止正在执行的任务 647 | if manager_instance is not None: 648 | try: 649 | # 取消队列中的任务 650 | print("正在取消所有待执行任务...") 651 | 652 | # 清空任务队列 653 | task_queue_cleared = False 654 | try: 655 | while not manager_instance.task_queue.empty(): 656 | manager_instance.task_queue.get_nowait() 657 | task_queue_cleared = True 658 | except Exception as e: 659 | print(f"清空任务队列时出错: {e}") 660 | 661 | # 标记所有未开始任务为取消状态 662 | canceled_count = 0 663 | try: 664 | for task in manager_instance.tasks: 665 | if task.status == "pending": 666 | task.status = "canceled" 667 | canceled_count += 1 668 | print(f"已取消 {canceled_count} 个待执行任务") 669 | except Exception as e: 670 | print(f"标记取消任务时出错: {e}") 671 | 672 | # 终止正在执行的任务 673 | try: 674 | for task in manager_instance.tasks: 675 | if task.status == "running" and task.process: 676 | print(f"终止正在执行的任务: {task.name}") 677 | task.process.terminate() 678 | except Exception as e: 679 | print(f"终止运行任务时出错: {e}") 680 | 681 | # 尝试通过manager.stop()生成报告 682 | try: 683 | if manager_instance.is_running(): 684 | print("通过manager.stop()生成报告...") 685 | manager_instance.stop() 686 | except Exception as e: 687 | print(f"停止manager时出错: {e}") 688 | except Exception as e: 689 | print(f"取消任务时出错: {e}") 690 | else: 691 | print("未找到manager实例,无法取消任务") 692 | 693 | # 4. 终止所有子进程 - 作为最后的保障 694 | try: 695 | current_pid = os.getpid() 696 | all_children = psutil.Process(current_pid).children(recursive=True) 697 | if all_children: 698 | print(f"终止 {len(all_children)} 个子进程...") 699 | for child in all_children: 700 | try: 701 | child.terminate() 702 | except: 703 | pass 704 | 705 | # 等待子进程终止 706 | gone, alive = psutil.wait_procs(all_children, timeout=3) 707 | if alive: 708 | print(f"强制终止 {len(alive)} 个未响应进程...") 709 | for p in alive: 710 | try: 711 | p.kill() 712 | except: 713 | pass 714 | except Exception as e: 715 | print(f"终止子进程时出错: {e}") 716 | 717 | # 5. 最终退出 718 | print("立即退出程序...") 719 | os._exit(0) # 强制退出 720 | 721 | # 注册信号处理器 722 | signal.signal(signal.SIGINT, signal_handler) 723 | signal.signal(signal.SIGTERM, signal_handler) 724 | 725 | try: 726 | # 检查是否存在帮助参数或没有提供任何参数 727 | if len(sys.argv) <= 1 or sys.argv[1] in ['-h', '--help']: 728 | print_help_message() 729 | sys.exit(0) 730 | 731 | # 有参数但不是帮助参数,视为配置文件路径 732 | config_path = sys.argv[1] 733 | 734 | # 检查配置文件是否存在 735 | if not os.path.exists(config_path): 736 | print(f"\033[1;31m错误:配置文件 '{config_path}' 不存在!\033[0m") 737 | print_help_message() 738 | sys.exit(1) 739 | 740 | # 创建并启动任务流管理器 741 | manager = TaskFlow(config_path) 742 | manager_thread = Thread(target=manager.run) 743 | manager_thread.start() 744 | manager_thread.join() 745 | except Exception as e: 746 | print(f"任务流管理器运行出错: {str(e)}") 747 | if 'manager' in globals() and manager.is_running(): 748 | manager.stop() 749 | finally: 750 | if 'manager' in globals() and manager.is_running(): 751 | manager.stop() 752 | if 'manager_thread' in locals() and manager_thread.is_alive(): 753 | manager_thread.join() 754 | 755 | def print_help_message(): 756 | """打印帮助信息""" 757 | print("\n\033[1;36m=== MultiTaskFlow 使用帮助 ===\033[0m") 758 | print("\033[1m用法:\033[0m python MultiTaskFlow.py <配置文件路径>") 759 | print("\n\033[1m参数:\033[0m") 760 | print(" <配置文件路径> YAML格式的任务配置文件路径") 761 | print(" -h, --help 显示此帮助信息并退出") 762 | 763 | print("\n\033[1m命令行使用示例:\033[0m") 764 | print(" # 使用python:配置文件启动任务流") 765 | print(" python MultiTaskFlow.py tasks.yaml") 766 | print("") 767 | print(" # 使用python:后台运行并记录日志") 768 | print(" nohup python MultiTaskFlow.py my_tasks.yaml > taskflow.log 2>&1 &") 769 | print("") 770 | print(" # 使用脚本启动") 771 | print(" bash start_taskflow.sh") 772 | print("") 773 | print(" # 使用脚本停止") 774 | print(" bash stop_taskflow.sh") 775 | print("") 776 | print(" # 使用shell命令启动") 777 | print(" taskflow my_tasks.yaml") 778 | 779 | print("\n\033[1m配置文件格式示例:\033[0m") 780 | print("""# 任务流配置示例 781 | # 每个任务包含名称和要执行的命令 782 | # 任务将按照列表顺序依次执行 783 | 784 | - name: "示例任务1" 785 | command: "python example1.py" 786 | status: "pending" 787 | 788 | - name: "示例任务2" 789 | command: "python example2.py" 790 | status: "pending" 791 | """) 792 | 793 | print("\n\033[1m环境变量配置:\033[0m") 794 | print(" 可在当前目录创建 .env 文件,配置以下环境变量:") 795 | print(" PUSH_TOKEN - 消息推送令牌 (用于任务完成通知)") 796 | print(" PUSH_CHANNEL - 推送渠道 (TODO)") 797 | 798 | print("\n\033[1m更多信息:\033[0m") 799 | print("请访问 GitHub 项目页面查看详细文档:") 800 | print("\033[1;34mhttps://github.com/Polaris-F/MultiTaskFlow\033[0m\n") 801 | 802 | if __name__ == "__main__": 803 | """ 804 | 任务流管理器的使用示例 805 | """ 806 | main() -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "multitaskflow" 7 | version = "0.1.3" 8 | description = "一个用于管理和监控多个任务执行的Python工具" 9 | readme = "README.md" 10 | authors = [ 11 | {name = "Polaris", email = "polaris0532@outlook.com"} 12 | ] 13 | license = "MIT" 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "Intended Audience :: Science/Research", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Operating System :: OS Independent", 24 | "Topic :: Scientific/Engineering", 25 | "Topic :: Software Development :: Libraries", 26 | ] 27 | requires-python = ">=3.6" 28 | dependencies = [ 29 | "pyyaml>=5.1", 30 | "psutil>=5.8.0", 31 | "python-dotenv>=0.19.0", 32 | "requests>=2.25.0", 33 | "build>=0.10.0", # 添加构建工具依赖 34 | ] 35 | 36 | [project.urls] 37 | "Homepage" = "https://github.com/Polaris-F/MultiTaskFlow" 38 | "Bug Tracker" = "https://github.com/Polaris-F/MultiTaskFlow/issues" 39 | "主页" = "https://github.com/Polaris-F/MultiTaskFlow" 40 | "问题追踪" = "https://github.com/Polaris-F/MultiTaskFlow/issues" 41 | 42 | [project.scripts] 43 | taskflow = "multitaskflow.task_flow:main" 44 | 45 | [tool.setuptools] 46 | packages = ["multitaskflow"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml>=5.1 2 | psutil>=5.8.0 3 | python-dotenv>=0.19.0 4 | requests>=2.25.0 -------------------------------------------------------------------------------- /taskflowPro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #======= 用户可配置变量 ======= 4 | # 配置你的conda环境名称 5 | CONDA_ENV="pytorch24" 6 | # 配置你的主脚本名称(如果不使用shell命令) 7 | SCRIPT_NAME="MultiTaskFlow.py" 8 | # 配置PID文件名称 9 | PID_FILE_NAME="multitaskflow.pid" 10 | # 任务配置文件路径(完整路径,例如:/path/to/your/TaskFlow.yaml) 11 | TASK_CONFIG="path/to/your/TaskFlow.yaml" 12 | # 配置等待时间(秒) 13 | WAIT_TIME=3 14 | #============================ 15 | 16 | # 定义颜色输出 17 | GREEN='\033[0;32m' 18 | RED='\033[0;31m' 19 | YELLOW='\033[0;33m' 20 | BLUE='\033[0;34m' 21 | NC='\033[0m' # 无颜色 22 | 23 | # 检查命令行参数 24 | if [ $# -lt 1 ]; then 25 | echo -e "${RED}错误: 缺少操作参数${NC}" 26 | echo -e "用法: $0 [start|stop] [配置文件路径]" 27 | echo -e "示例: $0 start" 28 | echo -e " $0 stop" 29 | echo -e " $0 start /path/to/your/CustomTask.yaml" 30 | exit 1 31 | fi 32 | 33 | ACTION=$1 34 | shift # 移除第一个参数 35 | 36 | # 检查是否有自定义配置文件路径 37 | if [ $# -gt 0 ]; then 38 | TASK_CONFIG="$1" 39 | fi 40 | 41 | ## 获取当前脚本所在目录 42 | current_dir=$(cd $(dirname $0); pwd) 43 | echo -e "${BLUE}当前脚本目录: ${current_dir}${NC}" 44 | 45 | ## 检查配置文件并设置工作目录 46 | if [ -z "$TASK_CONFIG" ]; then 47 | echo -e "${RED}错误: 未指定任务配置文件路径${NC}" 48 | exit 1 49 | fi 50 | 51 | if [ ! -f "$TASK_CONFIG" ]; then 52 | echo -e "${RED}错误: 配置文件 '$TASK_CONFIG' 不存在${NC}" 53 | exit 1 54 | fi 55 | 56 | # 提取配置文件所在目录作为工作目录 57 | target_dir=$(dirname "$TASK_CONFIG") 58 | config_file=$(basename "$TASK_CONFIG") 59 | echo -e "${BLUE}任务配置文件: ${TASK_CONFIG}${NC}" 60 | echo -e "${BLUE}工作目录: ${target_dir}${NC}" 61 | 62 | ## 日志目录和PID文件路径 63 | log_dir="${target_dir}/logs" 64 | pid_file="${log_dir}/${PID_FILE_NAME}" 65 | 66 | ## 根据操作执行不同功能 67 | case $ACTION in 68 | start) 69 | # 启动任务函数 70 | ## 获取当前日期 71 | current_date=$(date +%Y%m%d_%H%M%S) 72 | echo -e "${BLUE}运行日期: ${current_date}${NC}" 73 | 74 | ## 检查日志目录 75 | if [ ! -d "$log_dir" ]; then 76 | mkdir -p "$log_dir" 77 | echo -e "${BLUE}创建日志目录: ${log_dir}${NC}" 78 | fi 79 | 80 | ## 日志文件名 81 | log_file="${log_dir}/TaskFlow_full_${current_date}.log" 82 | echo -e "${BLUE}日志将保存至: ${log_file}${NC}" 83 | 84 | ## 激活conda环境 85 | echo -e "${BLUE}正在激活conda环境 '${CONDA_ENV}'...${NC}" 86 | source $(conda info --base)/etc/profile.d/conda.sh 87 | conda activate $CONDA_ENV || { echo -e "${YELLOW}conda环境 '${CONDA_ENV}' 激活失败,尝试继续执行...${NC}"; } 88 | 89 | # 检查是否存在taskflow命令 90 | if command -v taskflow &> /dev/null; then 91 | echo -e "${GREEN}发现taskflow命令,使用shell命令运行${NC}" 92 | CMD="cd ${target_dir} && taskflow ${config_file}" 93 | else 94 | # 检查脚本是否存在 95 | if [ ! -f "${target_dir}/${SCRIPT_NAME}" ]; then 96 | echo -e "${YELLOW}警告: ${target_dir}/${SCRIPT_NAME} 文件不存在!${NC}" 97 | exit 1 98 | fi 99 | echo -e "${GREEN}使用Python脚本运行${NC}" 100 | CMD="cd ${target_dir} && python ${SCRIPT_NAME} ${config_file}" 101 | fi 102 | 103 | ## 运行脚本并保存PID 104 | echo -e "${GREEN}启动任务流管理器...${NC}" 105 | echo -e "${BLUE}执行命令: ${CMD}${NC}" 106 | nohup bash -c "$CMD" > "$log_file" 2>&1 & 107 | PID=$! 108 | 109 | ## 保存PID到logs目录下的文件 110 | echo $PID > ${pid_file} 111 | echo -e "${GREEN}任务流已在后台启动,PID: ${PID}${NC}" 112 | echo -e "${BLUE}PID保存至: ${pid_file}${NC}" 113 | echo -e "${BLUE}要查看日志,请运行: tail -f ${log_file}${NC}" 114 | echo -e "${BLUE}要终止进程,请运行: $0 stop ${TASK_CONFIG}${NC}" 115 | ;; 116 | 117 | stop) 118 | # 打印标题 119 | echo -e "${BLUE}===========================================${NC}" 120 | echo -e "${RED}TaskFlow进程终止工具${NC}" 121 | echo -e "${BLUE}===========================================${NC}" 122 | 123 | # 首先检查logs目录下的PID文件是否存在 124 | if [ -f ${pid_file} ]; then 125 | PID=$(cat ${pid_file}) 126 | echo -e "${BLUE}找到PID文件: ${pid_file}${NC}" 127 | 128 | if ps -p $PID > /dev/null; then 129 | echo -e "${YELLOW}进程正在运行 (PID: $PID)${NC}" 130 | echo -e "${RED}正在终止进程...${NC}" 131 | kill -9 $PID 132 | rm ${pid_file} 133 | echo -e "${GREEN}✓ 进程已成功终止${NC}" 134 | echo -e "${GREEN}✓ 已删除PID文件${NC}" 135 | else 136 | echo -e "${YELLOW}PID文件存在,但进程 (PID: $PID) 已不存在${NC}" 137 | rm ${pid_file} 138 | echo -e "${GREEN}✓ 已清理PID文件${NC}" 139 | fi 140 | else 141 | echo -e "${YELLOW}未找到PID文件: ${pid_file}${NC}" 142 | echo -e "${BLUE}尝试查找TaskFlow进程...${NC}" 143 | 144 | # 检查是否存在taskflow命令 145 | if command -v taskflow &> /dev/null; then 146 | echo -e "${BLUE}检查taskflow命令启动的进程...${NC}" 147 | TASKFLOW_PID=$(ps aux | grep "[t]askflow.*${config_file}" | grep "$target_dir" | awk '{print $2}') 148 | 149 | if [ ! -z "$TASKFLOW_PID" ]; then 150 | echo -e "${YELLOW}找到taskflow进程 (PID: $TASKFLOW_PID)${NC}" 151 | echo -e "${RED}正在终止进程...${NC}" 152 | kill -9 $TASKFLOW_PID 153 | echo -e "${GREEN}✓ 进程已成功终止${NC}" 154 | else 155 | echo -e "${YELLOW}未找到使用taskflow命令启动的进程${NC}" 156 | fi 157 | fi 158 | 159 | # 检查Python脚本启动的进程 160 | echo -e "${BLUE}检查Python脚本启动的进程...${NC}" 161 | PYTHON_PID=$(ps aux | grep "[p]ython.*${SCRIPT_NAME}.*${config_file}" | grep "$target_dir" | awk '{print $2}') 162 | 163 | if [ ! -z "$PYTHON_PID" ]; then 164 | echo -e "${YELLOW}找到Python进程 (PID: $PYTHON_PID)${NC}" 165 | echo -e "${RED}正在终止进程...${NC}" 166 | kill -9 $PYTHON_PID 167 | echo -e "${GREEN}✓ 进程已成功终止${NC}" 168 | else 169 | echo -e "${YELLOW}未找到使用Python启动的${SCRIPT_NAME}进程${NC}" 170 | fi 171 | fi 172 | 173 | # 查找子进程 - 更通用的方法 174 | echo -e "${BLUE}查找可能的子进程...${NC}" 175 | # 获取所有Python进程 176 | CHILD_PROCESSES=$(ps aux | grep "[p]ython" | grep "$target_dir" | grep -v "taskflow\|${SCRIPT_NAME}\|grep" | awk '{print $2}') 177 | 178 | if [ ! -z "$CHILD_PROCESSES" ]; then 179 | echo -e "${YELLOW}发现Python子进程:${NC}" 180 | ps aux | grep "[p]ython" | grep "$target_dir" | grep -v "taskflow\|${SCRIPT_NAME}\|grep" | awk '{printf " PID: %s CMD: %s\n", $2, $11}' 181 | 182 | # 终止所有子进程 183 | for pid in $CHILD_PROCESSES; do 184 | echo -e "${RED}正在终止子进程 (PID: $pid)...${NC}" 185 | kill -9 $pid 186 | done 187 | echo -e "${GREEN}✓ 所有子进程已终止${NC}" 188 | 189 | # 等待进程完全终止 190 | echo -e "${BLUE}等待${WAIT_TIME}秒确保所有进程完全终止...${NC}" 191 | sleep $WAIT_TIME 192 | 193 | # 再次检查是否有残留进程 194 | REMAINING=$(ps aux | grep "[p]ython" | grep "$target_dir" | grep -v "taskflow\|${SCRIPT_NAME}\|grep" | wc -l) 195 | if [ $REMAINING -gt 0 ]; then 196 | echo -e "${YELLOW}警告: 仍有${REMAINING}个进程未终止,请手动检查${NC}" 197 | else 198 | echo -e "${GREEN}✓ 确认所有进程已终止${NC}" 199 | fi 200 | else 201 | echo -e "${GREEN}✓ 未发现Python子进程${NC}" 202 | fi 203 | 204 | echo -e "${GREEN}✓ 操作完成${NC}" 205 | echo -e "${BLUE}===========================================${NC}" 206 | ;; 207 | 208 | *) 209 | echo -e "${RED}错误: 未知操作 ${ACTION}${NC}" 210 | echo -e "支持的操作: start, stop" 211 | exit 1 212 | ;; 213 | esac --------------------------------------------------------------------------------