multiCMD 1.51__tar.gz → 1.53__tar.gz
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.
- multicmd-1.53/PKG-INFO +436 -0
- multicmd-1.53/README.md +412 -0
- multicmd-1.53/multiCMD.egg-info/PKG-INFO +436 -0
- {multicmd-1.51 → multicmd-1.53}/multiCMD.egg-info/SOURCES.txt +12 -2
- multicmd-1.53/multiCMD.py +2353 -0
- {multicmd-1.51 → multicmd-1.53}/setup.py +0 -3
- multicmd-1.53/tests/test_api.py +53 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_async_executor.py +67 -0
- multicmd-1.53/tests/test_cli.py +171 -0
- multicmd-1.53/tests/test_cli_input.py +109 -0
- multicmd-1.53/tests/test_cli_summary.py +103 -0
- multicmd-1.53/tests/test_display.py +115 -0
- multicmd-1.53/tests/test_expand_ranges.py +84 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_helpers.py +19 -0
- multicmd-1.53/tests/test_parsing.py +59 -0
- multicmd-1.53/tests/test_pool.py +70 -0
- multicmd-1.53/tests/test_process_groups.py +155 -0
- multicmd-1.53/tests/test_pty.py +74 -0
- multicmd-1.53/tests/test_reader.py +131 -0
- multicmd-1.53/tests/test_supervision.py +207 -0
- multicmd-1.53/tests/test_windows.py +116 -0
- multicmd-1.51/PKG-INFO +0 -288
- multicmd-1.51/README.md +0 -262
- multicmd-1.51/multiCMD.egg-info/PKG-INFO +0 -288
- multicmd-1.51/multiCMD.egg-info/requires.txt +0 -1
- multicmd-1.51/multiCMD.py +0 -1394
- multicmd-1.51/tests/test_cli.py +0 -45
- multicmd-1.51/tests/test_expand_ranges.py +0 -48
- {multicmd-1.51 → multicmd-1.53}/multiCMD.egg-info/dependency_links.txt +0 -0
- {multicmd-1.51 → multicmd-1.53}/multiCMD.egg-info/entry_points.txt +0 -0
- {multicmd-1.51 → multicmd-1.53}/multiCMD.egg-info/top_level.txt +0 -0
- {multicmd-1.51 → multicmd-1.53}/setup.cfg +0 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_ping.py +0 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_regressions.py +0 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_run_command.py +0 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_run_commands.py +0 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_sudo.py +0 -0
- {multicmd-1.51 → multicmd-1.53}/tests/test_task.py +0 -0
multicmd-1.53/PKG-INFO
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: multiCMD
|
|
3
|
+
Version: 1.53
|
|
4
|
+
Summary: Run commands simultaneously
|
|
5
|
+
Home-page: https://github.com/yufei-pan/multiCMD
|
|
6
|
+
Author: Yufei Pan
|
|
7
|
+
Author-email: pan@zopyr.us
|
|
8
|
+
License: GPLv3+
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# multiCMD
|
|
26
|
+
|
|
27
|
+
Run many commands at the same time — from the shell or from Python.
|
|
28
|
+
|
|
29
|
+
`multiCMD` is a small, dependency-light helper (a single `multiCMD.py` module) for
|
|
30
|
+
launching and supervising multiple subprocesses concurrently. It streams each
|
|
31
|
+
command's output in real time (optionally colorized per command), captures
|
|
32
|
+
stdout/stderr/return codes, supports per-command inactivity timeouts, and can
|
|
33
|
+
expand range patterns like `host[1-10]` into many commands at once.
|
|
34
|
+
|
|
35
|
+
It works both as a command-line tool and as an importable wrapper around
|
|
36
|
+
`subprocess` for your own automation scripts.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- Run a batch of commands in parallel with a bounded pool of worker threads.
|
|
41
|
+
- Live output that stays readable in parallel runs: `[#]` prefixes, per-command colors on a terminal, and progress lines that redraw in place.
|
|
42
|
+
- Capture `stdout`, `stderr`, return code and timing per command via the `Task` object.
|
|
43
|
+
- Per-command **inactivity timeout** that kills the command and everything it started (see [Timeout semantics](#timeout-semantics)).
|
|
44
|
+
- Range/pattern expansion: `host[1-3]`, `[01-03]`, `[a-f]`, `[A-C]`, `[1-2,a-b]`, variables, and `{...}` expressions.
|
|
45
|
+
- Commands from files or stdin (`-f`), `{}` templates filled from a list (`-a`), a `--dry-run` preview, and an end-of-run summary.
|
|
46
|
+
- pty mode, so commands print live instead of in buffered bursts.
|
|
47
|
+
- Fire-and-forget async mode (`wait_for_return=False`) plus an `AsyncExecutor` for managing long-lived batches.
|
|
48
|
+
- Optional `sudo` wrapping, and an opt-in shell mode for pipes and redirects.
|
|
49
|
+
- No third-party runtime dependencies; requires Python >= 3.6.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install multiCMD
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This installs the module and three equivalent console entry points:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
mcmd
|
|
61
|
+
multiCMD
|
|
62
|
+
multicmd
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also just drop `multiCMD.py` into your project and import it directly.
|
|
66
|
+
|
|
67
|
+
## Command-line usage
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
$ mcmd -h
|
|
71
|
+
usage: mcmd [-h] [-p] [-t SECONDS] [-m max_threads] [--sudo] [-q] [-n]
|
|
72
|
+
[--shell] [-f FILE] [-a FILE] [--summary | --no-summary]
|
|
73
|
+
[--no-prefix] [--no-color] [--pty | --no-pty] [-V]
|
|
74
|
+
[command ...]
|
|
75
|
+
|
|
76
|
+
Run multiple commands in parallel
|
|
77
|
+
|
|
78
|
+
positional arguments:
|
|
79
|
+
command commands to run ( with -a: templates, {} is replaced
|
|
80
|
+
by each input line )
|
|
81
|
+
|
|
82
|
+
options:
|
|
83
|
+
-h, --help show this help message and exit
|
|
84
|
+
-p, --parse Parse ranged input and expand them into multiple
|
|
85
|
+
commands
|
|
86
|
+
-t, --timeout SECONDS
|
|
87
|
+
kill a command after SECONDS with no output at all ( 0
|
|
88
|
+
= never, the default )
|
|
89
|
+
-m, --max_threads max_threads
|
|
90
|
+
maximum number of commands to run at once ( 0 = all )
|
|
91
|
+
--sudo use sudo for commands
|
|
92
|
+
-q, --quiet quiet mode: hide command output ( the summary still
|
|
93
|
+
shows )
|
|
94
|
+
-n, --dry-run print each command, quoted, instead of running it
|
|
95
|
+
--shell run each command through /bin/sh -c ( pipes, ;, > and
|
|
96
|
+
globs work )
|
|
97
|
+
-f, --file FILE read commands from FILE, one per line ( - for stdin;
|
|
98
|
+
blank lines and # comments are skipped )
|
|
99
|
+
-a, --arg-file FILE run each command template once per line of FILE, with
|
|
100
|
+
{} replaced by the line ( - for stdin )
|
|
101
|
+
--summary list every command in the end-of-run summary (
|
|
102
|
+
default: only failed ones )
|
|
103
|
+
--no-summary do not print the end-of-run summary
|
|
104
|
+
--no-prefix do not prefix output lines with [#] when commands run
|
|
105
|
+
at once
|
|
106
|
+
--no-color do not color output lines per command
|
|
107
|
+
--pty give commands a pseudo-terminal so they print live (
|
|
108
|
+
default when stdout is a terminal )
|
|
109
|
+
--no-pty give commands plain pipes
|
|
110
|
+
-V, --version show program's version number and exit
|
|
111
|
+
|
|
112
|
+
exit status:
|
|
113
|
+
0 every command succeeded, or --dry-run
|
|
114
|
+
N one command: its own exit code ( killed by signal S: 128+S )
|
|
115
|
+
N several commands: how many failed, capped at 101
|
|
116
|
+
2 usage error
|
|
117
|
+
130 interrupted with Ctrl-C ( 128+S after SIGTERM / SIGHUP )
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Examples
|
|
121
|
+
|
|
122
|
+
Run two commands sequentially (default `--max_threads 1`):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
mcmd "echo hello" "echo world"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Run them concurrently:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
mcmd -m 4 "echo hello" "echo world"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Expand a range into many commands and run 8 at a time:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
mcmd -p -m 8 "ping -c1 192.168.1.[1-254]"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Kill any command that goes silent for more than 30 seconds (along with everything it started):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
mcmd -t 30 -m 16 "long-running-task [1-100]"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run with `sudo` (falls back gracefully if `sudo` is unavailable or you are already root):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
mcmd --sudo "systemctl restart myservice"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Preview what a range expands to without running anything:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
mcmd -n -p "ping -c1 192.168.1.[1-5]"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Run one command per host from a file, 16 at a time:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
mcmd -m 16 -a hosts.txt "ssh {} uptime"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Run a list of jobs from stdin:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
generate_jobs | mcmd -m 8 -f -
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use pipes and redirects:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
mcmd --shell -m 4 "dmesg | tail -n 5" "journalctl -n 5 > /tmp/j.log"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> Each positional argument is one command. It is split into arguments with shell
|
|
177
|
+
> quoting rules (`shlex`), but not run through a shell unless you pass `--shell`.
|
|
178
|
+
> With `-m/--max_threads > 1`, at most that many commands run at once, each with two
|
|
179
|
+
> threads draining its output. `stdin` is connected to `/dev/null` — multiCMD does
|
|
180
|
+
> not feed input to commands.
|
|
181
|
+
|
|
182
|
+
### Exit status
|
|
183
|
+
|
|
184
|
+
| Situation | Exit code |
|
|
185
|
+
|---|---|
|
|
186
|
+
| Every command succeeded, or `--dry-run` | `0` |
|
|
187
|
+
| Exactly one command (after `-p` expansion and input files) | its own exit code; killed by signal N → `128+N` |
|
|
188
|
+
| Several commands | the number that failed, capped at `101` |
|
|
189
|
+
| Usage error (bad option, unbalanced quote, no commands) | `2` |
|
|
190
|
+
| Ctrl-C | `130` (`128+N` after SIGTERM / SIGHUP) |
|
|
191
|
+
|
|
192
|
+
### Input files and templates
|
|
193
|
+
|
|
194
|
+
`-f FILE` reads one command per line (blank lines and `#` comments are skipped).
|
|
195
|
+
`-a FILE` turns each positional command into a template and runs it once per line of
|
|
196
|
+
FILE, with `{}` replaced by the line; a template without `{}` gets the line as its
|
|
197
|
+
last argument. `-` reads stdin. The line always stays one argument, and with `-p` only
|
|
198
|
+
the template is range-expanded, never the line.
|
|
199
|
+
|
|
200
|
+
With `--shell`, an `-a` line is never pasted into the shell code: it is passed to
|
|
201
|
+
`/bin/sh -c` as `"$1"`, so `{}` may be written bare, as `"{}"` or as `'{}'`, and the
|
|
202
|
+
line is still only data. A dry run shows this as `/bin/sh -c 'echo "$1"' mcmd LINE`.
|
|
203
|
+
|
|
204
|
+
### Output in parallel runs
|
|
205
|
+
|
|
206
|
+
When more than one command can run at once, each output line starts with `[#]` (the
|
|
207
|
+
command's position, the same `#` the summary shows), and on a terminal each command
|
|
208
|
+
gets its own color. `--no-prefix` and `--no-color` (or the `NO_COLOR` environment
|
|
209
|
+
variable) turn them off. A command's `\r` progress line redraws in place; if another
|
|
210
|
+
command printed in between, the update starts a new line instead of overwriting it.
|
|
211
|
+
|
|
212
|
+
When the run ends with more than one command, mcmd prints a summary: a table of the
|
|
213
|
+
failed commands (all of them with `--summary`) and a tally line. `-q` hides command
|
|
214
|
+
output but keeps the summary; `--no-summary` hides it too.
|
|
215
|
+
|
|
216
|
+
### pty mode
|
|
217
|
+
|
|
218
|
+
Many programs buffer their output in 4–8 KB blocks when it goes to a pipe, so they
|
|
219
|
+
look stuck and then print in bursts. When mcmd's own output is a terminal (and `-q` is
|
|
220
|
+
not set), commands get a pseudo-terminal instead, so they print line by line.
|
|
221
|
+
stdout and stderr are then merged, programs may print colors and progress bars, and
|
|
222
|
+
`PAGER`/`GIT_PAGER` are set to `cat` so nothing waits for a keypress. Use `--no-pty` to
|
|
223
|
+
turn it off, or `--pty` to force it. In Python, pass `pty=True`. POSIX only.
|
|
224
|
+
|
|
225
|
+
## Range / pattern expansion (`-p` / `parse=True`)
|
|
226
|
+
|
|
227
|
+
When parsing is enabled, bracketed patterns are expanded into the cartesian
|
|
228
|
+
product of all options:
|
|
229
|
+
|
|
230
|
+
| Pattern | Expands to |
|
|
231
|
+
| -------------------- | ------------------------------------------- |
|
|
232
|
+
| `host[1-3]` | `host1`, `host2`, `host3` |
|
|
233
|
+
| `host[01-03]` | `host01`, `host02`, `host03` (zero-padded) |
|
|
234
|
+
| `item[a-c]` | `itema`, `itemb`, `itemc` |
|
|
235
|
+
| `v[a-f]` | `va` … `vf` (hex range) |
|
|
236
|
+
| `x[1-2,a-b]` | `x1`, `x2`, `xa`, `xb` (comma list) |
|
|
237
|
+
| `[1-2]-[a-b]` | `1-a`, `1-b`, `2-a`, `2-b` (multiple groups) |
|
|
238
|
+
| `host[A-C]` | `hostA`, `hostB`, `hostC` (case kept) |
|
|
239
|
+
| `n[08-0b]` | `n08`, `n09`, `n0a`, `n0b` (hex, padded) |
|
|
240
|
+
| `[web1\:8080]` | `web1:8080` (`\:` is a literal colon) |
|
|
241
|
+
| `[n:3]host[1-n]` | `host1`, `host2`, `host3` (variables) |
|
|
242
|
+
| `host[{2+3}]` | `host5` (`{...}` is evaluated as Python) |
|
|
243
|
+
|
|
244
|
+
Notes:
|
|
245
|
+
|
|
246
|
+
- Decimal padding follows the **shorter** endpoint, so `[0-10]` yields
|
|
247
|
+
`0..10` (unpadded) while `[01-10]` yields `01..10`.
|
|
248
|
+
- `name:value` inside brackets assigns a variable (the bracket itself produces
|
|
249
|
+
no output) that later brackets can reference.
|
|
250
|
+
- Variables are shared by the commands of one call (one `mcmd` run, one `run_commands`
|
|
251
|
+
call) and never leak into the next.
|
|
252
|
+
- `name:value` is an assignment only with exactly one `:` and a valid name before it;
|
|
253
|
+
IPv6 addresses like `[fe80::1]` pass through, and `\:` gives a literal colon.
|
|
254
|
+
- `{expr}` is evaluated as a Python expression with the current variables in
|
|
255
|
+
scope. Only use this with trusted input.
|
|
256
|
+
|
|
257
|
+
## Python API
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
import multiCMD
|
|
261
|
+
|
|
262
|
+
# Run a single command, return its stdout lines
|
|
263
|
+
out = multiCMD.run_command(["echo", "hello"], quiet=True)
|
|
264
|
+
# -> ["hello"]
|
|
265
|
+
|
|
266
|
+
# Run several in parallel
|
|
267
|
+
results = multiCMD.run_commands(
|
|
268
|
+
[["echo", "hello"], ["echo", "world"]],
|
|
269
|
+
max_threads=4, quiet=True,
|
|
270
|
+
)
|
|
271
|
+
# -> [["hello"], ["world"]]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Getting return codes and the full `Task`
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# Just the return code
|
|
278
|
+
rc = multiCMD.run_command(["false"], return_code_only=True, quiet=True) # -> 1
|
|
279
|
+
|
|
280
|
+
# The full Task object (command, returncode, stdout, stderr)
|
|
281
|
+
task = multiCMD.run_command(["echo", "hi"], return_object=True, quiet=True)
|
|
282
|
+
print(task.returncode, task.stdout, task.stderr) # 0 ['hi'] []
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Shell mode, prefixes, colors and pty
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
multiCMD.run_command("dmesg | tail -n 3", shell=True) # run through /bin/sh -c
|
|
289
|
+
multiCMD.run_commands(cmds, max_threads=8, prefix=True) # [#] on each line
|
|
290
|
+
multiCMD.run_commands(cmds, max_threads=8, color=False) # never color
|
|
291
|
+
multiCMD.run_command(["python3", "train.py"], pty=True) # live output, stderr merged
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`raise_error=True` raises `multiCMD.CommandError`; its `.tasks` holds the failed `Task`
|
|
295
|
+
objects and its message lists each one with its return code and last error line.
|
|
296
|
+
`Task` also records `start_time`, `end_time` and `duration`. With `dry_run=True` nothing
|
|
297
|
+
runs; each command is printed once, quoted so it can be pasted into a shell.
|
|
298
|
+
|
|
299
|
+
### Asynchronous / fire-and-forget
|
|
300
|
+
|
|
301
|
+
Use `quiet=True` with `wait_for_return=False` to launch commands on daemon
|
|
302
|
+
threads. The returned `Task` objects are updated in place as commands finish:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
tasks = multiCMD.run_commands(
|
|
306
|
+
[["sleep", "2"], ["sleep", "1"]],
|
|
307
|
+
max_threads=2, quiet=True,
|
|
308
|
+
wait_for_return=False, return_object=True,
|
|
309
|
+
)
|
|
310
|
+
# tasks[i].returncode is None until that command completes
|
|
311
|
+
|
|
312
|
+
# Later, block until everything launched this way has finished:
|
|
313
|
+
multiCMD.join_threads()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
For managing larger or repeated batches, use `AsyncExecutor`:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
ex = multiCMD.AsyncExecutor(max_threads=8, timeout=30, quiet=True)
|
|
320
|
+
ex.run_command(["./worker", "--job", "1"])
|
|
321
|
+
ex.run_commands([["./worker", "--job", "2"], ["./worker", "--job", "3"]])
|
|
322
|
+
ex.join() # wait and print any failures
|
|
323
|
+
print(ex.get_return_codes())
|
|
324
|
+
print(ex.get_results())
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`run_command(s)` returns right away and queues the commands. `wait()`, `join()` and
|
|
328
|
+
`stop()` wait until everything is done by default; a `timeout=` is one overall limit in
|
|
329
|
+
seconds (the constructor's `timeout` is only the per-command inactivity timeout).
|
|
330
|
+
`AsyncExecutor` takes the same `shell`, `prefix`, `color` and `pty` options as
|
|
331
|
+
`run_commands`, as constructor defaults or per call. One dispatcher thread
|
|
332
|
+
starts each queued command only after taking a permit from the semaphore, so the
|
|
333
|
+
executor holds at most one thread per permit plus the dispatcher, however many
|
|
334
|
+
commands are queued. A command's `task.thread` is `None` until it starts, and goes
|
|
335
|
+
back to `None` once it has finished and the executor has joined its thread, so a
|
|
336
|
+
large batch does not keep a `Thread` object per finished command. `stop()` skips
|
|
337
|
+
commands that have not started yet.
|
|
338
|
+
|
|
339
|
+
### Range expansion from Python
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
multiCMD.run_commands([["echo", "[0-10]"]], quiet=True, parse=True)
|
|
343
|
+
# -> [["0"], ["1"], ..., ["10"]]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Using sudo
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
multiCMD.set_sudo(True) # validates sudo is present and you aren't root
|
|
350
|
+
multiCMD.run_command(["systemctl", "restart", "nginx"])
|
|
351
|
+
# or per-call:
|
|
352
|
+
multiCMD.run_command(["id"], use_sudo=True)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
If `sudo` is not on `PATH`, or you are already root, the request is ignored with
|
|
356
|
+
a warning instead of failing.
|
|
357
|
+
|
|
358
|
+
## Timeout semantics
|
|
359
|
+
|
|
360
|
+
`timeout` is an **inactivity timeout**, not a maximum total runtime. A command is
|
|
361
|
+
killed once it has produced **no output at all** on stdout or stderr for `timeout`
|
|
362
|
+
seconds. Any byte counts, including a progress line redrawn with `\r` and a partial
|
|
363
|
+
line without a newline.
|
|
364
|
+
|
|
365
|
+
On timeout, the command **and everything it started** are killed, the task's return
|
|
366
|
+
code is set to `124`, and `Timeout!` is appended to its stderr. `timeout=0` (the
|
|
367
|
+
default, in the API and on the command line) disables it.
|
|
368
|
+
|
|
369
|
+
## Process groups and signals
|
|
370
|
+
|
|
371
|
+
Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
|
|
372
|
+
reaches the whole process tree, not just the first process. On Python 3.11+ this uses
|
|
373
|
+
`Popen(process_group=0)`; older Pythons start the command through a tiny exec wrapper.
|
|
374
|
+
Commands that try to read the terminal (a password prompt, for example) fail at once
|
|
375
|
+
instead of hanging. The library does this by blocking SIGTTIN/SIGTTOU for the command,
|
|
376
|
+
which covers commands started directly or through bash; `mcmd` also ignores them, which
|
|
377
|
+
every command inherits through any shell.
|
|
378
|
+
|
|
379
|
+
Because the commands are not in your program's process group, signals sent to that
|
|
380
|
+
group do not reach them. multiCMD handles this for you: a call waiting on the main
|
|
381
|
+
thread that gets Ctrl-C stops its commands before re-raising, `mcmd` stops them on
|
|
382
|
+
SIGTERM / SIGHUP (unless it was started with them ignored, as under `nohup`), and any
|
|
383
|
+
commands still running when the interpreter exits are killed. A program that handles
|
|
384
|
+
SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
|
|
385
|
+
arrives), can call `multiCMD.kill_all()` from its handler.
|
|
386
|
+
|
|
387
|
+
## Key functions and objects
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
run_command(command, timeout=0, max_threads=1, quiet=False, dry_run=False,
|
|
391
|
+
with_stdErr=False, return_code_only=False, return_object=False,
|
|
392
|
+
wait_for_return=True, sem=None, use_sudo=..., raise_error=False,
|
|
393
|
+
prefix=False, color=None, pty=False, shell=False)
|
|
394
|
+
|
|
395
|
+
run_commands(commands, timeout=0, max_threads=1, quiet=False, dry_run=False,
|
|
396
|
+
with_stdErr=False, return_code_only=False, return_object=False,
|
|
397
|
+
parse=False, wait_for_return=True, sem=None, use_sudo=...,
|
|
398
|
+
raise_error=False, prefix=False, color=None, pty=False, shell=False)
|
|
399
|
+
|
|
400
|
+
ping(hosts, timeout=1, max_threads=0, ...) # returns True/False reachability
|
|
401
|
+
join_threads(threads=..., timeout=None) # join fire-and-forget threads
|
|
402
|
+
kill_all(sig=None) # stop every running command and what it started
|
|
403
|
+
set_sudo(use_sudo) # enable/disable sudo globally
|
|
404
|
+
|
|
405
|
+
class Task: # command, returncode, stdout, stderr, start_time, end_time, duration
|
|
406
|
+
class CommandError: # raised by raise_error=True; .tasks = the failed Tasks
|
|
407
|
+
class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The module also bundles a few terminal/formatting helpers used internally and
|
|
411
|
+
reusable on their own: `pretty_format_table`, `parseTable`, `print_progress_bar`,
|
|
412
|
+
`format_bytes`, `get_terminal_size`, `input_with_timeout_and_countdown`, and
|
|
413
|
+
`slugify`.
|
|
414
|
+
|
|
415
|
+
## Platform support
|
|
416
|
+
|
|
417
|
+
Linux and macOS are fully supported. Windows is best-effort and untested: commands run
|
|
418
|
+
and time out, but a timeout or stop only reaches the first process, pty mode is not
|
|
419
|
+
available, and shell mode needs command strings (`-a` with `--shell` is not supported).
|
|
420
|
+
|
|
421
|
+
## Development
|
|
422
|
+
|
|
423
|
+
Run the test suite with [pytest](https://pytest.org):
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
pip install pytest
|
|
427
|
+
pytest # full suite under tests/
|
|
428
|
+
pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
|
|
436
|
+
```
|