multiCMD 1.54__tar.gz → 1.56__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.
Files changed (38) hide show
  1. {multicmd-1.54 → multicmd-1.56}/PKG-INFO +127 -23
  2. {multicmd-1.54 → multicmd-1.56}/README.md +126 -22
  3. {multicmd-1.54 → multicmd-1.56}/multiCMD.egg-info/PKG-INFO +127 -23
  4. {multicmd-1.54 → multicmd-1.56}/multiCMD.egg-info/SOURCES.txt +2 -0
  5. {multicmd-1.54 → multicmd-1.56}/multiCMD.py +1088 -230
  6. multicmd-1.56/tests/test_async_executor.py +819 -0
  7. multicmd-1.56/tests/test_env.py +965 -0
  8. multicmd-1.56/tests/test_interrupts.py +623 -0
  9. multicmd-1.56/tests/test_pool.py +194 -0
  10. multicmd-1.56/tests/test_process_groups.py +281 -0
  11. {multicmd-1.54 → multicmd-1.56}/tests/test_pty.py +1 -0
  12. {multicmd-1.54 → multicmd-1.56}/tests/test_run_command.py +45 -0
  13. multicmd-1.56/tests/test_supervision.py +500 -0
  14. multicmd-1.54/tests/test_async_executor.py +0 -251
  15. multicmd-1.54/tests/test_pool.py +0 -70
  16. multicmd-1.54/tests/test_process_groups.py +0 -155
  17. multicmd-1.54/tests/test_supervision.py +0 -207
  18. {multicmd-1.54 → multicmd-1.56}/LICENSE +0 -0
  19. {multicmd-1.54 → multicmd-1.56}/multiCMD.egg-info/dependency_links.txt +0 -0
  20. {multicmd-1.54 → multicmd-1.56}/multiCMD.egg-info/entry_points.txt +0 -0
  21. {multicmd-1.54 → multicmd-1.56}/multiCMD.egg-info/top_level.txt +0 -0
  22. {multicmd-1.54 → multicmd-1.56}/setup.cfg +0 -0
  23. {multicmd-1.54 → multicmd-1.56}/setup.py +0 -0
  24. {multicmd-1.54 → multicmd-1.56}/tests/test_api.py +0 -0
  25. {multicmd-1.54 → multicmd-1.56}/tests/test_cli.py +0 -0
  26. {multicmd-1.54 → multicmd-1.56}/tests/test_cli_input.py +0 -0
  27. {multicmd-1.54 → multicmd-1.56}/tests/test_cli_summary.py +0 -0
  28. {multicmd-1.54 → multicmd-1.56}/tests/test_display.py +0 -0
  29. {multicmd-1.54 → multicmd-1.56}/tests/test_expand_ranges.py +0 -0
  30. {multicmd-1.54 → multicmd-1.56}/tests/test_helpers.py +0 -0
  31. {multicmd-1.54 → multicmd-1.56}/tests/test_parsing.py +0 -0
  32. {multicmd-1.54 → multicmd-1.56}/tests/test_ping.py +0 -0
  33. {multicmd-1.54 → multicmd-1.56}/tests/test_reader.py +0 -0
  34. {multicmd-1.54 → multicmd-1.56}/tests/test_regressions.py +0 -0
  35. {multicmd-1.54 → multicmd-1.56}/tests/test_run_commands.py +0 -0
  36. {multicmd-1.54 → multicmd-1.56}/tests/test_sudo.py +0 -0
  37. {multicmd-1.54 → multicmd-1.56}/tests/test_task.py +0 -0
  38. {multicmd-1.54 → multicmd-1.56}/tests/test_windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiCMD
3
- Version: 1.54
3
+ Version: 1.56
4
4
  Summary: Run commands simultaneously
5
5
  Home-page: https://github.com/yufei-pan/multiCMD
6
6
  Author: Yufei Pan
