tretool 0.2.1__py3-none-any.whl → 1.0.0__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.
- tretool/__init__.py +39 -12
- tretool/config.py +406 -170
- tretool/decoratorlib.py +423 -0
- tretool/encoding.py +404 -75
- tretool/httplib.py +730 -0
- tretool/jsonlib.py +619 -151
- tretool/logger.py +712 -0
- tretool/mathlib.py +0 -33
- tretool/path.py +19 -0
- tretool/platformlib.py +469 -314
- tretool/plugin.py +437 -237
- tretool/smartCache.py +569 -0
- tretool/tasklib.py +730 -0
- tretool/transform/docx.py +544 -0
- tretool/transform/pdf.py +273 -95
- tretool/ziplib.py +664 -0
- {tretool-0.2.1.dist-info → tretool-1.0.0.dist-info}/METADATA +11 -5
- tretool-1.0.0.dist-info/RECORD +24 -0
- tretool/markfunc.py +0 -152
- tretool/memorizeTools.py +0 -24
- tretool/writeLog.py +0 -69
- tretool-0.2.1.dist-info/RECORD +0 -20
- {tretool-0.2.1.dist-info → tretool-1.0.0.dist-info}/WHEEL +0 -0
- {tretool-0.2.1.dist-info → tretool-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {tretool-0.2.1.dist-info → tretool-1.0.0.dist-info}/top_level.txt +0 -0
tretool/tasklib.py
ADDED
@@ -0,0 +1,730 @@
|
|
1
|
+
import os
|
2
|
+
import psutil
|
3
|
+
import time
|
4
|
+
import signal
|
5
|
+
import subprocess
|
6
|
+
import threading
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import List, Dict, Optional, Callable, Set, Tuple, Union
|
9
|
+
from enum import Enum, auto
|
10
|
+
from dataclasses import dataclass, field
|
11
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
12
|
+
import logging
|
13
|
+
from logging.handlers import RotatingFileHandler
|
14
|
+
import platform
|
15
|
+
import json
|
16
|
+
from collections import defaultdict
|
17
|
+
|
18
|
+
# 日志配置
|
19
|
+
def setup_logging(log_file: str = 'process_manager.log', max_size: int = 10):
|
20
|
+
"""配置日志记录"""
|
21
|
+
logger = logging.getLogger('ProcessManager')
|
22
|
+
logger.setLevel(logging.INFO)
|
23
|
+
|
24
|
+
formatter = logging.Formatter(
|
25
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
26
|
+
)
|
27
|
+
|
28
|
+
# 控制台处理器
|
29
|
+
console_handler = logging.StreamHandler()
|
30
|
+
console_handler.setFormatter(formatter)
|
31
|
+
|
32
|
+
# 文件处理器(轮转日志)
|
33
|
+
file_handler = RotatingFileHandler(
|
34
|
+
log_file, maxBytes=max_size*1024*1024, backupCount=5
|
35
|
+
)
|
36
|
+
file_handler.setFormatter(formatter)
|
37
|
+
|
38
|
+
logger.addHandler(console_handler)
|
39
|
+
logger.addHandler(file_handler)
|
40
|
+
return logger
|
41
|
+
|
42
|
+
logger = setup_logging()
|
43
|
+
|
44
|
+
class ProcessStatus(Enum):
|
45
|
+
"""进程状态枚举"""
|
46
|
+
RUNNING = auto()
|
47
|
+
STOPPED = auto()
|
48
|
+
ZOMBIE = auto()
|
49
|
+
UNKNOWN = auto()
|
50
|
+
IDLE = auto()
|
51
|
+
SLEEPING = auto()
|
52
|
+
TRACING_STOP = auto()
|
53
|
+
|
54
|
+
@dataclass
|
55
|
+
class ProcessInfo:
|
56
|
+
"""进程信息数据类"""
|
57
|
+
pid: int
|
58
|
+
name: str
|
59
|
+
status: ProcessStatus
|
60
|
+
cpu_percent: float
|
61
|
+
memory_mb: float
|
62
|
+
create_time: float
|
63
|
+
cmdline: List[str]
|
64
|
+
threads: int
|
65
|
+
username: str
|
66
|
+
connections: List[dict]
|
67
|
+
open_files: List[str]
|
68
|
+
children: List['ProcessInfo'] = field(default_factory=list)
|
69
|
+
exit_code: Optional[int] = None
|
70
|
+
cpu_times: Optional[dict] = None
|
71
|
+
io_counters: Optional[dict] = None
|
72
|
+
environ: Optional[dict] = None
|
73
|
+
|
74
|
+
class ProcessObserver(ABC):
|
75
|
+
"""观察者抽象基类"""
|
76
|
+
|
77
|
+
@abstractmethod
|
78
|
+
def on_process_start(self, pid: int, info: ProcessInfo):
|
79
|
+
"""进程启动回调"""
|
80
|
+
pass
|
81
|
+
|
82
|
+
@abstractmethod
|
83
|
+
def on_process_stop(self, pid: int, info: ProcessInfo):
|
84
|
+
"""进程停止回调"""
|
85
|
+
pass
|
86
|
+
|
87
|
+
@abstractmethod
|
88
|
+
def on_process_update(self, pid: int, info: ProcessInfo):
|
89
|
+
"""进程状态更新回调"""
|
90
|
+
pass
|
91
|
+
|
92
|
+
@abstractmethod
|
93
|
+
def on_resource_alert(self, pid: int, alert_type: str, info: ProcessInfo):
|
94
|
+
"""资源警报回调"""
|
95
|
+
pass
|
96
|
+
|
97
|
+
class LoggingObserver(ProcessObserver):
|
98
|
+
"""日志记录观察者实现"""
|
99
|
+
|
100
|
+
def __init__(self, logger: logging.Logger = None):
|
101
|
+
self.logger = logger or logging.getLogger('ProcessObserver')
|
102
|
+
|
103
|
+
def on_process_start(self, pid: int, info: ProcessInfo):
|
104
|
+
self.logger.info(
|
105
|
+
f"Process started - PID: {pid}, "
|
106
|
+
f"Name: {info.name}, "
|
107
|
+
f"User: {info.username}, "
|
108
|
+
f"CMD: {' '.join(info.cmdline[:1])}..."
|
109
|
+
)
|
110
|
+
|
111
|
+
def on_process_stop(self, pid: int, info: ProcessInfo):
|
112
|
+
exit_code = info.exit_code if info.exit_code is not None else 'N/A'
|
113
|
+
self.logger.info(
|
114
|
+
f"Process stopped - PID: {pid}, "
|
115
|
+
f"ExitCode: {exit_code}, "
|
116
|
+
f"Duration: {time.time() - info.create_time:.2f}s"
|
117
|
+
)
|
118
|
+
|
119
|
+
def on_process_update(self, pid: int, info: ProcessInfo):
|
120
|
+
self.logger.debug(
|
121
|
+
f"Process update - PID: {pid}, "
|
122
|
+
f"CPU: {info.cpu_percent:.1f}%, "
|
123
|
+
f"MEM: {info.memory_mb:.1f}MB, "
|
124
|
+
f"Threads: {info.threads}"
|
125
|
+
)
|
126
|
+
|
127
|
+
def on_resource_alert(self, pid: int, alert_type: str, info: ProcessInfo):
|
128
|
+
alert_msg = {
|
129
|
+
'high_cpu': f"High CPU usage ({info.cpu_percent:.1f}%)",
|
130
|
+
'high_memory': f"High memory usage ({info.memory_mb:.1f}MB)",
|
131
|
+
'many_threads': f"Too many threads ({info.threads})",
|
132
|
+
'many_connections': f"Too many connections ({len(info.connections)})",
|
133
|
+
'many_files': f"Too many open files ({len(info.open_files)})"
|
134
|
+
}.get(alert_type, alert_type)
|
135
|
+
|
136
|
+
self.logger.warning(
|
137
|
+
f"Resource alert - PID: {pid} {alert_msg}, "
|
138
|
+
f"Name: {info.name}"
|
139
|
+
)
|
140
|
+
|
141
|
+
class ResourceGuardObserver(ProcessObserver):
|
142
|
+
"""资源守护观察者(自动终止异常进程)"""
|
143
|
+
|
144
|
+
def __init__(self,
|
145
|
+
max_cpu: float = 90.0,
|
146
|
+
max_memory_mb: float = 1024,
|
147
|
+
max_threads: int = 100,
|
148
|
+
max_connections: int = 50,
|
149
|
+
max_files: int = 500,
|
150
|
+
max_duration: float = 3600):
|
151
|
+
self.max_cpu = max_cpu
|
152
|
+
self.max_memory = max_memory_mb
|
153
|
+
self.max_threads = max_threads
|
154
|
+
self.max_connections = max_connections
|
155
|
+
self.max_files = max_files
|
156
|
+
self.max_duration = max_duration
|
157
|
+
self.violations = defaultdict(dict)
|
158
|
+
|
159
|
+
def on_process_start(self, pid: int, info: ProcessInfo):
|
160
|
+
self.violations[pid] = {
|
161
|
+
'start_time': time.time(),
|
162
|
+
'high_cpu_count': 0,
|
163
|
+
'high_memory_count': 0,
|
164
|
+
'high_threads_count': 0,
|
165
|
+
'high_connections_count': 0,
|
166
|
+
'high_files_count': 0
|
167
|
+
}
|
168
|
+
|
169
|
+
def on_process_stop(self, pid: int, info: ProcessInfo):
|
170
|
+
self.violations.pop(pid, None)
|
171
|
+
|
172
|
+
def on_process_update(self, pid: int, info: ProcessInfo):
|
173
|
+
if pid not in self.violations:
|
174
|
+
return
|
175
|
+
|
176
|
+
violations = self.violations[pid]
|
177
|
+
|
178
|
+
# 检查运行时长
|
179
|
+
if time.time() - info.create_time > self.max_duration:
|
180
|
+
self._terminate(pid, reason="Timeout exceeded")
|
181
|
+
return
|
182
|
+
|
183
|
+
# 检查资源使用
|
184
|
+
if info.cpu_percent > self.max_cpu:
|
185
|
+
violations['high_cpu_count'] += 1
|
186
|
+
else:
|
187
|
+
violations['high_cpu_count'] = 0
|
188
|
+
|
189
|
+
if info.memory_mb > self.max_memory:
|
190
|
+
violations['high_memory_count'] += 1
|
191
|
+
else:
|
192
|
+
violations['high_memory_count'] = 0
|
193
|
+
|
194
|
+
if info.threads > self.max_threads:
|
195
|
+
violations['high_threads_count'] += 1
|
196
|
+
else:
|
197
|
+
violations['high_threads_count'] = 0
|
198
|
+
|
199
|
+
if len(info.connections) > self.max_connections:
|
200
|
+
violations['high_connections_count'] += 1
|
201
|
+
else:
|
202
|
+
violations['high_connections_count'] = 0
|
203
|
+
|
204
|
+
if len(info.open_files) > self.max_files:
|
205
|
+
violations['high_files_count'] += 1
|
206
|
+
else:
|
207
|
+
violations['high_files_count'] = 0
|
208
|
+
|
209
|
+
# 连续3次超标则终止
|
210
|
+
if any(count >= 3 for count in violations.values() if isinstance(count, int)):
|
211
|
+
reason = ", ".join(
|
212
|
+
f"{k.replace('_count', '')}"
|
213
|
+
for k, v in violations.items()
|
214
|
+
if isinstance(v, int) and v >= 3
|
215
|
+
)
|
216
|
+
self._terminate(pid, reason=f"Resource violation: {reason}")
|
217
|
+
|
218
|
+
def on_resource_alert(self, pid: int, alert_type: str, info: ProcessInfo):
|
219
|
+
pass # 已在update中处理
|
220
|
+
|
221
|
+
def _terminate(self, pid: int, reason: str):
|
222
|
+
try:
|
223
|
+
os.kill(pid, signal.SIGTERM)
|
224
|
+
logger.warning(f"Terminated process {pid}, reason: {reason}")
|
225
|
+
time.sleep(1)
|
226
|
+
if psutil.pid_exists(pid):
|
227
|
+
os.kill(pid, signal.SIGKILL)
|
228
|
+
logger.warning(f"Force killed process {pid}")
|
229
|
+
except ProcessLookupError:
|
230
|
+
pass
|
231
|
+
finally:
|
232
|
+
self.violations.pop(pid, None)
|
233
|
+
|
234
|
+
class ProcessError(Exception):
|
235
|
+
"""进程操作异常基类"""
|
236
|
+
pass
|
237
|
+
|
238
|
+
class ProcessNotFoundError(ProcessError):
|
239
|
+
"""进程未找到异常"""
|
240
|
+
pass
|
241
|
+
|
242
|
+
class ProcessPermissionError(ProcessError):
|
243
|
+
"""进程权限异常"""
|
244
|
+
pass
|
245
|
+
|
246
|
+
class ProcessManager:
|
247
|
+
"""进程管理器主类"""
|
248
|
+
|
249
|
+
def __init__(self, monitor_interval: float = 1.0):
|
250
|
+
self._observers: List[ProcessObserver] = []
|
251
|
+
self._monitor_thread: Optional[threading.Thread] = None
|
252
|
+
self._monitor_interval = monitor_interval
|
253
|
+
self._should_monitor = False
|
254
|
+
self._process_cache: Dict[int, ProcessInfo] = {}
|
255
|
+
self._executor = ThreadPoolExecutor(max_workers=8)
|
256
|
+
self._lock = threading.RLock()
|
257
|
+
self._start_time = time.time()
|
258
|
+
self._system = platform.system()
|
259
|
+
|
260
|
+
# 自动添加日志观察者
|
261
|
+
self.add_observer(LoggingObserver(logger))
|
262
|
+
|
263
|
+
def __enter__(self):
|
264
|
+
"""上下文管理器入口"""
|
265
|
+
self.monitor_start(self._monitor_interval)
|
266
|
+
return self
|
267
|
+
|
268
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
269
|
+
"""上下文管理器退出"""
|
270
|
+
self.monitor_stop()
|
271
|
+
self._executor.shutdown(wait=True)
|
272
|
+
|
273
|
+
def start_process(
|
274
|
+
self,
|
275
|
+
command: Union[str, List[str]],
|
276
|
+
cwd: Optional[str] = None,
|
277
|
+
env: Optional[Dict[str, str]] = None,
|
278
|
+
shell: bool = False,
|
279
|
+
capture_output: bool = False,
|
280
|
+
**kwargs
|
281
|
+
) -> ProcessInfo:
|
282
|
+
"""
|
283
|
+
启动新进程
|
284
|
+
|
285
|
+
参数:
|
286
|
+
command: 命令字符串或列表
|
287
|
+
cwd: 工作目录
|
288
|
+
env: 环境变量
|
289
|
+
shell: 是否使用shell执行
|
290
|
+
capture_output: 是否捕获输出
|
291
|
+
kwargs: 其他subprocess.Popen参数
|
292
|
+
|
293
|
+
返回:
|
294
|
+
ProcessInfo对象
|
295
|
+
|
296
|
+
抛出:
|
297
|
+
ProcessError: 启动失败时
|
298
|
+
"""
|
299
|
+
try:
|
300
|
+
if isinstance(command, str) and not shell:
|
301
|
+
command = [cmd.strip() for cmd in command.split() if cmd.strip()]
|
302
|
+
|
303
|
+
stdout = subprocess.PIPE if capture_output else None
|
304
|
+
stderr = subprocess.PIPE if capture_output else None
|
305
|
+
|
306
|
+
proc = psutil.Popen(
|
307
|
+
command,
|
308
|
+
cwd=cwd,
|
309
|
+
env=env or os.environ,
|
310
|
+
stdout=stdout,
|
311
|
+
stderr=stderr,
|
312
|
+
shell=shell,
|
313
|
+
**kwargs
|
314
|
+
)
|
315
|
+
|
316
|
+
info = self._get_process_info(proc.pid)
|
317
|
+
self._notify_observers('start', proc.pid, info)
|
318
|
+
|
319
|
+
# 启动线程监控子进程输出
|
320
|
+
if capture_output:
|
321
|
+
self._executor.submit(self._capture_output, proc)
|
322
|
+
|
323
|
+
return info
|
324
|
+
except Exception as e:
|
325
|
+
raise ProcessError(f"Failed to start process: {str(e)}")
|
326
|
+
|
327
|
+
def stop_process(
|
328
|
+
self,
|
329
|
+
pid: int,
|
330
|
+
timeout: int = 5,
|
331
|
+
force: bool = False,
|
332
|
+
kill_children: bool = True
|
333
|
+
) -> bool:
|
334
|
+
"""
|
335
|
+
停止指定进程
|
336
|
+
|
337
|
+
参数:
|
338
|
+
pid: 进程ID
|
339
|
+
timeout: 等待超时(秒)
|
340
|
+
force: 是否强制杀死
|
341
|
+
kill_children: 是否杀死子进程
|
342
|
+
|
343
|
+
返回:
|
344
|
+
是否成功停止
|
345
|
+
|
346
|
+
抛出:
|
347
|
+
ProcessNotFoundError: 进程不存在时
|
348
|
+
ProcessError: 其他错误
|
349
|
+
"""
|
350
|
+
try:
|
351
|
+
proc = psutil.Process(pid)
|
352
|
+
info = self._get_process_info(pid)
|
353
|
+
|
354
|
+
if kill_children:
|
355
|
+
for child in proc.children(recursive=True):
|
356
|
+
try:
|
357
|
+
if force:
|
358
|
+
child.kill()
|
359
|
+
else:
|
360
|
+
child.terminate()
|
361
|
+
except psutil.NoSuchProcess:
|
362
|
+
continue
|
363
|
+
|
364
|
+
if force:
|
365
|
+
proc.kill()
|
366
|
+
else:
|
367
|
+
proc.terminate()
|
368
|
+
|
369
|
+
try:
|
370
|
+
proc.wait(timeout=timeout)
|
371
|
+
except psutil.TimeoutExpired:
|
372
|
+
if force:
|
373
|
+
return False
|
374
|
+
try:
|
375
|
+
proc.kill()
|
376
|
+
proc.wait(timeout=1)
|
377
|
+
except:
|
378
|
+
return False
|
379
|
+
|
380
|
+
self._notify_observers('stop', pid, info)
|
381
|
+
return True
|
382
|
+
except psutil.NoSuchProcess:
|
383
|
+
raise ProcessNotFoundError(f"Process {pid} not found")
|
384
|
+
except psutil.AccessDenied:
|
385
|
+
raise ProcessPermissionError(f"No permission to access process {pid}")
|
386
|
+
except Exception as e:
|
387
|
+
raise ProcessError(f"Failed to stop process {pid}: {str(e)}")
|
388
|
+
|
389
|
+
def list_processes(
|
390
|
+
self,
|
391
|
+
name_filter: Optional[str] = None,
|
392
|
+
user_filter: Optional[str] = None,
|
393
|
+
include_children: bool = False,
|
394
|
+
only_running: bool = True
|
395
|
+
) -> List[ProcessInfo]:
|
396
|
+
"""
|
397
|
+
获取进程列表
|
398
|
+
|
399
|
+
参数:
|
400
|
+
name_filter: 进程名过滤
|
401
|
+
user_filter: 用户名过滤
|
402
|
+
include_children: 是否包含子进程
|
403
|
+
only_running: 是否只返回运行中的进程
|
404
|
+
|
405
|
+
返回:
|
406
|
+
进程信息列表
|
407
|
+
"""
|
408
|
+
processes = []
|
409
|
+
for proc in psutil.process_iter(['pid', 'name', 'status', 'username']):
|
410
|
+
try:
|
411
|
+
if only_running and proc.info['status'] != psutil.STATUS_RUNNING:
|
412
|
+
continue
|
413
|
+
|
414
|
+
if name_filter and name_filter.lower() not in proc.info['name'].lower():
|
415
|
+
continue
|
416
|
+
|
417
|
+
if user_filter and user_filter.lower() != proc.info['username'].lower():
|
418
|
+
continue
|
419
|
+
|
420
|
+
info = self._get_process_info(proc.info['pid'])
|
421
|
+
processes.append(info)
|
422
|
+
|
423
|
+
if include_children:
|
424
|
+
for child in proc.children(recursive=True):
|
425
|
+
try:
|
426
|
+
processes.append(self._get_process_info(child.pid))
|
427
|
+
except psutil.NoSuchProcess:
|
428
|
+
continue
|
429
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
430
|
+
continue
|
431
|
+
return processes
|
432
|
+
|
433
|
+
def get_process_info(self, pid: int) -> ProcessInfo:
|
434
|
+
"""
|
435
|
+
获取指定进程详细信息
|
436
|
+
|
437
|
+
参数:
|
438
|
+
pid: 进程ID
|
439
|
+
|
440
|
+
返回:
|
441
|
+
ProcessInfo对象
|
442
|
+
|
443
|
+
抛出:
|
444
|
+
ProcessNotFoundError: 进程不存在时
|
445
|
+
"""
|
446
|
+
try:
|
447
|
+
return self._get_process_info(pid)
|
448
|
+
except psutil.NoSuchProcess:
|
449
|
+
raise ProcessNotFoundError(f"Process {pid} not found")
|
450
|
+
|
451
|
+
def monitor_start(self, interval: float = None) -> None:
|
452
|
+
"""启动后台监控"""
|
453
|
+
if self._monitor_thread and self._monitor_thread.is_alive():
|
454
|
+
return
|
455
|
+
|
456
|
+
if interval is not None:
|
457
|
+
self._monitor_interval = interval
|
458
|
+
|
459
|
+
self._should_monitor = True
|
460
|
+
self._monitor_thread = threading.Thread(
|
461
|
+
target=self._monitor_loop,
|
462
|
+
daemon=True,
|
463
|
+
name="ProcessMonitor"
|
464
|
+
)
|
465
|
+
self._monitor_thread.start()
|
466
|
+
logger.info("Process monitor started")
|
467
|
+
|
468
|
+
def monitor_stop(self) -> None:
|
469
|
+
"""停止监控"""
|
470
|
+
if not self._should_monitor:
|
471
|
+
return
|
472
|
+
|
473
|
+
self._should_monitor = False
|
474
|
+
if self._monitor_thread:
|
475
|
+
self._monitor_thread.join(timeout=2.0)
|
476
|
+
logger.info("Process monitor stopped")
|
477
|
+
|
478
|
+
def add_observer(self, observer: ProcessObserver) -> None:
|
479
|
+
"""添加观察者"""
|
480
|
+
with self._lock:
|
481
|
+
if observer not in self._observers:
|
482
|
+
self._observers.append(observer)
|
483
|
+
logger.debug(f"Added observer: {observer.__class__.__name__}")
|
484
|
+
|
485
|
+
def remove_observer(self, observer: ProcessObserver) -> None:
|
486
|
+
"""移除观察者"""
|
487
|
+
with self._lock:
|
488
|
+
if observer in self._observers:
|
489
|
+
self._observers.remove(observer)
|
490
|
+
logger.debug(f"Removed observer: {observer.__class__.__name__}")
|
491
|
+
|
492
|
+
def get_stats(self) -> dict:
|
493
|
+
"""获取管理器统计信息"""
|
494
|
+
return {
|
495
|
+
"uptime": time.time() - self._start_time,
|
496
|
+
"monitoring": self._should_monitor,
|
497
|
+
"monitor_interval": self._monitor_interval,
|
498
|
+
"observed_processes": len(self._process_cache),
|
499
|
+
"observers": len(self._observers)
|
500
|
+
}
|
501
|
+
|
502
|
+
def _monitor_loop(self) -> None:
|
503
|
+
"""监控主循环"""
|
504
|
+
logger.info("Monitor loop started")
|
505
|
+
while self._should_monitor:
|
506
|
+
start_time = time.time()
|
507
|
+
current_processes = {}
|
508
|
+
|
509
|
+
try:
|
510
|
+
for proc in psutil.process_iter(['pid', 'name', 'status']):
|
511
|
+
try:
|
512
|
+
pid = proc.info['pid']
|
513
|
+
info = self._get_process_info(pid)
|
514
|
+
current_processes[pid] = info
|
515
|
+
|
516
|
+
# 检查资源使用情况
|
517
|
+
self._check_resources(pid, info)
|
518
|
+
|
519
|
+
# 处理新增或状态变化的进程
|
520
|
+
if pid not in self._process_cache:
|
521
|
+
self._notify_observers('start', pid, info)
|
522
|
+
elif info.status != self._process_cache[pid].status:
|
523
|
+
self._notify_observers('update', pid, info)
|
524
|
+
|
525
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
526
|
+
continue
|
527
|
+
|
528
|
+
# 处理已退出的进程
|
529
|
+
for pid in set(self._process_cache) - set(current_processes):
|
530
|
+
info = self._process_cache[pid]
|
531
|
+
info.status = ProcessStatus.STOPPED
|
532
|
+
self._notify_observers('stop', pid, info)
|
533
|
+
|
534
|
+
self._process_cache = current_processes
|
535
|
+
|
536
|
+
except Exception as e:
|
537
|
+
logger.error(f"Monitor loop error: {str(e)}", exc_info=True)
|
538
|
+
|
539
|
+
# 精确控制间隔时间
|
540
|
+
elapsed = time.time() - start_time
|
541
|
+
sleep_time = max(0, self._monitor_interval - elapsed)
|
542
|
+
time.sleep(sleep_time)
|
543
|
+
|
544
|
+
def _get_process_info(self, pid: int) -> ProcessInfo:
|
545
|
+
"""获取进程详细信息"""
|
546
|
+
proc = psutil.Process(pid)
|
547
|
+
with proc.oneshot():
|
548
|
+
mem_info = proc.memory_info()
|
549
|
+
status = self._convert_status(proc.status())
|
550
|
+
|
551
|
+
try:
|
552
|
+
connections = proc.connections()
|
553
|
+
except (psutil.AccessDenied, psutil.Error):
|
554
|
+
connections = []
|
555
|
+
|
556
|
+
try:
|
557
|
+
open_files = [f.path for f in proc.open_files()]
|
558
|
+
except (psutil.AccessDenied, psutil.Error):
|
559
|
+
open_files = []
|
560
|
+
|
561
|
+
try:
|
562
|
+
cpu_times = proc.cpu_times()._asdict()
|
563
|
+
except (psutil.AccessDenied, psutil.Error):
|
564
|
+
cpu_times = None
|
565
|
+
|
566
|
+
try:
|
567
|
+
io_counters = proc.io_counters()._asdict()
|
568
|
+
except (psutil.AccessDenied, psutil.Error):
|
569
|
+
io_counters = None
|
570
|
+
|
571
|
+
try:
|
572
|
+
environ = proc.environ()
|
573
|
+
except (psutil.AccessDenied, psutil.Error):
|
574
|
+
environ = None
|
575
|
+
|
576
|
+
children = []
|
577
|
+
try:
|
578
|
+
for child in proc.children(recursive=False):
|
579
|
+
try:
|
580
|
+
children.append(self._get_process_info(child.pid))
|
581
|
+
except psutil.NoSuchProcess:
|
582
|
+
continue
|
583
|
+
except psutil.AccessDenied:
|
584
|
+
pass
|
585
|
+
|
586
|
+
return ProcessInfo(
|
587
|
+
pid=pid,
|
588
|
+
name=proc.name(),
|
589
|
+
status=status,
|
590
|
+
cpu_percent=proc.cpu_percent(),
|
591
|
+
memory_mb=mem_info.rss / (1024 * 1024),
|
592
|
+
create_time=proc.create_time(),
|
593
|
+
cmdline=proc.cmdline(),
|
594
|
+
threads=proc.num_threads(),
|
595
|
+
username=proc.username(),
|
596
|
+
connections=connections,
|
597
|
+
open_files=open_files,
|
598
|
+
children=children,
|
599
|
+
exit_code=proc.returncode if status == ProcessStatus.STOPPED else None,
|
600
|
+
cpu_times=cpu_times,
|
601
|
+
io_counters=io_counters,
|
602
|
+
environ=environ
|
603
|
+
)
|
604
|
+
|
605
|
+
def _check_resources(self, pid: int, info: ProcessInfo) -> None:
|
606
|
+
"""检查资源使用情况并触发警报"""
|
607
|
+
thresholds = [
|
608
|
+
('high_cpu', info.cpu_percent > 90),
|
609
|
+
('high_memory', info.memory_mb > 1024),
|
610
|
+
('many_threads', info.threads > 100),
|
611
|
+
('many_connections', len(info.connections) > 50),
|
612
|
+
('many_files', len(info.open_files) > 500)
|
613
|
+
]
|
614
|
+
|
615
|
+
for alert_type, condition in thresholds:
|
616
|
+
if condition:
|
617
|
+
self._notify_observers('alert', pid, info, alert_type)
|
618
|
+
|
619
|
+
def _notify_observers(self, event_type: str, pid: int, info: ProcessInfo, alert_type: str = None):
|
620
|
+
"""通知所有观察者"""
|
621
|
+
def notify_task():
|
622
|
+
with self._lock:
|
623
|
+
observers = self._observers.copy()
|
624
|
+
|
625
|
+
for observer in observers:
|
626
|
+
try:
|
627
|
+
if event_type == 'start':
|
628
|
+
observer.on_process_start(pid, info)
|
629
|
+
elif event_type == 'stop':
|
630
|
+
observer.on_process_stop(pid, info)
|
631
|
+
elif event_type == 'update':
|
632
|
+
observer.on_process_update(pid, info)
|
633
|
+
elif event_type == 'alert':
|
634
|
+
observer.on_resource_alert(pid, alert_type, info)
|
635
|
+
except Exception as e:
|
636
|
+
logger.error(f"Observer notification failed: {str(e)}", exc_info=True)
|
637
|
+
|
638
|
+
self._executor.submit(notify_task)
|
639
|
+
|
640
|
+
def _capture_output(self, proc: psutil.Popen):
|
641
|
+
"""捕获进程输出"""
|
642
|
+
def reader(stream, callback):
|
643
|
+
try:
|
644
|
+
for line in iter(stream.readline, b''):
|
645
|
+
if line:
|
646
|
+
callback(line.decode('utf-8', errors='replace').strip())
|
647
|
+
except ValueError:
|
648
|
+
pass # Stream closed
|
649
|
+
|
650
|
+
def stdout_callback(line):
|
651
|
+
logger.info(f"Process {proc.pid} stdout: {line}")
|
652
|
+
|
653
|
+
def stderr_callback(line):
|
654
|
+
logger.warning(f"Process {proc.pid} stderr: {line}")
|
655
|
+
|
656
|
+
stdout_thread = threading.Thread(
|
657
|
+
target=reader,
|
658
|
+
args=(proc.stdout, stdout_callback),
|
659
|
+
daemon=True
|
660
|
+
)
|
661
|
+
|
662
|
+
stderr_thread = threading.Thread(
|
663
|
+
target=reader,
|
664
|
+
args=(proc.stderr, stderr_callback),
|
665
|
+
daemon=True
|
666
|
+
)
|
667
|
+
|
668
|
+
stdout_thread.start()
|
669
|
+
stderr_thread.start()
|
670
|
+
|
671
|
+
stdout_thread.join()
|
672
|
+
stderr_thread.join()
|
673
|
+
|
674
|
+
def _convert_status(self, status: str) -> ProcessStatus:
|
675
|
+
"""转换状态字符串为枚举"""
|
676
|
+
status = status.upper()
|
677
|
+
status_map = {
|
678
|
+
"RUNNING": ProcessStatus.RUNNING,
|
679
|
+
"SLEEPING": ProcessStatus.SLEEPING,
|
680
|
+
"IDLE": ProcessStatus.IDLE,
|
681
|
+
"STOPPED": ProcessStatus.STOPPED,
|
682
|
+
"TRACING_STOP": ProcessStatus.TRACING_STOP,
|
683
|
+
"ZOMBIE": ProcessStatus.ZOMBIE
|
684
|
+
}
|
685
|
+
return status_map.get(status, ProcessStatus.UNKNOWN)
|
686
|
+
|
687
|
+
# 示例用法
|
688
|
+
if __name__ == "__main__":
|
689
|
+
# 创建进程管理器
|
690
|
+
with ProcessManager(monitor_interval=2.0) as manager:
|
691
|
+
# 添加资源守护观察者
|
692
|
+
guard = ResourceGuardObserver(
|
693
|
+
max_cpu=80.0,
|
694
|
+
max_memory_mb=512,
|
695
|
+
max_threads=50,
|
696
|
+
max_duration=30
|
697
|
+
)
|
698
|
+
manager.add_observer(guard)
|
699
|
+
|
700
|
+
# 启动一个进程
|
701
|
+
try:
|
702
|
+
print("Starting a process...")
|
703
|
+
proc_info = manager.start_process(
|
704
|
+
["python", "-c", "while True: pass"], # 模拟CPU密集型进程
|
705
|
+
capture_output=True
|
706
|
+
)
|
707
|
+
print(f"Started process PID: {proc_info.pid}")
|
708
|
+
|
709
|
+
# 查看进程列表
|
710
|
+
print("\nProcess list:")
|
711
|
+
for p in manager.list_processes(only_running=True)[:5]:
|
712
|
+
print(f"PID: {p.pid}, Name: {p.name}, CPU: {p.cpu_percent}%")
|
713
|
+
|
714
|
+
# 让监控运行一段时间
|
715
|
+
print("\nMonitoring for 10 seconds...")
|
716
|
+
time.sleep(10)
|
717
|
+
|
718
|
+
except KeyboardInterrupt:
|
719
|
+
print("\nInterrupted by user")
|
720
|
+
except Exception as e:
|
721
|
+
print(f"Error: {str(e)}")
|
722
|
+
finally:
|
723
|
+
# 停止所有我们启动的进程
|
724
|
+
for p in manager.list_processes():
|
725
|
+
if "python" in p.name.lower():
|
726
|
+
try:
|
727
|
+
print(f"Stopping process {p.pid}...")
|
728
|
+
manager.stop_process(p.pid)
|
729
|
+
except Exception as e:
|
730
|
+
print(f"Error stopping process {p.pid}: {str(e)}")
|