bitool 0.1.2__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.
Files changed (51) hide show
  1. bitool/__init__.py +27 -0
  2. bitool/cmd/__init__.py +65 -0
  3. bitool/cmd/_base.py +105 -0
  4. bitool/cmd/_condition.py +60 -0
  5. bitool/cmd/_scheduler.py +548 -0
  6. bitool/cmd/env.py +454 -0
  7. bitool/cmd/git.py +123 -0
  8. bitool/cmd/io.py +248 -0
  9. bitool/cmd/pdf.py +385 -0
  10. bitool/cmd/run.py +300 -0
  11. bitool/cmd/toml.py +237 -0
  12. bitool/cmd/version.py +630 -0
  13. bitool/consts.py +14 -0
  14. bitool/core/__init__.py +7 -0
  15. bitool/core/app.py +142 -0
  16. bitool/core/commands.py +194 -0
  17. bitool/core/config.py +647 -0
  18. bitool/core/env.py +18 -0
  19. bitool/core/logger.py +237 -0
  20. bitool/core/plugin.py +117 -0
  21. bitool/core/workspace.py +76 -0
  22. bitool/models/__init__.py +3 -0
  23. bitool/models/version.py +173 -0
  24. bitool/scripts/__init__.py +1 -0
  25. bitool/scripts/bumpversion.py +189 -0
  26. bitool/scripts/clearscreen.py +37 -0
  27. bitool/scripts/envpy.py +161 -0
  28. bitool/scripts/envrs.py +119 -0
  29. bitool/scripts/filedate.py +246 -0
  30. bitool/scripts/filelevel.py +191 -0
  31. bitool/scripts/gittool.py +178 -0
  32. bitool/scripts/img2pdf.py +151 -0
  33. bitool/scripts/pdf2img.py +139 -0
  34. bitool/scripts/piptool.py +130 -0
  35. bitool/scripts/pymake.py +345 -0
  36. bitool/scripts/sshcopyid.py +491 -0
  37. bitool/scripts/taskkill.py +366 -0
  38. bitool/scripts/which.py +227 -0
  39. bitool/types.py +7 -0
  40. bitool/utils/__init__.py +9 -0
  41. bitool/utils/cli_parser.py +412 -0
  42. bitool/utils/executor.py +881 -0
  43. bitool/utils/profiler.py +369 -0
  44. bitool/utils/task.py +133 -0
  45. bitool/utils/task_group.py +668 -0
  46. bitool/utils/tests/__init__.py +0 -0
  47. bitool/utils/tests/test_profiler.py +487 -0
  48. bitool-0.1.2.dist-info/METADATA +154 -0
  49. bitool-0.1.2.dist-info/RECORD +51 -0
  50. bitool-0.1.2.dist-info/WHEEL +4 -0
  51. bitool-0.1.2.dist-info/entry_points.txt +15 -0
