pyallel 1.1.0__tar.gz → 1.2.1__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.1.0 → pyallel-1.2.1}/PKG-INFO +41 -6
- {pyallel-1.1.0 → pyallel-1.2.1}/README.md +40 -5
- {pyallel-1.1.0 → pyallel-1.2.1}/pyproject.toml +1 -1
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/constants.py +1 -1
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/main.py +5 -5
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/parser.py +10 -2
- pyallel-1.2.1/src/pyallel/process.py +72 -0
- pyallel-1.1.0/src/pyallel/process.py → pyallel-1.2.1/src/pyallel/process_group.py +14 -95
- pyallel-1.2.1/src/pyallel/process_group_manager.py +95 -0
- {pyallel-1.1.0 → pyallel-1.2.1}/LICENSE +0 -0
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/__init__.py +0 -0
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/colours.py +0 -0
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/errors.py +0 -0
- {pyallel-1.1.0 → pyallel-1.2.1}/src/pyallel/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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
|
|
@@ -48,10 +48,10 @@ usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
|
|
|
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
|
+
if you were running the command directly in a shell, some examples are below
|
|
55
55
|
|
|
56
56
|
"MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
|
|
57
57
|
"mypy | tee -a mypy.log" <- use pipes to redirect output
|
|
@@ -60,6 +60,14 @@ positional arguments:
|
|
|
60
60
|
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
61
61
|
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
62
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
|
|
63
71
|
|
|
64
72
|
options:
|
|
65
73
|
-h, --help show this help message and exit
|
|
@@ -90,26 +98,53 @@ You can also build an executable with the following (executables will be written
|
|
|
90
98
|
> The `arch=x86_64` values in the following code blocks can be replaced with `arch=aarch64` and
|
|
91
99
|
> any other architecture that is supported by docker to build an executable for that given architecture
|
|
92
100
|
|
|
101
|
+
> [!NOTE]
|
|
102
|
+
> To build aarch64 binaries on an x86_64 host machine, you will need to run the following
|
|
103
|
+
> commands to setup qemu to allow this to work
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
sudo apt-get install qemu binfmt-support qemu-user-static && \
|
|
107
|
+
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
|
108
|
+
```
|
|
109
|
+
|
|
93
110
|
#### Build for generic linux
|
|
94
111
|
|
|
95
112
|
```bash
|
|
96
|
-
docker build --tag pyallel --build-arg 'arch=x86_64'
|
|
113
|
+
docker build --tag pyallel --build-arg 'arch=x86_64' --build-arg "uid=$(id -u)" . && \
|
|
114
|
+
docker run -e 'arch=x86_64' --rm --volume "$(pwd):/src" pyallel
|
|
97
115
|
```
|
|
98
116
|
|
|
99
117
|
#### Build for alpine linux
|
|
100
118
|
|
|
101
119
|
```bash
|
|
102
|
-
docker build --tag pyallel-alpine --build-arg 'arch=x86_64' --file Dockerfile.alpine . &&
|
|
120
|
+
docker build --tag pyallel-alpine --build-arg 'arch=x86_64' --build-arg "uid=$(id -u)" --file Dockerfile.alpine . && \
|
|
121
|
+
docker run -e 'arch=x86_64' --rm --volume "$(pwd):/src" pyallel-alpine
|
|
103
122
|
```
|
|
104
123
|
|
|
105
124
|
#### Build locally
|
|
106
125
|
|
|
107
126
|
```bash
|
|
108
|
-
python -m venv .venv &&
|
|
127
|
+
python -m venv .venv && \
|
|
128
|
+
source .venv/bin/activate && \
|
|
129
|
+
pip install . -r requirements_build.txt && \
|
|
130
|
+
./build.sh
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Build all
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
./build_all.sh
|
|
109
137
|
```
|
|
110
138
|
|
|
111
139
|
## TODOs
|
|
112
140
|
|
|
141
|
+
- [x] Add support to have commands depend on other commands (some commands must complete
|
|
142
|
+
before a given command can start)
|
|
143
|
+
- [ ] Add a debug mode that logs debug information to a log file
|
|
144
|
+
- [ ] Fix wrapping of long commands in the command status line
|
|
145
|
+
- [ ] Fix wrapping of very long lines in command output that pushes other commands off the
|
|
146
|
+
screen
|
|
147
|
+
- [ ] Add support to state how many lines a command can use for it's output in interactive mode
|
|
113
148
|
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
114
149
|
(such as a REPL)
|
|
115
150
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -26,10 +26,10 @@ usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
|
|
|
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
|
+
if you were running the command directly in a shell, some examples are below
|
|
33
33
|
|
|
34
34
|
"MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
|
|
35
35
|
"mypy | tee -a mypy.log" <- use pipes to redirect output
|
|
@@ -38,6 +38,14 @@ positional arguments:
|
|
|
38
38
|
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
39
39
|
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
40
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
|
|
41
49
|
|
|
42
50
|
options:
|
|
43
51
|
-h, --help show this help message and exit
|
|
@@ -68,26 +76,53 @@ You can also build an executable with the following (executables will be written
|
|
|
68
76
|
> The `arch=x86_64` values in the following code blocks can be replaced with `arch=aarch64` and
|
|
69
77
|
> any other architecture that is supported by docker to build an executable for that given architecture
|
|
70
78
|
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> To build aarch64 binaries on an x86_64 host machine, you will need to run the following
|
|
81
|
+
> commands to setup qemu to allow this to work
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
sudo apt-get install qemu binfmt-support qemu-user-static && \
|
|
85
|
+
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
|
86
|
+
```
|
|
87
|
+
|
|
71
88
|
#### Build for generic linux
|
|
72
89
|
|
|
73
90
|
```bash
|
|
74
|
-
docker build --tag pyallel --build-arg 'arch=x86_64'
|
|
91
|
+
docker build --tag pyallel --build-arg 'arch=x86_64' --build-arg "uid=$(id -u)" . && \
|
|
92
|
+
docker run -e 'arch=x86_64' --rm --volume "$(pwd):/src" pyallel
|
|
75
93
|
```
|
|
76
94
|
|
|
77
95
|
#### Build for alpine linux
|
|
78
96
|
|
|
79
97
|
```bash
|
|
80
|
-
docker build --tag pyallel-alpine --build-arg 'arch=x86_64' --file Dockerfile.alpine . &&
|
|
98
|
+
docker build --tag pyallel-alpine --build-arg 'arch=x86_64' --build-arg "uid=$(id -u)" --file Dockerfile.alpine . && \
|
|
99
|
+
docker run -e 'arch=x86_64' --rm --volume "$(pwd):/src" pyallel-alpine
|
|
81
100
|
```
|
|
82
101
|
|
|
83
102
|
#### Build locally
|
|
84
103
|
|
|
85
104
|
```bash
|
|
86
|
-
python -m venv .venv &&
|
|
105
|
+
python -m venv .venv && \
|
|
106
|
+
source .venv/bin/activate && \
|
|
107
|
+
pip install . -r requirements_build.txt && \
|
|
108
|
+
./build.sh
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Build all
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
./build_all.sh
|
|
87
115
|
```
|
|
88
116
|
|
|
89
117
|
## TODOs
|
|
90
118
|
|
|
119
|
+
- [x] Add support to have commands depend on other commands (some commands must complete
|
|
120
|
+
before a given command can start)
|
|
121
|
+
- [ ] Add a debug mode that logs debug information to a log file
|
|
122
|
+
- [ ] Fix wrapping of long commands in the command status line
|
|
123
|
+
- [ ] Fix wrapping of very long lines in command output that pushes other commands off the
|
|
124
|
+
screen
|
|
125
|
+
- [ ] Add support to state how many lines a command can use for it's output in interactive mode
|
|
91
126
|
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
92
127
|
(such as a REPL)
|
|
93
128
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -8,23 +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
19
|
) -> int:
|
|
20
|
-
|
|
21
|
-
*
|
|
20
|
+
process_group_manager = ProcessGroupManager.from_args(
|
|
21
|
+
*args,
|
|
22
22
|
colours=colours,
|
|
23
23
|
interactive=interactive,
|
|
24
24
|
timer=timer,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
return
|
|
27
|
+
return process_group_manager.stream()
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def run(*args: str) -> int:
|
|
@@ -19,10 +19,10 @@ class Arguments:
|
|
|
19
19
|
return msg
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
COMMANDS_HELP = r"""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 ."
|
|
23
23
|
|
|
24
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
|
|
25
|
+
if you were running the command directly in a shell, some examples are below
|
|
26
26
|
|
|
27
27
|
"MYPY_FORCE_COLOR=1 mypy ." <- provide environment variables
|
|
28
28
|
"mypy | tee -a mypy.log" <- use pipes to redirect output
|
|
@@ -31,6 +31,14 @@ if you were running the command directly in a shell, some examples are below:
|
|
|
31
31
|
"echo \$SHELL" or "\$(echo mypy .)" <- expand variables and commands to evaluate (must be escaped)
|
|
32
32
|
"pytest . && mypy . || echo failed!" <- use AND (&&) and OR (||) to run commands conditionally
|
|
33
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
|
|
34
42
|
"""
|
|
35
43
|
|
|
36
44
|
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import signal
|
|
4
|
-
import subprocess
|
|
5
|
-
import tempfile
|
|
6
3
|
import time
|
|
7
4
|
from collections import defaultdict
|
|
8
5
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Any, BinaryIO
|
|
10
|
-
from uuid import UUID, uuid4
|
|
11
6
|
|
|
12
7
|
from pyallel import constants
|
|
13
8
|
from pyallel.colours import Colours
|
|
14
9
|
from pyallel.errors import InvalidExecutableError, InvalidExecutableErrors
|
|
10
|
+
from pyallel.process import Process
|
|
15
11
|
|
|
16
12
|
|
|
17
13
|
def get_num_lines(output: str, columns: int | None = None) -> int:
|
|
@@ -36,15 +32,18 @@ class ProcessGroup:
|
|
|
36
32
|
processes: list[Process]
|
|
37
33
|
interactive: bool = False
|
|
38
34
|
timer: bool = False
|
|
39
|
-
output: dict[
|
|
35
|
+
output: dict[int, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
40
36
|
process_lines: list[int] = field(default_factory=list)
|
|
41
|
-
completed_processes: set[
|
|
37
|
+
completed_processes: set[int] = field(default_factory=set)
|
|
42
38
|
exit_code: int = 0
|
|
43
39
|
interrupt_count: int = 0
|
|
44
40
|
passed: bool = True
|
|
45
41
|
icon: int = 0
|
|
46
42
|
colours: Colours = field(default_factory=Colours)
|
|
47
43
|
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
self.process_lines = [0 for _ in self.processes]
|
|
46
|
+
|
|
48
47
|
def stream(self) -> int:
|
|
49
48
|
for process in self.processes:
|
|
50
49
|
process.run()
|
|
@@ -83,11 +82,6 @@ class ProcessGroup:
|
|
|
83
82
|
running_process = None
|
|
84
83
|
interrupted = False
|
|
85
84
|
|
|
86
|
-
print(
|
|
87
|
-
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} ",
|
|
88
|
-
flush=True,
|
|
89
|
-
)
|
|
90
|
-
|
|
91
85
|
while True:
|
|
92
86
|
output = ""
|
|
93
87
|
for process in self.processes:
|
|
@@ -212,7 +206,7 @@ class ProcessGroup:
|
|
|
212
206
|
|
|
213
207
|
return output
|
|
214
208
|
|
|
215
|
-
def
|
|
209
|
+
def handle_signal(self, signum: int) -> None:
|
|
216
210
|
for process in self.processes:
|
|
217
211
|
if self.interrupt_count == 0:
|
|
218
212
|
process.interrupt()
|
|
@@ -226,16 +220,17 @@ class ProcessGroup:
|
|
|
226
220
|
def from_commands(
|
|
227
221
|
cls,
|
|
228
222
|
*commands: str,
|
|
229
|
-
colours: Colours,
|
|
223
|
+
colours: Colours | None = None,
|
|
230
224
|
interactive: bool = False,
|
|
231
225
|
timer: bool = False,
|
|
232
226
|
) -> ProcessGroup:
|
|
227
|
+
colours = colours or Colours()
|
|
233
228
|
processes: list[Process] = []
|
|
234
229
|
errors: list[InvalidExecutableError] = []
|
|
235
230
|
|
|
236
|
-
for command in commands:
|
|
231
|
+
for i, command in enumerate(commands):
|
|
237
232
|
try:
|
|
238
|
-
processes.append(Process
|
|
233
|
+
processes.append(Process(i + 1, command))
|
|
239
234
|
except InvalidExecutableError as e:
|
|
240
235
|
errors.append(e)
|
|
241
236
|
|
|
@@ -249,23 +244,15 @@ class ProcessGroup:
|
|
|
249
244
|
colours=colours,
|
|
250
245
|
)
|
|
251
246
|
|
|
252
|
-
signal.signal(signal.SIGINT, process_group._handle_signal)
|
|
253
|
-
signal.signal(signal.SIGTERM, process_group._handle_signal)
|
|
254
|
-
|
|
255
247
|
return process_group
|
|
256
248
|
|
|
257
|
-
def complete_output(self,
|
|
249
|
+
def complete_output(self, all: bool = False) -> str:
|
|
258
250
|
num_processes = len(self.processes)
|
|
259
251
|
lines = constants.LINES() - (2 * num_processes)
|
|
260
252
|
remainder = lines % num_processes
|
|
261
253
|
tail = lines // num_processes
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
self.process_lines = []
|
|
265
|
-
|
|
266
|
-
for process in self.processes:
|
|
267
|
-
self.process_lines.append(tail)
|
|
268
|
-
|
|
254
|
+
for i in range(num_processes):
|
|
255
|
+
self.process_lines[i] = tail
|
|
269
256
|
if remainder:
|
|
270
257
|
self.process_lines[-1] += remainder - 2
|
|
271
258
|
else:
|
|
@@ -319,71 +306,3 @@ class ProcessGroup:
|
|
|
319
306
|
output += f"\n{self.colours.red_bold}Abort!{self.colours.reset_colour}"
|
|
320
307
|
|
|
321
308
|
return output
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
@dataclass
|
|
325
|
-
class Process:
|
|
326
|
-
id: UUID = field(repr=False, compare=False)
|
|
327
|
-
command: str
|
|
328
|
-
start: float = 0.0
|
|
329
|
-
end: float = 0.0
|
|
330
|
-
_fd: BinaryIO | None = field(init=False, repr=False, compare=False, default=None)
|
|
331
|
-
_process: subprocess.Popen[bytes] | None = field(
|
|
332
|
-
init=False, repr=False, compare=False, default=None
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
def run(self) -> None:
|
|
336
|
-
self.start = time.perf_counter()
|
|
337
|
-
fd, fd_name = tempfile.mkstemp()
|
|
338
|
-
self._fd = open(fd_name, "rb")
|
|
339
|
-
self._process = subprocess.Popen(
|
|
340
|
-
self.command,
|
|
341
|
-
stdin=subprocess.DEVNULL,
|
|
342
|
-
stdout=fd,
|
|
343
|
-
stderr=subprocess.STDOUT,
|
|
344
|
-
shell=True,
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
def __del__(self) -> None:
|
|
348
|
-
if self._fd:
|
|
349
|
-
self._fd.close()
|
|
350
|
-
|
|
351
|
-
def poll(self) -> int | None:
|
|
352
|
-
if self._process:
|
|
353
|
-
poll = self._process.poll()
|
|
354
|
-
if poll is not None and not self.end:
|
|
355
|
-
self.end = time.perf_counter()
|
|
356
|
-
return poll
|
|
357
|
-
return None
|
|
358
|
-
|
|
359
|
-
def read(self) -> bytes:
|
|
360
|
-
if self._fd:
|
|
361
|
-
return self._fd.read()
|
|
362
|
-
return b""
|
|
363
|
-
|
|
364
|
-
def readline(self) -> bytes:
|
|
365
|
-
if self._fd:
|
|
366
|
-
return self._fd.readline()
|
|
367
|
-
return b""
|
|
368
|
-
|
|
369
|
-
def return_code(self) -> int | None:
|
|
370
|
-
if self._process:
|
|
371
|
-
return self._process.returncode
|
|
372
|
-
return None
|
|
373
|
-
|
|
374
|
-
def interrupt(self) -> None:
|
|
375
|
-
if self._process:
|
|
376
|
-
self._process.send_signal(signal.SIGINT)
|
|
377
|
-
|
|
378
|
-
def kill(self) -> None:
|
|
379
|
-
if self._process:
|
|
380
|
-
self._process.send_signal(signal.SIGKILL)
|
|
381
|
-
|
|
382
|
-
def wait(self) -> int:
|
|
383
|
-
if self._process:
|
|
384
|
-
return self._process.wait()
|
|
385
|
-
return -1
|
|
386
|
-
|
|
387
|
-
@classmethod
|
|
388
|
-
def from_command(cls, command: str) -> Process:
|
|
389
|
-
return cls(id=uuid4(), command=command)
|
|
@@ -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
|