multiCMD 1.55__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.
- {multicmd-1.55 → multicmd-1.56}/PKG-INFO +50 -73
- {multicmd-1.55 → multicmd-1.56}/README.md +49 -72
- {multicmd-1.55 → multicmd-1.56}/multiCMD.egg-info/PKG-INFO +50 -73
- {multicmd-1.55 → multicmd-1.56}/multiCMD.egg-info/SOURCES.txt +1 -1
- {multicmd-1.55 → multicmd-1.56}/multiCMD.py +273 -910
- {multicmd-1.55 → multicmd-1.56}/tests/test_async_executor.py +15 -267
- multicmd-1.56/tests/test_interrupts.py +623 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_pool.py +0 -24
- {multicmd-1.55 → multicmd-1.56}/tests/test_process_groups.py +35 -148
- {multicmd-1.55 → multicmd-1.56}/tests/test_pty.py +0 -27
- {multicmd-1.55 → multicmd-1.56}/tests/test_run_command.py +45 -0
- multicmd-1.55/tests/test_settle.py +0 -2412
- {multicmd-1.55 → multicmd-1.56}/LICENSE +0 -0
- {multicmd-1.55 → multicmd-1.56}/multiCMD.egg-info/dependency_links.txt +0 -0
- {multicmd-1.55 → multicmd-1.56}/multiCMD.egg-info/entry_points.txt +0 -0
- {multicmd-1.55 → multicmd-1.56}/multiCMD.egg-info/top_level.txt +0 -0
- {multicmd-1.55 → multicmd-1.56}/setup.cfg +0 -0
- {multicmd-1.55 → multicmd-1.56}/setup.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_api.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_cli.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_cli_input.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_cli_summary.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_display.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_env.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_expand_ranges.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_helpers.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_parsing.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_ping.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_reader.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_regressions.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_run_commands.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_sudo.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_supervision.py +0 -0
- {multicmd-1.55 → multicmd-1.56}/tests/test_task.py +0 -0
- {multicmd-1.55 → 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.
|
|
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
|
|
@@ -411,67 +411,37 @@ default, in the API and on the command line) disables it.
|
|
|
411
411
|
|
|
412
412
|
## Process groups and signals
|
|
413
413
|
|
|
414
|
-
Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
before the exit on one of multiCMD's threads does not start: it fails (return code -1, "not started:
|
|
432
|
-
the interpreter is exiting"), as the thread would be frozen with the interpreter and the command
|
|
433
|
-
outlive it; a command run from an `atexit` handler, a `weakref.finalize` callback or a `__del__`
|
|
434
|
-
after that (on the main thread) runs as usual. A program that handles
|
|
435
|
-
SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
|
|
436
|
-
arrives), can call `multiCMD.kill_all()` from its handler. A command still running a second
|
|
437
|
-
after SIGKILL (in uninterruptible sleep, or not yours to signal) is no longer tracked, so later
|
|
438
|
-
calls and the exit do not wait for it again; multiCMD names its pid on stderr, unless quiet.
|
|
439
|
-
|
|
440
|
-
A Ctrl-C (or a signal handler's exception, such as a SIGTERM handler's `sys.exit()`) that lands
|
|
441
|
-
while a command starts stops it like any other, from the moment `Popen` knows the child's pid.
|
|
442
|
-
One that lands while the command is being forked can leave it running unregistered, as in 1.54:
|
|
443
|
-
if Python raises it as the fork returns, before `Popen` has stored the pid, neither the call nor
|
|
444
|
-
`kill_all()` nor the exit hook knows the command, which runs on in its own process group. Python
|
|
445
|
-
raises those on the main thread only, so this concerns serial runs (`max_threads=1`); and the window
|
|
446
|
-
is the fork's own length, some 60-150 µs per command however long it then runs. That is the only place
|
|
447
|
-
one Ctrl-C can lose a command. Before Python 3.11 a second one can too: on Python 3.6-3.10 a second
|
|
448
|
-
Ctrl-C that lands while the first is still on its way out of the command's start, before multiCMD has
|
|
449
|
-
begun to deal with it, is raised before multiCMD has registered the command (one that lands so while a
|
|
450
|
-
call waits for a running command leaves it registered but not signalled: `kill_all()` and the exit hook
|
|
451
|
-
still reach it), and on 3.6-3.9, where Python raises a Ctrl-C at nearly any instruction, a second one at a
|
|
452
|
-
few other moments of that handling can do the same, and even a first one, at a few instructions between the
|
|
453
|
-
steps of a run, can leave a command registered but not signalled. From Python 3.11 no other window is known where
|
|
454
|
-
a Ctrl-C loses a command. On every Python, two Ctrl-Cs at nearly the same moment, the first cutting short the stop
|
|
455
|
-
of a command that timed out or of what a finished command left running with its output open, can leave that
|
|
456
|
-
not signalled: `kill_all()` still reaches it, and so does the exit hook, except on Python 3.6-3.10 for what a
|
|
457
|
-
finished command left running, which can then outlive the program.
|
|
458
|
-
On Python 3.6-3.9 and 3.14 a Ctrl-C that lands just as Python's `threading` takes one of its own
|
|
459
|
-
locks with `with` (as `Thread.start()` does, for a serial run's output readers too) can leave that
|
|
460
|
-
lock held, and the program then hangs as its threads end. That is CPython's own, and any threaded
|
|
461
|
-
program can meet it; Python 3.10-3.13 do not leave the lock held.
|
|
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.
|
|
462
431
|
|
|
463
432
|
multiCMD never touches SIGINT's handler (`mcmd` sets its own SIGTERM / SIGHUP handlers, nothing
|
|
464
|
-
else).
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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).
|
|
475
445
|
|
|
476
446
|
## Key functions and objects
|
|
477
447
|
|
|
@@ -521,6 +491,23 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
|
521
491
|
|
|
522
492
|
`test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
|
|
523
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
|
+
|
|
524
511
|
## Upgrading to 1.55 from 1.54
|
|
525
512
|
|
|
526
513
|
- New: a per-call environment. `env=` (the whole environment, as `subprocess`'s) and `extra_env=`
|
|
@@ -544,20 +531,10 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
|
544
531
|
and the exit hook finish the kill. The limits below are the exceptions.
|
|
545
532
|
- Once the interpreter exits, a command queued on one of multiCMD's threads that has not started fails
|
|
546
533
|
with -1 ("not started: the interpreter is exiting") instead of starting and outliving the program.
|
|
547
|
-
- A command
|
|
534
|
+
- A command not seen to exit a second after SIGKILL is no longer tracked, so later `kill_all()` calls and the
|
|
548
535
|
exit do not wait for it again; its pid is named on stderr, unless quiet.
|
|
549
|
-
- Known limits
|
|
550
|
-
|
|
551
|
-
(serial runs only, some 60-150 µs per command). On Python 3.6-3.10 a second Ctrl-C right after a
|
|
552
|
-
first one can lose a command that is starting, or leave one that a call waits for registered but not
|
|
553
|
-
signalled; on 3.6-3.9 a second one at a few other moments can lose it too, and a first one at a few
|
|
554
|
-
instructions can leave it registered but not signalled. On every Python, two Ctrl-Cs at nearly the same
|
|
555
|
-
moment, the first cutting short the stop of a timed-out command or of a finished command's background
|
|
556
|
-
processes, can leave them not signalled: `kill_all()` still reaches them, and so does the exit hook, except
|
|
557
|
-
on Python 3.6-3.10 for a finished command's background processes, which can then outlive the program. On
|
|
558
|
-
Python 3.6-3.9 and 3.14 a Ctrl-C just as
|
|
559
|
-
`threading` takes one of its own locks can leave that lock held and hang the program as its threads end
|
|
560
|
-
(CPython's own).
|
|
536
|
+
- Known limits: see [Process groups and signals](#process-groups-and-signals) and
|
|
537
|
+
[docs/limitations.md](docs/limitations.md).
|
|
561
538
|
|
|
562
539
|
## License
|
|
563
540
|
|
|
@@ -385,67 +385,37 @@ default, in the API and on the command line) disables it.
|
|
|
385
385
|
|
|
386
386
|
## Process groups and signals
|
|
387
387
|
|
|
388
|
-
Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
before the exit on one of multiCMD's threads does not start: it fails (return code -1, "not started:
|
|
406
|
-
the interpreter is exiting"), as the thread would be frozen with the interpreter and the command
|
|
407
|
-
outlive it; a command run from an `atexit` handler, a `weakref.finalize` callback or a `__del__`
|
|
408
|
-
after that (on the main thread) runs as usual. A program that handles
|
|
409
|
-
SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
|
|
410
|
-
arrives), can call `multiCMD.kill_all()` from its handler. A command still running a second
|
|
411
|
-
after SIGKILL (in uninterruptible sleep, or not yours to signal) is no longer tracked, so later
|
|
412
|
-
calls and the exit do not wait for it again; multiCMD names its pid on stderr, unless quiet.
|
|
413
|
-
|
|
414
|
-
A Ctrl-C (or a signal handler's exception, such as a SIGTERM handler's `sys.exit()`) that lands
|
|
415
|
-
while a command starts stops it like any other, from the moment `Popen` knows the child's pid.
|
|
416
|
-
One that lands while the command is being forked can leave it running unregistered, as in 1.54:
|
|
417
|
-
if Python raises it as the fork returns, before `Popen` has stored the pid, neither the call nor
|
|
418
|
-
`kill_all()` nor the exit hook knows the command, which runs on in its own process group. Python
|
|
419
|
-
raises those on the main thread only, so this concerns serial runs (`max_threads=1`); and the window
|
|
420
|
-
is the fork's own length, some 60-150 µs per command however long it then runs. That is the only place
|
|
421
|
-
one Ctrl-C can lose a command. Before Python 3.11 a second one can too: on Python 3.6-3.10 a second
|
|
422
|
-
Ctrl-C that lands while the first is still on its way out of the command's start, before multiCMD has
|
|
423
|
-
begun to deal with it, is raised before multiCMD has registered the command (one that lands so while a
|
|
424
|
-
call waits for a running command leaves it registered but not signalled: `kill_all()` and the exit hook
|
|
425
|
-
still reach it), and on 3.6-3.9, where Python raises a Ctrl-C at nearly any instruction, a second one at a
|
|
426
|
-
few other moments of that handling can do the same, and even a first one, at a few instructions between the
|
|
427
|
-
steps of a run, can leave a command registered but not signalled. From Python 3.11 no other window is known where
|
|
428
|
-
a Ctrl-C loses a command. On every Python, two Ctrl-Cs at nearly the same moment, the first cutting short the stop
|
|
429
|
-
of a command that timed out or of what a finished command left running with its output open, can leave that
|
|
430
|
-
not signalled: `kill_all()` still reaches it, and so does the exit hook, except on Python 3.6-3.10 for what a
|
|
431
|
-
finished command left running, which can then outlive the program.
|
|
432
|
-
On Python 3.6-3.9 and 3.14 a Ctrl-C that lands just as Python's `threading` takes one of its own
|
|
433
|
-
locks with `with` (as `Thread.start()` does, for a serial run's output readers too) can leave that
|
|
434
|
-
lock held, and the program then hangs as its threads end. That is CPython's own, and any threaded
|
|
435
|
-
program can meet it; Python 3.10-3.13 do not leave the lock held.
|
|
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.
|
|
436
405
|
|
|
437
406
|
multiCMD never touches SIGINT's handler (`mcmd` sets its own SIGTERM / SIGHUP handlers, nothing
|
|
438
|
-
else).
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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).
|
|
449
419
|
|
|
450
420
|
## Key functions and objects
|
|
451
421
|
|
|
@@ -495,6 +465,23 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
|
495
465
|
|
|
496
466
|
`test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
|
|
497
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
|
+
|
|
498
485
|
## Upgrading to 1.55 from 1.54
|
|
499
486
|
|
|
500
487
|
- New: a per-call environment. `env=` (the whole environment, as `subprocess`'s) and `extra_env=`
|
|
@@ -518,20 +505,10 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
|
518
505
|
and the exit hook finish the kill. The limits below are the exceptions.
|
|
519
506
|
- Once the interpreter exits, a command queued on one of multiCMD's threads that has not started fails
|
|
520
507
|
with -1 ("not started: the interpreter is exiting") instead of starting and outliving the program.
|
|
521
|
-
- A command
|
|
508
|
+
- A command not seen to exit a second after SIGKILL is no longer tracked, so later `kill_all()` calls and the
|
|
522
509
|
exit do not wait for it again; its pid is named on stderr, unless quiet.
|
|
523
|
-
- Known limits
|
|
524
|
-
|
|
525
|
-
(serial runs only, some 60-150 µs per command). On Python 3.6-3.10 a second Ctrl-C right after a
|
|
526
|
-
first one can lose a command that is starting, or leave one that a call waits for registered but not
|
|
527
|
-
signalled; on 3.6-3.9 a second one at a few other moments can lose it too, and a first one at a few
|
|
528
|
-
instructions can leave it registered but not signalled. On every Python, two Ctrl-Cs at nearly the same
|
|
529
|
-
moment, the first cutting short the stop of a timed-out command or of a finished command's background
|
|
530
|
-
processes, can leave them not signalled: `kill_all()` still reaches them, and so does the exit hook, except
|
|
531
|
-
on Python 3.6-3.10 for a finished command's background processes, which can then outlive the program. On
|
|
532
|
-
Python 3.6-3.9 and 3.14 a Ctrl-C just as
|
|
533
|
-
`threading` takes one of its own locks can leave that lock held and hang the program as its threads end
|
|
534
|
-
(CPython's own).
|
|
510
|
+
- Known limits: see [Process groups and signals](#process-groups-and-signals) and
|
|
511
|
+
[docs/limitations.md](docs/limitations.md).
|
|
535
512
|
|
|
536
513
|
## License
|
|
537
514
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multiCMD
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -411,67 +411,37 @@ default, in the API and on the command line) disables it.
|
|
|
411
411
|
|
|
412
412
|
## Process groups and signals
|
|
413
413
|
|
|
414
|
-
Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
before the exit on one of multiCMD's threads does not start: it fails (return code -1, "not started:
|
|
432
|
-
the interpreter is exiting"), as the thread would be frozen with the interpreter and the command
|
|
433
|
-
outlive it; a command run from an `atexit` handler, a `weakref.finalize` callback or a `__del__`
|
|
434
|
-
after that (on the main thread) runs as usual. A program that handles
|
|
435
|
-
SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
|
|
436
|
-
arrives), can call `multiCMD.kill_all()` from its handler. A command still running a second
|
|
437
|
-
after SIGKILL (in uninterruptible sleep, or not yours to signal) is no longer tracked, so later
|
|
438
|
-
calls and the exit do not wait for it again; multiCMD names its pid on stderr, unless quiet.
|
|
439
|
-
|
|
440
|
-
A Ctrl-C (or a signal handler's exception, such as a SIGTERM handler's `sys.exit()`) that lands
|
|
441
|
-
while a command starts stops it like any other, from the moment `Popen` knows the child's pid.
|
|
442
|
-
One that lands while the command is being forked can leave it running unregistered, as in 1.54:
|
|
443
|
-
if Python raises it as the fork returns, before `Popen` has stored the pid, neither the call nor
|
|
444
|
-
`kill_all()` nor the exit hook knows the command, which runs on in its own process group. Python
|
|
445
|
-
raises those on the main thread only, so this concerns serial runs (`max_threads=1`); and the window
|
|
446
|
-
is the fork's own length, some 60-150 µs per command however long it then runs. That is the only place
|
|
447
|
-
one Ctrl-C can lose a command. Before Python 3.11 a second one can too: on Python 3.6-3.10 a second
|
|
448
|
-
Ctrl-C that lands while the first is still on its way out of the command's start, before multiCMD has
|
|
449
|
-
begun to deal with it, is raised before multiCMD has registered the command (one that lands so while a
|
|
450
|
-
call waits for a running command leaves it registered but not signalled: `kill_all()` and the exit hook
|
|
451
|
-
still reach it), and on 3.6-3.9, where Python raises a Ctrl-C at nearly any instruction, a second one at a
|
|
452
|
-
few other moments of that handling can do the same, and even a first one, at a few instructions between the
|
|
453
|
-
steps of a run, can leave a command registered but not signalled. From Python 3.11 no other window is known where
|
|
454
|
-
a Ctrl-C loses a command. On every Python, two Ctrl-Cs at nearly the same moment, the first cutting short the stop
|
|
455
|
-
of a command that timed out or of what a finished command left running with its output open, can leave that
|
|
456
|
-
not signalled: `kill_all()` still reaches it, and so does the exit hook, except on Python 3.6-3.10 for what a
|
|
457
|
-
finished command left running, which can then outlive the program.
|
|
458
|
-
On Python 3.6-3.9 and 3.14 a Ctrl-C that lands just as Python's `threading` takes one of its own
|
|
459
|
-
locks with `with` (as `Thread.start()` does, for a serial run's output readers too) can leave that
|
|
460
|
-
lock held, and the program then hangs as its threads end. That is CPython's own, and any threaded
|
|
461
|
-
program can meet it; Python 3.10-3.13 do not leave the lock held.
|
|
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.
|
|
462
431
|
|
|
463
432
|
multiCMD never touches SIGINT's handler (`mcmd` sets its own SIGTERM / SIGHUP handlers, nothing
|
|
464
|
-
else).
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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).
|
|
475
445
|
|
|
476
446
|
## Key functions and objects
|
|
477
447
|
|
|
@@ -521,6 +491,23 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
|
521
491
|
|
|
522
492
|
`test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
|
|
523
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
|
+
|
|
524
511
|
## Upgrading to 1.55 from 1.54
|
|
525
512
|
|
|
526
513
|
- New: a per-call environment. `env=` (the whole environment, as `subprocess`'s) and `extra_env=`
|
|
@@ -544,20 +531,10 @@ pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
|
544
531
|
and the exit hook finish the kill. The limits below are the exceptions.
|
|
545
532
|
- Once the interpreter exits, a command queued on one of multiCMD's threads that has not started fails
|
|
546
533
|
with -1 ("not started: the interpreter is exiting") instead of starting and outliving the program.
|
|
547
|
-
- A command
|
|
534
|
+
- A command not seen to exit a second after SIGKILL is no longer tracked, so later `kill_all()` calls and the
|
|
548
535
|
exit do not wait for it again; its pid is named on stderr, unless quiet.
|
|
549
|
-
- Known limits
|
|
550
|
-
|
|
551
|
-
(serial runs only, some 60-150 µs per command). On Python 3.6-3.10 a second Ctrl-C right after a
|
|
552
|
-
first one can lose a command that is starting, or leave one that a call waits for registered but not
|
|
553
|
-
signalled; on 3.6-3.9 a second one at a few other moments can lose it too, and a first one at a few
|
|
554
|
-
instructions can leave it registered but not signalled. On every Python, two Ctrl-Cs at nearly the same
|
|
555
|
-
moment, the first cutting short the stop of a timed-out command or of a finished command's background
|
|
556
|
-
processes, can leave them not signalled: `kill_all()` still reaches them, and so does the exit hook, except
|
|
557
|
-
on Python 3.6-3.10 for a finished command's background processes, which can then outlive the program. On
|
|
558
|
-
Python 3.6-3.9 and 3.14 a Ctrl-C just as
|
|
559
|
-
`threading` takes one of its own locks can leave that lock held and hang the program as its threads end
|
|
560
|
-
(CPython's own).
|
|
536
|
+
- Known limits: see [Process groups and signals](#process-groups-and-signals) and
|
|
537
|
+
[docs/limitations.md](docs/limitations.md).
|
|
561
538
|
|
|
562
539
|
## License
|
|
563
540
|
|
|
@@ -16,6 +16,7 @@ tests/test_display.py
|
|
|
16
16
|
tests/test_env.py
|
|
17
17
|
tests/test_expand_ranges.py
|
|
18
18
|
tests/test_helpers.py
|
|
19
|
+
tests/test_interrupts.py
|
|
19
20
|
tests/test_parsing.py
|
|
20
21
|
tests/test_ping.py
|
|
21
22
|
tests/test_pool.py
|
|
@@ -25,7 +26,6 @@ tests/test_reader.py
|
|
|
25
26
|
tests/test_regressions.py
|
|
26
27
|
tests/test_run_command.py
|
|
27
28
|
tests/test_run_commands.py
|
|
28
|
-
tests/test_settle.py
|
|
29
29
|
tests/test_sudo.py
|
|
30
30
|
tests/test_supervision.py
|
|
31
31
|
tests/test_task.py
|