@@ -0,0 +1,881 @@
1
+ """子进程命令执行模块.
2
+
3
+ 本模块提供一个智能的子进程执行器,根据系统 CPU 配置自动选择最优的
4
+ 命令执行方式,并将所有输出记录到日志系统中.
5
+
6
+ 核心功能
7
+ --------
8
+ - 根据 CPU 核心数自动选择串行/并行执行策略
9
+ - 实时捕获并记录命令的标准输出和标准错误
10
+ - 支持超时控制和进程管理
11
+ - 线程安全的执行环境
12
+ - 可扩展的命令执行策略
13
+
14
+ 快速开始
15
+ --------
16
+ >>> from pytola_core.command import execute
17
+ >>> result = execute(["ffmpeg", "-i", "input.mp4", "output.avi"])
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import subprocess
23
+ import threading
24
+ import time
25
+ from dataclasses import dataclass, field
26
+ from enum import Enum
27
+ from io import TextIOBase
28
+ from pathlib import Path
29
+ from typing import Callable, List, Sequence, Union, overload
30
+
31
+ from ..core import ConfigMixin, logger
32
+
33
+ __all__ = ["execute"]
34
+
35
+
36
+ class RunMode(str, Enum):
37
+ """执行模式常量."""
38
+
39
+ SERIAL = "serial"
40
+ CONCURRENT = "concurrent"
41
+ STREAMING = "streaming"
42
+
43
+
44
+ class _DefaultExecutorConfig(ConfigMixin):
45
+ """默认执行器配置."""
46
+
47
+ TIMEOUT: int = 30 # 默认超时时间(秒)
48
+ STREAM_CHUNK_SIZE: int = 1024
49
+ ENABLE_STREAMING: bool = True
50
+
51
+
52
+ _conf = _DefaultExecutorConfig()
53
+
54
+
55
+ @dataclass
56
+ class RunResult:
57
+ """命令执行结果.
58
+
59
+ Attributes
60
+ ----------
61
+ returncode : int
62
+ 命令返回码,0 表示成功
63
+ stdout : str
64
+ 标准输出内容
65
+ stderr : str
66
+ 标准错误内容
67
+ execution_time : float
68
+ 执行耗时(秒)
69
+ mode : str
70
+ 使用的执行模式
71
+ cmd : list[str]
72
+ 执行的命令
73
+ """
74
+
75
+ returncode: int
76
+ stdout: str
77
+ stderr: str
78
+ execution_time: float
79
+ mode: str = RunMode.SERIAL
80
+ cmd: list[str] = field(default_factory=list)
81
+
82
+ @property
83
+ def success(self) -> bool:
84
+ """命令是否执行成功."""
85
+ return self.returncode == 0
86
+
87
+
88
+ # 类型别名 (使用 Union 以兼容 Python 3.8)
89
+ ProcessArgs = Sequence[Union[List[str], Callable[[], RunResult]]]
90
+
91
+
92
+ class ProcessExecutor:
93
+ """智能命令执行器.
94
+
95
+ 根据系统 CPU 配置自动选择最优执行策略,并将输出记录到日志系统.
96
+
97
+ Parameters
98
+ ----------
99
+ long_running_cmds : set[str], optional
100
+ 自定义长时间运行命令集合,默认使用内置集合
101
+
102
+ Examples
103
+ --------
104
+ >>> executor = ProcessExecutor()
105
+ >>> result = executor.run(["ls", "-la"])
106
+ >>> if result.success:
107
+ ... print("命令执行成功")
108
+ """
109
+
110
+ def _select_execution_mode(self, cmd: list[str]) -> str:
111
+ """根据系统配置选择执行模式.
112
+
113
+ 所有命令统一使用流式模式,实时显示执行进展.
114
+
115
+ Parameters
116
+ ----------
117
+ cmd : list[str]
118
+ 命令及其参数列表
119
+
120
+ Returns
121
+ -------
122
+ str
123
+ 选择的执行模式
124
+ """
125
+ if not cmd:
126
+ return RunMode.SERIAL
127
+
128
+ # 所有命令统一使用流式模式,实时显示输出
129
+ return RunMode.STREAMING
130
+
131
+ def run(
132
+ self,
133
+ cmd: list[str],
134
+ cwd: Path | None = None,
135
+ env: dict[str, str] | None = None,
136
+ timeout: int | None = None,
137
+ *,
138
+ check: bool = False,
139
+ capture_output: bool = True,
140
+ text: bool = True,
141
+ ) -> RunResult:
142
+ """执行命令并返回结果.
143
+
144
+ Parameters
145
+ ----------
146
+ cmd : list[str]
147
+ 命令及其参数列表
148
+ cwd : Path, optional
149
+ 工作目录
150
+ env : dict[str, str], optional
151
+ 环境变量
152
+ timeout : int, optional
153
+ 超时时间(秒),默认使用实例配置
154
+ check : bool, optional
155
+ 是否检查返回码,非零时抛出异常
156
+ capture_output : bool, optional
157
+ 是否捕获输出
158
+ text : bool, optional
159
+ 是否以文本模式处理输出
160
+
161
+ Returns
162
+ -------
163
+ RunResult
164
+ 命令执行结果
165
+
166
+ Raises
167
+ ------
168
+ subprocess.TimeoutExpired
169
+ 命令执行超时
170
+ subprocess.CalledProcessError
171
+ 命令返回非零退出码(当 check=True 时)
172
+ ValueError
173
+ 命令列表为空
174
+ """
175
+ if not cmd:
176
+ msg = "命令列表不能为空"
177
+ raise ValueError(msg)
178
+
179
+ effective_timeout: int = timeout if timeout is not None else _conf.TIMEOUT
180
+ mode: str = self._select_execution_mode(cmd=cmd)
181
+
182
+ logger.info(
183
+ f"开始执行命令: `{' '.join(cmd)}`, 模式:{mode}, 超时:{effective_timeout}秒"
184
+ )
185
+
186
+ if mode == RunMode.STREAMING:
187
+ return self._run_streaming(
188
+ cmd,
189
+ cwd,
190
+ env,
191
+ effective_timeout,
192
+ check=check,
193
+ capture_output=capture_output,
194
+ text=text,
195
+ )
196
+ elif mode == RunMode.CONCURRENT:
197
+ return self._run_concurrent(
198
+ cmd,
199
+ cwd,
200
+ env,
201
+ effective_timeout,
202
+ check=check,
203
+ capture_output=capture_output,
204
+ text=text,
205
+ )
206
+ else:
207
+ return self._run_serial(
208
+ cmd,
209
+ cwd,
210
+ env,
211
+ effective_timeout,
212
+ check=check,
213
+ capture_output=capture_output,
214
+ text=text,
215
+ )
216
+
217
+ def _run_serial(
218
+ self,
219
+ cmd: list[str],
220
+ cwd: Path | None,
221
+ env: dict[str, str] | None,
222
+ timeout: int,
223
+ *,
224
+ check: bool,
225
+ capture_output: bool,
226
+ text: bool,
227
+ ) -> RunResult:
228
+ """串行执行命令.
229
+
230
+ Parameters
231
+ ----------
232
+ cmd : list[str]
233
+ 命令及其参数列表
234
+ cwd : Path, optional
235
+ 工作目录
236
+ env : dict[str, str], optional
237
+ 环境变量
238
+ timeout : int
239
+ 超时时间(秒)
240
+ check : bool
241
+ 是否检查返回码
242
+ capture_output : bool
243
+ 是否捕获输出
244
+ text : bool
245
+ 是否以文本模式处理输出
246
+
247
+ Returns
248
+ -------
249
+ RunResult
250
+ 命令执行结果
251
+ """
252
+ start_time = time.time()
253
+ stdout_lines: list[str] = []
254
+ stderr_lines: list[str] = []
255
+ execution_time: float = 0.0
256
+
257
+ try:
258
+ # 使用 Popen 实现实时输出
259
+ # 注意:行缓冲(bufsize=1)仅在 text=True 时有效
260
+ process = subprocess.Popen( # noqa: S603
261
+ cmd,
262
+ cwd=cwd,
263
+ env=env,
264
+ stdout=subprocess.PIPE,
265
+ stderr=subprocess.PIPE,
266
+ text=text,
267
+ bufsize=1 if text else -1, # 行缓冲(仅文本模式)或使用默认缓冲
268
+ )
269
+
270
+ # 启动线程读取输出
271
+ stdout_thread = threading.Thread(
272
+ target=self._read_stream,
273
+ args=(process.stdout, stdout_lines, "STDOUT"),
274
+ daemon=True,
275
+ )
276
+ stderr_thread = threading.Thread(
277
+ target=self._read_stream,
278
+ args=(process.stderr, stderr_lines, "STDERR"),
279
+ daemon=True,
280
+ )
281
+
282
+ stdout_thread.start()
283
+ stderr_thread.start()
284
+
285
+ # 等待进程完成或超时
286
+ returncode: int = process.wait(timeout=timeout)
287
+
288
+ # 等待输出读取完成(设置合理的超时避免无限等待)
289
+ stdout_thread.join(timeout=5)
290
+ stderr_thread.join(timeout=5)
291
+
292
+ execution_time = time.time() - start_time
293
+
294
+ stdout_content = "\n".join(stdout_lines)
295
+ stderr_content = "\n".join(stderr_lines)
296
+
297
+ log_msg = f"命令执行完成: `{' '.join(cmd)}`, 返回码:{returncode}, 耗时:{execution_time:.2f}秒`"
298
+
299
+ # uv 工具特殊处理:返回码 2 表示有警告但成功
300
+ is_uv_command = cmd and cmd[0] == "uv"
301
+ is_uv_success = is_uv_command and returncode == 2
302
+
303
+ if returncode == 0 or is_uv_success:
304
+ logger.info(log_msg)
305
+ # uv 返回码 2 时添加警告提示
306
+ if is_uv_success:
307
+ logger.warning("uv 命令有警告信息,请检查上方输出")
308
+ # 已实时输出, 不再重复输出
309
+ else:
310
+ # 根据返回码决定日志级别
311
+ if returncode < 0:
312
+ logger.error(log_msg)
313
+ else:
314
+ logger.warning(log_msg)
315
+
316
+ # 失败时显示错误输出(排除 uv 的进度信息)
317
+ if stderr_content:
318
+ for line in stderr_content.strip().split("\n"):
319
+ if line.strip():
320
+ # uv 的进度信息不算错误
321
+ if is_uv_command and any(
322
+ keyword in line
323
+ for keyword in [
324
+ "Resolved",
325
+ "Building",
326
+ "Built",
327
+ "Prepared",
328
+ ]
329
+ ):
330
+ logger.info(f" uv 进度: `{line.strip()}`")
331
+ else:
332
+ logger.error(f" 错误: `{line.strip()}`")
333
+ elif stdout_content:
334
+ for line in stdout_content.strip().split("\n"):
335
+ if line.strip():
336
+ logger.warning(f" 输出: `{line.strip()}`")
337
+
338
+ if check and returncode != 0:
339
+ self._raise_on_failure(returncode, cmd, stdout_content, stderr_content)
340
+
341
+ return RunResult(
342
+ returncode=returncode,
343
+ stdout=stdout_content,
344
+ stderr=stderr_content,
345
+ execution_time=execution_time,
346
+ mode=RunMode.SERIAL,
347
+ cmd=cmd,
348
+ )
349
+
350
+ except subprocess.TimeoutExpired:
351
+ execution_time = time.time() - start_time
352
+ logger.error(f"命令执行超时: {' '.join(cmd)}, 超时时间:{timeout}秒")
353
+ raise
354
+ except FileNotFoundError as e:
355
+ execution_time = time.time() - start_time
356
+ logger.error(f"命令未找到: {' '.join(cmd)}, 错误:{e}")
357
+ raise
358
+ except (OSError, RuntimeError, ValueError) as e:
359
+ execution_time = time.time() - start_time
360
+ logger.error(f"命令执行异常: {' '.join(cmd)}, 错误:{e}", exc_info=True)
361
+ raise
362
+
363
+ def _run_concurrent(
364
+ self,
365
+ cmd: list[str],
366
+ cwd: Path | None,
367
+ env: dict[str, str] | None,
368
+ timeout: int,
369
+ *,
370
+ check: bool,
371
+ capture_output: bool,
372
+ text: bool,
373
+ ) -> RunResult:
374
+ """并发执行命令(使用线程池优化).
375
+
376
+ 注意: 对于单个命令,并发模式与串行模式相同.
377
+ 此方法预留接口用于未来批量命令执行优化.
378
+
379
+ Parameters
380
+ ----------
381
+ cmd : list[str]
382
+ 命令及其参数列表
383
+ cwd : Path, optional
384
+ 工作目录
385
+ env : dict[str, str], optional
386
+ 环境变量
387
+ timeout : int
388
+ 超时时间(秒)
389
+ check : bool
390
+ 是否检查返回码
391
+ capture_output : bool
392
+ 是否捕获输出
393
+ text : bool
394
+ 是否以文本模式处理输出
395
+
396
+ Returns
397
+ -------
398
+ RunResult
399
+ 命令执行结果
400
+ """
401
+ return self._run_serial(
402
+ cmd,
403
+ cwd,
404
+ env,
405
+ timeout,
406
+ check=check,
407
+ capture_output=capture_output,
408
+ text=text,
409
+ )
410
+
411
+ def _run_streaming(
412
+ self,
413
+ cmd: list[str],
414
+ cwd: Path | None,
415
+ env: dict[str, str] | None,
416
+ timeout: int,
417
+ *,
418
+ check: bool,
419
+ capture_output: bool,
420
+ text: bool,
421
+ ) -> RunResult:
422
+ """流式执行命令,实时捕获输出.
423
+
424
+ Parameters
425
+ ----------
426
+ cmd : list[str]
427
+ 命令及其参数列表
428
+ cwd : Path, optional
429
+ 工作目录
430
+ env : dict[str, str], optional
431
+ 环境变量
432
+ timeout : int
433
+ 超时时间(秒)
434
+ check : bool
435
+ 是否检查返回码
436
+ capture_output : bool
437
+ 是否捕获输出
438
+ text : bool
439
+ 是否以文本模式处理输出
440
+
441
+ Returns
442
+ -------
443
+ RunResult
444
+ 命令执行结果
445
+ """
446
+ start_time = time.time()
447
+ stdout_lines: list[str] = []
448
+ stderr_lines: list[str] = []
449
+ execution_time: float = 0.0
450
+
451
+ try:
452
+ process = subprocess.Popen( # noqa: S603
453
+ cmd,
454
+ cwd=cwd,
455
+ env=env,
456
+ stdout=subprocess.PIPE,
457
+ stderr=subprocess.PIPE,
458
+ text=text,
459
+ bufsize=1 if text else -1, # 行缓冲(仅文本模式)或使用默认缓冲
460
+ )
461
+
462
+ # 启动线程读取输出
463
+ stdout_thread = threading.Thread(
464
+ target=self._read_stream,
465
+ args=(process.stdout, stdout_lines, "STDOUT"),
466
+ daemon=True,
467
+ )
468
+ stderr_thread = threading.Thread(
469
+ target=self._read_stream,
470
+ args=(process.stderr, stderr_lines, "STDERR"),
471
+ daemon=True,
472
+ )
473
+
474
+ stdout_thread.start()
475
+ stderr_thread.start()
476
+
477
+ # 等待进程完成或超时
478
+ try:
479
+ returncode: int = process.wait(timeout=timeout)
480
+ except subprocess.TimeoutExpired:
481
+ process.kill()
482
+ execution_time = time.time() - start_time
483
+ logger.error(
484
+ f"流式命令执行超时: `{' '.join(cmd)}, 超时时间:{timeout}秒`"
485
+ )
486
+ raise
487
+
488
+ # 等待输出读取完成(设置合理的超时避免无限等待)
489
+ stdout_thread.join(timeout=5)
490
+ stderr_thread.join(timeout=5)
491
+
492
+ # 如果线程仍在运行,记录警告(daemon 线程会在主线程退出时自动终止)
493
+ if stdout_thread.is_alive() or stderr_thread.is_alive():
494
+ logger.warning("输出读取线程未在预期时间内完成")
495
+
496
+ execution_time = time.time() - start_time
497
+
498
+ stdout_content = "\n".join(stdout_lines)
499
+ stderr_content = "\n".join(stderr_lines)
500
+
501
+ log_msg = f"流式命令执行完成: `{' '.join(cmd)}, 返回码:{returncode}, 耗时:{execution_time:.2f}秒`"
502
+
503
+ # uv 工具特殊处理:返回码 2 表示有警告但成功
504
+ is_uv_command = cmd and cmd[0] == "uv"
505
+ is_uv_success = is_uv_command and returncode == 2
506
+
507
+ if returncode == 0 or is_uv_success:
508
+ logger.info(log_msg)
509
+ # uv 返回码 2 时添加警告提示
510
+ if is_uv_success:
511
+ logger.warning("uv 命令有警告信息,请检查上方输出")
512
+ # 流式模式已实时输出, 不再重复输出
513
+ else:
514
+ # 根据返回码决定日志级别
515
+ if returncode < 0:
516
+ logger.error(log_msg)
517
+ else:
518
+ logger.warning(log_msg)
519
+
520
+ # 失败时显示错误输出(排除 uv 的进度信息)
521
+ if stderr_content:
522
+ for line in stderr_content.strip().split("\n"):
523
+ if line.strip():
524
+ # uv 的进度信息不算错误
525
+ if is_uv_command and any(
526
+ keyword in line
527
+ for keyword in [
528
+ "Resolved",
529
+ "Building",
530
+ "Built",
531
+ "Prepared",
532
+ ]
533
+ ):
534
+ logger.info(f" uv 进度: `{line.strip()}`")
535
+ else:
536
+ logger.error(f" 错误: `{line.strip()}`")
537
+ elif stdout_content:
538
+ for line in stdout_content.strip().split("\n"):
539
+ if line.strip():
540
+ logger.warning(f" 输出: `{line.strip()}`")
541
+
542
+ if check and returncode != 0:
543
+ self._raise_on_failure(returncode, cmd, stdout_content, stderr_content)
544
+
545
+ return RunResult(
546
+ returncode=returncode,
547
+ stdout=stdout_content,
548
+ stderr=stderr_content,
549
+ execution_time=execution_time,
550
+ mode=RunMode.STREAMING,
551
+ cmd=cmd,
552
+ )
553
+
554
+ except FileNotFoundError as e:
555
+ execution_time = time.time() - start_time
556
+ logger.error(f"流式命令未找到: `{' '.join(cmd)}, 错误:{e}`")
557
+ raise
558
+ except (OSError, RuntimeError, ValueError) as e:
559
+ execution_time = time.time() - start_time
560
+ logger.error(
561
+ f"流式命令执行异常: `{' '.join(cmd)}, 错误:{e}`, exc_info=True"
562
+ )
563
+ raise
564
+
565
+ def _raise_on_failure(
566
+ self, returncode: int, cmd: list[str], stdout: str, stderr: str
567
+ ) -> None:
568
+ """命令失败时抛出异常.
569
+
570
+ Parameters
571
+ ----------
572
+ returncode : int
573
+ 命令返回码
574
+ cmd : list[str]
575
+ 命令及其参数列表
576
+ stdout : str
577
+ 标准输出内容
578
+ stderr : str
579
+ 标准错误内容
580
+
581
+ Raises
582
+ ------
583
+ subprocess.CalledProcessError
584
+ 命令返回非零退出码
585
+ """
586
+ raise subprocess.CalledProcessError(returncode, cmd, stdout, stderr)
587
+
588
+ def _read_stream(
589
+ self, stream: TextIOBase, output_list: list[str], stream_name: str
590
+ ) -> None:
591
+ """读取流式输出.
592
+
593
+ Parameters
594
+ ----------
595
+ stream : file-like object
596
+ 输出流
597
+ output_list : list[str]
598
+ 存储输出内容的列表
599
+ stream_name : str
600
+ 流名称(STDOUT/STDERR)
601
+ text_mode : bool
602
+ 是否为文本模式
603
+ """
604
+ try:
605
+ # 预分配列表容量以提升性能(估计值)
606
+ for line in stream:
607
+ # 处理bytes或str类型
608
+ line_str = (
609
+ line.decode("utf-8", errors="replace")
610
+ if isinstance(line, bytes)
611
+ else line
612
+ )
613
+
614
+ line_str = line_str.rstrip("\n")
615
+ if line_str.strip(): # 只记录非空白行
616
+ output_list.append(line_str)
617
+ # 实时记录输出到INFO级别,确保用户能看到执行进展
618
+ if stream_name == "STDERR":
619
+ logger.info(f"[STDERR] {line_str}")
620
+ else:
621
+ logger.info(f"[STDOUT] {line_str}")
622
+ except (OSError, RuntimeError) as e:
623
+ logger.error(f"读取{stream_name}流时发生错误:{e}")
624
+ except ValueError as e:
625
+ # 处理流关闭时的 ValueError
626
+ logger.debug(f"读取{stream_name}流时发生ValueError, 可能是流已关闭:{e}")
627
+
628
+ def run_batch(
629
+ self,
630
+ commands: Sequence[list[str] | Callable[[], RunResult]],
631
+ max_concurrent: int | None = None,
632
+ timeout: int | None = None,
633
+ ) -> list[RunResult]:
634
+ """批量执行命令.
635
+
636
+ Parameters
637
+ ----------
638
+ commands : Sequence[list[str] | Callable[[], RunResult]]
639
+ 命令列表或可调用对象列表
640
+ max_concurrent : int, optional
641
+ 最大并发数,默认根据 CPU 配置计算
642
+ timeout : int, optional
643
+ 每个命令的超时时间
644
+
645
+ Returns
646
+ -------
647
+ list[RunResult]
648
+ 命令执行结果列表
649
+ """
650
+ from concurrent.futures import ThreadPoolExecutor, as_completed
651
+
652
+ if not commands:
653
+ logger.info("批量执行命令列表为空, 直接返回")
654
+ return []
655
+
656
+ effective_timeout = timeout if timeout is not None else _conf.TIMEOUT
657
+ effective_max_workers = (
658
+ max_concurrent if max_concurrent is not None else _conf._MAX_WORKERS
659
+ )
660
+
661
+ logger.info(
662
+ f"开始批量执行 {len(commands)} 个命令, 最大并发数:{effective_max_workers}, 超时时间:{effective_timeout}秒"
663
+ )
664
+
665
+ results: list[RunResult] = []
666
+ batch_start_time = time.time()
667
+ completed_count = 0
668
+ total_count = len(commands)
669
+
670
+ with ThreadPoolExecutor(max_workers=effective_max_workers) as executor:
671
+ # 提交所有任务
672
+ future_to_cmd = {}
673
+ for idx, cmd in enumerate(commands):
674
+ if callable(cmd):
675
+ # 如果是可调用对象,直接提交
676
+ future = executor.submit(cmd)
677
+ future_to_cmd[future] = (cmd, idx)
678
+ logger.debug(
679
+ f"提交可调用任务 [{idx + 1}/{total_count}]: {cmd.__name__ if hasattr(cmd, '__name__') else 'lambda'}"
680
+ )
681
+ else:
682
+ # 如果是命令列表,调用 self.run
683
+ future = executor.submit(self.run, cmd, timeout=effective_timeout)
684
+ future_to_cmd[future] = (cmd, idx)
685
+ logger.debug(
686
+ f"提交命令任务 [{idx + 1}/{total_count}]: {' '.join(cmd)}"
687
+ )
688
+
689
+ # 收集结果
690
+ for future in as_completed(future_to_cmd):
691
+ cmd, idx = future_to_cmd[future]
692
+ completed_count += 1
693
+ try:
694
+ result = future.result()
695
+ results.append(result)
696
+
697
+ # 记录单个命令执行结果
698
+ cmd_str = (
699
+ " ".join(cmd)
700
+ if isinstance(cmd, list)
701
+ else (cmd.__name__ if hasattr(cmd, "__name__") else str(cmd))
702
+ )
703
+ if result.success:
704
+ logger.info(
705
+ f"[{completed_count}/{total_count}] 成功: {cmd_str} (耗时:{result.execution_time:.2f}秒)"
706
+ )
707
+ # 显示命令输出
708
+ if result.stdout:
709
+ for line in result.stdout.strip().split("\n"):
710
+ if line.strip():
711
+ logger.info(f" 输出: {line.strip()}")
712
+ else:
713
+ # 根据返回码决定日志级别
714
+ if result.returncode < 0:
715
+ logger.error(
716
+ f"[{completed_count}/{total_count}] 失败: {cmd_str} (返回码:{result.returncode})"
717
+ )
718
+ else:
719
+ logger.warning(
720
+ f"[{completed_count}/{total_count}] 失败: {cmd_str} (返回码:{result.returncode})"
721
+ )
722
+
723
+ # 显示错误输出
724
+ if result.stderr:
725
+ for line in result.stderr.strip().split("\n"):
726
+ if line.strip():
727
+ logger.error(f" 错误: {line.strip()}")
728
+ elif result.stdout:
729
+ for line in result.stdout.strip().split("\n"):
730
+ if line.strip():
731
+ logger.warning(f" 输出: {line.strip()}")
732
+ except (OSError, RuntimeError, ValueError) as e:
733
+ cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
734
+ logger.error(
735
+ f"[{completed_count}/{total_count}] 异常: {cmd_str}, 错误:{e}"
736
+ )
737
+ # 添加失败结果
738
+ cmd_list = cmd if isinstance(cmd, list) else []
739
+ results.append(
740
+ RunResult(
741
+ returncode=-1,
742
+ stdout="",
743
+ stderr=str(e),
744
+ execution_time=0.0,
745
+ mode="failed",
746
+ cmd=cmd_list,
747
+ )
748
+ )
749
+
750
+ batch_execution_time = time.time() - batch_start_time
751
+ success_count = sum(1 for r in results if r.returncode == 0)
752
+ failed_count = len(results) - success_count
753
+
754
+ logger.info(
755
+ f"批量命令执行完成 | 总数:{total_count} | 成功:{success_count} | 失败:{failed_count} | "
756
+ f"总耗时:{batch_execution_time:.2f}秒"
757
+ )
758
+
759
+ # 如果有失败的命令,输出详细信息
760
+ if failed_count > 0:
761
+ failed_results = [r for r in results if r.returncode != 0]
762
+ logger.warning("失败的命令详情:")
763
+ for r in failed_results:
764
+ cmd_str = " ".join(r.cmd) if r.cmd else "unknown"
765
+ logger.warning(f" - {cmd_str} (返回码:{r.returncode})")
766
+
767
+ return results
768
+
769
+
770
+ _executor = ProcessExecutor()
771
+
772
+
773
+ # 类型重载: 单个命令模式
774
+ @overload
775
+ def execute(
776
+ cmd: list[str],
777
+ cwd: Path | None = ...,
778
+ env: dict[str, str] | None = ...,
779
+ timeout: int | None = ...,
780
+ *,
781
+ check: bool = ...,
782
+ capture_output: bool = ...,
783
+ text: bool = ...,
784
+ max_concurrent: int | None = ...,
785
+ ) -> RunResult: ...
786
+
787
+
788
+ # 类型重载: 批量命令模式
789
+ @overload
790
+ def execute(
791
+ cmd: Sequence[list[str] | Callable[[], RunResult]],
792
+ cwd: Path | None = ...,
793
+ env: dict[str, str] | None = ...,
794
+ timeout: int | None = ...,
795
+ *,
796
+ check: bool = ...,
797
+ capture_output: bool = ...,
798
+ text: bool = ...,
799
+ max_concurrent: int | None = ...,
800
+ ) -> list[RunResult]: ...
801
+
802
+
803
+ def execute(
804
+ cmd: list[str] | Sequence[list[str] | Callable[[], RunResult]],
805
+ cwd: Path | None = None,
806
+ env: dict[str, str] | None = None,
807
+ timeout: int | None = None,
808
+ *,
809
+ check: bool = False,
810
+ capture_output: bool = True,
811
+ text: bool = True,
812
+ max_concurrent: int | None = None,
813
+ ) -> RunResult | list[RunResult]:
814
+ """执行命令或批量执行命令.
815
+
816
+ 智能判断输入类型:
817
+ - 如果 cmd 是 list[str],执行单个命令
818
+ - 如果 cmd 是 Sequence[list[str] | Callable],批量执行命令
819
+
820
+ Parameters
821
+ ----------
822
+ cmd : list[str] | Sequence[list[str] | Callable[[], RunResult]]
823
+ 单个命令列表或命令列表序列(支持可调用对象)
824
+ cwd : Path, optional
825
+ 工作目录(仅单个命令模式有效)
826
+ env : dict[str, str], optional
827
+ 环境变量(仅单个命令模式有效)
828
+ timeout : int, optional
829
+ 超时时间(秒),默认使用实例配置
830
+ check : bool, optional
831
+ 是否检查返回码,非零时抛出异常(仅单个命令模式有效)
832
+ capture_output : bool, optional
833
+ 是否捕获输出(仅单个命令模式有效)
834
+ text : bool, optional
835
+ 是否以文本模式处理输出(仅单个命令模式有效)
836
+ max_concurrent : int, optional
837
+ 最大并发数(仅批量命令模式有效)
838
+
839
+ Returns
840
+ -------
841
+ RunResult | list[RunResult]
842
+ 单个命令返回 RunResult,批量命令返回 list[RunResult]
843
+
844
+ Examples
845
+ --------
846
+ >>> # 执行单个命令
847
+ >>> result = execute(["ls", "-la"])
848
+ >>> print(result.returncode)
849
+
850
+ >>> # 批量执行命令
851
+ >>> results = execute([
852
+ ... ["echo", "hello"],
853
+ ... ["echo", "world"],
854
+ ... ])
855
+ >>> for r in results:
856
+ ... print(r.returncode)
857
+ """
858
+ # 判断是否为批量命令模式:检查第一个元素是否为 list 或 Callable
859
+ # 注意:单个命令如 ["echo", "hello"] 的第一个元素是字符串,不是 list
860
+ if cmd and isinstance(cmd[0], (list, Callable)):
861
+ # 批量命令模式:通过重载推断类型
862
+ return _executor.run_batch(
863
+ cmd, # ty: ignore[invalid-argument-type]
864
+ max_concurrent=max_concurrent,
865
+ timeout=timeout,
866
+ )
867
+ else:
868
+ # 单个命令模式:通过 isinstance 守卫确保类型安全
869
+ if not isinstance(cmd, list):
870
+ msg = f"命令列表应为 list[str],实际为 {type(cmd).__name__}"
871
+ raise TypeError(msg)
872
+ # 单个命令:直接传递,通过类型重载匹配第一个签名
873
+ return _executor.run(
874
+ cmd, # ty: ignore[invalid-argument-type]
875
+ cwd=cwd,
876
+ env=env,
877
+ timeout=timeout,
878
+ check=check,
879
+ capture_output=capture_output,
880
+ text=text,
881
+ )