@@ -71,8 +71,8 @@ You can also just drop `multiCMD.py` into your project and import it directly.
71
71
  ```bash
72
72
  $ mcmd -h
73
73
  usage: mcmd [-h] [-p] [-t SECONDS] [-m max_threads] [--sudo] [-q] [-n]
74
- [--shell] [-f FILE] [-a FILE] [--summary | --no-summary]
75
- [--no-prefix] [--no-color] [--pty | --no-pty] [-V]
74
+ [--shell] [-e NAME=VALUE] [-f FILE] [-a FILE] [--summary |
75
+ --no-summary] [--no-prefix] [--no-color] [--pty | --no-pty] [-V]
76
76
  [command ...]
77
77
 
78
78
  Run multiple commands in parallel
@@ -96,6 +96,8 @@ options:
96
96
  -n, --dry-run print each command, quoted, instead of running it
97
97
  --shell run each command through /bin/sh -c ( pipes, ;, > and
98
98
  globs work )
99
+ -e, --env NAME=VALUE set NAME to VALUE in the environment of every command
100
+ ( repeatable ); NAME alone removes it
99
101
  -f, --file FILE read commands from FILE, one per line ( - for stdin;
100
102
  blank lines and # comments are skipped )
101
103
  -a, --arg-file FILE run each command template once per line of FILE, with
@@ -293,10 +295,47 @@ multiCMD.run_commands(cmds, max_threads=8, color=False) # never color
293
295
  multiCMD.run_command(["python3", "train.py"], pty=True) # live output, stderr merged
294
296
  ```
295
297
 
298
+ ### Environment per call
299
+
300
+ ```python
301
+ multiCMD.run_command(["xfs_info", "/data"], extra_env={"LC_ALL": "C"}) # add / override variables
302
+ multiCMD.run_command(["env"], extra_env={"HTTP_PROXY": None}) # None removes one
303
+ multiCMD.run_command(["env"], env={"PATH": "/usr/bin", "HOME": "/tmp"}) # the whole environment, as subprocess's env
304
+ ```
305
+
306
+ `env` is the whole environment of the command, as `subprocess`'s `env` ( names and values
307
+ are `str`; on POSIX `bytes` and `os.PathLike` work too ). `extra_env` is applied on top of the
308
+ inherited environment, or of `env` when both are given; a `None` value removes the variable,
309
+ and an empty `extra_env` is the same as none. "Inherited" is `os.environ` as the process holds
310
+ it when the command starts: a variable set behind Python's back ( `os.putenv`, `os.unsetenv`, a
311
+ C library's `setenv` ) reaches commands started without `env` / `extra_env`, not the ones
312
+ given them. An inherited variable with an empty name ( a corrupt `environ` entry such as `=x` ) is left out of the environment of a command given `env` / `extra_env`, and out of an `env` copied from `os.environ`; an empty name in `extra_env` is a `ValueError`. Names are case-insensitive on Windows, so `extra_env={"Path": ...}` replaces `PATH`
313
+ there. On POSIX a command is looked up in the `PATH` of the environment it gets, on every Python
314
+ version; Windows searches the calling process's `PATH` whatever the command's environment says.
315
+ Before Python 3.11 the launcher starts in this process's environment and is handed the command's on
316
+ top of it, so the two together must fit the system's argument limit ( `getconf ARG_MAX`; 2 MiB on Linux
317
+ with the default stack, 128 KiB for one string: before Python 3.11 an entry travels as `_MULTICMD_ENV_<i>=NAME=VALUE`, 15 bytes
318
+ plus the digits of `i` more than its `NAME=VALUE` ); one that does not fit fails with return
319
+ code 126, `Argument list too long`, on every Python.
320
+ On Python 3.7-3.10 a command started with
321
+ neither `env` nor `extra_env` still gets the `LC_CTYPE=C.UTF-8` that interpreter's locale coercion
322
+ sets in a C locale ( as before ); a command given one of them gets exactly the environment described.
323
+ Both are per command: they never change `os.environ` and never reach another command, so
324
+ concurrent commands each see their own. `AsyncExecutor` takes them too, as defaults a call
325
+ overrides. `sudo` resets the environment by default, so with `use_sudo` they may not reach the
326
+ command. On the command line: `mcmd -e LC_ALL=C -e UNSET_ME "cmd"`; `-n` shows them as
327
+ `env LC_ALL=C cmd` ( `env -i ...` for a whole environment ).
328
+
296
329
  `raise_error=True` raises `multiCMD.CommandError`; its `.tasks` holds the failed `Task`
297
330
  objects and its message lists each one with its return code and last error line.
298
331
  `Task` also records `start_time`, `end_time` and `duration`. With `dry_run=True` nothing
299
332
  runs; each command is printed once, quoted so it can be pasted into a shell.
333
+ A command that is not found (return code 127), cannot be executed (126) or that multiCMD
334
+ itself could not run (-1, `task._error`) says why in its `task.stderr`; multiCMD also
335
+ prints a line saying so on stderr, unless `quiet=True`. Before Python 3.11 the exec of a
336
+ command given by its path, or of a file found in `PATH` that may not be executed, happens in
337
+ the launcher's process: it exits 127 / 126 with the reason as the command's own stderr, which
338
+ then shows with the command's output ( unless quiet ) instead of as a line of multiCMD's own.
300
339
 
