pyallel 1.2.6__tar.gz → 1.2.8__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.2.6 → pyallel-1.2.8}/PKG-INFO +1 -1
- {pyallel-1.2.6 → pyallel-1.2.8}/pyproject.toml +1 -1
- pyallel-1.2.8/src/pyallel/main.py +137 -0
- pyallel-1.2.8/src/pyallel/printer.py +286 -0
- pyallel-1.2.8/src/pyallel/process.py +73 -0
- pyallel-1.2.8/src/pyallel/process_group.py +79 -0
- pyallel-1.2.8/src/pyallel/process_group_manager.py +145 -0
- pyallel-1.2.6/src/pyallel/main.py +0 -88
- pyallel-1.2.6/src/pyallel/process.py +0 -72
- pyallel-1.2.6/src/pyallel/process_group.py +0 -334
- pyallel-1.2.6/src/pyallel/process_group_manager.py +0 -95
- {pyallel-1.2.6 → pyallel-1.2.8}/LICENSE +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/README.md +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/src/pyallel/__init__.py +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/src/pyallel/colours.py +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/src/pyallel/constants.py +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/src/pyallel/errors.py +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/src/pyallel/parser.py +0 -0
- {pyallel-1.2.6 → pyallel-1.2.8}/src/pyallel/py.typed +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from pyallel import constants
|
|
9
|
+
from pyallel.colours import Colours
|
|
10
|
+
from pyallel.errors import InvalidExecutableErrors
|
|
11
|
+
from pyallel.parser import Arguments, create_parser
|
|
12
|
+
from pyallel.printer import Printer
|
|
13
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_interactive(
|
|
17
|
+
process_group_manager: ProcessGroupManager, printer: Printer
|
|
18
|
+
) -> int:
|
|
19
|
+
while True:
|
|
20
|
+
process_group_manager.stream()
|
|
21
|
+
|
|
22
|
+
printer.clear_printed_lines()
|
|
23
|
+
output = process_group_manager.get_cur_process_group_output()
|
|
24
|
+
printer.print_progress_group_output(
|
|
25
|
+
output, process_group_manager._interrupt_count
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
poll = process_group_manager.poll()
|
|
29
|
+
if poll is not None:
|
|
30
|
+
printer.clear_printed_lines()
|
|
31
|
+
printer.print_progress_group_output(
|
|
32
|
+
output, process_group_manager._interrupt_count, tail_output=False
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if poll > 0:
|
|
36
|
+
return poll
|
|
37
|
+
|
|
38
|
+
printer.clear()
|
|
39
|
+
process_group_manager.run()
|
|
40
|
+
if not process_group_manager.next():
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
time.sleep(0.1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_non_interactive(
|
|
47
|
+
process_group_manager: ProcessGroupManager, printer: Printer
|
|
48
|
+
) -> int:
|
|
49
|
+
current_process = None
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
outputs = process_group_manager.stream()
|
|
53
|
+
|
|
54
|
+
for pg in outputs.process_group_outputs.values():
|
|
55
|
+
for output in pg.processes:
|
|
56
|
+
if current_process is None:
|
|
57
|
+
current_process = output.process
|
|
58
|
+
output = process_group_manager.get_process(output.id)
|
|
59
|
+
printer.print_process_output(
|
|
60
|
+
output, include_progress=False, include_timer=False
|
|
61
|
+
)
|
|
62
|
+
elif current_process is not output.process:
|
|
63
|
+
continue
|
|
64
|
+
else:
|
|
65
|
+
printer.print_process_output(output, include_cmd=False)
|
|
66
|
+
|
|
67
|
+
if output.process.poll() is not None:
|
|
68
|
+
printer.print_process_output(output, include_output=False)
|
|
69
|
+
current_process = None
|
|
70
|
+
|
|
71
|
+
poll = process_group_manager.poll()
|
|
72
|
+
if poll is not None:
|
|
73
|
+
if poll > 0:
|
|
74
|
+
return poll
|
|
75
|
+
|
|
76
|
+
process_group_manager.run()
|
|
77
|
+
if not process_group_manager.next():
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
time.sleep(0.1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run(*args: str) -> int:
|
|
84
|
+
parser = create_parser()
|
|
85
|
+
parsed_args = parser.parse_args(args=args, namespace=Arguments())
|
|
86
|
+
|
|
87
|
+
if parsed_args.version:
|
|
88
|
+
my_version = importlib.metadata.version("pyallel")
|
|
89
|
+
print(my_version)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
if not parsed_args.commands:
|
|
93
|
+
parser.print_help()
|
|
94
|
+
return 2
|
|
95
|
+
|
|
96
|
+
colours = Colours.from_colour(parsed_args.colour)
|
|
97
|
+
printer = Printer(colours, timer=parsed_args.timer)
|
|
98
|
+
|
|
99
|
+
interactive = True
|
|
100
|
+
if not parsed_args.interactive:
|
|
101
|
+
interactive = False
|
|
102
|
+
elif not constants.IN_TTY:
|
|
103
|
+
interactive = False
|
|
104
|
+
|
|
105
|
+
message = None
|
|
106
|
+
try:
|
|
107
|
+
process_group_manager = ProcessGroupManager.from_args(*parsed_args.commands)
|
|
108
|
+
process_group_manager.run()
|
|
109
|
+
|
|
110
|
+
if interactive:
|
|
111
|
+
exit_code = run_interactive(process_group_manager, printer)
|
|
112
|
+
else:
|
|
113
|
+
exit_code = run_non_interactive(process_group_manager, printer)
|
|
114
|
+
except InvalidExecutableErrors as e:
|
|
115
|
+
exit_code = 1
|
|
116
|
+
message = str(e)
|
|
117
|
+
except Exception:
|
|
118
|
+
exit_code = 1
|
|
119
|
+
message = traceback.format_exc()
|
|
120
|
+
|
|
121
|
+
if exit_code == 1:
|
|
122
|
+
if not message:
|
|
123
|
+
printer.error("\nFailed!")
|
|
124
|
+
else:
|
|
125
|
+
printer.error(f"Error: {message}")
|
|
126
|
+
elif exit_code == 0:
|
|
127
|
+
printer.ok("\nDone!")
|
|
128
|
+
|
|
129
|
+
return exit_code
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def entry_point() -> None:
|
|
133
|
+
sys.exit(run(*sys.argv[1:]))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
entry_point()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from pyallel import constants
|
|
6
|
+
from pyallel.colours import Colours
|
|
7
|
+
from pyallel.process import ProcessOutput
|
|
8
|
+
from pyallel.process_group import ProcessGroupOutput
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Printer:
|
|
12
|
+
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
13
|
+
self._colours = colours or Colours()
|
|
14
|
+
self._timer = timer
|
|
15
|
+
self._prefix = f"{self._colours.dim_on}=>{self._colours.dim_off} "
|
|
16
|
+
self._icon = 0
|
|
17
|
+
self._printed: list[tuple[bool, str, str]] = []
|
|
18
|
+
|
|
19
|
+
def write(
|
|
20
|
+
self,
|
|
21
|
+
line: str,
|
|
22
|
+
include_prefix: bool = False,
|
|
23
|
+
end: str = "\n",
|
|
24
|
+
truncate: bool = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
truncate_num = 0
|
|
27
|
+
prefix = self._prefix if include_prefix else ""
|
|
28
|
+
if prefix:
|
|
29
|
+
truncate_num = 6
|
|
30
|
+
if truncate:
|
|
31
|
+
columns = constants.COLUMNS() - truncate_num
|
|
32
|
+
if get_num_lines(line, columns) > 1:
|
|
33
|
+
line = truncate_line(line, columns)
|
|
34
|
+
print(f"{prefix}{line}", end=end, flush=True)
|
|
35
|
+
|
|
36
|
+
def info(self, msg: str) -> None:
|
|
37
|
+
self.write(
|
|
38
|
+
f"{self._colours.white_bold}{msg}{self._colours.reset_colour}",
|
|
39
|
+
include_prefix=False,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def ok(self, msg: str) -> None:
|
|
43
|
+
self.write(
|
|
44
|
+
f"{self._colours.green_bold}{msg}{self._colours.reset_colour}",
|
|
45
|
+
include_prefix=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def warn(self, msg: str) -> None:
|
|
49
|
+
self.write(
|
|
50
|
+
f"{self._colours.yellow_bold}{msg}{self._colours.reset_colour}",
|
|
51
|
+
include_prefix=False,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def error(self, msg: str) -> None:
|
|
55
|
+
self.write(
|
|
56
|
+
f"{self._colours.red_bold}{msg}{self._colours.reset_colour}",
|
|
57
|
+
include_prefix=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def generate_process_output(
|
|
61
|
+
self,
|
|
62
|
+
output: ProcessOutput,
|
|
63
|
+
tail_output: bool = False,
|
|
64
|
+
include_cmd: bool = True,
|
|
65
|
+
include_output: bool = True,
|
|
66
|
+
include_progress: bool = True,
|
|
67
|
+
include_timer: bool | None = None,
|
|
68
|
+
append_newlines: bool = False,
|
|
69
|
+
) -> list[tuple[bool, str, str]]:
|
|
70
|
+
out: list[tuple[bool, str, str]] = []
|
|
71
|
+
line_parts: tuple[bool, str, str]
|
|
72
|
+
|
|
73
|
+
if include_cmd:
|
|
74
|
+
status = self.generate_process_output_status(
|
|
75
|
+
output, include_progress, include_timer
|
|
76
|
+
)
|
|
77
|
+
line_parts = (False, status, "\n")
|
|
78
|
+
out.append(line_parts)
|
|
79
|
+
self._printed.append(line_parts)
|
|
80
|
+
|
|
81
|
+
if include_output:
|
|
82
|
+
lines = output.data.splitlines(keepends=True)
|
|
83
|
+
|
|
84
|
+
if tail_output:
|
|
85
|
+
output_lines = output.lines - 1
|
|
86
|
+
lines = lines[-output_lines:]
|
|
87
|
+
|
|
88
|
+
for line in lines:
|
|
89
|
+
prefix = True
|
|
90
|
+
end = line[-1]
|
|
91
|
+
if append_newlines and end != "\n":
|
|
92
|
+
end = "\n"
|
|
93
|
+
else:
|
|
94
|
+
line = line[:-1]
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
prev_line = self._printed[-1]
|
|
98
|
+
except IndexError:
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
if prev_line[2] != "\n":
|
|
102
|
+
prefix = False
|
|
103
|
+
|
|
104
|
+
line_parts = (prefix, line, end)
|
|
105
|
+
out.append(line_parts)
|
|
106
|
+
self._printed.append(line_parts)
|
|
107
|
+
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
def generate_process_output_status(
|
|
111
|
+
self,
|
|
112
|
+
output: ProcessOutput,
|
|
113
|
+
include_progress: bool = True,
|
|
114
|
+
include_timer: bool | None = None,
|
|
115
|
+
) -> str:
|
|
116
|
+
include_timer = include_timer if include_timer is not None else self._timer
|
|
117
|
+
|
|
118
|
+
passed = None
|
|
119
|
+
icon = ""
|
|
120
|
+
poll = output.process.poll()
|
|
121
|
+
if include_progress:
|
|
122
|
+
icon = constants.ICONS[self._icon]
|
|
123
|
+
if poll is not None:
|
|
124
|
+
passed = poll == 0
|
|
125
|
+
|
|
126
|
+
if passed is True:
|
|
127
|
+
colour = self._colours.green_bold
|
|
128
|
+
msg = "done"
|
|
129
|
+
icon = constants.TICK
|
|
130
|
+
elif passed is False:
|
|
131
|
+
colour = self._colours.red_bold
|
|
132
|
+
msg = "failed"
|
|
133
|
+
icon = constants.X
|
|
134
|
+
else:
|
|
135
|
+
colour = self._colours.white_bold
|
|
136
|
+
msg = "running"
|
|
137
|
+
|
|
138
|
+
if not icon:
|
|
139
|
+
msg += "..."
|
|
140
|
+
|
|
141
|
+
timer = ""
|
|
142
|
+
if include_timer:
|
|
143
|
+
end = output.process.end
|
|
144
|
+
if not output.process.end:
|
|
145
|
+
end = time.perf_counter()
|
|
146
|
+
elapsed = end - output.process.start
|
|
147
|
+
timer = f"({format_time_taken(elapsed)})"
|
|
148
|
+
|
|
149
|
+
command = output.process.command
|
|
150
|
+
if get_num_lines(output.process.command) > 1:
|
|
151
|
+
columns = constants.COLUMNS() - (len(msg) + len(timer) + 9)
|
|
152
|
+
command = truncate_line(command, columns)
|
|
153
|
+
|
|
154
|
+
out = f"{self._colours.white_bold}[{self._colours.reset_colour}{self._colours.blue_bold}{command}{self._colours.reset_colour}{self._colours.white_bold}]{self._colours.reset_colour}{colour} {msg} {icon}{self._colours.reset_colour}"
|
|
155
|
+
|
|
156
|
+
if timer:
|
|
157
|
+
out += f" {self._colours.dim_on}{timer}{self._colours.dim_off}"
|
|
158
|
+
|
|
159
|
+
return out
|
|
160
|
+
|
|
161
|
+
def generate_process_group_output(
|
|
162
|
+
self,
|
|
163
|
+
output: ProcessGroupOutput,
|
|
164
|
+
interrupt_count: int = 0,
|
|
165
|
+
tail_output: bool = True,
|
|
166
|
+
) -> list[tuple[bool, str, str]]:
|
|
167
|
+
set_process_lines(output, interrupt_count)
|
|
168
|
+
|
|
169
|
+
for out in output.processes:
|
|
170
|
+
self.generate_process_output(out, tail_output, append_newlines=True)
|
|
171
|
+
|
|
172
|
+
if interrupt_count == 1:
|
|
173
|
+
self._printed.append((False, "", "\n"))
|
|
174
|
+
self._printed.append(
|
|
175
|
+
(
|
|
176
|
+
False,
|
|
177
|
+
f"{self._colours.yellow_bold}Interrupt!{self._colours.reset_colour}",
|
|
178
|
+
"\n",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
elif interrupt_count == 2:
|
|
182
|
+
self._printed.append((False, "", "\n"))
|
|
183
|
+
self._printed.append(
|
|
184
|
+
(
|
|
185
|
+
False,
|
|
186
|
+
f"{self._colours.red_bold}Abort!{self._colours.reset_colour}",
|
|
187
|
+
"\n",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self._icon += 1
|
|
192
|
+
if self._icon == len(constants.ICONS):
|
|
193
|
+
self._icon = 0
|
|
194
|
+
|
|
195
|
+
return self._printed
|
|
196
|
+
|
|
197
|
+
def print_process_output(
|
|
198
|
+
self,
|
|
199
|
+
output: ProcessOutput,
|
|
200
|
+
tail_output: bool = False,
|
|
201
|
+
include_cmd: bool = True,
|
|
202
|
+
include_output: bool = True,
|
|
203
|
+
include_progress: bool = True,
|
|
204
|
+
include_timer: bool | None = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
for include_prefix, line, end in self.generate_process_output(
|
|
207
|
+
output,
|
|
208
|
+
tail_output,
|
|
209
|
+
include_cmd,
|
|
210
|
+
include_output,
|
|
211
|
+
include_progress,
|
|
212
|
+
include_timer,
|
|
213
|
+
):
|
|
214
|
+
self.write(line, include_prefix, end)
|
|
215
|
+
|
|
216
|
+
def print_progress_group_output(
|
|
217
|
+
self,
|
|
218
|
+
output: ProcessGroupOutput,
|
|
219
|
+
interrupt_count: int = 0,
|
|
220
|
+
tail_output: bool = True,
|
|
221
|
+
) -> None:
|
|
222
|
+
for include_prefix, line, end in self.generate_process_group_output(
|
|
223
|
+
output, interrupt_count, tail_output
|
|
224
|
+
):
|
|
225
|
+
self.write(line, include_prefix, end, truncate=tail_output)
|
|
226
|
+
|
|
227
|
+
def clear_printed_lines(self) -> None:
|
|
228
|
+
# Clear all the lines that were just printed
|
|
229
|
+
for _, _, end in self._printed:
|
|
230
|
+
if end == "\n":
|
|
231
|
+
self.write(
|
|
232
|
+
f"{constants.CLEAR_LINE}{constants.UP_LINE}{constants.CLEAR_LINE}",
|
|
233
|
+
end="",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.clear()
|
|
237
|
+
|
|
238
|
+
def clear(self) -> None:
|
|
239
|
+
self._printed.clear()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def set_process_lines(
|
|
243
|
+
output: ProcessGroupOutput,
|
|
244
|
+
interrupt_count: int = 0,
|
|
245
|
+
lines: int | None = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
lines = lines or constants.LINES() - 1
|
|
248
|
+
if interrupt_count:
|
|
249
|
+
lines -= 2
|
|
250
|
+
|
|
251
|
+
num_processes = len(output.processes)
|
|
252
|
+
remainder = lines % num_processes
|
|
253
|
+
tail = lines // num_processes
|
|
254
|
+
|
|
255
|
+
for out in output.processes:
|
|
256
|
+
out.lines = tail
|
|
257
|
+
if remainder:
|
|
258
|
+
output.processes[-1].lines += remainder
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_num_lines(line: str, columns: int | None = None) -> int:
|
|
262
|
+
lines = 0
|
|
263
|
+
columns = columns or constants.COLUMNS()
|
|
264
|
+
line = constants.ANSI_ESCAPE.sub("", line)
|
|
265
|
+
length = len(line)
|
|
266
|
+
line_lines = 1
|
|
267
|
+
if length > columns:
|
|
268
|
+
line_lines = length // columns
|
|
269
|
+
remainder = length % columns
|
|
270
|
+
if remainder:
|
|
271
|
+
line_lines += 1
|
|
272
|
+
lines += 1 * line_lines
|
|
273
|
+
return lines
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def truncate_line(line: str, columns: int | None = None) -> str:
|
|
277
|
+
columns = columns or constants.COLUMNS()
|
|
278
|
+
escaped_line = constants.ANSI_ESCAPE.sub("", line)
|
|
279
|
+
return "".join(escaped_line[:columns]) + "..."
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def format_time_taken(time_taken: float) -> str:
|
|
283
|
+
time_taken = round(time_taken, 1)
|
|
284
|
+
seconds = time_taken % (24 * 3600)
|
|
285
|
+
|
|
286
|
+
return f"{seconds}s"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import time
|
|
7
|
+
from typing import BinaryIO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProcessOutput:
|
|
11
|
+
def __init__(self, id: int, process: Process, data: str = "") -> None:
|
|
12
|
+
self.id = id
|
|
13
|
+
self.data = data
|
|
14
|
+
self.process = process
|
|
15
|
+
self.lines = -1
|
|
16
|
+
|
|
17
|
+
def merge(self, other: ProcessOutput) -> None:
|
|
18
|
+
self.data += other.data
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Process:
|
|
22
|
+
def __init__(self, id: int, command: str) -> None:
|
|
23
|
+
self.id = id
|
|
24
|
+
self.command = command
|
|
25
|
+
self.start = 0.0
|
|
26
|
+
self.end = 0.0
|
|
27
|
+
self._fd: BinaryIO
|
|
28
|
+
self._process: subprocess.Popen[bytes]
|
|
29
|
+
|
|
30
|
+
def run(self) -> None:
|
|
31
|
+
self.start = time.perf_counter()
|
|
32
|
+
fd, fd_name = tempfile.mkstemp()
|
|
33
|
+
self._fd = open(fd_name, "rb")
|
|
34
|
+
self._process = subprocess.Popen(
|
|
35
|
+
self.command,
|
|
36
|
+
stdin=subprocess.DEVNULL,
|
|
37
|
+
stdout=fd,
|
|
38
|
+
stderr=subprocess.STDOUT,
|
|
39
|
+
shell=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def __del__(self) -> None:
|
|
43
|
+
try:
|
|
44
|
+
self._fd.close()
|
|
45
|
+
except AttributeError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def poll(self) -> int | None:
|
|
49
|
+
poll = self._process.poll()
|
|
50
|
+
if poll is not None and not self.end:
|
|
51
|
+
self.end = time.perf_counter()
|
|
52
|
+
return poll
|
|
53
|
+
|
|
54
|
+
def read(self) -> bytes:
|
|
55
|
+
return self._fd.read()
|
|
56
|
+
|
|
57
|
+
def readline(self) -> bytes:
|
|
58
|
+
return self._fd.readline()
|
|
59
|
+
|
|
60
|
+
def return_code(self) -> int | None:
|
|
61
|
+
return self._process.returncode
|
|
62
|
+
|
|
63
|
+
def interrupt(self) -> None:
|
|
64
|
+
if hasattr(self, "_process"):
|
|
65
|
+
self._process.send_signal(signal.SIGINT)
|
|
66
|
+
|
|
67
|
+
def kill(self) -> None:
|
|
68
|
+
if hasattr(self, "_process"):
|
|
69
|
+
self._process.send_signal(signal.SIGKILL)
|
|
70
|
+
|
|
71
|
+
def wait(self) -> int:
|
|
72
|
+
return self._process.wait()
|
|
73
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from pyallel.errors import InvalidExecutableError, InvalidExecutableErrors
|
|
6
|
+
from pyallel.process import Process, ProcessOutput
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProcessGroupOutput:
|
|
10
|
+
def __init__(self, id: int, processes: Sequence[ProcessOutput]) -> None:
|
|
11
|
+
self.id = id
|
|
12
|
+
self.processes = processes
|
|
13
|
+
|
|
14
|
+
def merge(self, other: ProcessGroupOutput) -> None:
|
|
15
|
+
for i, _ in enumerate(self.processes):
|
|
16
|
+
self.processes[i].merge(other.processes[i])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProcessGroup:
|
|
20
|
+
def __init__(self, id: int, processes: list[Process]) -> None:
|
|
21
|
+
self.id = id
|
|
22
|
+
self.processes = processes
|
|
23
|
+
self._exit_code: int = 0
|
|
24
|
+
self._interrupt_count: int = 0
|
|
25
|
+
|
|
26
|
+
def run(self) -> None:
|
|
27
|
+
for process in self.processes:
|
|
28
|
+
process.run()
|
|
29
|
+
|
|
30
|
+
def poll(self) -> int | None:
|
|
31
|
+
polls: list[int | None] = [process.poll() for process in self.processes]
|
|
32
|
+
|
|
33
|
+
running = [p for p in polls if p is None]
|
|
34
|
+
failed = [p for p in polls if p is not None and p > 0]
|
|
35
|
+
|
|
36
|
+
if running:
|
|
37
|
+
return None
|
|
38
|
+
elif failed:
|
|
39
|
+
return 1
|
|
40
|
+
else:
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
def stream(self) -> ProcessGroupOutput:
|
|
44
|
+
return ProcessGroupOutput(
|
|
45
|
+
id=self.id,
|
|
46
|
+
processes=[
|
|
47
|
+
ProcessOutput(
|
|
48
|
+
id=process.id, process=process, data=process.read().decode()
|
|
49
|
+
)
|
|
50
|
+
for process in self.processes
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def handle_signal(self, _signum: int) -> None:
|
|
55
|
+
for process in self.processes:
|
|
56
|
+
if self._interrupt_count == 0:
|
|
57
|
+
process.interrupt()
|
|
58
|
+
else:
|
|
59
|
+
process.kill()
|
|
60
|
+
|
|
61
|
+
self._interrupt_count += 1
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_commands(cls, id: int, process_id: int, *commands: str) -> ProcessGroup:
|
|
65
|
+
processes: list[Process] = []
|
|
66
|
+
errors: list[InvalidExecutableError] = []
|
|
67
|
+
|
|
68
|
+
for i, command in enumerate(commands):
|
|
69
|
+
try:
|
|
70
|
+
processes.append(Process(i + process_id, command))
|
|
71
|
+
except InvalidExecutableError as e:
|
|
72
|
+
errors.append(e)
|
|
73
|
+
|
|
74
|
+
if errors:
|
|
75
|
+
raise InvalidExecutableErrors(*errors)
|
|
76
|
+
|
|
77
|
+
process_group = cls(id=id, processes=processes)
|
|
78
|
+
|
|
79
|
+
return process_group
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyallel.process import ProcessOutput
|
|
7
|
+
from pyallel.process_group import ProcessGroupOutput, ProcessGroup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProcessGroupManagerOutput:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
process_group_outputs: dict[int, ProcessGroupOutput] | None = None,
|
|
14
|
+
cur_process_group_id: int = 1,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.process_group_outputs = process_group_outputs or {}
|
|
17
|
+
self.cur_process_group_id = cur_process_group_id
|
|
18
|
+
|
|
19
|
+
def merge(self, other: ProcessGroupManagerOutput) -> None:
|
|
20
|
+
self.cur_process_group_id = other.cur_process_group_id
|
|
21
|
+
for key, value in other.process_group_outputs.items():
|
|
22
|
+
if key in self.process_group_outputs:
|
|
23
|
+
self.process_group_outputs[key].merge(value)
|
|
24
|
+
else:
|
|
25
|
+
self.process_group_outputs[key] = value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProcessGroupManager:
|
|
29
|
+
def __init__(self, process_groups: list[ProcessGroup]) -> None:
|
|
30
|
+
self._exit_code = 0
|
|
31
|
+
self._interrupt_count = 0
|
|
32
|
+
self._cur_process_group: ProcessGroup | None = None
|
|
33
|
+
self._process_groups = process_groups
|
|
34
|
+
self._output = ProcessGroupManagerOutput(
|
|
35
|
+
process_group_outputs={
|
|
36
|
+
pg.id: ProcessGroupOutput(
|
|
37
|
+
id=pg.id,
|
|
38
|
+
processes=[ProcessOutput(id=p.id, process=p) for p in pg.processes],
|
|
39
|
+
)
|
|
40
|
+
for pg in self._process_groups
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def run(self) -> None:
|
|
45
|
+
if self._process_groups:
|
|
46
|
+
self._cur_process_group = self._process_groups.pop(0)
|
|
47
|
+
self._cur_process_group.run()
|
|
48
|
+
else:
|
|
49
|
+
self._cur_process_group = None
|
|
50
|
+
|
|
51
|
+
def next(self) -> bool:
|
|
52
|
+
return True if self._cur_process_group or self._process_groups else False
|
|
53
|
+
|
|
54
|
+
def stream(self) -> ProcessGroupManagerOutput:
|
|
55
|
+
if self._cur_process_group is None:
|
|
56
|
+
return ProcessGroupManagerOutput()
|
|
57
|
+
|
|
58
|
+
output = ProcessGroupManagerOutput(
|
|
59
|
+
cur_process_group_id=self._cur_process_group.id,
|
|
60
|
+
process_group_outputs={
|
|
61
|
+
self._cur_process_group.id: self._cur_process_group.stream()
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self._output.merge(output)
|
|
66
|
+
|
|
67
|
+
return output
|
|
68
|
+
|
|
69
|
+
def get_cur_process_group_output(self) -> ProcessGroupOutput:
|
|
70
|
+
if self._cur_process_group:
|
|
71
|
+
return self._output.process_group_outputs[self._cur_process_group.id]
|
|
72
|
+
|
|
73
|
+
raise KeyError("no current process group output")
|
|
74
|
+
|
|
75
|
+
def get_process(self, id: int) -> ProcessOutput:
|
|
76
|
+
for pg in self._output.process_group_outputs.values():
|
|
77
|
+
for process in pg.processes:
|
|
78
|
+
if process.id == id:
|
|
79
|
+
return process
|
|
80
|
+
|
|
81
|
+
raise KeyError(f"process with id '{id}' not found")
|
|
82
|
+
|
|
83
|
+
def poll(self) -> int | None:
|
|
84
|
+
if self._cur_process_group is None:
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
poll = self._cur_process_group.poll()
|
|
88
|
+
|
|
89
|
+
if poll is not None and self._exit_code:
|
|
90
|
+
return self._exit_code
|
|
91
|
+
|
|
92
|
+
if self._interrupt_count > 1:
|
|
93
|
+
return self._exit_code
|
|
94
|
+
|
|
95
|
+
return poll
|
|
96
|
+
|
|
97
|
+
def handle_signal(self, signum: int, _frame: Any) -> None:
|
|
98
|
+
for process_group in self._process_groups:
|
|
99
|
+
process_group.handle_signal(signum)
|
|
100
|
+
|
|
101
|
+
self._exit_code = 128 + signum
|
|
102
|
+
self._interrupt_count += 1
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_args(cls, *args: str) -> ProcessGroupManager:
|
|
106
|
+
last_separator_index = 0
|
|
107
|
+
commands: list[str] = []
|
|
108
|
+
process_groups: list[ProcessGroup] = []
|
|
109
|
+
progress_group_id = 1
|
|
110
|
+
process_id = 1
|
|
111
|
+
|
|
112
|
+
for i, arg in enumerate(args):
|
|
113
|
+
if arg == ":::":
|
|
114
|
+
if i - 1 == 0:
|
|
115
|
+
pg = ProcessGroup.from_commands(
|
|
116
|
+
progress_group_id, process_id, args[0]
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
pg = ProcessGroup.from_commands(
|
|
120
|
+
progress_group_id, process_id, *commands[last_separator_index:]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
process_groups.append(pg)
|
|
124
|
+
process_id += len(pg.processes)
|
|
125
|
+
last_separator_index = i
|
|
126
|
+
progress_group_id += 1
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
commands.append(arg)
|
|
130
|
+
|
|
131
|
+
if len(process_groups) > 1:
|
|
132
|
+
last_separator_index -= 1
|
|
133
|
+
|
|
134
|
+
process_groups.append(
|
|
135
|
+
ProcessGroup.from_commands(
|
|
136
|
+
progress_group_id, process_id, *commands[last_separator_index:]
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
process_group_manager = cls(process_groups=process_groups)
|
|
141
|
+
|
|
142
|
+
signal.signal(signal.SIGINT, process_group_manager.handle_signal)
|
|
143
|
+
signal.signal(signal.SIGTERM, process_group_manager.handle_signal)
|
|
144
|
+
|
|
145
|
+
return process_group_manager
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib.metadata
|
|
4
|
-
import sys
|
|
5
|
-
import traceback
|
|
6
|
-
|
|
7
|
-
from pyallel import constants
|
|
8
|
-
from pyallel.colours import Colours
|
|
9
|
-
from pyallel.errors import InvalidExecutableErrors
|
|
10
|
-
from pyallel.parser import Arguments, create_parser
|
|
11
|
-
from pyallel.process_group_manager import ProcessGroupManager
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def main_loop(
|
|
15
|
-
*args: str,
|
|
16
|
-
colours: Colours,
|
|
17
|
-
interactive: bool = False,
|
|
18
|
-
timer: bool = False,
|
|
19
|
-
) -> int:
|
|
20
|
-
process_group_manager = ProcessGroupManager.from_args(
|
|
21
|
-
*args,
|
|
22
|
-
colours=colours,
|
|
23
|
-
interactive=interactive,
|
|
24
|
-
timer=timer,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
return process_group_manager.stream()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def run(*args: str) -> int:
|
|
31
|
-
parser = create_parser()
|
|
32
|
-
parsed_args = parser.parse_args(args=args, namespace=Arguments())
|
|
33
|
-
|
|
34
|
-
if parsed_args.version:
|
|
35
|
-
my_version = importlib.metadata.version("pyallel")
|
|
36
|
-
print(my_version)
|
|
37
|
-
return 0
|
|
38
|
-
|
|
39
|
-
if not parsed_args.commands:
|
|
40
|
-
parser.print_help()
|
|
41
|
-
return 2
|
|
42
|
-
|
|
43
|
-
colours = Colours.from_colour(parsed_args.colour)
|
|
44
|
-
|
|
45
|
-
interactive = True
|
|
46
|
-
if not parsed_args.interactive:
|
|
47
|
-
interactive = False
|
|
48
|
-
elif not constants.IN_TTY:
|
|
49
|
-
interactive = False
|
|
50
|
-
|
|
51
|
-
message = None
|
|
52
|
-
try:
|
|
53
|
-
exit_code = main_loop(
|
|
54
|
-
*parsed_args.commands,
|
|
55
|
-
colours=colours,
|
|
56
|
-
interactive=interactive,
|
|
57
|
-
timer=parsed_args.timer,
|
|
58
|
-
)
|
|
59
|
-
except InvalidExecutableErrors as e:
|
|
60
|
-
exit_code = 1
|
|
61
|
-
message = str(e)
|
|
62
|
-
except Exception:
|
|
63
|
-
exit_code = 1
|
|
64
|
-
message = traceback.format_exc()
|
|
65
|
-
|
|
66
|
-
if exit_code == 1:
|
|
67
|
-
if not message:
|
|
68
|
-
print(
|
|
69
|
-
f"{colours.dim_on}=>{colours.dim_off} {colours.red_bold}Failed!{colours.reset_colour}"
|
|
70
|
-
)
|
|
71
|
-
else:
|
|
72
|
-
print(
|
|
73
|
-
f"{colours.dim_on}=>{colours.dim_off} {colours.red_bold}Error: {message}{colours.reset_colour}"
|
|
74
|
-
)
|
|
75
|
-
elif exit_code == 0:
|
|
76
|
-
print(
|
|
77
|
-
f"{colours.dim_on}=>{colours.dim_off} {colours.green_bold}Done!{colours.reset_colour}"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
return exit_code
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def entry_point() -> None:
|
|
84
|
-
sys.exit(run(*sys.argv[1:]))
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if __name__ == "__main__":
|
|
88
|
-
entry_point()
|
|
@@ -1,72 +0,0 @@
|
|
|
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,334 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from collections import defaultdict
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
|
|
7
|
-
from pyallel import constants
|
|
8
|
-
from pyallel.colours import Colours
|
|
9
|
-
from pyallel.errors import InvalidExecutableError, InvalidExecutableErrors
|
|
10
|
-
from pyallel.process import Process
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_num_lines(output: str, columns: int | None = None) -> int:
|
|
14
|
-
lines = 0
|
|
15
|
-
columns = columns or constants.COLUMNS()
|
|
16
|
-
for line in output.splitlines():
|
|
17
|
-
line = constants.ANSI_ESCAPE.sub("", line)
|
|
18
|
-
length = len(line)
|
|
19
|
-
line_lines = 1
|
|
20
|
-
if length > columns:
|
|
21
|
-
line_lines = length // columns
|
|
22
|
-
remainder = length % columns
|
|
23
|
-
if remainder:
|
|
24
|
-
line_lines += 1
|
|
25
|
-
lines += 1 * line_lines
|
|
26
|
-
return lines
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def format_time_taken(time_taken: float) -> str:
|
|
30
|
-
time_taken = round(time_taken, 1)
|
|
31
|
-
seconds = time_taken % (24 * 3600)
|
|
32
|
-
|
|
33
|
-
return f"{seconds}s"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class ProcessGroup:
|
|
38
|
-
processes: list[Process]
|
|
39
|
-
interactive: bool = False
|
|
40
|
-
timer: bool = False
|
|
41
|
-
output: dict[int, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
42
|
-
process_lines: list[int] = field(default_factory=list)
|
|
43
|
-
completed_processes: set[int] = field(default_factory=set)
|
|
44
|
-
exit_code: int = 0
|
|
45
|
-
interrupt_count: int = 0
|
|
46
|
-
passed: bool = True
|
|
47
|
-
icon: int = 0
|
|
48
|
-
colours: Colours = field(default_factory=Colours)
|
|
49
|
-
|
|
50
|
-
def __post_init__(self) -> None:
|
|
51
|
-
self.process_lines = [0 for _ in self.processes]
|
|
52
|
-
|
|
53
|
-
def stream(self) -> int:
|
|
54
|
-
for process in self.processes:
|
|
55
|
-
process.run()
|
|
56
|
-
|
|
57
|
-
if not self.interactive:
|
|
58
|
-
return self.stream_non_interactive()
|
|
59
|
-
|
|
60
|
-
while True:
|
|
61
|
-
output = self.complete_output()
|
|
62
|
-
self.icon += 1
|
|
63
|
-
if self.icon == len(constants.ICONS):
|
|
64
|
-
self.icon = 0
|
|
65
|
-
|
|
66
|
-
print(output, end="", flush=True)
|
|
67
|
-
|
|
68
|
-
# Clear all the lines that were just printed
|
|
69
|
-
for _ in range(get_num_lines(output) - (1 if self.exit_code > 1 else 0)):
|
|
70
|
-
print(
|
|
71
|
-
f"{constants.CLEAR_LINE}{constants.UP_LINE}{constants.CLEAR_LINE}",
|
|
72
|
-
end="",
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
if len(self.completed_processes) == len(self.processes):
|
|
76
|
-
break
|
|
77
|
-
|
|
78
|
-
time.sleep(0.1)
|
|
79
|
-
|
|
80
|
-
print(self.complete_output(all=True), flush=True)
|
|
81
|
-
|
|
82
|
-
if not self.exit_code and not self.passed:
|
|
83
|
-
self.exit_code = 1
|
|
84
|
-
|
|
85
|
-
return self.exit_code
|
|
86
|
-
|
|
87
|
-
def stream_non_interactive(self) -> int:
|
|
88
|
-
running_process = None
|
|
89
|
-
interrupted = False
|
|
90
|
-
|
|
91
|
-
while True:
|
|
92
|
-
output = ""
|
|
93
|
-
for process in self.processes:
|
|
94
|
-
if (
|
|
95
|
-
running_process is None
|
|
96
|
-
and process.id not in self.completed_processes
|
|
97
|
-
):
|
|
98
|
-
output += self._get_command_status(process)
|
|
99
|
-
output += "\n"
|
|
100
|
-
running_process = process
|
|
101
|
-
elif running_process is not process:
|
|
102
|
-
# Need to do this to properly keep track of how long all the other
|
|
103
|
-
# commands are taking
|
|
104
|
-
process.poll()
|
|
105
|
-
continue
|
|
106
|
-
|
|
107
|
-
process_output = process.readline().decode()
|
|
108
|
-
|
|
109
|
-
if not self.output[process.id] and process_output:
|
|
110
|
-
process_output = self._prefix(process_output)
|
|
111
|
-
self.output[process.id].append(process_output)
|
|
112
|
-
output += process_output
|
|
113
|
-
elif process_output:
|
|
114
|
-
if self.output[process.id][-1][-1] != "\n":
|
|
115
|
-
self.output[process.id][-1] += process_output
|
|
116
|
-
else:
|
|
117
|
-
process_output = self._prefix(process_output)
|
|
118
|
-
self.output[process.id].append(process_output)
|
|
119
|
-
output += process_output
|
|
120
|
-
|
|
121
|
-
if process.poll() is not None:
|
|
122
|
-
if process.return_code() != 0:
|
|
123
|
-
self.passed = False
|
|
124
|
-
process_output = process.read().decode()
|
|
125
|
-
if process_output:
|
|
126
|
-
output += self._prefix(process_output)
|
|
127
|
-
|
|
128
|
-
if (output and output[-1] != "\n") or (
|
|
129
|
-
self.output[process.id]
|
|
130
|
-
and self.output[process.id][-1][-1] != "\n"
|
|
131
|
-
):
|
|
132
|
-
output += "\n"
|
|
133
|
-
|
|
134
|
-
output += self._get_command_status(
|
|
135
|
-
process,
|
|
136
|
-
passed=process.return_code() == 0,
|
|
137
|
-
timer=self.timer,
|
|
138
|
-
)
|
|
139
|
-
output += f"\n{self.colours.dim_on}=>{self.colours.dim_off} \n"
|
|
140
|
-
self.completed_processes.add(process.id)
|
|
141
|
-
running_process = None
|
|
142
|
-
|
|
143
|
-
if self.interrupt_count == 0:
|
|
144
|
-
pass
|
|
145
|
-
elif not interrupted and self.interrupt_count == 1:
|
|
146
|
-
if (output and output[-1] != "\n") or (
|
|
147
|
-
self.output[process.id]
|
|
148
|
-
and self.output[process.id][-1][-1] != "\n"
|
|
149
|
-
):
|
|
150
|
-
output += "\n"
|
|
151
|
-
output += f"{self.colours.dim_on}=>{self.colours.dim_off} \n{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.yellow_bold}Interrupt!{self.colours.reset_colour}\n{self.colours.dim_on}=>{self.colours.dim_off} \n"
|
|
152
|
-
interrupted = True
|
|
153
|
-
|
|
154
|
-
if output:
|
|
155
|
-
print(output, end="", flush=True)
|
|
156
|
-
|
|
157
|
-
if len(self.completed_processes) == len(self.processes):
|
|
158
|
-
break
|
|
159
|
-
|
|
160
|
-
time.sleep(0.01)
|
|
161
|
-
|
|
162
|
-
if self.interrupt_count == 2:
|
|
163
|
-
print(
|
|
164
|
-
f"{self.colours.dim_on}=>{self.colours.dim_off} {self.colours.red_bold}Abort!{self.colours.reset_colour}",
|
|
165
|
-
flush=True,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
if not self.exit_code and not self.passed:
|
|
169
|
-
self.exit_code = 1
|
|
170
|
-
|
|
171
|
-
return self.exit_code
|
|
172
|
-
|
|
173
|
-
def _prefix(self, output: str, keepend: bool = True) -> str:
|
|
174
|
-
prefixed_output = "\n".join(
|
|
175
|
-
f"{self.colours.dim_on}=>{self.colours.dim_off} {line}{self.colours.reset_colour}"
|
|
176
|
-
for line in output.splitlines()
|
|
177
|
-
)
|
|
178
|
-
if keepend and output and output[-1] == "\n":
|
|
179
|
-
prefixed_output += "\n"
|
|
180
|
-
return prefixed_output
|
|
181
|
-
|
|
182
|
-
def _get_command_status(
|
|
183
|
-
self,
|
|
184
|
-
process: Process,
|
|
185
|
-
icon: str | None = None,
|
|
186
|
-
passed: bool | None = None,
|
|
187
|
-
timer: bool = False,
|
|
188
|
-
) -> str:
|
|
189
|
-
if passed is True:
|
|
190
|
-
colour = self.colours.green_bold
|
|
191
|
-
msg = "done"
|
|
192
|
-
icon = icon or constants.TICK
|
|
193
|
-
elif passed is False:
|
|
194
|
-
colour = self.colours.red_bold
|
|
195
|
-
msg = "failed"
|
|
196
|
-
icon = icon or constants.X
|
|
197
|
-
else:
|
|
198
|
-
colour = self.colours.white_bold
|
|
199
|
-
msg = "running"
|
|
200
|
-
icon = icon or ""
|
|
201
|
-
if not icon:
|
|
202
|
-
msg += "..."
|
|
203
|
-
|
|
204
|
-
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}"
|
|
205
|
-
|
|
206
|
-
if timer:
|
|
207
|
-
end = process.end
|
|
208
|
-
if not process.end:
|
|
209
|
-
end = time.perf_counter()
|
|
210
|
-
elapsed = end - process.start
|
|
211
|
-
output += f" {self.colours.dim_on}({format_time_taken(elapsed)}){self.colours.dim_off}"
|
|
212
|
-
|
|
213
|
-
return output
|
|
214
|
-
|
|
215
|
-
def handle_signal(self, signum: int) -> None:
|
|
216
|
-
for process in self.processes:
|
|
217
|
-
if self.interrupt_count == 0:
|
|
218
|
-
process.interrupt()
|
|
219
|
-
else:
|
|
220
|
-
process.kill()
|
|
221
|
-
|
|
222
|
-
self.exit_code = 128 + signum
|
|
223
|
-
self.interrupt_count += 1
|
|
224
|
-
|
|
225
|
-
@classmethod
|
|
226
|
-
def from_commands(
|
|
227
|
-
cls,
|
|
228
|
-
*commands: str,
|
|
229
|
-
colours: Colours | None = None,
|
|
230
|
-
interactive: bool = False,
|
|
231
|
-
timer: bool = False,
|
|
232
|
-
) -> ProcessGroup:
|
|
233
|
-
colours = colours or Colours()
|
|
234
|
-
processes: list[Process] = []
|
|
235
|
-
errors: list[InvalidExecutableError] = []
|
|
236
|
-
|
|
237
|
-
for i, command in enumerate(commands):
|
|
238
|
-
try:
|
|
239
|
-
processes.append(Process(i + 1, command))
|
|
240
|
-
except InvalidExecutableError as e:
|
|
241
|
-
errors.append(e)
|
|
242
|
-
|
|
243
|
-
if errors:
|
|
244
|
-
raise InvalidExecutableErrors(*errors)
|
|
245
|
-
|
|
246
|
-
process_group = cls(
|
|
247
|
-
processes=processes,
|
|
248
|
-
interactive=interactive,
|
|
249
|
-
timer=timer,
|
|
250
|
-
colours=colours,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
return process_group
|
|
254
|
-
|
|
255
|
-
def complete_output(self, all: bool = False) -> str:
|
|
256
|
-
num_processes = len(self.processes)
|
|
257
|
-
lines = constants.LINES() - (2 * num_processes)
|
|
258
|
-
remainder = lines % num_processes
|
|
259
|
-
tail = lines // num_processes
|
|
260
|
-
for i in range(num_processes):
|
|
261
|
-
self.process_lines[i] = tail
|
|
262
|
-
if remainder:
|
|
263
|
-
self.process_lines[-1] += remainder - 2
|
|
264
|
-
else:
|
|
265
|
-
self.process_lines[-1] -= 2
|
|
266
|
-
|
|
267
|
-
output = ""
|
|
268
|
-
for i, process in enumerate(self.processes, start=1):
|
|
269
|
-
process_output = ""
|
|
270
|
-
if process.poll() is not None:
|
|
271
|
-
self.completed_processes.add(process.id)
|
|
272
|
-
if process.return_code() != 0:
|
|
273
|
-
self.passed = False
|
|
274
|
-
process_output += self._get_command_status(
|
|
275
|
-
process,
|
|
276
|
-
passed=process.return_code() == 0,
|
|
277
|
-
timer=self.timer,
|
|
278
|
-
)
|
|
279
|
-
process_output += "\n"
|
|
280
|
-
else:
|
|
281
|
-
process_output += self._get_command_status(
|
|
282
|
-
process,
|
|
283
|
-
icon=constants.ICONS[self.icon],
|
|
284
|
-
timer=self.timer,
|
|
285
|
-
)
|
|
286
|
-
process_output += "\n"
|
|
287
|
-
|
|
288
|
-
command_lines = get_num_lines(process_output)
|
|
289
|
-
p_output = process.read().decode()
|
|
290
|
-
if not self.output[process.id]:
|
|
291
|
-
self.output[process.id].append("")
|
|
292
|
-
self.output[process.id][0] += p_output
|
|
293
|
-
p_output = self.output[process.id][0]
|
|
294
|
-
p_output_lines_num = 0
|
|
295
|
-
if p_output:
|
|
296
|
-
if not all:
|
|
297
|
-
p_output_lines = p_output.splitlines()[-self.process_lines[i - 1] :]
|
|
298
|
-
p_output = ""
|
|
299
|
-
for line in p_output_lines:
|
|
300
|
-
if len(line) + 3 > constants.COLUMNS():
|
|
301
|
-
p_output += f"{''.join(line[:constants.COLUMNS()-3])}\n"
|
|
302
|
-
else:
|
|
303
|
-
p_output += line + "\n"
|
|
304
|
-
p_output = self._prefix(p_output)
|
|
305
|
-
if p_output and p_output[-1] != "\n":
|
|
306
|
-
p_output += "\n"
|
|
307
|
-
if i != num_processes:
|
|
308
|
-
p_output += "\n"
|
|
309
|
-
p_output_lines_num = get_num_lines(p_output)
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
not all
|
|
313
|
-
and (command_lines + p_output_lines_num) > self.process_lines[i - 1]
|
|
314
|
-
):
|
|
315
|
-
truncate = (command_lines + p_output_lines_num) - self.process_lines[
|
|
316
|
-
i - 1
|
|
317
|
-
]
|
|
318
|
-
p_output = "\n".join(p_output.splitlines()[truncate:])
|
|
319
|
-
p_output += "\n"
|
|
320
|
-
|
|
321
|
-
process_output += p_output
|
|
322
|
-
output += process_output
|
|
323
|
-
|
|
324
|
-
if self.interrupt_count == 0:
|
|
325
|
-
return output
|
|
326
|
-
|
|
327
|
-
if self.interrupt_count == 1:
|
|
328
|
-
output += (
|
|
329
|
-
f"\n{self.colours.yellow_bold}Interrupt!{self.colours.reset_colour}"
|
|
330
|
-
)
|
|
331
|
-
elif self.interrupt_count == 2:
|
|
332
|
-
output += f"\n{self.colours.red_bold}Abort!{self.colours.reset_colour}"
|
|
333
|
-
|
|
334
|
-
return output
|
|
@@ -1,95 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|