pyallel 1.0.1__tar.gz → 1.2.0__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.
- {pyallel-1.0.1 → pyallel-1.2.0}/PKG-INFO +26 -9
- {pyallel-1.0.1 → pyallel-1.2.0}/README.md +25 -8
- {pyallel-1.0.1 → pyallel-1.2.0}/pyproject.toml +1 -1
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/main.py +5 -8
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/parser.py +18 -11
- pyallel-1.2.0/src/pyallel/process.py +76 -0
- pyallel-1.0.1/src/pyallel/process.py → pyallel-1.2.0/src/pyallel/process_group.py +10 -126
- pyallel-1.2.0/src/pyallel/process_group_manager.py +95 -0
- {pyallel-1.0.1 → pyallel-1.2.0}/LICENSE +0 -0
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/__init__.py +0 -0
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/colours.py +0 -0
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/constants.py +0 -0
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/errors.py +0 -0
- {pyallel-1.0.1 → pyallel-1.2.0}/src/pyallel/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
5
5
|
Home-page: https://github.com/Danthewaann/pyallel
|
|
6
6
|
License: MIT
|
|
@@ -43,24 +43,38 @@ pip install pyallel
|
|
|
43
43
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
usage: pyallel [-h] [-t] [-n] [-V] [
|
|
46
|
+
usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
|
|
47
47
|
|
|
48
48
|
Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
49
49
|
|
|
50
50
|
positional arguments:
|
|
51
|
-
commands list of quoted commands to run e.g "mypy ." "black ."
|
|
51
|
+
commands list of quoted commands to run in parallel e.g "mypy ." "black ."
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
each command is executed inside a shell, so shell syntax is supported as
|
|
54
|
+
if you were running the command directly in a shell, some examples are below
|
|
54
55
|
|
|
55
|
-
"MYPY_FORCE_COLOR=1 mypy ."
|
|
56
|
+
"MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
|
|
57
|
+
"mypy | tee -a mypy.log" <- use pipes to redirect output
|
|
58
|
+
"cat > test.log < other.log" <- use input and output redirection
|
|
59
|
+
"mypy .; pytest ." <- run commands one at a time in sequence
|
|
60
|
+
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
61
|
+
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
62
|
+
|
|
63
|
+
commands can be grouped using the group separator symbol (:::)
|
|
64
|
+
|
|
65
|
+
"echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
66
|
+
|
|
67
|
+
the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
|
|
68
|
+
|
|
69
|
+
command groups are ran in the sequence you provide them, and if a command group fails
|
|
70
|
+
(if a command fails inside the command group) the rest of the command groups in the sequence are not run
|
|
56
71
|
|
|
57
72
|
options:
|
|
58
73
|
-h, --help show this help message and exit
|
|
59
74
|
-t, --no-timer don't time how long each command is taking
|
|
60
75
|
-n, --non-interactive
|
|
61
76
|
run in non-interactive mode
|
|
62
|
-
-V, --
|
|
63
|
-
-v, --version print version and exit
|
|
77
|
+
-V, --version print version and exit
|
|
64
78
|
--colour {yes,no,auto}
|
|
65
79
|
colour terminal output, defaults to "auto"
|
|
66
80
|
```
|
|
@@ -104,8 +118,11 @@ python -m venv .venv && source .venv/bin/activate && pip install . -r requiremen
|
|
|
104
118
|
|
|
105
119
|
## TODOs
|
|
106
120
|
|
|
107
|
-
- [
|
|
108
|
-
|
|
121
|
+
- [x] Add support to have commands depend on other commands (some commands must complete
|
|
122
|
+
before a given command can start)
|
|
123
|
+
- [ ] Add support to state how many lines a command can use for it's output in interactive mode
|
|
124
|
+
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
125
|
+
(such as a REPL)
|
|
109
126
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
110
127
|
`errorformat`)
|
|
111
128
|
- [ ] Allow list of files to be provided to supply as input arguments to each command
|
|
@@ -21,24 +21,38 @@ pip install pyallel
|
|
|
21
21
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
usage: pyallel [-h] [-t] [-n] [-V] [
|
|
24
|
+
usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
|
|
25
25
|
|
|
26
26
|
Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
27
27
|
|
|
28
28
|
positional arguments:
|
|
29
|
-
commands list of quoted commands to run e.g "mypy ." "black ."
|
|
29
|
+
commands list of quoted commands to run in parallel e.g "mypy ." "black ."
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
each command is executed inside a shell, so shell syntax is supported as
|
|
32
|
+
if you were running the command directly in a shell, some examples are below
|
|
32
33
|
|
|
33
|
-
"MYPY_FORCE_COLOR=1 mypy ."
|
|
34
|
+
"MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
|
|
35
|
+
"mypy | tee -a mypy.log" <- use pipes to redirect output
|
|
36
|
+
"cat > test.log < other.log" <- use input and output redirection
|
|
37
|
+
"mypy .; pytest ." <- run commands one at a time in sequence
|
|
38
|
+
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
39
|
+
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
40
|
+
|
|
41
|
+
commands can be grouped using the group separator symbol (:::)
|
|
42
|
+
|
|
43
|
+
"echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
44
|
+
|
|
45
|
+
the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
|
|
46
|
+
|
|
47
|
+
command groups are ran in the sequence you provide them, and if a command group fails
|
|
48
|
+
(if a command fails inside the command group) the rest of the command groups in the sequence are not run
|
|
34
49
|
|
|
35
50
|
options:
|
|
36
51
|
-h, --help show this help message and exit
|
|
37
52
|
-t, --no-timer don't time how long each command is taking
|
|
38
53
|
-n, --non-interactive
|
|
39
54
|
run in non-interactive mode
|
|
40
|
-
-V, --
|
|
41
|
-
-v, --version print version and exit
|
|
55
|
+
-V, --version print version and exit
|
|
42
56
|
--colour {yes,no,auto}
|
|
43
57
|
colour terminal output, defaults to "auto"
|
|
44
58
|
```
|
|
@@ -82,8 +96,11 @@ python -m venv .venv && source .venv/bin/activate && pip install . -r requiremen
|
|
|
82
96
|
|
|
83
97
|
## TODOs
|
|
84
98
|
|
|
85
|
-
- [
|
|
86
|
-
|
|
99
|
+
- [x] Add support to have commands depend on other commands (some commands must complete
|
|
100
|
+
before a given command can start)
|
|
101
|
+
- [ ] Add support to state how many lines a command can use for it's output in interactive mode
|
|
102
|
+
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
103
|
+
(such as a REPL)
|
|
87
104
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
88
105
|
`errorformat`)
|
|
89
106
|
- [ ] Allow list of files to be provided to supply as input arguments to each command
|
|
@@ -8,25 +8,23 @@ from pyallel import constants
|
|
|
8
8
|
from pyallel.colours import Colours
|
|
9
9
|
from pyallel.errors import InvalidExecutableErrors
|
|
10
10
|
from pyallel.parser import Arguments, create_parser
|
|
11
|
-
from pyallel.
|
|
11
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def main_loop(
|
|
15
|
-
*
|
|
15
|
+
*args: str,
|
|
16
16
|
colours: Colours,
|
|
17
17
|
interactive: bool = False,
|
|
18
18
|
timer: bool = False,
|
|
19
|
-
verbose: bool = False,
|
|
20
19
|
) -> int:
|
|
21
|
-
|
|
22
|
-
*
|
|
20
|
+
process_group_manager = ProcessGroupManager.from_args(
|
|
21
|
+
*args,
|
|
23
22
|
colours=colours,
|
|
24
23
|
interactive=interactive,
|
|
25
24
|
timer=timer,
|
|
26
|
-
verbose=verbose,
|
|
27
25
|
)
|
|
28
26
|
|
|
29
|
-
return
|
|
27
|
+
return process_group_manager.stream()
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
def run(*args: str) -> int:
|
|
@@ -57,7 +55,6 @@ def run(*args: str) -> int:
|
|
|
57
55
|
colours=colours,
|
|
58
56
|
interactive=interactive,
|
|
59
57
|
timer=parsed_args.timer,
|
|
60
|
-
verbose=parsed_args.verbose,
|
|
61
58
|
)
|
|
62
59
|
except InvalidExecutableErrors as e:
|
|
63
60
|
exit_code = 1
|
|
@@ -9,7 +9,6 @@ class Arguments:
|
|
|
9
9
|
commands: list[str]
|
|
10
10
|
interactive: bool
|
|
11
11
|
timer: bool
|
|
12
|
-
verbose: bool
|
|
13
12
|
version: bool
|
|
14
13
|
|
|
15
14
|
def __repr__(self) -> str:
|
|
@@ -20,11 +19,26 @@ class Arguments:
|
|
|
20
19
|
return msg
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
COMMANDS_HELP = """list of quoted commands to run e.g "mypy ." "black ."
|
|
22
|
+
COMMANDS_HELP = r"""list of quoted commands to run in parallel e.g "mypy ." "black ."
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
each command is executed inside a shell, so shell syntax is supported as
|
|
25
|
+
if you were running the command directly in a shell, some examples are below
|
|
26
26
|
|
|
27
|
-
"MYPY_FORCE_COLOR=1 mypy ."
|
|
27
|
+
"MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
|
|
28
|
+
"mypy | tee -a mypy.log" <- use pipes to redirect output
|
|
29
|
+
"cat > test.log < other.log" <- use input and output redirection
|
|
30
|
+
"mypy .; pytest ." <- run commands one at a time in sequence
|
|
31
|
+
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
32
|
+
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
33
|
+
|
|
34
|
+
commands can be grouped using the group separator symbol (:::)
|
|
35
|
+
|
|
36
|
+
"echo boil kettle" "sleep 1" ::: "echo make coffee"
|
|
37
|
+
|
|
38
|
+
the above will print "boil kettle" and sleep for 1 second first before printing "make coffee"
|
|
39
|
+
|
|
40
|
+
command groups are ran in the sequence you provide them, and if a command group fails
|
|
41
|
+
(if a command fails inside the command group) the rest of the command groups in the sequence are not run
|
|
28
42
|
"""
|
|
29
43
|
|
|
30
44
|
|
|
@@ -57,13 +71,6 @@ def create_parser() -> ArgumentParser:
|
|
|
57
71
|
)
|
|
58
72
|
parser.add_argument(
|
|
59
73
|
"-V",
|
|
60
|
-
"--verbose",
|
|
61
|
-
help="run in verbose mode",
|
|
62
|
-
action="store_true",
|
|
63
|
-
default=False,
|
|
64
|
-
)
|
|
65
|
-
parser.add_argument(
|
|
66
|
-
"-v",
|
|
67
74
|
"--version",
|
|
68
75
|
help="print version and exit",
|
|
69
76
|
action="store_true",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import BinaryIO
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Process:
|
|
13
|
+
id: int
|
|
14
|
+
command: str
|
|
15
|
+
start: float = 0.0
|
|
16
|
+
end: float = 0.0
|
|
17
|
+
_fd: BinaryIO | None = field(init=False, repr=False, compare=False, default=None)
|
|
18
|
+
_process: subprocess.Popen[bytes] | None = field(
|
|
19
|
+
init=False, repr=False, compare=False, default=None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def run(self) -> None:
|
|
23
|
+
self.start = time.perf_counter()
|
|
24
|
+
fd, fd_name = tempfile.mkstemp()
|
|
25
|
+
self._fd = open(fd_name, "rb")
|
|
26
|
+
self._process = subprocess.Popen(
|
|
27
|
+
self.command,
|
|
28
|
+
stdin=subprocess.DEVNULL,
|
|
29
|
+
stdout=fd,
|
|
30
|
+
stderr=subprocess.STDOUT,
|
|
31
|
+
shell=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __del__(self) -> None:
|
|
35
|
+
if self._fd:
|
|
36
|
+
self._fd.close()
|
|
37
|
+
|
|
38
|
+
def poll(self) -> int | None:
|
|
39
|
+
if self._process:
|
|
40
|
+
poll = self._process.poll()
|
|
41
|
+
if poll is not None and not self.end:
|
|
42
|
+
self.end = time.perf_counter()
|
|
43
|
+
return poll
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def read(self) -> bytes:
|
|
47
|
+
if self._fd:
|
|
48
|
+
return self._fd.read()
|
|
49
|
+
return b""
|
|
50
|
+
|
|
51
|
+
def readline(self) -> bytes:
|
|
52
|
+
if self._fd:
|
|
53
|
+
return self._fd.readline()
|
|
54
|
+
return b""
|
|
55
|
+
|
|
56
|
+
def return_code(self) -> int | None:
|
|
57
|
+
if self._process:
|
|
58
|
+
return self._process.returncode
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def interrupt(self) -> None:
|
|
62
|
+
if self._process:
|
|
63
|
+
self._process.send_signal(signal.SIGINT)
|
|
64
|
+
|
|
65
|
+
def kill(self) -> None:
|
|
66
|
+
if self._process:
|
|
67
|
+
self._process.send_signal(signal.SIGKILL)
|
|
68
|
+
|
|
69
|
+
def wait(self) -> int:
|
|
70
|
+
if self._process:
|
|
71
|
+
return self._process.wait()
|
|
72
|
+
return -1
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_command(cls, id: int, command: str) -> Process:
|
|
76
|
+
return cls(id=id, command=command)
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
import shlex
|
|
5
|
-
import shutil
|
|
6
|
-
import signal
|
|
7
|
-
import subprocess
|
|
8
|
-
import tempfile
|
|
9
3
|
import time
|
|
10
4
|
from collections import defaultdict
|
|
11
5
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Any, BinaryIO
|
|
13
|
-
from uuid import UUID, uuid4
|
|
14
6
|
|
|
15
7
|
from pyallel import constants
|
|
16
8
|
from pyallel.colours import Colours
|
|
17
9
|
from pyallel.errors import InvalidExecutableError, InvalidExecutableErrors
|
|
10
|
+
from pyallel.process import Process
|
|
18
11
|
|
|
19
12
|
|
|
20
13
|
def get_num_lines(output: str, columns: int | None = None) -> int:
|
|
@@ -39,10 +32,9 @@ class ProcessGroup:
|
|
|
39
32
|
processes: list[Process]
|
|
40
33
|
interactive: bool = False
|
|
41
34
|
timer: bool = False
|
|
42
|
-
|
|
43
|
-
output: dict[UUID, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
35
|
+
output: dict[int, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
44
36
|
process_lines: list[int] = field(default_factory=list)
|
|
45
|
-
completed_processes: set[
|
|
37
|
+
completed_processes: set[int] = field(default_factory=set)
|
|
46
38
|
exit_code: int = 0
|
|
47
39
|
interrupt_count: int = 0
|
|
48
40
|
passed: bool = True
|
|
@@ -87,11 +79,6 @@ class ProcessGroup:
|
|
|
87
79
|
running_process = None
|
|
88
80
|
interrupted = False
|
|
89
81
|
|
|
90
|
-
print(
|
|
91
|
-
f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.white_bold}Running commands...{self.colours.reset_colour}\n{self.colours.dim_on}=>{self.colours.dim_off} ",
|
|
92
|
-
flush=True,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
82
|
while True:
|
|
96
83
|
output = ""
|
|
97
84
|
for process in self.processes:
|
|
@@ -99,7 +86,7 @@ class ProcessGroup:
|
|
|
99
86
|
running_process is None
|
|
100
87
|
and process.id not in self.completed_processes
|
|
101
88
|
):
|
|
102
|
-
output += self._get_command_status(process
|
|
89
|
+
output += self._get_command_status(process)
|
|
103
90
|
output += "\n"
|
|
104
91
|
running_process = process
|
|
105
92
|
elif running_process is not process:
|
|
@@ -138,7 +125,6 @@ class ProcessGroup:
|
|
|
138
125
|
output += self._get_command_status(
|
|
139
126
|
process,
|
|
140
127
|
passed=process.return_code() == 0,
|
|
141
|
-
verbose=self.verbose,
|
|
142
128
|
timer=self.timer,
|
|
143
129
|
)
|
|
144
130
|
output += f"\n{self.colours.dim_on}=>{self.colours.dim_off} \n"
|
|
@@ -189,7 +175,6 @@ class ProcessGroup:
|
|
|
189
175
|
process: Process,
|
|
190
176
|
icon: str | None = None,
|
|
191
177
|
passed: bool | None = None,
|
|
192
|
-
verbose: bool = False,
|
|
193
178
|
timer: bool = False,
|
|
194
179
|
) -> str:
|
|
195
180
|
if passed is True:
|
|
@@ -207,12 +192,7 @@ class ProcessGroup:
|
|
|
207
192
|
if not icon:
|
|
208
193
|
msg += "..."
|
|
209
194
|
|
|
210
|
-
output = f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.white_bold}[{self.colours.reset_colour}{self.colours.blue_bold}{process.
|
|
211
|
-
|
|
212
|
-
if verbose:
|
|
213
|
-
output += f" {' '.join(process.args)}"
|
|
214
|
-
|
|
215
|
-
output += f"{self.colours.reset_colour}{self.colours.white_bold}]{self.colours.reset_colour}{colour} {msg} {icon}{self.colours.reset_colour}"
|
|
195
|
+
output = f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.white_bold}[{self.colours.reset_colour}{self.colours.blue_bold}{process.command}{self.colours.reset_colour}{self.colours.white_bold}]{self.colours.reset_colour}{colour} {msg} {icon}{self.colours.reset_colour}"
|
|
216
196
|
|
|
217
197
|
if timer:
|
|
218
198
|
end = process.end
|
|
@@ -223,7 +203,7 @@ class ProcessGroup:
|
|
|
223
203
|
|
|
224
204
|
return output
|
|
225
205
|
|
|
226
|
-
def
|
|
206
|
+
def handle_signal(self, signum: int) -> None:
|
|
227
207
|
for process in self.processes:
|
|
228
208
|
if self.interrupt_count == 0:
|
|
229
209
|
process.interrupt()
|
|
@@ -237,17 +217,17 @@ class ProcessGroup:
|
|
|
237
217
|
def from_commands(
|
|
238
218
|
cls,
|
|
239
219
|
*commands: str,
|
|
240
|
-
colours: Colours,
|
|
220
|
+
colours: Colours | None = None,
|
|
241
221
|
interactive: bool = False,
|
|
242
222
|
timer: bool = False,
|
|
243
|
-
verbose: bool = False,
|
|
244
223
|
) -> ProcessGroup:
|
|
224
|
+
colours = colours or Colours()
|
|
245
225
|
processes: list[Process] = []
|
|
246
226
|
errors: list[InvalidExecutableError] = []
|
|
247
227
|
|
|
248
|
-
for command in commands:
|
|
228
|
+
for i, command in enumerate(commands):
|
|
249
229
|
try:
|
|
250
|
-
processes.append(Process.from_command(command))
|
|
230
|
+
processes.append(Process.from_command(i + 1, command))
|
|
251
231
|
except InvalidExecutableError as e:
|
|
252
232
|
errors.append(e)
|
|
253
233
|
|
|
@@ -258,13 +238,9 @@ class ProcessGroup:
|
|
|
258
238
|
processes=processes,
|
|
259
239
|
interactive=interactive,
|
|
260
240
|
timer=timer,
|
|
261
|
-
verbose=verbose,
|
|
262
241
|
colours=colours,
|
|
263
242
|
)
|
|
264
243
|
|
|
265
|
-
signal.signal(signal.SIGINT, process_group._handle_signal)
|
|
266
|
-
signal.signal(signal.SIGTERM, process_group._handle_signal)
|
|
267
|
-
|
|
268
244
|
return process_group
|
|
269
245
|
|
|
270
246
|
def complete_output(self, tail: int = 20, all: bool = False) -> str:
|
|
@@ -293,7 +269,6 @@ class ProcessGroup:
|
|
|
293
269
|
output += self._get_command_status(
|
|
294
270
|
process,
|
|
295
271
|
passed=process.return_code() == 0,
|
|
296
|
-
verbose=self.verbose,
|
|
297
272
|
timer=self.timer,
|
|
298
273
|
)
|
|
299
274
|
output += "\n"
|
|
@@ -301,7 +276,6 @@ class ProcessGroup:
|
|
|
301
276
|
output += self._get_command_status(
|
|
302
277
|
process,
|
|
303
278
|
icon=constants.ICONS[self.icon],
|
|
304
|
-
verbose=self.verbose,
|
|
305
279
|
timer=self.timer,
|
|
306
280
|
)
|
|
307
281
|
output += "\n"
|
|
@@ -334,93 +308,3 @@ class ProcessGroup:
|
|
|
334
308
|
output += f"\n{self.colours.red_bold}Abort!{self.colours.reset_colour}"
|
|
335
309
|
|
|
336
310
|
return output
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
@dataclass
|
|
340
|
-
class Process:
|
|
341
|
-
id: UUID = field(repr=False, compare=False)
|
|
342
|
-
name: str
|
|
343
|
-
args: list[str] = field(default_factory=list)
|
|
344
|
-
env: dict[str, str] = field(default_factory=dict)
|
|
345
|
-
start: float = 0.0
|
|
346
|
-
end: float = 0.0
|
|
347
|
-
_fd: BinaryIO | None = field(init=False, repr=False, compare=False, default=None)
|
|
348
|
-
_process: subprocess.Popen[bytes] | None = field(
|
|
349
|
-
init=False, repr=False, compare=False, default=None
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
def run(self) -> None:
|
|
353
|
-
self.start = time.perf_counter()
|
|
354
|
-
fd, fd_name = tempfile.mkstemp()
|
|
355
|
-
self._fd = open(fd_name, "rb")
|
|
356
|
-
self._process = subprocess.Popen(
|
|
357
|
-
[self.name, *self.args],
|
|
358
|
-
stdin=subprocess.DEVNULL,
|
|
359
|
-
stdout=fd,
|
|
360
|
-
stderr=subprocess.STDOUT,
|
|
361
|
-
env=self.env,
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
def __del__(self) -> None:
|
|
365
|
-
if self._fd:
|
|
366
|
-
self._fd.close()
|
|
367
|
-
|
|
368
|
-
def poll(self) -> int | None:
|
|
369
|
-
if self._process:
|
|
370
|
-
poll = self._process.poll()
|
|
371
|
-
if poll is not None and not self.end:
|
|
372
|
-
self.end = time.perf_counter()
|
|
373
|
-
return poll
|
|
374
|
-
return None
|
|
375
|
-
|
|
376
|
-
def read(self) -> bytes:
|
|
377
|
-
if self._fd:
|
|
378
|
-
return self._fd.read()
|
|
379
|
-
return b""
|
|
380
|
-
|
|
381
|
-
def readline(self) -> bytes:
|
|
382
|
-
if self._fd:
|
|
383
|
-
return self._fd.readline()
|
|
384
|
-
return b""
|
|
385
|
-
|
|
386
|
-
def return_code(self) -> int | None:
|
|
387
|
-
if self._process:
|
|
388
|
-
return self._process.returncode
|
|
389
|
-
return None
|
|
390
|
-
|
|
391
|
-
def interrupt(self) -> None:
|
|
392
|
-
if self._process:
|
|
393
|
-
self._process.send_signal(signal.SIGINT)
|
|
394
|
-
|
|
395
|
-
def kill(self) -> None:
|
|
396
|
-
if self._process:
|
|
397
|
-
self._process.send_signal(signal.SIGKILL)
|
|
398
|
-
|
|
399
|
-
def wait(self) -> int:
|
|
400
|
-
if self._process:
|
|
401
|
-
return self._process.wait()
|
|
402
|
-
return -1
|
|
403
|
-
|
|
404
|
-
@classmethod
|
|
405
|
-
def from_command(cls, command: str) -> Process:
|
|
406
|
-
env = os.environ.copy()
|
|
407
|
-
args = command.split()
|
|
408
|
-
|
|
409
|
-
parsed_args: list[str] = []
|
|
410
|
-
for arg in args:
|
|
411
|
-
if "=" in arg:
|
|
412
|
-
name, env_value = arg.split("=")
|
|
413
|
-
env[name] = env_value
|
|
414
|
-
else:
|
|
415
|
-
parsed_args.append(arg)
|
|
416
|
-
|
|
417
|
-
if not shutil.which(parsed_args[0]):
|
|
418
|
-
raise InvalidExecutableError(parsed_args[0])
|
|
419
|
-
|
|
420
|
-
str_args = shlex.split(" ".join(parsed_args[1:]))
|
|
421
|
-
return cls(
|
|
422
|
-
id=uuid4(),
|
|
423
|
-
name=parsed_args[0],
|
|
424
|
-
args=str_args,
|
|
425
|
-
env=env,
|
|
426
|
-
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pyallel.colours import Colours
|
|
8
|
+
from pyallel.process_group import ProcessGroup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ProcessGroupManager:
|
|
13
|
+
process_groups: list[ProcessGroup]
|
|
14
|
+
interactive: bool = False
|
|
15
|
+
colours: Colours = field(default_factory=Colours)
|
|
16
|
+
|
|
17
|
+
def stream(self) -> int:
|
|
18
|
+
exit_code = 0
|
|
19
|
+
|
|
20
|
+
if not self.interactive:
|
|
21
|
+
print(
|
|
22
|
+
f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.white_bold}Running commands...{self.colours.reset_colour}\n{self.colours.dim_on}=>{self.colours.dim_off} ",
|
|
23
|
+
flush=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
for process_group in self.process_groups:
|
|
27
|
+
exit_code = process_group.stream()
|
|
28
|
+
if exit_code > 0:
|
|
29
|
+
break
|
|
30
|
+
|
|
31
|
+
return exit_code
|
|
32
|
+
|
|
33
|
+
def handle_signal(self, signum: int, _frame: Any) -> None:
|
|
34
|
+
for process_group in self.process_groups:
|
|
35
|
+
process_group.handle_signal(signum)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_args(
|
|
39
|
+
cls,
|
|
40
|
+
*args: str,
|
|
41
|
+
colours: Colours | None = None,
|
|
42
|
+
interactive: bool = False,
|
|
43
|
+
timer: bool = False,
|
|
44
|
+
) -> ProcessGroupManager:
|
|
45
|
+
colours = colours or Colours()
|
|
46
|
+
last_separator_index = 0
|
|
47
|
+
commands: list[str] = []
|
|
48
|
+
process_groups: list[ProcessGroup] = []
|
|
49
|
+
|
|
50
|
+
for i, arg in enumerate(args):
|
|
51
|
+
if arg == ":::":
|
|
52
|
+
if i - 1 == 0:
|
|
53
|
+
process_groups.append(
|
|
54
|
+
ProcessGroup.from_commands(
|
|
55
|
+
args[0],
|
|
56
|
+
colours=colours,
|
|
57
|
+
interactive=interactive,
|
|
58
|
+
timer=timer,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
process_groups.append(
|
|
63
|
+
ProcessGroup.from_commands(
|
|
64
|
+
*commands[last_separator_index:],
|
|
65
|
+
colours=colours,
|
|
66
|
+
interactive=interactive,
|
|
67
|
+
timer=timer,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
last_separator_index = i
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
commands.append(arg)
|
|
75
|
+
|
|
76
|
+
if len(process_groups) > 1:
|
|
77
|
+
last_separator_index -= 1
|
|
78
|
+
|
|
79
|
+
process_groups.append(
|
|
80
|
+
ProcessGroup.from_commands(
|
|
81
|
+
*commands[last_separator_index:],
|
|
82
|
+
colours=colours,
|
|
83
|
+
interactive=interactive,
|
|
84
|
+
timer=timer,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
process_group_manager = cls(
|
|
89
|
+
process_groups=process_groups, interactive=interactive, colours=colours
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
signal.signal(signal.SIGINT, process_group_manager.handle_signal)
|
|
93
|
+
signal.signal(signal.SIGTERM, process_group_manager.handle_signal)
|
|
94
|
+
|
|
95
|
+
return process_group_manager
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|