301
340
  ### Asynchronous / fire-and-forget
302
341
 
@@ -309,7 +348,8 @@ tasks = multiCMD.run_commands(
309
348
  max_threads=2, quiet=True,
310
349
  wait_for_return=False, return_object=True,
311
350
  )
312
- # tasks[i].returncode is None until that command completes
351
+ # tasks[i].returncode is None until that command completes; tasks[i].thread.join(timeout)
352
+ # waits for one command ( a command whose thread could not start fails with -1, its thread already done )
313
353
 
314
354
  # Later, block until everything launched this way has finished:
315
355
  multiCMD.join_threads()
@@ -336,7 +376,8 @@ executor holds at most one thread per permit plus the dispatcher, however many
336
376
  commands are queued. A command's `task.thread` is `None` until it starts, and goes
337
377
  back to `None` once it has finished and the executor has joined its thread, so a
338
378
  large batch does not keep a `Thread` object per finished command. `stop()` skips
339
- commands that have not started yet.
379
+ commands that have not started yet, and so does a Ctrl-C ( or an error ) that interrupts
380
+ `run_command(s)`, for that call's commands.
340
381
 
341
382
  ### Range expansion from Python
342
383
 
@@ -370,21 +411,37 @@ default, in the API and on the command line) disables it.
370
411
 
371
412
  ## Process groups and signals
372
413
 
373
- Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
374
- reaches the whole process tree, not just the first process. On Python 3.11+ this uses
375
- `Popen(process_group=0)`; older Pythons start the command through a tiny exec wrapper.
376
- Commands that try to read the terminal (a password prompt, for example) fail at once
377
- instead of hanging. The library does this by blocking SIGTTIN/SIGTTOU for the command,
378
- which covers commands started directly or through bash; `mcmd` also ignores them, which
379
- every command inherits through any shell.
380
-
381
- Because the commands are not in your program's process group, signals sent to that
382
- group do not reach them. multiCMD handles this for you: a call waiting on the main
383
- thread that gets Ctrl-C stops its commands before re-raising, `mcmd` stops them on
384
- SIGTERM / SIGHUP (unless it was started with them ignored, as under `nohup`), and any
385
- commands still running when the interpreter exits are killed. A program that handles
386
- SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
387
- arrives), can call `multiCMD.kill_all()` from its handler.
414
+ Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C reaches the
415
+ whole process tree, not just the first process. On Python 3.11+ this uses `Popen(process_group=0)`;
416
+ older Pythons start the command through a tiny exec wrapper. A command that reads the terminal (a
417
+ password prompt, for example) fails at once instead of hanging: multiCMD blocks SIGTTIN/SIGTTOU for
418
+ it, and `mcmd` also ignores them, which every command inherits through any shell.
419
+
420
+ Signals sent to your program's group do not reach the commands, so multiCMD stops them itself. One
421
+ Ctrl-C (or a SIGTERM handler's `sys.exit()`) in a call waiting on the main thread, `join_threads()`
422
+ and `AsyncExecutor.wait()` included, stops every command, skips the queued ones and
423
+ re-raises, giving them up to 5 s to exit; a second Ctrl-C ends that wait at once. `mcmd` stops its
424
+ commands on SIGTERM / SIGHUP (unless started with them ignored, as under `nohup`). At interpreter exit
425
+ the commands still running are killed, and a command queued on one of multiCMD's threads fails (-1,
426
+ "not started: the interpreter is exiting") instead of outliving the program; one run from an `atexit`
427
+ handler after that, on the main thread, runs as usual. A program that handles SIGTERM itself, or runs
428
+ multiCMD from its own threads (where Ctrl-C never arrives), can call `multiCMD.kill_all()` from its
429
+ handler. A command not seen to exit a second after SIGKILL is no longer tracked; its pid is named on
430
+ stderr, unless quiet.
431
+
432
+ multiCMD never touches SIGINT's handler (`mcmd` sets its own SIGTERM / SIGHUP handlers, nothing
433
+ else). What your handler raises while a call waits for its commands is handled as a Ctrl-C.
434
+
435
+ A command is registered from the moment `Popen` returns it: a Ctrl-C that Python raises inside
436
+ `Popen` as a serial run starts a command (some 50-70 µs each) can leave it running, unknown to
437
+ `kill_all()`. Two Ctrl-Cs microseconds apart can leave a command running, still registered, until
438
+ `kill_all()` or the exit stops it. On Python 3.6-3.9, which raise a Ctrl-C at nearly any instruction,
439
+ even one at a few points between the steps of a run can leave its command registered but not signalled.
440
+ On Python 3.6-3.9 and 3.14 a Ctrl-C at the wrong instruction of a `with` on a lock can leave it held: one of
441
+ `threading`'s can hang the program as its threads end (CPython's own), and one of multiCMD's (an
442
+ `AsyncExecutor`'s queue, the live output) hangs that executor, or the next call that is not quiet, if the
443
+ program keeps going.
444
+ These and the other known limits are in [docs/limitations.md](docs/limitations.md).
388
445
 
