pyxllib 0.3.197__py3-none-any.whl → 0.3.200__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyxllib/__init__.py +21 -21
- pyxllib/algo/__init__.py +8 -8
- pyxllib/algo/disjoint.py +54 -54
- pyxllib/algo/geo.py +541 -541
- pyxllib/algo/intervals.py +964 -964
- pyxllib/algo/matcher.py +389 -389
- pyxllib/algo/newbie.py +166 -166
- pyxllib/algo/pupil.py +629 -629
- pyxllib/algo/shapelylib.py +67 -67
- pyxllib/algo/specialist.py +241 -241
- pyxllib/algo/stat.py +494 -494
- pyxllib/algo/treelib.py +149 -149
- pyxllib/algo/unitlib.py +66 -66
- pyxllib/autogui/__init__.py +5 -5
- pyxllib/autogui/activewin.py +246 -246
- pyxllib/autogui/all.py +9 -9
- pyxllib/autogui/autogui.py +852 -852
- pyxllib/autogui/uiautolib.py +362 -362
- pyxllib/autogui/virtualkey.py +102 -102
- pyxllib/autogui/wechat.py +827 -827
- pyxllib/autogui/wechat_msg.py +421 -421
- pyxllib/autogui/wxautolib.py +84 -84
- pyxllib/cv/__init__.py +5 -5
- pyxllib/cv/expert.py +267 -267
- pyxllib/cv/imfile.py +159 -159
- pyxllib/cv/imhash.py +39 -39
- pyxllib/cv/pupil.py +9 -9
- pyxllib/cv/rgbfmt.py +1525 -1525
- pyxllib/cv/slidercaptcha.py +137 -137
- pyxllib/cv/trackbartools.py +251 -251
- pyxllib/cv/xlcvlib.py +1040 -1040
- pyxllib/cv/xlpillib.py +423 -423
- pyxllib/data/echarts.py +240 -240
- pyxllib/data/jsonlib.py +89 -89
- pyxllib/data/oss.py +72 -72
- pyxllib/data/pglib.py +1127 -1127
- pyxllib/data/sqlite.py +568 -568
- pyxllib/data/sqllib.py +297 -297
- pyxllib/ext/JLineViewer.py +505 -505
- pyxllib/ext/__init__.py +6 -6
- pyxllib/ext/demolib.py +246 -246
- pyxllib/ext/drissionlib.py +277 -277
- pyxllib/ext/kq5034lib.py +12 -12
- pyxllib/ext/old.py +663 -663
- pyxllib/ext/qt.py +449 -449
- pyxllib/ext/robustprocfile.py +497 -497
- pyxllib/ext/seleniumlib.py +76 -76
- pyxllib/ext/tk.py +173 -173
- pyxllib/ext/unixlib.py +827 -827
- pyxllib/ext/utools.py +351 -351
- pyxllib/ext/webhook.py +124 -119
- pyxllib/ext/win32lib.py +40 -40
- pyxllib/ext/wjxlib.py +88 -88
- pyxllib/ext/wpsapi.py +124 -124
- pyxllib/ext/xlwork.py +9 -9
- pyxllib/ext/yuquelib.py +1105 -1105
- pyxllib/file/__init__.py +17 -17
- pyxllib/file/docxlib.py +761 -761
- pyxllib/file/gitlib.py +309 -309
- pyxllib/file/libreoffice.py +165 -165
- pyxllib/file/movielib.py +148 -148
- pyxllib/file/newbie.py +10 -10
- pyxllib/file/onenotelib.py +1469 -1469
- pyxllib/file/packlib/__init__.py +330 -330
- pyxllib/file/packlib/zipfile.py +2441 -2441
- pyxllib/file/pdflib.py +426 -426
- pyxllib/file/pupil.py +185 -185
- pyxllib/file/specialist/__init__.py +685 -685
- pyxllib/file/specialist/dirlib.py +799 -799
- pyxllib/file/specialist/download.py +193 -193
- pyxllib/file/specialist/filelib.py +2829 -2829
- pyxllib/file/xlsxlib.py +3131 -3131
- pyxllib/file/xlsyncfile.py +341 -341
- pyxllib/prog/__init__.py +5 -5
- pyxllib/prog/cachetools.py +64 -64
- pyxllib/prog/deprecatedlib.py +233 -233
- pyxllib/prog/filelock.py +42 -42
- pyxllib/prog/ipyexec.py +253 -253
- pyxllib/prog/multiprogs.py +940 -940
- pyxllib/prog/newbie.py +451 -451
- pyxllib/prog/pupil.py +1197 -1197
- pyxllib/prog/sitepackages.py +33 -33
- pyxllib/prog/specialist/__init__.py +391 -391
- pyxllib/prog/specialist/bc.py +203 -203
- pyxllib/prog/specialist/browser.py +497 -497
- pyxllib/prog/specialist/common.py +347 -347
- pyxllib/prog/specialist/datetime.py +198 -198
- pyxllib/prog/specialist/tictoc.py +240 -240
- pyxllib/prog/specialist/xllog.py +180 -180
- pyxllib/prog/xlosenv.py +108 -108
- pyxllib/stdlib/__init__.py +17 -17
- pyxllib/stdlib/tablepyxl/__init__.py +10 -10
- pyxllib/stdlib/tablepyxl/style.py +303 -303
- pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
- pyxllib/text/__init__.py +8 -8
- pyxllib/text/ahocorasick.py +39 -39
- pyxllib/text/airscript.js +744 -744
- pyxllib/text/charclasslib.py +121 -121
- pyxllib/text/jiebalib.py +267 -267
- pyxllib/text/jinjalib.py +32 -32
- pyxllib/text/jsa_ai_prompt.md +271 -271
- pyxllib/text/jscode.py +922 -922
- pyxllib/text/latex/__init__.py +158 -158
- pyxllib/text/levenshtein.py +303 -303
- pyxllib/text/nestenv.py +1215 -1215
- pyxllib/text/newbie.py +300 -300
- pyxllib/text/pupil/__init__.py +8 -8
- pyxllib/text/pupil/common.py +1121 -1121
- pyxllib/text/pupil/xlalign.py +326 -326
- pyxllib/text/pycode.py +47 -47
- pyxllib/text/specialist/__init__.py +8 -8
- pyxllib/text/specialist/common.py +112 -112
- pyxllib/text/specialist/ptag.py +186 -186
- pyxllib/text/spellchecker.py +172 -172
- pyxllib/text/templates/echart_base.html +10 -10
- pyxllib/text/templates/highlight_code.html +16 -16
- pyxllib/text/templates/latex_editor.html +102 -102
- pyxllib/text/vbacode.py +17 -17
- pyxllib/text/xmllib.py +747 -747
- pyxllib/xl.py +42 -39
- pyxllib/xlcv.py +17 -17
- {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/METADATA +1 -1
- pyxllib-0.3.200.dist-info/RECORD +126 -0
- {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/licenses/LICENSE +190 -190
- pyxllib-0.3.197.dist-info/RECORD +0 -126
- {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/WHEEL +0 -0
pyxllib/prog/multiprogs.py
CHANGED
@@ -1,940 +1,940 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
# @Author : 陈坤泽
|
4
|
-
# @Email : 877362867@qq.com
|
5
|
-
# @Date : 2024/11/12
|
6
|
-
|
7
|
-
from collections import defaultdict
|
8
|
-
from types import SimpleNamespace
|
9
|
-
from unittest.mock import Mock
|
10
|
-
import ctypes
|
11
|
-
import datetime
|
12
|
-
import os
|
13
|
-
import socketserver
|
14
|
-
import subprocess
|
15
|
-
import sys
|
16
|
-
import time
|
17
|
-
import textwrap
|
18
|
-
import threading
|
19
|
-
|
20
|
-
from deprecated import deprecated
|
21
|
-
from loguru import logger
|
22
|
-
from croniter import croniter
|
23
|
-
import pandas as pd
|
24
|
-
|
25
|
-
from fastapi import FastAPI
|
26
|
-
from fastapi.responses import PlainTextResponse
|
27
|
-
|
28
|
-
from pyxllib.prog.specialist import parse_datetime
|
29
|
-
from pyxllib.algo.stat import print_full_dataframe
|
30
|
-
from pyxllib.file.specialist import XlPath
|
31
|
-
|
32
|
-
|
33
|
-
def __1_定时工具():
|
34
|
-
pass
|
35
|
-
|
36
|
-
|
37
|
-
class SchedulerUtils:
|
38
|
-
@classmethod
|
39
|
-
def calculate_future_time(cls, start_time, wait_seconds):
|
40
|
-
""" 计算延迟时间
|
41
|
-
|
42
|
-
:param datetime start_time: 开始时间
|
43
|
-
:param int wait_seconds: 等待秒数
|
44
|
-
todo 先只支持秒数这种标准秒数,后续可以考虑支持更多智能的"1小时"等这种解析
|
45
|
-
"""
|
46
|
-
return start_time + datetime.timedelta(seconds=wait_seconds)
|
47
|
-
|
48
|
-
@classmethod
|
49
|
-
def calculate_next_cron_time(cls, cron_tag, base_time=None):
|
50
|
-
""" 使用crontab标记的运行周期,然后计算相对当前时间,下一次要启动运行的时间
|
51
|
-
|
52
|
-
:param str cron_tag: 自定义的cron标记,跟asp的差不多,但星期几部分做了调整
|
53
|
-
30 2 * * 1: 这部分是时间和日期的设定,具体含义如下:
|
54
|
-
30: 表示分钟,即每小时的第 30 分钟。
|
55
|
-
2: 表示小时,即凌晨 2 点。
|
56
|
-
第三个星号 *: 表示日,这里的星号意味着每天。
|
57
|
-
第四个星号 *: 表示月份,星号同样表示每个月。
|
58
|
-
1: 表示星期中的日子,这里的 1 代表星期一,7表示星期日。不能写1~7以外的值。
|
59
|
-
:param datetime base_time: 基于哪个时间点计算下次时间
|
60
|
-
"""
|
61
|
-
|
62
|
-
# 如果没有提供基准时间,则使用当前时间
|
63
|
-
if base_time is None:
|
64
|
-
base_time = datetime.datetime.now()
|
65
|
-
# 初始化 croniter 对象
|
66
|
-
cron = croniter(cron_tag, base_time)
|
67
|
-
# 计算下一次运行时间
|
68
|
-
next_time = cron.get_next(datetime.datetime)
|
69
|
-
return next_time
|
70
|
-
|
71
|
-
@classmethod
|
72
|
-
def wait_until_time(cls, dst_time):
|
73
|
-
"""
|
74
|
-
:param datetime dst_time: 一直等待到目标时间
|
75
|
-
期间可以用time.sleep进行等待
|
76
|
-
"""
|
77
|
-
# 一般来说,只要计算一轮待等待秒数就行。但是time.sleep机制好像不一定准确的,所以使用无限循环重试会更好。
|
78
|
-
while True:
|
79
|
-
# 先计算当前时间和目标时间的相差秒数
|
80
|
-
wait_seconds = (dst_time - datetime.datetime.now()).total_seconds()
|
81
|
-
if wait_seconds <= 0:
|
82
|
-
break
|
83
|
-
time.sleep(max(1, wait_seconds)) # 最少等待1秒
|
84
|
-
|
85
|
-
@classmethod
|
86
|
-
def smart_wait(cls, start_time, end_time, wait_tag, print_mode=0):
|
87
|
-
""" 智能等待,一般用在对进程的管理重启上
|
88
|
-
|
89
|
-
:param datetime start_time: 程序启动的时间
|
90
|
-
:param datetime end_time: 程序结束的时间
|
91
|
-
:param str|float|int wait_tag: 等待标记
|
92
|
-
str,按crontab解析
|
93
|
-
在end_time后满足条件的下次时间重启
|
94
|
-
int|float,表示等待的秒数
|
95
|
-
正值是end_time往后等待,负值是start_time开始计算下次时间。
|
96
|
-
比如1点开始的程序,等待半小时,但是首次运行到2点才结束
|
97
|
-
那么正值就是2:30再下次运行
|
98
|
-
但是负值表示1:30就要运行,已经错过了,马上2点结束就立即启动复跑
|
99
|
-
"""
|
100
|
-
# 1 尝试把wait_tag转成数值
|
101
|
-
try:
|
102
|
-
wait_tag = float(wait_tag)
|
103
|
-
except ValueError: # 转不成也没关系
|
104
|
-
pass
|
105
|
-
|
106
|
-
if start_time is None:
|
107
|
-
start_time = datetime.datetime.now()
|
108
|
-
if end_time is None:
|
109
|
-
end_time = datetime.datetime.now()
|
110
|
-
|
111
|
-
# 2 计算下一次启动时间
|
112
|
-
if isinstance(wait_tag, str):
|
113
|
-
# 按照crontab解析
|
114
|
-
next_time = cls.calculate_next_cron_time(wait_tag, end_time)
|
115
|
-
elif wait_tag >= 0:
|
116
|
-
# 正值则是从end_time开始往后等待
|
117
|
-
next_time = cls.calculate_future_time(end_time, wait_tag)
|
118
|
-
elif wait_tag < 0:
|
119
|
-
# 负值则是从start_time开始往前等待
|
120
|
-
next_time = cls.calculate_future_time(start_time, wait_tag)
|
121
|
-
else:
|
122
|
-
raise ValueError
|
123
|
-
|
124
|
-
if print_mode:
|
125
|
-
print(f'等待到时间{next_time}...')
|
126
|
-
|
127
|
-
cls.wait_until_time(next_time)
|
128
|
-
|
129
|
-
|
130
|
-
def __2_程序管理():
|
131
|
-
pass
|
132
|
-
|
133
|
-
|
134
|
-
def find_free_ports(count=1):
|
135
|
-
""" 随机获得可用端口
|
136
|
-
|
137
|
-
:param count: 需要的端口数量(会保证给出的端口号不重复)
|
138
|
-
:return: list
|
139
|
-
"""
|
140
|
-
ports = set()
|
141
|
-
while len(ports) < count:
|
142
|
-
with socketserver.TCPServer(("localhost", 0), None) as s:
|
143
|
-
ports.add(s.server_address[1])
|
144
|
-
|
145
|
-
return list(ports)
|
146
|
-
|
147
|
-
|
148
|
-
class ProgramWorker(SimpleNamespace):
|
149
|
-
"""
|
150
|
-
代表一个单独的程序(进程),基于 SimpleNamespace 实现。
|
151
|
-
"""
|
152
|
-
|
153
|
-
def __init__(self, name,
|
154
|
-
cmd,
|
155
|
-
shell=False,
|
156
|
-
run=True,
|
157
|
-
port=None,
|
158
|
-
locations=None,
|
159
|
-
raw_cmd=None,
|
160
|
-
**attrs):
|
161
|
-
"""
|
162
|
-
:param name: 程序昵称
|
163
|
-
:param program: 程序对象
|
164
|
-
:param port: 是否有执行所在端口
|
165
|
-
:param locations: 是否有url地址映射,一般用于nginx等配置
|
166
|
-
"""
|
167
|
-
super().__init__(**attrs)
|
168
|
-
|
169
|
-
# 执行程序需要使用的参数
|
170
|
-
self.cmd = cmd
|
171
|
-
self.shell = shell
|
172
|
-
self.run = run
|
173
|
-
|
174
|
-
# 关键参数
|
175
|
-
self.name = name
|
176
|
-
self.program = None
|
177
|
-
self.raw_cmd = raw_cmd
|
178
|
-
|
179
|
-
# 这两个是比较特别的属性,在我的工程框架中常用
|
180
|
-
self.port = port
|
181
|
-
self.locations = locations
|
182
|
-
|
183
|
-
# 特殊调度模式,需要用到程序启动、结束时间
|
184
|
-
self.last_start_time = None
|
185
|
-
self.last_end_time = None
|
186
|
-
|
187
|
-
def _set_pdeathsig(self, sig=None):
|
188
|
-
""" 在主服务退出时,这些程序也会全部自动关闭 """
|
189
|
-
|
190
|
-
def callable():
|
191
|
-
import signal
|
192
|
-
sig2 = signal.SIGTERM if sig is None else sig
|
193
|
-
libc = ctypes.CDLL("libc.so.6")
|
194
|
-
return libc.prctl(1, sig2)
|
195
|
-
|
196
|
-
if sys.platform == 'win32':
|
197
|
-
# windows系统暂设为空
|
198
|
-
return None
|
199
|
-
else:
|
200
|
-
return callable
|
201
|
-
|
202
|
-
def lanuch(self):
|
203
|
-
""" 启动程序 """
|
204
|
-
self.last_start_time = datetime.datetime.now()
|
205
|
-
self.last_end_time = None
|
206
|
-
|
207
|
-
if not self.run:
|
208
|
-
# 如果不需要立即启动,则返回一个 Mock 对象
|
209
|
-
proc = Mock()
|
210
|
-
proc.pid = None
|
211
|
-
proc.poll.return_value = 'tag'
|
212
|
-
self.program = proc
|
213
|
-
return self.program
|
214
|
-
|
215
|
-
kwargs = {}
|
216
|
-
for name in ['stdin', 'stdout', 'stderr']:
|
217
|
-
if hasattr(self, name):
|
218
|
-
kwargs[name] = getattr(self, name)
|
219
|
-
|
220
|
-
if sys.platform == 'win32':
|
221
|
-
self.program = subprocess.Popen(self.cmd, shell=self.shell, **kwargs)
|
222
|
-
else:
|
223
|
-
self.program = subprocess.Popen(self.cmd, shell=self.shell, **kwargs,
|
224
|
-
preexec_fn=self._set_pdeathsig())
|
225
|
-
|
226
|
-
return self.program
|
227
|
-
|
228
|
-
def terminate(self):
|
229
|
-
"""
|
230
|
-
比较优雅结束进程的方法
|
231
|
-
"""
|
232
|
-
if self.program is not None:
|
233
|
-
self.program.terminate()
|
234
|
-
if self.last_end_time is None:
|
235
|
-
self.last_end_time = datetime.datetime.now()
|
236
|
-
|
237
|
-
def kill(self):
|
238
|
-
"""
|
239
|
-
有时候需要强硬的kill方法来结束进程
|
240
|
-
"""
|
241
|
-
if self.program is not None:
|
242
|
-
self.program.kill()
|
243
|
-
if self.last_end_time is None:
|
244
|
-
self.last_end_time = datetime.datetime.now()
|
245
|
-
|
246
|
-
def is_running(self):
|
247
|
-
""" 检查程序是否在运行 """
|
248
|
-
status = self.program is not None and self.program.poll() is None
|
249
|
-
if not status and self.last_end_time is None: # 检测到程序运行结束,则标记下
|
250
|
-
self.last_end_time = datetime.datetime.now()
|
251
|
-
return status
|
252
|
-
|
253
|
-
|
254
|
-
class MultiProgramLauncher:
|
255
|
-
"""
|
256
|
-
管理多个程序的启动与终止
|
257
|
-
"""
|
258
|
-
|
259
|
-
def __init__(self):
|
260
|
-
self.workers = []
|
261
|
-
self.scheduler = None
|
262
|
-
|
263
|
-
def __1_进场管理的核心底层函数(self):
|
264
|
-
pass
|
265
|
-
|
266
|
-
def init_scheduler(self):
|
267
|
-
from apscheduler.schedulers.background import BackgroundScheduler
|
268
|
-
# from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
269
|
-
|
270
|
-
if self.scheduler is None:
|
271
|
-
self.scheduler = BackgroundScheduler()
|
272
|
-
|
273
|
-
return self.scheduler
|
274
|
-
|
275
|
-
def worker_add_schedule(self, worker, schedule=None, misfire_grace_time=None):
|
276
|
-
""" 将程序添加为定时任务
|
277
|
-
|
278
|
-
:param int|float|str|list schedule: 定时任务配置,如果提供则添加到 APScheduler 调度器
|
279
|
-
int/float: 每隔多少秒执行一次,可能同时有多个实例存在
|
280
|
-
tuple(value1, value2): 只允许单实例运行,这里记录的是上次实例结束后,等待多久再开启下次实例
|
281
|
-
此时第1个数值,正数表示等待上一次运行结束后,下次开始运行前的等待时间
|
282
|
-
负数表示是在上次启动后,等待多久就可以运行下一次
|
283
|
-
但下一次一定会在上一次运行结束后再续上
|
284
|
-
第2个参数,为了实现这套功能,其实需要一个监视器不断监控程序的运行状态
|
285
|
-
第2个参数是监控器检测的频率秒数
|
286
|
-
str: cron 表达式,支持 5 或 6 个字段
|
287
|
-
list[datetime]: 在指定时间节点列表运行
|
288
|
-
值只要是可以解析为datetime类型的都可以,底层使用特殊的解析器
|
289
|
-
"""
|
290
|
-
from datetime import datetime, timedelta
|
291
|
-
|
292
|
-
def task():
|
293
|
-
""" 启动任务并更新状态 """
|
294
|
-
# todo schedule不一定都是单实例阻塞情景的需求,以后有需要可以扩展支持多实例同时存在的非阻塞模式
|
295
|
-
if worker.is_running():
|
296
|
-
logger.warning(f'由于程序"{name}"在上一次周期还没运行完,新周期不重复启动')
|
297
|
-
else:
|
298
|
-
worker.lanuch()
|
299
|
-
|
300
|
-
self.init_scheduler()
|
301
|
-
|
302
|
-
name = worker.name
|
303
|
-
|
304
|
-
# 处理 schedule 类型
|
305
|
-
if isinstance(schedule, (int, float)):
|
306
|
-
# 如果是单一数值,按固定间隔执行,不考虑任务是否完成
|
307
|
-
self.scheduler.add_job(task, 'interval', seconds=abs(schedule))
|
308
|
-
logger.info(f"已添加定时任务:{name},监测频率:{schedule} 秒")
|
309
|
-
|
310
|
-
elif isinstance(schedule, tuple) and len(schedule) == 2:
|
311
|
-
wait_time, monitor_frequency = schedule
|
312
|
-
|
313
|
-
def interval_task():
|
314
|
-
# 如果还没有启动过任务,直接启动
|
315
|
-
if worker.last_start_time is None:
|
316
|
-
worker.lanuch()
|
317
|
-
return
|
318
|
-
|
319
|
-
# 如果任务正在运行,则不启动新任务
|
320
|
-
if worker.is_running():
|
321
|
-
return
|
322
|
-
|
323
|
-
# 计算下次启动时间
|
324
|
-
if wait_time < 0: # 基于上次启动时间
|
325
|
-
next_run = worker.last_start_time + timedelta(seconds=abs(wait_time))
|
326
|
-
else: # 基于上次结束时间
|
327
|
-
next_run = worker.last_end_time + timedelta(seconds=wait_time)
|
328
|
-
|
329
|
-
# 看现在是否需要重启
|
330
|
-
if datetime.now() >= next_run:
|
331
|
-
worker.lanuch()
|
332
|
-
|
333
|
-
# 以 monitor_frequency 作为定时检查的间隔
|
334
|
-
self.scheduler.add_job(interval_task, 'interval', seconds=monitor_frequency)
|
335
|
-
logger.info(f"已添加定时任务:{name},等待时间: {wait_time} 秒,监测频率: {monitor_frequency} 秒")
|
336
|
-
|
337
|
-
elif isinstance(schedule, str):
|
338
|
-
from apscheduler.triggers.cron import CronTrigger
|
339
|
-
cron_parts = schedule.split()
|
340
|
-
# 如果是 5 个字段,则补齐 "秒" 字段为 '0'
|
341
|
-
if len(cron_parts) == 5:
|
342
|
-
cron_parts.insert(0, '0')
|
343
|
-
# 检查是否为有效的 6 字段 cron 表达式
|
344
|
-
if len(cron_parts) == 6:
|
345
|
-
# 统一处理为 6 字段格式
|
346
|
-
# 把我自定义的cron的星期标记转换为aps的星期标记。前者用1234567,后者用0123456表示星期一到星期日
|
347
|
-
x = cron_parts[5]
|
348
|
-
if x != '*': # 写0或7都表示周日
|
349
|
-
if x == '0':
|
350
|
-
x = '7'
|
351
|
-
x = (int(x) - 1)
|
352
|
-
self.scheduler.add_job(
|
353
|
-
task,
|
354
|
-
CronTrigger(
|
355
|
-
second=cron_parts[0],
|
356
|
-
minute=cron_parts[1],
|
357
|
-
hour=cron_parts[2],
|
358
|
-
day=cron_parts[3],
|
359
|
-
month=cron_parts[4],
|
360
|
-
day_of_week=x,
|
361
|
-
),
|
362
|
-
# 检测的时候有概率错过了精确时间点。但一般不论延迟了多久,都要补运行上。
|
363
|
-
misfire_grace_time=misfire_grace_time,
|
364
|
-
)
|
365
|
-
logger.info(f"已添加定时任务:{name},触发器: cron,表达式: {schedule}")
|
366
|
-
else:
|
367
|
-
logger.warning(f"无效的 cron 表达式:{schedule}")
|
368
|
-
|
369
|
-
elif isinstance(schedule, list):
|
370
|
-
# 支持 list[datetime] 格式
|
371
|
-
for run_time in schedule:
|
372
|
-
run_time = parse_datetime(run_time)
|
373
|
-
self.scheduler.add_job(task, 'date', run_date=run_time)
|
374
|
-
logger.info(f"已添加定时任务:{name},触发器: dates,运行时间: {schedule}")
|
375
|
-
|
376
|
-
else:
|
377
|
-
logger.warning(f"无效的调度格式,跳过任务:{name}")
|
378
|
-
|
379
|
-
def add_program_cmd(self,
|
380
|
-
cmd,
|
381
|
-
name=None,
|
382
|
-
shell=False,
|
383
|
-
run=True,
|
384
|
-
schedule=None,
|
385
|
-
**attrs):
|
386
|
-
"""
|
387
|
-
启动一个程序,或仅存储任务,并添加进管理列表
|
388
|
-
|
389
|
-
:param cmd: 启动程序的命令
|
390
|
-
:param name: 程序名称,如果未提供则从cmd中自动获取
|
391
|
-
:param shell:
|
392
|
-
False,(优先推荐)直接跟系统交互,此时cmd应该输入数组格式
|
393
|
-
True,启用shell进行交互操作,这种情况会更适合管道等模式的处理,此时cmd应该输入字符串格式
|
394
|
-
:param int|float|str|list schedule: 定时任务配置,如果提供则添加到 APScheduler 调度器
|
395
|
-
:param run: 如果为True,则立即启动程序;否则仅存储任务信息
|
396
|
-
:param attrs: 其他需要传递给ProgramWorker的参数
|
397
|
-
:return: 返回一个ProgramWorker实例,表示启动的程序或存储的任务
|
398
|
-
"""
|
399
|
-
# 1 如果未显式传入name,自动从cmd中获取程序名称
|
400
|
-
# logger.info(cmd)
|
401
|
-
if name is None:
|
402
|
-
_cmd = cmd.split() if isinstance(cmd, str) else cmd
|
403
|
-
name = _cmd[0]
|
404
|
-
|
405
|
-
# 2 创建 ProgramWorker 实例
|
406
|
-
worker = ProgramWorker(name, cmd, shell, run, raw_cmd=cmd, **attrs)
|
407
|
-
self.workers.append(worker)
|
408
|
-
|
409
|
-
# 3 如果有 schedule 配置,添加调度任务;否则就是正常的启动任务
|
410
|
-
if schedule:
|
411
|
-
self.worker_add_schedule(worker, schedule=schedule, misfire_grace_time=attrs.get('misfire_grace_time'))
|
412
|
-
else:
|
413
|
-
worker.lanuch()
|
414
|
-
|
415
|
-
return worker
|
416
|
-
|
417
|
-
def add_program_cmd2(self, cmd, ports=1, name=None, **kwargs):
|
418
|
-
"""
|
419
|
-
增强版 add_program_cmd,支持 ports 的处理
|
420
|
-
|
421
|
-
:param ports:
|
422
|
-
int 表示要开启的进程数,端口号随机生成
|
423
|
-
list 表示指定的端口号
|
424
|
-
None 不做特殊配置
|
425
|
-
"""
|
426
|
-
# 1 处理 ports 参数,找到空闲端口或使用指定端口
|
427
|
-
if isinstance(ports, int):
|
428
|
-
ports = find_free_ports(ports)
|
429
|
-
|
430
|
-
# 2 处理 locations 参数,自动设置默认 URL 映射
|
431
|
-
if name is None:
|
432
|
-
_cmd = cmd.split() if isinstance(cmd, str) else cmd
|
433
|
-
name = _cmd[0]
|
434
|
-
|
435
|
-
# 3 遍历端口,依次启动进程
|
436
|
-
workers = []
|
437
|
-
if ports:
|
438
|
-
for port in ports:
|
439
|
-
cmd_with_port = cmd + [f'--port', str(port)]
|
440
|
-
kwargs['port'] = port
|
441
|
-
worker = self.add_program_cmd(cmd_with_port, name=f'{name}:{port}', **kwargs)
|
442
|
-
workers.append(worker)
|
443
|
-
else:
|
444
|
-
workers = [self.add_program_cmd(cmd, name=name, **kwargs)]
|
445
|
-
|
446
|
-
return workers
|
447
|
-
|
448
|
-
def add_program_cmd3(self, cmd, ports=None, locations=None, *, devices=None, **kwargs):
|
449
|
-
"""
|
450
|
-
增强版 add_program_cmd2,支持 devices 等其他更多特殊的扩展参数的处理
|
451
|
-
|
452
|
-
:param locations: URL 映射规则
|
453
|
-
:param int|str|None devices: 使用的设备编号(显卡编号或 CPU)
|
454
|
-
未设置的时候,使用cpu运行
|
455
|
-
:return: 返回一个包含所有启动程序的 worker 列表。
|
456
|
-
"""
|
457
|
-
# 1 处理locations
|
458
|
-
if locations:
|
459
|
-
if isinstance(locations, str):
|
460
|
-
locations = [locations]
|
461
|
-
for i, x in enumerate(locations):
|
462
|
-
if not isinstance(x, dict):
|
463
|
-
locations[i] = {x: x}
|
464
|
-
kwargs['locations'] = locations
|
465
|
-
|
466
|
-
# 2 处理 devices 参数,设置显卡编号或使用 CPU
|
467
|
-
if devices is not None:
|
468
|
-
os.environ['CUDA_VISIBLE_DEVICES'] = str(devices)
|
469
|
-
elif 'CUDA_VISIBLE_DEVICES' in os.environ:
|
470
|
-
del os.environ['CUDA_VISIBLE_DEVICES'] # 如果没设置 devices,就清除环境变量,使用 CPU
|
471
|
-
|
472
|
-
# 3 处理 ports 参数
|
473
|
-
workers = self.add_program_cmd2(cmd, ports=ports, **kwargs)
|
474
|
-
|
475
|
-
return workers
|
476
|
-
|
477
|
-
def __2_各种添加进程的机制(self):
|
478
|
-
pass
|
479
|
-
|
480
|
-
def add_program_python(self, py_file, args='',
|
481
|
-
ports=None, locations=None,
|
482
|
-
name=None, shell=False, executer=None,
|
483
|
-
**kwargs):
|
484
|
-
""" 添加并启动一个Python文件作为后台程序
|
485
|
-
|
486
|
-
:param str|list args:
|
487
|
-
"""
|
488
|
-
if executer is None:
|
489
|
-
executer = sys.executable
|
490
|
-
cmd = [str(executer), str(py_file)]
|
491
|
-
if isinstance(args, str):
|
492
|
-
cmd.append(args)
|
493
|
-
else:
|
494
|
-
cmd += list(args)
|
495
|
-
return self.add_program_cmd3(cmd, name, ports=ports, locations=locations, shell=shell, **kwargs)
|
496
|
-
|
497
|
-
def add_program_python_module(self, module, args='',
|
498
|
-
ports=None, locations=None,
|
499
|
-
name=None, shell=False, executer=None,
|
500
|
-
**kwargs):
|
501
|
-
"""
|
502
|
-
添加并启动一个Python模块作为后台程序
|
503
|
-
|
504
|
-
:param module: 要执行的Python模块名(python -m 后面的部分)
|
505
|
-
:param str|list args: 模块的参数
|
506
|
-
:param name: 进程的名称,默认为模块名
|
507
|
-
"""
|
508
|
-
if executer is None:
|
509
|
-
executer = sys.executable
|
510
|
-
cmd = [f'{executer}', '-m', f'{module}']
|
511
|
-
if isinstance(args, str):
|
512
|
-
cmd.append(args)
|
513
|
-
else:
|
514
|
-
cmd += list(args)
|
515
|
-
if name is None:
|
516
|
-
name = module
|
517
|
-
return self.add_program_cmd3(cmd, ports=ports, name=name, locations=locations, shell=shell, **kwargs)
|
518
|
-
|
519
|
-
def add_prog(self, prog, extcmds='',
|
520
|
-
ports=None, locations=None, *,
|
521
|
-
name=None, devices=None,
|
522
|
-
executer=None,
|
523
|
-
run=True,
|
524
|
-
schedule=None,
|
525
|
-
):
|
526
|
-
"""
|
527
|
-
:param int|list ports:
|
528
|
-
:param str|list extcmds:
|
529
|
-
"""
|
530
|
-
if locations is None:
|
531
|
-
locations = f'/api/{prog.split('.')[-1]}'
|
532
|
-
self.add_program_python_module(prog,
|
533
|
-
extcmds,
|
534
|
-
ports=ports, locations=locations,
|
535
|
-
name=name, devices=devices,
|
536
|
-
executer=executer,
|
537
|
-
run=run,
|
538
|
-
schedule=schedule,
|
539
|
-
)
|
540
|
-
|
541
|
-
def add_server(self, prog, ports=None, locations=None, *args, **kwargs):
|
542
|
-
""" 我自己部署的服务,基本都有特定的start_server启动函数 """
|
543
|
-
self.add_prog(prog, extcmds='start_server', ports=ports, locations=locations, *args, **kwargs)
|
544
|
-
|
545
|
-
def add_os_command_task(self,
|
546
|
-
script_content,
|
547
|
-
extension=None,
|
548
|
-
shell=False,
|
549
|
-
run=True,
|
550
|
-
name=None,
|
551
|
-
schedule=None,
|
552
|
-
**kwargs):
|
553
|
-
"""
|
554
|
-
添加一个操作系统命令或脚本(如 .bat、.sh、.ps1 等)并启动。
|
555
|
-
|
556
|
-
:param script_content: 脚本的内容(字符串)
|
557
|
-
:param extension: 脚本文件的扩展名(如 .bat, .sh, .ps1);如果为 None 则根据操作系统自动选择
|
558
|
-
:param shell: 是否使用 shell 执行
|
559
|
-
:param run: 是否立即启动
|
560
|
-
:param name: 程序名称
|
561
|
-
:param schedule: 定时任务配置
|
562
|
-
"""
|
563
|
-
# 1 自动选择脚本文件扩展名
|
564
|
-
if extension is None:
|
565
|
-
if sys.platform == 'win32':
|
566
|
-
extension = '.ps1' # Windows 默认使用 PowerShell
|
567
|
-
else:
|
568
|
-
extension = '.sh' # Linux 和 macOS 默认使用 Bash
|
569
|
-
|
570
|
-
# 2 添加编码配置到脚本内容
|
571
|
-
if extension == '.bat':
|
572
|
-
# 为 .bat 文件添加 UTF-8 支持
|
573
|
-
script_content = f"chcp 65001 >nul\n{script_content}"
|
574
|
-
elif extension == '.ps1':
|
575
|
-
# 为 .ps1 文件添加 UTF-8 支持
|
576
|
-
script_content = f"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n" \
|
577
|
-
f"[Console]::InputEncoding = [System.Text.Encoding]::UTF8\n{script_content}"
|
578
|
-
|
579
|
-
# 3 创建临时脚本文件
|
580
|
-
script_file = XlPath.create_tempfile_path(extension)
|
581
|
-
script_file.write_text(script_content)
|
582
|
-
|
583
|
-
# 4 根据文件扩展名和操作系统选择执行命令
|
584
|
-
if extension == '.sh':
|
585
|
-
cmd = ['bash', str(script_file)]
|
586
|
-
elif extension == '.ps1':
|
587
|
-
if sys.platform == 'win32':
|
588
|
-
cmd = ['powershell', '-ExecutionPolicy', 'Bypass', '-File', str(script_file)]
|
589
|
-
else:
|
590
|
-
raise ValueError("PowerShell 脚本仅在 Windows 系统上受支持")
|
591
|
-
elif extension == '.bat':
|
592
|
-
cmd = [str(script_file)] # 直接运行 .bat 文件
|
593
|
-
else:
|
594
|
-
raise ValueError(f"不支持的脚本类型: {extension}")
|
595
|
-
|
596
|
-
# 5 使用 add_program_cmd3 启动脚本
|
597
|
-
return self.add_program_cmd3(
|
598
|
-
cmd=cmd,
|
599
|
-
name=name or f"command_{script_file.stem}",
|
600
|
-
shell=shell,
|
601
|
-
run=run,
|
602
|
-
schedule=schedule,
|
603
|
-
**kwargs
|
604
|
-
)
|
605
|
-
|
606
|
-
def __3_nginx相关(self):
|
607
|
-
pass
|
608
|
-
|
609
|
-
def get_all_locations(self):
|
610
|
-
locations = defaultdict(list)
|
611
|
-
for worker in self.workers:
|
612
|
-
if not worker.locations:
|
613
|
-
continue
|
614
|
-
for x in worker.locations:
|
615
|
-
for dst, src in x.items():
|
616
|
-
if worker.port: # 250126周日21:02,有端口才添加
|
617
|
-
locations[dst].append(f'localhost:{worker.port}{src}')
|
618
|
-
return locations
|
619
|
-
|
620
|
-
def configure_nginx(self, nginx_template, locations=None):
|
621
|
-
if locations is None:
|
622
|
-
locations = self.get_all_locations()
|
623
|
-
|
624
|
-
upstreams = [] # 外部的配置
|
625
|
-
servers = [nginx_template.rstrip()] # 内部的配置
|
626
|
-
|
627
|
-
for dst, srcs in locations.items():
|
628
|
-
if len(srcs) == 1: # 只有1个不开负载
|
629
|
-
server = f'location {dst} {{\n\tproxy_pass http://{srcs[0]};\n}}\n'
|
630
|
-
else: # 有多个端口功能则开负载
|
631
|
-
hosts = '\n'.join([f'\tserver {src.split("/")[0]};' for src in srcs])
|
632
|
-
upstream_name = 'upstream' + str(len(upstreams) + 1)
|
633
|
-
upstreams.append(f'upstream {upstream_name} {{\n{hosts}\n}}\n')
|
634
|
-
sub_urls = [src.split('/', maxsplit=1)[1] for src in srcs]
|
635
|
-
assert len(set(sub_urls)) == 1, f'负载均衡的子url必须一致 {sub_urls}'
|
636
|
-
server = f'location {dst} {{\n\tproxy_pass http://{upstream_name}/{sub_urls[0]};\n}}\n'
|
637
|
-
servers.append(server)
|
638
|
-
|
639
|
-
content = '\n'.join(upstreams) + '\nserver {\n' + textwrap.indent('\n'.join(servers), '\t') + '}'
|
640
|
-
return content
|
641
|
-
|
642
|
-
def __4_多程序管理(self):
|
643
|
-
pass
|
644
|
-
|
645
|
-
def count_running(self):
|
646
|
-
return sum(1 for worker in self.workers if worker.is_running())
|
647
|
-
|
648
|
-
def list_workers(self):
|
649
|
-
"""返回所有任务的状态 DataFrame"""
|
650
|
-
ls = []
|
651
|
-
for worker in self.workers:
|
652
|
-
ls.append({
|
653
|
-
'name': worker.name,
|
654
|
-
'pid': worker.program.pid if worker.program else None,
|
655
|
-
'poll': worker.program.poll() if worker.program else None,
|
656
|
-
'args': worker.raw_cmd,
|
657
|
-
'port': worker.port,
|
658
|
-
'locations': worker.locations,
|
659
|
-
})
|
660
|
-
return pd.DataFrame(ls)
|
661
|
-
|
662
|
-
def cleanup_finished(self, exit_code=None):
|
663
|
-
"""
|
664
|
-
清除已运行完的程序
|
665
|
-
|
666
|
-
:param exit_code:
|
667
|
-
None - 清除所有已结束的程序(默认行为)。
|
668
|
-
0 - 只清除正常结束的程序。
|
669
|
-
非0 - 只清除异常结束的程序。
|
670
|
-
"""
|
671
|
-
new_workers = []
|
672
|
-
for worker in self.workers:
|
673
|
-
code = worker.program.poll()
|
674
|
-
if code is None: # 进程仍在运行
|
675
|
-
new_workers.append(worker)
|
676
|
-
elif exit_code is None or code == exit_code:
|
677
|
-
print(f'清理已结束的程序: {worker.name} (pid: {worker.program.pid}, exit code: {code})')
|
678
|
-
|
679
|
-
self.workers = new_workers
|
680
|
-
|
681
|
-
def stop_all(self):
|
682
|
-
""" 停止所有后台程序 """
|
683
|
-
# 关闭所有调度器
|
684
|
-
if self.scheduler:
|
685
|
-
self.scheduler.shutdown()
|
686
|
-
|
687
|
-
# 停止所有单启动任务
|
688
|
-
for worker in self.workers:
|
689
|
-
worker.terminate()
|
690
|
-
self.workers = []
|
691
|
-
|
692
|
-
def proc_cmd(self, cmd):
|
693
|
-
if cmd == 'kill':
|
694
|
-
self.stop_all()
|
695
|
-
return False
|
696
|
-
elif cmd == 'count':
|
697
|
-
print(f'有{self.count_running()}个程序正在运行')
|
698
|
-
elif cmd.startswith('cleanup'):
|
699
|
-
args = cmd.split()
|
700
|
-
exit_code = int(args[1]) if len(args) > 1 else None
|
701
|
-
self.cleanup_finished(exit_code)
|
702
|
-
print("清理完成")
|
703
|
-
elif cmd == 'list': # 列出所有程序(转df查看)
|
704
|
-
df = self.list_workers()
|
705
|
-
print_full_dataframe(df)
|
706
|
-
return True
|
707
|
-
|
708
|
-
def run_endless(self, cmd=True, wait_seconds=1, *, debug_port=None):
|
709
|
-
"""
|
710
|
-
一直运行,直到用户输入 kill 命令或 Ctrl+C
|
711
|
-
|
712
|
-
:param bool cmd: 是否支持命令行input输入指令监控状态的模式
|
713
|
-
默认支持,但在有scheduler调度的情况不建议开启,有input阻塞其他子程的风险
|
714
|
-
:param int|float wait_seconds: 每次循环之间停顿秒数,用来给其他子程等运行时间,避免阻塞
|
715
|
-
:param int debug_port: 是否要开一个后端服务,支持查询程序运行状态
|
716
|
-
|
717
|
-
poll:
|
718
|
-
如果进程仍在运行,poll()方法返回None。
|
719
|
-
如果进程已经结束,poll()方法返回进程的退出码(exit code)。
|
720
|
-
如果进程正常结束,退出码通常为0。
|
721
|
-
如果进程异常结束,退出码通常是一个非零值,表示异常的类型或错误码。
|
722
|
-
"""
|
723
|
-
if self.scheduler: # 如果有定时任务,启动调度器
|
724
|
-
self.scheduler.start()
|
725
|
-
|
726
|
-
port = debug_port
|
727
|
-
if port:
|
728
|
-
dashboard = LauncherDashboard(self)
|
729
|
-
threading.Thread(target=lambda: dashboard.run(port), daemon=True).start()
|
730
|
-
|
731
|
-
try:
|
732
|
-
while True:
|
733
|
-
if cmd:
|
734
|
-
_cmd = input(">>> ")
|
735
|
-
if not self.proc_cmd(_cmd):
|
736
|
-
break
|
737
|
-
time.sleep(wait_seconds)
|
738
|
-
except KeyboardInterrupt:
|
739
|
-
print("\n检测到 Ctrl+C,正在终止所有程序...")
|
740
|
-
self.stop_all()
|
741
|
-
print("所有程序已终止,退出。")
|
742
|
-
|
743
|
-
|
744
|
-
@deprecated(reason='已改名ProgramWorker')
|
745
|
-
class ProcessWorker(ProgramWorker):
|
746
|
-
pass
|
747
|
-
|
748
|
-
|
749
|
-
@deprecated(reason='已改名MultiProgramLauncher')
|
750
|
-
class MultiProcessLauncher(MultiProgramLauncher):
|
751
|
-
pass
|
752
|
-
|
753
|
-
|
754
|
-
class LauncherDashboard:
|
755
|
-
def __init__(self, launcher):
|
756
|
-
self.launcher = launcher
|
757
|
-
self.app = FastAPI()
|
758
|
-
self.setup_routes()
|
759
|
-
|
760
|
-
def setup_routes(self):
|
761
|
-
@self.app.get("/", response_class=PlainTextResponse)
|
762
|
-
async def home():
|
763
|
-
"""主页,展示所有任务状态"""
|
764
|
-
df = self.launcher.list_workers()
|
765
|
-
return self.render_text(df)
|
766
|
-
|
767
|
-
@self.app.get("/count", response_class=PlainTextResponse)
|
768
|
-
async def get_count():
|
769
|
-
"""返回正在运行的任务数量"""
|
770
|
-
count = self.launcher.count_running()
|
771
|
-
return f"正在运行的任务数量: {count}\n"
|
772
|
-
|
773
|
-
@self.app.post("/stop", response_class=PlainTextResponse)
|
774
|
-
async def stop_all():
|
775
|
-
"""停止所有任务"""
|
776
|
-
self.launcher.stop_all()
|
777
|
-
return "所有任务已停止。\n"
|
778
|
-
|
779
|
-
@self.app.post("/cleanup", response_class=PlainTextResponse)
|
780
|
-
async def cleanup(exit_code: int = None):
|
781
|
-
"""清理已完成的任务"""
|
782
|
-
self.launcher.cleanup_finished(exit_code)
|
783
|
-
return "清理完成。\n"
|
784
|
-
|
785
|
-
def render_text(self, df):
|
786
|
-
"""生成纯文本格式的任务状态"""
|
787
|
-
if df.empty:
|
788
|
-
return "没有任务正在运行。\n"
|
789
|
-
|
790
|
-
# 调整表头以显示新的属性:port 和 locations
|
791
|
-
output = ["当前任务状态:\n"]
|
792
|
-
output.append(f"{'编号':<5} {'名称':<15} {'PID':<10} {'状态':<10} {'端口':<10} {'位置':<20} {'命令'}")
|
793
|
-
output.append("-" * 120)
|
794
|
-
|
795
|
-
for idx, (_, row) in enumerate(df.iterrows(), start=1):
|
796
|
-
name = row['name']
|
797
|
-
|
798
|
-
# 更鲁棒地处理 pid,考虑 NaN 情况
|
799
|
-
pid = "N/A" if pd.isna(row['pid']) else int(row['pid'])
|
800
|
-
|
801
|
-
# 获取任务状态
|
802
|
-
status = "运行中" if row['poll'] is None else "已结束"
|
803
|
-
|
804
|
-
# 处理 args,确保路径展示更清晰
|
805
|
-
args = [a for a in row['args'] if a] if isinstance(row['args'], list) else []
|
806
|
-
args_str = "[" + ", ".join(
|
807
|
-
repr(arg).replace("\\\\", "\\") if '\\' in arg else repr(arg) for arg in args
|
808
|
-
) + "]"
|
809
|
-
|
810
|
-
# 处理 port 和 locations
|
811
|
-
port = row['port'] if not pd.isna(row['port']) else "N/A"
|
812
|
-
locations = str(row['locations'])
|
813
|
-
|
814
|
-
# 格式化输出
|
815
|
-
output.append(f"{idx:<5} {name:<15} {pid:<10} {status:<10} {port:<10} {locations:<20} {args_str}")
|
816
|
-
|
817
|
-
output.append("\n")
|
818
|
-
return "\n".join(output)
|
819
|
-
|
820
|
-
def run(self, port=8080):
|
821
|
-
"""启动 FastAPI 服务"""
|
822
|
-
import uvicorn
|
823
|
-
uvicorn.run(self.app, host="0.0.0.0", port=port, log_level="warning")
|
824
|
-
|
825
|
-
|
826
|
-
def __3_装饰器工具():
|
827
|
-
pass
|
828
|
-
|
829
|
-
|
830
|
-
def support_multi_processes_hyx(default_processes=1):
|
831
|
-
""" 对函数进行扩展,支持并发多进程运行
|
832
|
-
增加重跑
|
833
|
-
注意被装饰的函数,需要支持 process_count、process_id 两个参数,来获得总进程数,当前进程id的信息
|
834
|
-
"""
|
835
|
-
|
836
|
-
def decorator(func):
|
837
|
-
|
838
|
-
def wrapper(*args, **kwargs):
|
839
|
-
process_count = int(kwargs.pop('process_count', default_processes))
|
840
|
-
process_id = kwargs.pop('process_id', None)
|
841
|
-
shell = kwargs.pop('shell', False)
|
842
|
-
|
843
|
-
if process_count == 1 or process_id is not None:
|
844
|
-
if process_id is None:
|
845
|
-
return func(*args, **kwargs)
|
846
|
-
else:
|
847
|
-
return func(*args, **kwargs, process_count=process_count, process_id=int(process_id))
|
848
|
-
else:
|
849
|
-
mpl = MultiProcessLauncher()
|
850
|
-
for i in range(int(process_count)):
|
851
|
-
if isinstance(process_id, int) and i != process_id:
|
852
|
-
continue
|
853
|
-
|
854
|
-
'''
|
855
|
-
sys.argv[0] 为 /Users/youx/NutstoreCloudBridge/slns/xlproject/xlproject/m2404ragdata/b清洗/hyx240806统计图表数.py
|
856
|
-
将其转化为 xlproject.m2404ragdata.b清洗.hyx240806统计图表数
|
857
|
-
'''
|
858
|
-
header = 'xlproject.code4101'
|
859
|
-
|
860
|
-
root_directory_name = 'xlproject'
|
861
|
-
occurrence = 2
|
862
|
-
path = sys.argv[0]
|
863
|
-
|
864
|
-
# 查找第 occurrence 次出现的 root_directory_name 位置
|
865
|
-
positions = [i for i in range(len(path)) if path.startswith(root_directory_name, i)]
|
866
|
-
if len(positions) < occurrence:
|
867
|
-
raise ValueError(
|
868
|
-
f"Path does not contain {occurrence} occurrences of the root directory name: {root_directory_name}")
|
869
|
-
|
870
|
-
# 提取并转换为模块名称
|
871
|
-
index = positions[occurrence - 1]
|
872
|
-
module_name = os.path.splitext(path[index:])[0].replace(os.path.sep, '.')
|
873
|
-
|
874
|
-
# todo 这样使用有个坑,process_count、process_id都是以str类型传入的,开发者下游使用容易出问题
|
875
|
-
cmds = [
|
876
|
-
'run_python_module',
|
877
|
-
'--wait_mode',
|
878
|
-
'60',
|
879
|
-
module_name,
|
880
|
-
func.__name__,
|
881
|
-
'--process_count', str(process_count),
|
882
|
-
'--process_id', str(i)
|
883
|
-
]
|
884
|
-
cmds.extend(map(str, args)) # 添加位置参数
|
885
|
-
for k, v in kwargs.items():
|
886
|
-
cmds.append(f'--{k}') # 添加关键字参数的键
|
887
|
-
cmds.append(str(v)) # 添加关键字参数的值
|
888
|
-
|
889
|
-
mpl.add_program_python_module(header, cmds, shell=shell)
|
890
|
-
mpl.run_endless()
|
891
|
-
|
892
|
-
return wrapper
|
893
|
-
|
894
|
-
return decorator
|
895
|
-
|
896
|
-
|
897
|
-
def support_multi_processes(default_processes=1):
|
898
|
-
""" 对函数进行扩展,支持并发多进程运行
|
899
|
-
|
900
|
-
注意被装饰的函数,需要支持 process_count、process_id 两个参数,来获得总进程数,当前进程id的信息
|
901
|
-
"""
|
902
|
-
|
903
|
-
def decorator(func):
|
904
|
-
|
905
|
-
def wrapper(*args, **kwargs):
|
906
|
-
process_count = int(kwargs.pop('process_count', default_processes))
|
907
|
-
process_id = kwargs.pop('process_id', None)
|
908
|
-
shell = kwargs.pop('shell', False)
|
909
|
-
|
910
|
-
if process_count == 1 or process_id is not None:
|
911
|
-
if process_id is None:
|
912
|
-
return func(*args, **kwargs)
|
913
|
-
else:
|
914
|
-
return func(*args, **kwargs, process_count=process_count, process_id=int(process_id))
|
915
|
-
else:
|
916
|
-
mpl = MultiProcessLauncher()
|
917
|
-
for i in range(int(process_count)):
|
918
|
-
if isinstance(process_id, int) and i != process_id:
|
919
|
-
continue
|
920
|
-
|
921
|
-
# todo 这样使用有个坑,process_count、process_id都是以str类型传入的,开发者下游使用容易出问题
|
922
|
-
cmds = [func.__name__,
|
923
|
-
'--process_count', str(process_count),
|
924
|
-
'--process_id', str(i)
|
925
|
-
]
|
926
|
-
cmds.extend(map(str, args)) # 添加位置参数
|
927
|
-
for k, v in kwargs.items():
|
928
|
-
cmds.append(f'--{k}') # 添加关键字参数的键
|
929
|
-
cmds.append(str(v)) # 添加关键字参数的值
|
930
|
-
|
931
|
-
mpl.add_program_python(sys.argv[1], cmds, shell=shell)
|
932
|
-
mpl.run_endless()
|
933
|
-
|
934
|
-
return wrapper
|
935
|
-
|
936
|
-
return decorator
|
937
|
-
|
938
|
-
|
939
|
-
if __name__ == '__main__':
|
940
|
-
pass
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# @Author : 陈坤泽
|
4
|
+
# @Email : 877362867@qq.com
|
5
|
+
# @Date : 2024/11/12
|
6
|
+
|
7
|
+
from collections import defaultdict
|
8
|
+
from types import SimpleNamespace
|
9
|
+
from unittest.mock import Mock
|
10
|
+
import ctypes
|
11
|
+
import datetime
|
12
|
+
import os
|
13
|
+
import socketserver
|
14
|
+
import subprocess
|
15
|
+
import sys
|
16
|
+
import time
|
17
|
+
import textwrap
|
18
|
+
import threading
|
19
|
+
|
20
|
+
from deprecated import deprecated
|
21
|
+
from loguru import logger
|
22
|
+
from croniter import croniter
|
23
|
+
import pandas as pd
|
24
|
+
|
25
|
+
from fastapi import FastAPI
|
26
|
+
from fastapi.responses import PlainTextResponse
|
27
|
+
|
28
|
+
from pyxllib.prog.specialist import parse_datetime
|
29
|
+
from pyxllib.algo.stat import print_full_dataframe
|
30
|
+
from pyxllib.file.specialist import XlPath
|
31
|
+
|
32
|
+
|
33
|
+
def __1_定时工具():
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class SchedulerUtils:
|
38
|
+
@classmethod
|
39
|
+
def calculate_future_time(cls, start_time, wait_seconds):
|
40
|
+
""" 计算延迟时间
|
41
|
+
|
42
|
+
:param datetime start_time: 开始时间
|
43
|
+
:param int wait_seconds: 等待秒数
|
44
|
+
todo 先只支持秒数这种标准秒数,后续可以考虑支持更多智能的"1小时"等这种解析
|
45
|
+
"""
|
46
|
+
return start_time + datetime.timedelta(seconds=wait_seconds)
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def calculate_next_cron_time(cls, cron_tag, base_time=None):
|
50
|
+
""" 使用crontab标记的运行周期,然后计算相对当前时间,下一次要启动运行的时间
|
51
|
+
|
52
|
+
:param str cron_tag: 自定义的cron标记,跟asp的差不多,但星期几部分做了调整
|
53
|
+
30 2 * * 1: 这部分是时间和日期的设定,具体含义如下:
|
54
|
+
30: 表示分钟,即每小时的第 30 分钟。
|
55
|
+
2: 表示小时,即凌晨 2 点。
|
56
|
+
第三个星号 *: 表示日,这里的星号意味着每天。
|
57
|
+
第四个星号 *: 表示月份,星号同样表示每个月。
|
58
|
+
1: 表示星期中的日子,这里的 1 代表星期一,7表示星期日。不能写1~7以外的值。
|
59
|
+
:param datetime base_time: 基于哪个时间点计算下次时间
|
60
|
+
"""
|
61
|
+
|
62
|
+
# 如果没有提供基准时间,则使用当前时间
|
63
|
+
if base_time is None:
|
64
|
+
base_time = datetime.datetime.now()
|
65
|
+
# 初始化 croniter 对象
|
66
|
+
cron = croniter(cron_tag, base_time)
|
67
|
+
# 计算下一次运行时间
|
68
|
+
next_time = cron.get_next(datetime.datetime)
|
69
|
+
return next_time
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def wait_until_time(cls, dst_time):
|
73
|
+
"""
|
74
|
+
:param datetime dst_time: 一直等待到目标时间
|
75
|
+
期间可以用time.sleep进行等待
|
76
|
+
"""
|
77
|
+
# 一般来说,只要计算一轮待等待秒数就行。但是time.sleep机制好像不一定准确的,所以使用无限循环重试会更好。
|
78
|
+
while True:
|
79
|
+
# 先计算当前时间和目标时间的相差秒数
|
80
|
+
wait_seconds = (dst_time - datetime.datetime.now()).total_seconds()
|
81
|
+
if wait_seconds <= 0:
|
82
|
+
break
|
83
|
+
time.sleep(max(1, wait_seconds)) # 最少等待1秒
|
84
|
+
|
85
|
+
@classmethod
|
86
|
+
def smart_wait(cls, start_time, end_time, wait_tag, print_mode=0):
|
87
|
+
""" 智能等待,一般用在对进程的管理重启上
|
88
|
+
|
89
|
+
:param datetime start_time: 程序启动的时间
|
90
|
+
:param datetime end_time: 程序结束的时间
|
91
|
+
:param str|float|int wait_tag: 等待标记
|
92
|
+
str,按crontab解析
|
93
|
+
在end_time后满足条件的下次时间重启
|
94
|
+
int|float,表示等待的秒数
|
95
|
+
正值是end_time往后等待,负值是start_time开始计算下次时间。
|
96
|
+
比如1点开始的程序,等待半小时,但是首次运行到2点才结束
|
97
|
+
那么正值就是2:30再下次运行
|
98
|
+
但是负值表示1:30就要运行,已经错过了,马上2点结束就立即启动复跑
|
99
|
+
"""
|
100
|
+
# 1 尝试把wait_tag转成数值
|
101
|
+
try:
|
102
|
+
wait_tag = float(wait_tag)
|
103
|
+
except ValueError: # 转不成也没关系
|
104
|
+
pass
|
105
|
+
|
106
|
+
if start_time is None:
|
107
|
+
start_time = datetime.datetime.now()
|
108
|
+
if end_time is None:
|
109
|
+
end_time = datetime.datetime.now()
|
110
|
+
|
111
|
+
# 2 计算下一次启动时间
|
112
|
+
if isinstance(wait_tag, str):
|
113
|
+
# 按照crontab解析
|
114
|
+
next_time = cls.calculate_next_cron_time(wait_tag, end_time)
|
115
|
+
elif wait_tag >= 0:
|
116
|
+
# 正值则是从end_time开始往后等待
|
117
|
+
next_time = cls.calculate_future_time(end_time, wait_tag)
|
118
|
+
elif wait_tag < 0:
|
119
|
+
# 负值则是从start_time开始往前等待
|
120
|
+
next_time = cls.calculate_future_time(start_time, wait_tag)
|
121
|
+
else:
|
122
|
+
raise ValueError
|
123
|
+
|
124
|
+
if print_mode:
|
125
|
+
print(f'等待到时间{next_time}...')
|
126
|
+
|
127
|
+
cls.wait_until_time(next_time)
|
128
|
+
|
129
|
+
|
130
|
+
def __2_程序管理():
|
131
|
+
pass
|
132
|
+
|
133
|
+
|
134
|
+
def find_free_ports(count=1):
|
135
|
+
""" 随机获得可用端口
|
136
|
+
|
137
|
+
:param count: 需要的端口数量(会保证给出的端口号不重复)
|
138
|
+
:return: list
|
139
|
+
"""
|
140
|
+
ports = set()
|
141
|
+
while len(ports) < count:
|
142
|
+
with socketserver.TCPServer(("localhost", 0), None) as s:
|
143
|
+
ports.add(s.server_address[1])
|
144
|
+
|
145
|
+
return list(ports)
|
146
|
+
|
147
|
+
|
148
|
+
class ProgramWorker(SimpleNamespace):
|
149
|
+
"""
|
150
|
+
代表一个单独的程序(进程),基于 SimpleNamespace 实现。
|
151
|
+
"""
|
152
|
+
|
153
|
+
def __init__(self, name,
|
154
|
+
cmd,
|
155
|
+
shell=False,
|
156
|
+
run=True,
|
157
|
+
port=None,
|
158
|
+
locations=None,
|
159
|
+
raw_cmd=None,
|
160
|
+
**attrs):
|
161
|
+
"""
|
162
|
+
:param name: 程序昵称
|
163
|
+
:param program: 程序对象
|
164
|
+
:param port: 是否有执行所在端口
|
165
|
+
:param locations: 是否有url地址映射,一般用于nginx等配置
|
166
|
+
"""
|
167
|
+
super().__init__(**attrs)
|
168
|
+
|
169
|
+
# 执行程序需要使用的参数
|
170
|
+
self.cmd = cmd
|
171
|
+
self.shell = shell
|
172
|
+
self.run = run
|
173
|
+
|
174
|
+
# 关键参数
|
175
|
+
self.name = name
|
176
|
+
self.program = None
|
177
|
+
self.raw_cmd = raw_cmd
|
178
|
+
|
179
|
+
# 这两个是比较特别的属性,在我的工程框架中常用
|
180
|
+
self.port = port
|
181
|
+
self.locations = locations
|
182
|
+
|
183
|
+
# 特殊调度模式,需要用到程序启动、结束时间
|
184
|
+
self.last_start_time = None
|
185
|
+
self.last_end_time = None
|
186
|
+
|
187
|
+
def _set_pdeathsig(self, sig=None):
|
188
|
+
""" 在主服务退出时,这些程序也会全部自动关闭 """
|
189
|
+
|
190
|
+
def callable():
|
191
|
+
import signal
|
192
|
+
sig2 = signal.SIGTERM if sig is None else sig
|
193
|
+
libc = ctypes.CDLL("libc.so.6")
|
194
|
+
return libc.prctl(1, sig2)
|
195
|
+
|
196
|
+
if sys.platform == 'win32':
|
197
|
+
# windows系统暂设为空
|
198
|
+
return None
|
199
|
+
else:
|
200
|
+
return callable
|
201
|
+
|
202
|
+
def lanuch(self):
|
203
|
+
""" 启动程序 """
|
204
|
+
self.last_start_time = datetime.datetime.now()
|
205
|
+
self.last_end_time = None
|
206
|
+
|
207
|
+
if not self.run:
|
208
|
+
# 如果不需要立即启动,则返回一个 Mock 对象
|
209
|
+
proc = Mock()
|
210
|
+
proc.pid = None
|
211
|
+
proc.poll.return_value = 'tag'
|
212
|
+
self.program = proc
|
213
|
+
return self.program
|
214
|
+
|
215
|
+
kwargs = {}
|
216
|
+
for name in ['stdin', 'stdout', 'stderr']:
|
217
|
+
if hasattr(self, name):
|
218
|
+
kwargs[name] = getattr(self, name)
|
219
|
+
|
220
|
+
if sys.platform == 'win32':
|
221
|
+
self.program = subprocess.Popen(self.cmd, shell=self.shell, **kwargs)
|
222
|
+
else:
|
223
|
+
self.program = subprocess.Popen(self.cmd, shell=self.shell, **kwargs,
|
224
|
+
preexec_fn=self._set_pdeathsig())
|
225
|
+
|
226
|
+
return self.program
|
227
|
+
|
228
|
+
def terminate(self):
|
229
|
+
"""
|
230
|
+
比较优雅结束进程的方法
|
231
|
+
"""
|
232
|
+
if self.program is not None:
|
233
|
+
self.program.terminate()
|
234
|
+
if self.last_end_time is None:
|
235
|
+
self.last_end_time = datetime.datetime.now()
|
236
|
+
|
237
|
+
def kill(self):
|
238
|
+
"""
|
239
|
+
有时候需要强硬的kill方法来结束进程
|
240
|
+
"""
|
241
|
+
if self.program is not None:
|
242
|
+
self.program.kill()
|
243
|
+
if self.last_end_time is None:
|
244
|
+
self.last_end_time = datetime.datetime.now()
|
245
|
+
|
246
|
+
def is_running(self):
|
247
|
+
""" 检查程序是否在运行 """
|
248
|
+
status = self.program is not None and self.program.poll() is None
|
249
|
+
if not status and self.last_end_time is None: # 检测到程序运行结束,则标记下
|
250
|
+
self.last_end_time = datetime.datetime.now()
|
251
|
+
return status
|
252
|
+
|
253
|
+
|
254
|
+
class MultiProgramLauncher:
|
255
|
+
"""
|
256
|
+
管理多个程序的启动与终止
|
257
|
+
"""
|
258
|
+
|
259
|
+
def __init__(self):
|
260
|
+
self.workers = []
|
261
|
+
self.scheduler = None
|
262
|
+
|
263
|
+
def __1_进场管理的核心底层函数(self):
|
264
|
+
pass
|
265
|
+
|
266
|
+
def init_scheduler(self):
|
267
|
+
from apscheduler.schedulers.background import BackgroundScheduler
|
268
|
+
# from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
269
|
+
|
270
|
+
if self.scheduler is None:
|
271
|
+
self.scheduler = BackgroundScheduler()
|
272
|
+
|
273
|
+
return self.scheduler
|
274
|
+
|
275
|
+
def worker_add_schedule(self, worker, schedule=None, misfire_grace_time=None):
|
276
|
+
""" 将程序添加为定时任务
|
277
|
+
|
278
|
+
:param int|float|str|list schedule: 定时任务配置,如果提供则添加到 APScheduler 调度器
|
279
|
+
int/float: 每隔多少秒执行一次,可能同时有多个实例存在
|
280
|
+
tuple(value1, value2): 只允许单实例运行,这里记录的是上次实例结束后,等待多久再开启下次实例
|
281
|
+
此时第1个数值,正数表示等待上一次运行结束后,下次开始运行前的等待时间
|
282
|
+
负数表示是在上次启动后,等待多久就可以运行下一次
|
283
|
+
但下一次一定会在上一次运行结束后再续上
|
284
|
+
第2个参数,为了实现这套功能,其实需要一个监视器不断监控程序的运行状态
|
285
|
+
第2个参数是监控器检测的频率秒数
|
286
|
+
str: cron 表达式,支持 5 或 6 个字段
|
287
|
+
list[datetime]: 在指定时间节点列表运行
|
288
|
+
值只要是可以解析为datetime类型的都可以,底层使用特殊的解析器
|
289
|
+
"""
|
290
|
+
from datetime import datetime, timedelta
|
291
|
+
|
292
|
+
def task():
|
293
|
+
""" 启动任务并更新状态 """
|
294
|
+
# todo schedule不一定都是单实例阻塞情景的需求,以后有需要可以扩展支持多实例同时存在的非阻塞模式
|
295
|
+
if worker.is_running():
|
296
|
+
logger.warning(f'由于程序"{name}"在上一次周期还没运行完,新周期不重复启动')
|
297
|
+
else:
|
298
|
+
worker.lanuch()
|
299
|
+
|
300
|
+
self.init_scheduler()
|
301
|
+
|
302
|
+
name = worker.name
|
303
|
+
|
304
|
+
# 处理 schedule 类型
|
305
|
+
if isinstance(schedule, (int, float)):
|
306
|
+
# 如果是单一数值,按固定间隔执行,不考虑任务是否完成
|
307
|
+
self.scheduler.add_job(task, 'interval', seconds=abs(schedule))
|
308
|
+
logger.info(f"已添加定时任务:{name},监测频率:{schedule} 秒")
|
309
|
+
|
310
|
+
elif isinstance(schedule, tuple) and len(schedule) == 2:
|
311
|
+
wait_time, monitor_frequency = schedule
|
312
|
+
|
313
|
+
def interval_task():
|
314
|
+
# 如果还没有启动过任务,直接启动
|
315
|
+
if worker.last_start_time is None:
|
316
|
+
worker.lanuch()
|
317
|
+
return
|
318
|
+
|
319
|
+
# 如果任务正在运行,则不启动新任务
|
320
|
+
if worker.is_running():
|
321
|
+
return
|
322
|
+
|
323
|
+
# 计算下次启动时间
|
324
|
+
if wait_time < 0: # 基于上次启动时间
|
325
|
+
next_run = worker.last_start_time + timedelta(seconds=abs(wait_time))
|
326
|
+
else: # 基于上次结束时间
|
327
|
+
next_run = worker.last_end_time + timedelta(seconds=wait_time)
|
328
|
+
|
329
|
+
# 看现在是否需要重启
|
330
|
+
if datetime.now() >= next_run:
|
331
|
+
worker.lanuch()
|
332
|
+
|
333
|
+
# 以 monitor_frequency 作为定时检查的间隔
|
334
|
+
self.scheduler.add_job(interval_task, 'interval', seconds=monitor_frequency)
|
335
|
+
logger.info(f"已添加定时任务:{name},等待时间: {wait_time} 秒,监测频率: {monitor_frequency} 秒")
|
336
|
+
|
337
|
+
elif isinstance(schedule, str):
|
338
|
+
from apscheduler.triggers.cron import CronTrigger
|
339
|
+
cron_parts = schedule.split()
|
340
|
+
# 如果是 5 个字段,则补齐 "秒" 字段为 '0'
|
341
|
+
if len(cron_parts) == 5:
|
342
|
+
cron_parts.insert(0, '0')
|
343
|
+
# 检查是否为有效的 6 字段 cron 表达式
|
344
|
+
if len(cron_parts) == 6:
|
345
|
+
# 统一处理为 6 字段格式
|
346
|
+
# 把我自定义的cron的星期标记转换为aps的星期标记。前者用1234567,后者用0123456表示星期一到星期日
|
347
|
+
x = cron_parts[5]
|
348
|
+
if x != '*': # 写0或7都表示周日
|
349
|
+
if x == '0':
|
350
|
+
x = '7'
|
351
|
+
x = (int(x) - 1)
|
352
|
+
self.scheduler.add_job(
|
353
|
+
task,
|
354
|
+
CronTrigger(
|
355
|
+
second=cron_parts[0],
|
356
|
+
minute=cron_parts[1],
|
357
|
+
hour=cron_parts[2],
|
358
|
+
day=cron_parts[3],
|
359
|
+
month=cron_parts[4],
|
360
|
+
day_of_week=x,
|
361
|
+
),
|
362
|
+
# 检测的时候有概率错过了精确时间点。但一般不论延迟了多久,都要补运行上。
|
363
|
+
misfire_grace_time=misfire_grace_time,
|
364
|
+
)
|
365
|
+
logger.info(f"已添加定时任务:{name},触发器: cron,表达式: {schedule}")
|
366
|
+
else:
|
367
|
+
logger.warning(f"无效的 cron 表达式:{schedule}")
|
368
|
+
|
369
|
+
elif isinstance(schedule, list):
|
370
|
+
# 支持 list[datetime] 格式
|
371
|
+
for run_time in schedule:
|
372
|
+
run_time = parse_datetime(run_time)
|
373
|
+
self.scheduler.add_job(task, 'date', run_date=run_time)
|
374
|
+
logger.info(f"已添加定时任务:{name},触发器: dates,运行时间: {schedule}")
|
375
|
+
|
376
|
+
else:
|
377
|
+
logger.warning(f"无效的调度格式,跳过任务:{name}")
|
378
|
+
|
379
|
+
def add_program_cmd(self,
|
380
|
+
cmd,
|
381
|
+
name=None,
|
382
|
+
shell=False,
|
383
|
+
run=True,
|
384
|
+
schedule=None,
|
385
|
+
**attrs):
|
386
|
+
"""
|
387
|
+
启动一个程序,或仅存储任务,并添加进管理列表
|
388
|
+
|
389
|
+
:param cmd: 启动程序的命令
|
390
|
+
:param name: 程序名称,如果未提供则从cmd中自动获取
|
391
|
+
:param shell:
|
392
|
+
False,(优先推荐)直接跟系统交互,此时cmd应该输入数组格式
|
393
|
+
True,启用shell进行交互操作,这种情况会更适合管道等模式的处理,此时cmd应该输入字符串格式
|
394
|
+
:param int|float|str|list schedule: 定时任务配置,如果提供则添加到 APScheduler 调度器
|
395
|
+
:param run: 如果为True,则立即启动程序;否则仅存储任务信息
|
396
|
+
:param attrs: 其他需要传递给ProgramWorker的参数
|
397
|
+
:return: 返回一个ProgramWorker实例,表示启动的程序或存储的任务
|
398
|
+
"""
|
399
|
+
# 1 如果未显式传入name,自动从cmd中获取程序名称
|
400
|
+
# logger.info(cmd)
|
401
|
+
if name is None:
|
402
|
+
_cmd = cmd.split() if isinstance(cmd, str) else cmd
|
403
|
+
name = _cmd[0]
|
404
|
+
|
405
|
+
# 2 创建 ProgramWorker 实例
|
406
|
+
worker = ProgramWorker(name, cmd, shell, run, raw_cmd=cmd, **attrs)
|
407
|
+
self.workers.append(worker)
|
408
|
+
|
409
|
+
# 3 如果有 schedule 配置,添加调度任务;否则就是正常的启动任务
|
410
|
+
if schedule:
|
411
|
+
self.worker_add_schedule(worker, schedule=schedule, misfire_grace_time=attrs.get('misfire_grace_time'))
|
412
|
+
else:
|
413
|
+
worker.lanuch()
|
414
|
+
|
415
|
+
return worker
|
416
|
+
|
417
|
+
def add_program_cmd2(self, cmd, ports=1, name=None, **kwargs):
|
418
|
+
"""
|
419
|
+
增强版 add_program_cmd,支持 ports 的处理
|
420
|
+
|
421
|
+
:param ports:
|
422
|
+
int 表示要开启的进程数,端口号随机生成
|
423
|
+
list 表示指定的端口号
|
424
|
+
None 不做特殊配置
|
425
|
+
"""
|
426
|
+
# 1 处理 ports 参数,找到空闲端口或使用指定端口
|
427
|
+
if isinstance(ports, int):
|
428
|
+
ports = find_free_ports(ports)
|
429
|
+
|
430
|
+
# 2 处理 locations 参数,自动设置默认 URL 映射
|
431
|
+
if name is None:
|
432
|
+
_cmd = cmd.split() if isinstance(cmd, str) else cmd
|
433
|
+
name = _cmd[0]
|
434
|
+
|
435
|
+
# 3 遍历端口,依次启动进程
|
436
|
+
workers = []
|
437
|
+
if ports:
|
438
|
+
for port in ports:
|
439
|
+
cmd_with_port = cmd + [f'--port', str(port)]
|
440
|
+
kwargs['port'] = port
|
441
|
+
worker = self.add_program_cmd(cmd_with_port, name=f'{name}:{port}', **kwargs)
|
442
|
+
workers.append(worker)
|
443
|
+
else:
|
444
|
+
workers = [self.add_program_cmd(cmd, name=name, **kwargs)]
|
445
|
+
|
446
|
+
return workers
|
447
|
+
|
448
|
+
def add_program_cmd3(self, cmd, ports=None, locations=None, *, devices=None, **kwargs):
|
449
|
+
"""
|
450
|
+
增强版 add_program_cmd2,支持 devices 等其他更多特殊的扩展参数的处理
|
451
|
+
|
452
|
+
:param locations: URL 映射规则
|
453
|
+
:param int|str|None devices: 使用的设备编号(显卡编号或 CPU)
|
454
|
+
未设置的时候,使用cpu运行
|
455
|
+
:return: 返回一个包含所有启动程序的 worker 列表。
|
456
|
+
"""
|
457
|
+
# 1 处理locations
|
458
|
+
if locations:
|
459
|
+
if isinstance(locations, str):
|
460
|
+
locations = [locations]
|
461
|
+
for i, x in enumerate(locations):
|
462
|
+
if not isinstance(x, dict):
|
463
|
+
locations[i] = {x: x}
|
464
|
+
kwargs['locations'] = locations
|
465
|
+
|
466
|
+
# 2 处理 devices 参数,设置显卡编号或使用 CPU
|
467
|
+
if devices is not None:
|
468
|
+
os.environ['CUDA_VISIBLE_DEVICES'] = str(devices)
|
469
|
+
elif 'CUDA_VISIBLE_DEVICES' in os.environ:
|
470
|
+
del os.environ['CUDA_VISIBLE_DEVICES'] # 如果没设置 devices,就清除环境变量,使用 CPU
|
471
|
+
|
472
|
+
# 3 处理 ports 参数
|
473
|
+
workers = self.add_program_cmd2(cmd, ports=ports, **kwargs)
|
474
|
+
|
475
|
+
return workers
|
476
|
+
|
477
|
+
def __2_各种添加进程的机制(self):
|
478
|
+
pass
|
479
|
+
|
480
|
+
def add_program_python(self, py_file, args='',
|
481
|
+
ports=None, locations=None,
|
482
|
+
name=None, shell=False, executer=None,
|
483
|
+
**kwargs):
|
484
|
+
""" 添加并启动一个Python文件作为后台程序
|
485
|
+
|
486
|
+
:param str|list args:
|
487
|
+
"""
|
488
|
+
if executer is None:
|
489
|
+
executer = sys.executable
|
490
|
+
cmd = [str(executer), str(py_file)]
|
491
|
+
if isinstance(args, str):
|
492
|
+
cmd.append(args)
|
493
|
+
else:
|
494
|
+
cmd += list(args)
|
495
|
+
return self.add_program_cmd3(cmd, name, ports=ports, locations=locations, shell=shell, **kwargs)
|
496
|
+
|
497
|
+
def add_program_python_module(self, module, args='',
|
498
|
+
ports=None, locations=None,
|
499
|
+
name=None, shell=False, executer=None,
|
500
|
+
**kwargs):
|
501
|
+
"""
|
502
|
+
添加并启动一个Python模块作为后台程序
|
503
|
+
|
504
|
+
:param module: 要执行的Python模块名(python -m 后面的部分)
|
505
|
+
:param str|list args: 模块的参数
|
506
|
+
:param name: 进程的名称,默认为模块名
|
507
|
+
"""
|
508
|
+
if executer is None:
|
509
|
+
executer = sys.executable
|
510
|
+
cmd = [f'{executer}', '-m', f'{module}']
|
511
|
+
if isinstance(args, str):
|
512
|
+
cmd.append(args)
|
513
|
+
else:
|
514
|
+
cmd += list(args)
|
515
|
+
if name is None:
|
516
|
+
name = module
|
517
|
+
return self.add_program_cmd3(cmd, ports=ports, name=name, locations=locations, shell=shell, **kwargs)
|
518
|
+
|
519
|
+
def add_prog(self, prog, extcmds='',
|
520
|
+
ports=None, locations=None, *,
|
521
|
+
name=None, devices=None,
|
522
|
+
executer=None,
|
523
|
+
run=True,
|
524
|
+
schedule=None,
|
525
|
+
):
|
526
|
+
"""
|
527
|
+
:param int|list ports:
|
528
|
+
:param str|list extcmds:
|
529
|
+
"""
|
530
|
+
if locations is None:
|
531
|
+
locations = f'/api/{prog.split('.')[-1]}'
|
532
|
+
self.add_program_python_module(prog,
|
533
|
+
extcmds,
|
534
|
+
ports=ports, locations=locations,
|
535
|
+
name=name, devices=devices,
|
536
|
+
executer=executer,
|
537
|
+
run=run,
|
538
|
+
schedule=schedule,
|
539
|
+
)
|
540
|
+
|
541
|
+
def add_server(self, prog, ports=None, locations=None, *args, **kwargs):
|
542
|
+
""" 我自己部署的服务,基本都有特定的start_server启动函数 """
|
543
|
+
self.add_prog(prog, extcmds='start_server', ports=ports, locations=locations, *args, **kwargs)
|
544
|
+
|
545
|
+
def add_os_command_task(self,
|
546
|
+
script_content,
|
547
|
+
extension=None,
|
548
|
+
shell=False,
|
549
|
+
run=True,
|
550
|
+
name=None,
|
551
|
+
schedule=None,
|
552
|
+
**kwargs):
|
553
|
+
"""
|
554
|
+
添加一个操作系统命令或脚本(如 .bat、.sh、.ps1 等)并启动。
|
555
|
+
|
556
|
+
:param script_content: 脚本的内容(字符串)
|
557
|
+
:param extension: 脚本文件的扩展名(如 .bat, .sh, .ps1);如果为 None 则根据操作系统自动选择
|
558
|
+
:param shell: 是否使用 shell 执行
|
559
|
+
:param run: 是否立即启动
|
560
|
+
:param name: 程序名称
|
561
|
+
:param schedule: 定时任务配置
|
562
|
+
"""
|
563
|
+
# 1 自动选择脚本文件扩展名
|
564
|
+
if extension is None:
|
565
|
+
if sys.platform == 'win32':
|
566
|
+
extension = '.ps1' # Windows 默认使用 PowerShell
|
567
|
+
else:
|
568
|
+
extension = '.sh' # Linux 和 macOS 默认使用 Bash
|
569
|
+
|
570
|
+
# 2 添加编码配置到脚本内容
|
571
|
+
if extension == '.bat':
|
572
|
+
# 为 .bat 文件添加 UTF-8 支持
|
573
|
+
script_content = f"chcp 65001 >nul\n{script_content}"
|
574
|
+
elif extension == '.ps1':
|
575
|
+
# 为 .ps1 文件添加 UTF-8 支持
|
576
|
+
script_content = f"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n" \
|
577
|
+
f"[Console]::InputEncoding = [System.Text.Encoding]::UTF8\n{script_content}"
|
578
|
+
|
579
|
+
# 3 创建临时脚本文件
|
580
|
+
script_file = XlPath.create_tempfile_path(extension)
|
581
|
+
script_file.write_text(script_content)
|
582
|
+
|
583
|
+
# 4 根据文件扩展名和操作系统选择执行命令
|
584
|
+
if extension == '.sh':
|
585
|
+
cmd = ['bash', str(script_file)]
|
586
|
+
elif extension == '.ps1':
|
587
|
+
if sys.platform == 'win32':
|
588
|
+
cmd = ['powershell', '-ExecutionPolicy', 'Bypass', '-File', str(script_file)]
|
589
|
+
else:
|
590
|
+
raise ValueError("PowerShell 脚本仅在 Windows 系统上受支持")
|
591
|
+
elif extension == '.bat':
|
592
|
+
cmd = [str(script_file)] # 直接运行 .bat 文件
|
593
|
+
else:
|
594
|
+
raise ValueError(f"不支持的脚本类型: {extension}")
|
595
|
+
|
596
|
+
# 5 使用 add_program_cmd3 启动脚本
|
597
|
+
return self.add_program_cmd3(
|
598
|
+
cmd=cmd,
|
599
|
+
name=name or f"command_{script_file.stem}",
|
600
|
+
shell=shell,
|
601
|
+
run=run,
|
602
|
+
schedule=schedule,
|
603
|
+
**kwargs
|
604
|
+
)
|
605
|
+
|
606
|
+
def __3_nginx相关(self):
|
607
|
+
pass
|
608
|
+
|
609
|
+
def get_all_locations(self):
|
610
|
+
locations = defaultdict(list)
|
611
|
+
for worker in self.workers:
|
612
|
+
if not worker.locations:
|
613
|
+
continue
|
614
|
+
for x in worker.locations:
|
615
|
+
for dst, src in x.items():
|
616
|
+
if worker.port: # 250126周日21:02,有端口才添加
|
617
|
+
locations[dst].append(f'localhost:{worker.port}{src}')
|
618
|
+
return locations
|
619
|
+
|
620
|
+
def configure_nginx(self, nginx_template, locations=None):
|
621
|
+
if locations is None:
|
622
|
+
locations = self.get_all_locations()
|
623
|
+
|
624
|
+
upstreams = [] # 外部的配置
|
625
|
+
servers = [nginx_template.rstrip()] # 内部的配置
|
626
|
+
|
627
|
+
for dst, srcs in locations.items():
|
628
|
+
if len(srcs) == 1: # 只有1个不开负载
|
629
|
+
server = f'location {dst} {{\n\tproxy_pass http://{srcs[0]};\n}}\n'
|
630
|
+
else: # 有多个端口功能则开负载
|
631
|
+
hosts = '\n'.join([f'\tserver {src.split("/")[0]};' for src in srcs])
|
632
|
+
upstream_name = 'upstream' + str(len(upstreams) + 1)
|
633
|
+
upstreams.append(f'upstream {upstream_name} {{\n{hosts}\n}}\n')
|
634
|
+
sub_urls = [src.split('/', maxsplit=1)[1] for src in srcs]
|
635
|
+
assert len(set(sub_urls)) == 1, f'负载均衡的子url必须一致 {sub_urls}'
|
636
|
+
server = f'location {dst} {{\n\tproxy_pass http://{upstream_name}/{sub_urls[0]};\n}}\n'
|
637
|
+
servers.append(server)
|
638
|
+
|
639
|
+
content = '\n'.join(upstreams) + '\nserver {\n' + textwrap.indent('\n'.join(servers), '\t') + '}'
|
640
|
+
return content
|
641
|
+
|
642
|
+
def __4_多程序管理(self):
|
643
|
+
pass
|
644
|
+
|
645
|
+
def count_running(self):
|
646
|
+
return sum(1 for worker in self.workers if worker.is_running())
|
647
|
+
|
648
|
+
def list_workers(self):
|
649
|
+
"""返回所有任务的状态 DataFrame"""
|
650
|
+
ls = []
|
651
|
+
for worker in self.workers:
|
652
|
+
ls.append({
|
653
|
+
'name': worker.name,
|
654
|
+
'pid': worker.program.pid if worker.program else None,
|
655
|
+
'poll': worker.program.poll() if worker.program else None,
|
656
|
+
'args': worker.raw_cmd,
|
657
|
+
'port': worker.port,
|
658
|
+
'locations': worker.locations,
|
659
|
+
})
|
660
|
+
return pd.DataFrame(ls)
|
661
|
+
|
662
|
+
def cleanup_finished(self, exit_code=None):
|
663
|
+
"""
|
664
|
+
清除已运行完的程序
|
665
|
+
|
666
|
+
:param exit_code:
|
667
|
+
None - 清除所有已结束的程序(默认行为)。
|
668
|
+
0 - 只清除正常结束的程序。
|
669
|
+
非0 - 只清除异常结束的程序。
|
670
|
+
"""
|
671
|
+
new_workers = []
|
672
|
+
for worker in self.workers:
|
673
|
+
code = worker.program.poll()
|
674
|
+
if code is None: # 进程仍在运行
|
675
|
+
new_workers.append(worker)
|
676
|
+
elif exit_code is None or code == exit_code:
|
677
|
+
print(f'清理已结束的程序: {worker.name} (pid: {worker.program.pid}, exit code: {code})')
|
678
|
+
|
679
|
+
self.workers = new_workers
|
680
|
+
|
681
|
+
def stop_all(self):
|
682
|
+
""" 停止所有后台程序 """
|
683
|
+
# 关闭所有调度器
|
684
|
+
if self.scheduler:
|
685
|
+
self.scheduler.shutdown()
|
686
|
+
|
687
|
+
# 停止所有单启动任务
|
688
|
+
for worker in self.workers:
|
689
|
+
worker.terminate()
|
690
|
+
self.workers = []
|
691
|
+
|
692
|
+
def proc_cmd(self, cmd):
|
693
|
+
if cmd == 'kill':
|
694
|
+
self.stop_all()
|
695
|
+
return False
|
696
|
+
elif cmd == 'count':
|
697
|
+
print(f'有{self.count_running()}个程序正在运行')
|
698
|
+
elif cmd.startswith('cleanup'):
|
699
|
+
args = cmd.split()
|
700
|
+
exit_code = int(args[1]) if len(args) > 1 else None
|
701
|
+
self.cleanup_finished(exit_code)
|
702
|
+
print("清理完成")
|
703
|
+
elif cmd == 'list': # 列出所有程序(转df查看)
|
704
|
+
df = self.list_workers()
|
705
|
+
print_full_dataframe(df)
|
706
|
+
return True
|
707
|
+
|
708
|
+
def run_endless(self, cmd=True, wait_seconds=1, *, debug_port=None):
|
709
|
+
"""
|
710
|
+
一直运行,直到用户输入 kill 命令或 Ctrl+C
|
711
|
+
|
712
|
+
:param bool cmd: 是否支持命令行input输入指令监控状态的模式
|
713
|
+
默认支持,但在有scheduler调度的情况不建议开启,有input阻塞其他子程的风险
|
714
|
+
:param int|float wait_seconds: 每次循环之间停顿秒数,用来给其他子程等运行时间,避免阻塞
|
715
|
+
:param int debug_port: 是否要开一个后端服务,支持查询程序运行状态
|
716
|
+
|
717
|
+
poll:
|
718
|
+
如果进程仍在运行,poll()方法返回None。
|
719
|
+
如果进程已经结束,poll()方法返回进程的退出码(exit code)。
|
720
|
+
如果进程正常结束,退出码通常为0。
|
721
|
+
如果进程异常结束,退出码通常是一个非零值,表示异常的类型或错误码。
|
722
|
+
"""
|
723
|
+
if self.scheduler: # 如果有定时任务,启动调度器
|
724
|
+
self.scheduler.start()
|
725
|
+
|
726
|
+
port = debug_port
|
727
|
+
if port:
|
728
|
+
dashboard = LauncherDashboard(self)
|
729
|
+
threading.Thread(target=lambda: dashboard.run(port), daemon=True).start()
|
730
|
+
|
731
|
+
try:
|
732
|
+
while True:
|
733
|
+
if cmd:
|
734
|
+
_cmd = input(">>> ")
|
735
|
+
if not self.proc_cmd(_cmd):
|
736
|
+
break
|
737
|
+
time.sleep(wait_seconds)
|
738
|
+
except KeyboardInterrupt:
|
739
|
+
print("\n检测到 Ctrl+C,正在终止所有程序...")
|
740
|
+
self.stop_all()
|
741
|
+
print("所有程序已终止,退出。")
|
742
|
+
|
743
|
+
|
744
|
+
@deprecated(reason='已改名ProgramWorker')
|
745
|
+
class ProcessWorker(ProgramWorker):
|
746
|
+
pass
|
747
|
+
|
748
|
+
|
749
|
+
@deprecated(reason='已改名MultiProgramLauncher')
|
750
|
+
class MultiProcessLauncher(MultiProgramLauncher):
|
751
|
+
pass
|
752
|
+
|
753
|
+
|
754
|
+
class LauncherDashboard:
|
755
|
+
def __init__(self, launcher):
|
756
|
+
self.launcher = launcher
|
757
|
+
self.app = FastAPI()
|
758
|
+
self.setup_routes()
|
759
|
+
|
760
|
+
def setup_routes(self):
|
761
|
+
@self.app.get("/", response_class=PlainTextResponse)
|
762
|
+
async def home():
|
763
|
+
"""主页,展示所有任务状态"""
|
764
|
+
df = self.launcher.list_workers()
|
765
|
+
return self.render_text(df)
|
766
|
+
|
767
|
+
@self.app.get("/count", response_class=PlainTextResponse)
|
768
|
+
async def get_count():
|
769
|
+
"""返回正在运行的任务数量"""
|
770
|
+
count = self.launcher.count_running()
|
771
|
+
return f"正在运行的任务数量: {count}\n"
|
772
|
+
|
773
|
+
@self.app.post("/stop", response_class=PlainTextResponse)
|
774
|
+
async def stop_all():
|
775
|
+
"""停止所有任务"""
|
776
|
+
self.launcher.stop_all()
|
777
|
+
return "所有任务已停止。\n"
|
778
|
+
|
779
|
+
@self.app.post("/cleanup", response_class=PlainTextResponse)
|
780
|
+
async def cleanup(exit_code: int = None):
|
781
|
+
"""清理已完成的任务"""
|
782
|
+
self.launcher.cleanup_finished(exit_code)
|
783
|
+
return "清理完成。\n"
|
784
|
+
|
785
|
+
def render_text(self, df):
|
786
|
+
"""生成纯文本格式的任务状态"""
|
787
|
+
if df.empty:
|
788
|
+
return "没有任务正在运行。\n"
|
789
|
+
|
790
|
+
# 调整表头以显示新的属性:port 和 locations
|
791
|
+
output = ["当前任务状态:\n"]
|
792
|
+
output.append(f"{'编号':<5} {'名称':<15} {'PID':<10} {'状态':<10} {'端口':<10} {'位置':<20} {'命令'}")
|
793
|
+
output.append("-" * 120)
|
794
|
+
|
795
|
+
for idx, (_, row) in enumerate(df.iterrows(), start=1):
|
796
|
+
name = row['name']
|
797
|
+
|
798
|
+
# 更鲁棒地处理 pid,考虑 NaN 情况
|
799
|
+
pid = "N/A" if pd.isna(row['pid']) else int(row['pid'])
|
800
|
+
|
801
|
+
# 获取任务状态
|
802
|
+
status = "运行中" if row['poll'] is None else "已结束"
|
803
|
+
|
804
|
+
# 处理 args,确保路径展示更清晰
|
805
|
+
args = [a for a in row['args'] if a] if isinstance(row['args'], list) else []
|
806
|
+
args_str = "[" + ", ".join(
|
807
|
+
repr(arg).replace("\\\\", "\\") if '\\' in arg else repr(arg) for arg in args
|
808
|
+
) + "]"
|
809
|
+
|
810
|
+
# 处理 port 和 locations
|
811
|
+
port = row['port'] if not pd.isna(row['port']) else "N/A"
|
812
|
+
locations = str(row['locations'])
|
813
|
+
|
814
|
+
# 格式化输出
|
815
|
+
output.append(f"{idx:<5} {name:<15} {pid:<10} {status:<10} {port:<10} {locations:<20} {args_str}")
|
816
|
+
|
817
|
+
output.append("\n")
|
818
|
+
return "\n".join(output)
|
819
|
+
|
820
|
+
def run(self, port=8080):
|
821
|
+
"""启动 FastAPI 服务"""
|
822
|
+
import uvicorn
|
823
|
+
uvicorn.run(self.app, host="0.0.0.0", port=port, log_level="warning")
|
824
|
+
|
825
|
+
|
826
|
+
def __3_装饰器工具():
|
827
|
+
pass
|
828
|
+
|
829
|
+
|
830
|
+
def support_multi_processes_hyx(default_processes=1):
|
831
|
+
""" 对函数进行扩展,支持并发多进程运行
|
832
|
+
增加重跑
|
833
|
+
注意被装饰的函数,需要支持 process_count、process_id 两个参数,来获得总进程数,当前进程id的信息
|
834
|
+
"""
|
835
|
+
|
836
|
+
def decorator(func):
|
837
|
+
|
838
|
+
def wrapper(*args, **kwargs):
|
839
|
+
process_count = int(kwargs.pop('process_count', default_processes))
|
840
|
+
process_id = kwargs.pop('process_id', None)
|
841
|
+
shell = kwargs.pop('shell', False)
|
842
|
+
|
843
|
+
if process_count == 1 or process_id is not None:
|
844
|
+
if process_id is None:
|
845
|
+
return func(*args, **kwargs)
|
846
|
+
else:
|
847
|
+
return func(*args, **kwargs, process_count=process_count, process_id=int(process_id))
|
848
|
+
else:
|
849
|
+
mpl = MultiProcessLauncher()
|
850
|
+
for i in range(int(process_count)):
|
851
|
+
if isinstance(process_id, int) and i != process_id:
|
852
|
+
continue
|
853
|
+
|
854
|
+
'''
|
855
|
+
sys.argv[0] 为 /Users/youx/NutstoreCloudBridge/slns/xlproject/xlproject/m2404ragdata/b清洗/hyx240806统计图表数.py
|
856
|
+
将其转化为 xlproject.m2404ragdata.b清洗.hyx240806统计图表数
|
857
|
+
'''
|
858
|
+
header = 'xlproject.code4101'
|
859
|
+
|
860
|
+
root_directory_name = 'xlproject'
|
861
|
+
occurrence = 2
|
862
|
+
path = sys.argv[0]
|
863
|
+
|
864
|
+
# 查找第 occurrence 次出现的 root_directory_name 位置
|
865
|
+
positions = [i for i in range(len(path)) if path.startswith(root_directory_name, i)]
|
866
|
+
if len(positions) < occurrence:
|
867
|
+
raise ValueError(
|
868
|
+
f"Path does not contain {occurrence} occurrences of the root directory name: {root_directory_name}")
|
869
|
+
|
870
|
+
# 提取并转换为模块名称
|
871
|
+
index = positions[occurrence - 1]
|
872
|
+
module_name = os.path.splitext(path[index:])[0].replace(os.path.sep, '.')
|
873
|
+
|
874
|
+
# todo 这样使用有个坑,process_count、process_id都是以str类型传入的,开发者下游使用容易出问题
|
875
|
+
cmds = [
|
876
|
+
'run_python_module',
|
877
|
+
'--wait_mode',
|
878
|
+
'60',
|
879
|
+
module_name,
|
880
|
+
func.__name__,
|
881
|
+
'--process_count', str(process_count),
|
882
|
+
'--process_id', str(i)
|
883
|
+
]
|
884
|
+
cmds.extend(map(str, args)) # 添加位置参数
|
885
|
+
for k, v in kwargs.items():
|
886
|
+
cmds.append(f'--{k}') # 添加关键字参数的键
|
887
|
+
cmds.append(str(v)) # 添加关键字参数的值
|
888
|
+
|
889
|
+
mpl.add_program_python_module(header, cmds, shell=shell)
|
890
|
+
mpl.run_endless()
|
891
|
+
|
892
|
+
return wrapper
|
893
|
+
|
894
|
+
return decorator
|
895
|
+
|
896
|
+
|
897
|
+
def support_multi_processes(default_processes=1):
|
898
|
+
""" 对函数进行扩展,支持并发多进程运行
|
899
|
+
|
900
|
+
注意被装饰的函数,需要支持 process_count、process_id 两个参数,来获得总进程数,当前进程id的信息
|
901
|
+
"""
|
902
|
+
|
903
|
+
def decorator(func):
|
904
|
+
|
905
|
+
def wrapper(*args, **kwargs):
|
906
|
+
process_count = int(kwargs.pop('process_count', default_processes))
|
907
|
+
process_id = kwargs.pop('process_id', None)
|
908
|
+
shell = kwargs.pop('shell', False)
|
909
|
+
|
910
|
+
if process_count == 1 or process_id is not None:
|
911
|
+
if process_id is None:
|
912
|
+
return func(*args, **kwargs)
|
913
|
+
else:
|
914
|
+
return func(*args, **kwargs, process_count=process_count, process_id=int(process_id))
|
915
|
+
else:
|
916
|
+
mpl = MultiProcessLauncher()
|
917
|
+
for i in range(int(process_count)):
|
918
|
+
if isinstance(process_id, int) and i != process_id:
|
919
|
+
continue
|
920
|
+
|
921
|
+
# todo 这样使用有个坑,process_count、process_id都是以str类型传入的,开发者下游使用容易出问题
|
922
|
+
cmds = [func.__name__,
|
923
|
+
'--process_count', str(process_count),
|
924
|
+
'--process_id', str(i)
|
925
|
+
]
|
926
|
+
cmds.extend(map(str, args)) # 添加位置参数
|
927
|
+
for k, v in kwargs.items():
|
928
|
+
cmds.append(f'--{k}') # 添加关键字参数的键
|
929
|
+
cmds.append(str(v)) # 添加关键字参数的值
|
930
|
+
|
931
|
+
mpl.add_program_python(sys.argv[1], cmds, shell=shell)
|
932
|
+
mpl.run_endless()
|
933
|
+
|
934
|
+
return wrapper
|
935
|
+
|
936
|
+
return decorator
|
937
|
+
|
938
|
+
|
939
|
+
if __name__ == '__main__':
|
940
|
+
pass
|