389
446
  ## Key functions and objects
390
447
 
@@ -392,12 +449,13 @@ arrives), can call `multiCMD.kill_all()` from its handler.
392
449
  run_command(command, timeout=0, max_threads=1, quiet=False, dry_run=False,
393
450
  with_stdErr=False, return_code_only=False, return_object=False,
394
451
  wait_for_return=True, sem=None, use_sudo=..., raise_error=False,
395
- prefix=False, color=None, pty=False, shell=False)
452
+ prefix=False, color=None, pty=False, shell=False, env=None, extra_env=None)
396
453
 
397
454
  run_commands(commands, timeout=0, max_threads=1, quiet=False, dry_run=False,
398
455
  with_stdErr=False, return_code_only=False, return_object=False,
399
456
  parse=False, wait_for_return=True, sem=None, use_sudo=...,
400
- raise_error=False, prefix=False, color=None, pty=False, shell=False)
457
+ raise_error=False, prefix=False, color=None, pty=False, shell=False,
458
+ env=None, extra_env=None)
401
459
 
402
460
  ping(hosts, timeout=1, max_threads=0, ...) # returns True/False reachability
403
461
  join_threads(threads=..., timeout=None) # join fire-and-forget threads
@@ -406,7 +464,8 @@ set_sudo(use_sudo) # enable/disable sudo globally
406
464
 
407
465
  class Task: # command, returncode, stdout, stderr, start_time, end_time, duration
408
466
  class CommandError: # raised by raise_error=True; .tasks = the failed Tasks
409
- class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes
467
+ class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes;
468
+ # takes the options above, env / extra_env as defaults a call overrides
410
469
  ```
411
470
 
412
471
  The module also bundles a few terminal/formatting helpers used internally and
@@ -432,6 +491,51 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
432
491
 
433
492
  `test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
434
493
 
494
+ ## Upgrading to 1.56 from 1.55
495
+
496
+ - Fix: a command that times out while it redraws a `\r` progress line on stderr keeps its `Timeout!`
497
+ line; 1.55 could replace it with the progress line, leaving return code 124 without the marker.
498
+ - Simpler interrupt handling, some 700 lines lighter. One Ctrl-C still stops every command.
499
+ What changes is a few rare corners, each in [docs/limitations.md](docs/limitations.md):
500
+ - two Ctrl-Cs microseconds apart can leave a command running, still registered, until `kill_all()`
501
+ or the exit stops it; 1.55 stopped it at once;
502
+ - a Ctrl-C can lose a serial run's starting command anywhere inside `Popen` (about 51 µs per start on
503
+ Python 3.14), not only inside the fork (about 41 µs);
504
+ - a Ctrl-C that leaves `Popen.poll()`'s lock held makes that command's stop about 2 s slower;
505
+ - an ordinary exception your own signal handler raises as a serial run starts a command is recorded as
506
+ a start failure (-1) and not raised, as in 1.54;
507
+ - `join_threads()` on another thread can return before a batch that is still starting.
508
+ - `__running_threads_lock` and `_live_procs_lock`, unused since 1.55 and kept only for code that imported
509
+ them, are gone.
510
+
511
+ ## Upgrading to 1.55 from 1.54
512
+
513
+ - New: a per-call environment. `env=` (the whole environment, as `subprocess`'s) and `extra_env=`
514
+ (added on top; a `None` value removes a variable) on `run_command`, `run_commands` and
515
+ `AsyncExecutor` (constructor defaults, overridden per call), and `mcmd -e NAME=VALUE` / `-e NAME`;
516
+ see [Environment per call](#environment-per-call).
517
+ - `quiet=True` now hides the lines about a command that could not be run: `Command path not found`
518
+ (127), `Command cannot be executed` (126) and the error of a command multiCMD could not run (-1),
519
+ which 1.54 printed even when quiet, partly on stdout. The reasons are still in `task.stderr`. Warnings
520
+ about the run's setup (sudo not found or not usable, no pty) still print.
521
+ - A Ctrl-C in `join_threads()` stops the commands it waits for, skips the ones still queued, waits up
522
+ to 5 s for them and re-raises; 1.54 re-raised and left them running. A Ctrl-C that interrupts
523
+ `AsyncExecutor.run_command(s)` skips that call's commands.
524
+ - A SIGTERM handler's `sys.exit()`, or whatever your own signal handler raises while a call waits for
525
+ its commands, stops them and records them as stopped before it is raised on, as a Ctrl-C does; 1.54
526
+ did that only for `KeyboardInterrupt`.
527
+ - A Ctrl-C while a command starts, times out or is stopped, or while multiCMD reads the output that
528
+ a finished command's background processes still hold open, no longer leaves the command or those
529
+ processes running unknown to `kill_all()` and the exit hook, or known to them but never signalled.
530
+ A second Ctrl-C ends the wait for the commands at once; each is still sent SIGTERM, and `kill_all()`
531
+ and the exit hook finish the kill. The limits below are the exceptions.
532
+ - Once the interpreter exits, a command queued on one of multiCMD's threads that has not started fails
533
+ with -1 ("not started: the interpreter is exiting") instead of starting and outliving the program.
534
+ - A command not seen to exit a second after SIGKILL is no longer tracked, so later `kill_all()` calls and the
535
+ exit do not wait for it again; its pid is named on stderr, unless quiet.
536
+ - Known limits: see [Process groups and signals](#process-groups-and-signals) and
537
+ [docs/limitations.md](docs/limitations.md).
538
+
435
539
  ## License
436
540
 
437
541
  GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
@@ -45,8 +45,8 @@ You can also just drop `multiCMD.py` into your project and import it directly.
45
45
  ```bash
46
46
  $ mcmd -h
47
47
  usage: mcmd [-h] [-p] [-t SECONDS] [-m max_threads] [--sudo] [-q] [-n]
48
- [--shell] [-f FILE] [-a FILE] [--summary | --no-summary]
49
- [--no-prefix] [--no-color] [--pty | --no-pty] [-V]
48
+ [--shell] [-e NAME=VALUE] [-f FILE] [-a FILE] [--summary |
49
+ --no-summary] [--no-prefix] [--no-color] [--pty | --no-pty] [-V]
50
50
  [command ...]
51
51
 
52
52
  Run multiple commands in parallel
@@ -70,6 +70,8 @@ options:
70
70
  -n, --dry-run print each command, quoted, instead of running it
71
71
  --shell run each command through /bin/sh -c ( pipes, ;, > and
72
72
  globs work )
73
+ -e, --env NAME=VALUE set NAME to VALUE in the environment of every command
74
+ ( repeatable ); NAME alone removes it
73
75
  -f, --file FILE read commands from FILE, one per line ( - for stdin;
74
76
  blank lines and # comments are skipped )
75
77
  -a, --arg-file FILE run each command template once per line of FILE, with
@@ -267,10 +269,47 @@ multiCMD.run_commands(cmds, max_threads=8, color=False) # never color
267
269
  multiCMD.run_command(["python3", "train.py"], pty=True) # live output, stderr merged
268
270
  ```
269
271
 
272
+ ### Environment per call
273
+
274
+ ```python
275
+ multiCMD.run_command(["xfs_info", "/data"], extra_env={"LC_ALL": "C"}) # add / override variables
276
+ multiCMD.run_command(["env"], extra_env={"HTTP_PROXY": None}) # None removes one
277
+ multiCMD.run_command(["env"], env={"PATH": "/usr/bin", "HOME": "/tmp"}) # the whole environment, as subprocess's env
278
+ ```
279
+
280
+ `env` is the whole environment of the command, as `subprocess`'s `env` ( names and values
281
+ are `str`; on POSIX `bytes` and `os.PathLike` work too ). `extra_env` is applied on top of the
282
+ inherited environment, or of `env` when both are given; a `None` value removes the variable,
283
+ and an empty `extra_env` is the same as none. "Inherited" is `os.environ` as the process holds
284
+ it when the command starts: a variable set behind Python's back ( `os.putenv`, `os.unsetenv`, a
285
+ C library's `setenv` ) reaches commands started without `env` / `extra_env`, not the ones
286
+ given them. An inherited variable with an empty name ( a corrupt `environ` entry such as `=x` ) is left out of the environment of a command given `env` / `extra_env`, and out of an `env` copied from `os.environ`; an empty name in `extra_env` is a `ValueError`. Names are case-insensitive on Windows, so `extra_env={"Path": ...}` replaces `PATH`
287
+ there. On POSIX a command is looked up in the `PATH` of the environment it gets, on every Python
288
+ version; Windows searches the calling process's `PATH` whatever the command's environment says.
289
+ Before Python 3.11 the launcher starts in this process's environment and is handed the command's on
290
+ top of it, so the two together must fit the system's argument limit ( `getconf ARG_MAX`; 2 MiB on Linux
291
+ with the default stack, 128 KiB for one string: before Python 3.11 an entry travels as `_MULTICMD_ENV_<i>=NAME=VALUE`, 15 bytes
292
+ plus the digits of `i` more than its `NAME=VALUE` ); one that does not fit fails with return
293
+ code 126, `Argument list too long`, on every Python.
294
+ On Python 3.7-3.10 a command started with
295
+ neither `env` nor `extra_env` still gets the `LC_CTYPE=C.UTF-8` that interpreter's locale coercion
296
+ sets in a C locale ( as before ); a command given one of them gets exactly the environment described.
297
+ Both are per command: they never change `os.environ` and never reach another command, so
298
+ concurrent commands each see their own. `AsyncExecutor` takes them too, as defaults a call
299
+ overrides. `sudo` resets the environment by default, so with `use_sudo` they may not reach the
300
+ command. On the command line: `mcmd -e LC_ALL=C -e UNSET_ME "cmd"`; `-n` shows them as
301
+ `env LC_ALL=C cmd` ( `env -i ...` for a whole environment ).
302
+
270
303
  `raise_error=True` raises `multiCMD.CommandError`; its `.tasks` holds the failed `Task`
271
304
  objects and its message lists each one with its return code and last error line.
272
305
  `Task` also records `start_time`, `end_time` and `duration`. With `dry_run=True` nothing
273
306
  runs; each command is printed once, quoted so it can be pasted into a shell.
307
+ A command that is not found (return code 127), cannot be executed (126) or that multiCMD
308
+ itself could not run (-1, `task._error`) says why in its `task.stderr`; multiCMD also
309
+ prints a line saying so on stderr, unless `quiet=True`. Before Python 3.11 the exec of a
310
+ command given by its path, or of a file found in `PATH` that may not be executed, happens in
311
+ the launcher's process: it exits 127 / 126 with the reason as the command's own stderr, which
312
+ then shows with the command's output ( unless quiet ) instead of as a line of multiCMD's own.
274
313
 
275
314
  ### Asynchronous / fire-and-forget
276
315
 
@@ -283,7 +322,8 @@ tasks = multiCMD.run_commands(
283
322
  max_threads=2, quiet=True,
284
323
  wait_for_return=False, return_object=True,
285
324
  )
286
- # tasks[i].returncode is None until that command completes
325
+ # tasks[i].returncode is None until that command completes; tasks[i].thread.join(timeout)
326
+ # waits for one command ( a command whose thread could not start fails with -1, its thread already done )
287
327
 
288
328
  # Later, block until everything launched this way has finished:
289
329
  multiCMD.join_threads()
@@ -310,7 +350,8 @@ executor holds at most one thread per permit plus the dispatcher, however many
310
350
  commands are queued. A command's `task.thread` is `None` until it starts, and goes
311
351
  back to `None` once it has finished and the executor has joined its thread, so a
312
352
  large batch does not keep a `Thread` object per finished command. `stop()` skips
313
- commands that have not started yet.
353
+ commands that have not started yet, and so does a Ctrl-C ( or an error ) that interrupts
354
+ `run_command(s)`, for that call's commands.
314
355
 
315
356
  ### Range expansion from Python
316
357
 
@@ -344,21 +385,37 @@ default, in the API and on the command line) disables it.
344
385
 
345
386
  ## Process groups and signals
346
387
 
347
- Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
348
- reaches the whole process tree, not just the first process. On Python 3.11+ this uses
349
- `Popen(process_group=0)`; older Pythons start the command through a tiny exec wrapper.
350
- Commands that try to read the terminal (a password prompt, for example) fail at once
351
- instead of hanging. The library does this by blocking SIGTTIN/SIGTTOU for the command,
352
- which covers commands started directly or through bash; `mcmd` also ignores them, which
353
- every command inherits through any shell.
354
-
355
- Because the commands are not in your program's process group, signals sent to that
356
- group do not reach them. multiCMD handles this for you: a call waiting on the main
357
- thread that gets Ctrl-C stops its commands before re-raising, `mcmd` stops them on
358
- SIGTERM / SIGHUP (unless it was started with them ignored, as under `nohup`), and any
359
- commands still running when the interpreter exits are killed. A program that handles
360
- SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
361
- arrives), can call `multiCMD.kill_all()` from its handler.
388
+ Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C reaches the
389
+ whole process tree, not just the first process. On Python 3.11+ this uses `Popen(process_group=0)`;
390
+ older Pythons start the command through a tiny exec wrapper. A command that reads the terminal (a
391
+ password prompt, for example) fails at once instead of hanging: multiCMD blocks SIGTTIN/SIGTTOU for
392
+ it, and `mcmd` also ignores them, which every command inherits through any shell.
393
+
394
+ Signals sent to your program's group do not reach the commands, so multiCMD stops them itself. One
395
+ Ctrl-C (or a SIGTERM handler's `sys.exit()`) in a call waiting on the main thread, `join_threads()`
396
+ and `AsyncExecutor.wait()` included, stops every command, skips the queued ones and
397
+ re-raises, giving them up to 5 s to exit; a second Ctrl-C ends that wait at once. `mcmd` stops its
398
+ commands on SIGTERM / SIGHUP (unless started with them ignored, as under `nohup`). At interpreter exit
399
+ the commands still running are killed, and a command queued on one of multiCMD's threads fails (-1,
400
+ "not started: the interpreter is exiting") instead of outliving the program; one run from an `atexit`
401
+ handler after that, on the main thread, runs as usual. A program that handles SIGTERM itself, or runs
402
+ multiCMD from its own threads (where Ctrl-C never arrives), can call `multiCMD.kill_all()` from its
403
+ handler. A command not seen to exit a second after SIGKILL is no longer tracked; its pid is named on
404
+ stderr, unless quiet.
405
+
406
+ multiCMD never touches SIGINT's handler (`mcmd` sets its own SIGTERM / SIGHUP handlers, nothing
407
+ else). What your handler raises while a call waits for its commands is handled as a Ctrl-C.
408
+
409
+ A command is registered from the moment `Popen` returns it: a Ctrl-C that Python raises inside
410
+ `Popen` as a serial run starts a command (some 50-70 µs each) can leave it running, unknown to
411
+ `kill_all()`. Two Ctrl-Cs microseconds apart can leave a command running, still registered, until
412
+ `kill_all()` or the exit stops it. On Python 3.6-3.9, which raise a Ctrl-C at nearly any instruction,
413
+ even one at a few points between the steps of a run can leave its command registered but not signalled.
414
+ On Python 3.6-3.9 and 3.14 a Ctrl-C at the wrong instruction of a `with` on a lock can leave it held: one of
415
+ `threading`'s can hang the program as its threads end (CPython's own), and one of multiCMD's (an
416
+ `AsyncExecutor`'s queue, the live output) hangs that executor, or the next call that is not quiet, if the
417
+ program keeps going.
418
+ These and the other known limits are in [docs/limitations.md](docs/limitations.md).
362
419
 
363
420
  ## Key functions and objects
364
421
 
@@ -366,12 +423,13 @@ arrives), can call `multiCMD.kill_all()` from its handler.
366
423
  run_command(command, timeout=0, max_threads=1, quiet=False, dry_run=False,
367
424
  with_stdErr=False, return_code_only=False, return_object=False,
368
425
  wait_for_return=True, sem=None, use_sudo=..., raise_error=False,
369
- prefix=False, color=None, pty=False, shell=False)
426
+ prefix=False, color=None, pty=False, shell=False, env=None, extra_env=None)
370
427
 
371
428
  run_commands(commands, timeout=0, max_threads=1, quiet=False, dry_run=False,
372
429
  with_stdErr=False, return_code_only=False, return_object=False,
373
430
  parse=False, wait_for_return=True, sem=None, use_sudo=...,
374
- raise_error=False, prefix=False, color=None, pty=False, shell=False)
431
+ raise_error=False, prefix=False, color=None, pty=False, shell=False,
432
+ env=None, extra_env=None)
375
433
 
376
434
  ping(hosts, timeout=1, max_threads=0, ...) # returns True/False reachability
377
435
  join_threads(threads=..., timeout=None) # join fire-and-forget threads
@@ -380,7 +438,8 @@ set_sudo(use_sudo) # enable/disable sudo globally
380
438
 
381
439
  class Task: # command, returncode, stdout, stderr, start_time, end_time, duration
382
440
  class CommandError: # raised by raise_error=True; .tasks = the failed Tasks
383
- class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes
441
+ class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes;
442
+ # takes the options above, env / extra_env as defaults a call overrides
384
443
  ```
385
444
 
386
445
  The module also bundles a few terminal/formatting helpers used internally and
@@ -406,6 +465,51 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
406
465
 
407
466
  `test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
408
467
 
468
+ ## Upgrading to 1.56 from 1.55
469
+
470
+ - Fix: a command that times out while it redraws a `\r` progress line on stderr keeps its `Timeout!`
471
+ line; 1.55 could replace it with the progress line, leaving return code 124 without the marker.
472
+ - Simpler interrupt handling, some 700 lines lighter. One Ctrl-C still stops every command.
473
+ What changes is a few rare corners, each in [docs/limitations.md](docs/limitations.md):
474
+ - two Ctrl-Cs microseconds apart can leave a command running, still registered, until `kill_all()`
475
+ or the exit stops it; 1.55 stopped it at once;
476
+ - a Ctrl-C can lose a serial run's starting command anywhere inside `Popen` (about 51 µs per start on
477
+ Python 3.14), not only inside the fork (about 41 µs);
478
+ - a Ctrl-C that leaves `Popen.poll()`'s lock held makes that command's stop about 2 s slower;
479
+ - an ordinary exception your own signal handler raises as a serial run starts a command is recorded as
480
+ a start failure (-1) and not raised, as in 1.54;
481
+ - `join_threads()` on another thread can return before a batch that is still starting.
482
+ - `__running_threads_lock` and `_live_procs_lock`, unused since 1.55 and kept only for code that imported
483
+ them, are gone.
484
+
485
+ ## Upgrading to 1.55 from 1.54
486
+
487
+ - New: a per-call environment. `env=` (the whole environment, as `subprocess`'s) and `extra_env=`
488
+ (added on top; a `None` value removes a variable) on `run_command`, `run_commands` and
489
+ `AsyncExecutor` (constructor defaults, overridden per call), and `mcmd -e NAME=VALUE` / `-e NAME`;
490
+ see [Environment per call](#environment-per-call).
491
+ - `quiet=True` now hides the lines about a command that could not be run: `Command path not found`
492
+ (127), `Command cannot be executed` (126) and the error of a command multiCMD could not run (-1),
493
+ which 1.54 printed even when quiet, partly on stdout. The reasons are still in `task.stderr`. Warnings
494
+ about the run's setup (sudo not found or not usable, no pty) still print.
495
+ - A Ctrl-C in `join_threads()` stops the commands it waits for, skips the ones still queued, waits up
496
+ to 5 s for them and re-raises; 1.54 re-raised and left them running. A Ctrl-C that interrupts
497
+ `AsyncExecutor.run_command(s)` skips that call's commands.
498
+ - A SIGTERM handler's `sys.exit()`, or whatever your own signal handler raises while a call waits for
499
+ its commands, stops them and records them as stopped before it is raised on, as a Ctrl-C does; 1.54
500
+ did that only for `KeyboardInterrupt`.
501
+ - A Ctrl-C while a command starts, times out or is stopped, or while multiCMD reads the output that
502
+ a finished command's background processes still hold open, no longer leaves the command or those
503
+ processes running unknown to `kill_all()` and the exit hook, or known to them but never signalled.
504
+ A second Ctrl-C ends the wait for the commands at once; each is still sent SIGTERM, and `kill_all()`
505
+ and the exit hook finish the kill. The limits below are the exceptions.
506
+ - Once the interpreter exits, a command queued on one of multiCMD's threads that has not started fails
507
+ with -1 ("not started: the interpreter is exiting") instead of starting and outliving the program.
508
+ - A command not seen to exit a second after SIGKILL is no longer tracked, so later `kill_all()` calls and the
509
+ exit do not wait for it again; its pid is named on stderr, unless quiet.
510
+ - Known limits: see [Process groups and signals](#process-groups-and-signals) and
511
+ [docs/limitations.md](docs/limitations.md).
512
+
409
513
  ## License
410
514
 
411
515
  GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).