pyallel 1.3.4__tar.gz → 1.3.5__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.3.4 → pyallel-1.3.5}/PKG-INFO +1 -1
- {pyallel-1.3.4 → pyallel-1.3.5}/pyproject.toml +1 -1
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/colours.py +0 -5
- pyallel-1.3.5/src/pyallel/main.py +87 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/printer.py +175 -148
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/process_group_manager.py +14 -4
- pyallel-1.3.4/src/pyallel/main.py +0 -137
- {pyallel-1.3.4 → pyallel-1.3.5}/LICENSE +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/README.md +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/__init__.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/constants.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/errors.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/parser.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/process.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/process_group.py +0 -0
- {pyallel-1.3.4 → pyallel-1.3.5}/src/pyallel/py.typed +0 -0
|
@@ -13,11 +13,6 @@ class Colours:
|
|
|
13
13
|
blue_bold: str = "\033[1;34m"
|
|
14
14
|
red_bold: str = "\033[1;31m"
|
|
15
15
|
yellow_bold: str = "\033[1;33m"
|
|
16
|
-
clear_line: str = "\033[2K"
|
|
17
|
-
clear_screen: str = "\033[2J"
|
|
18
|
-
save_cursor: str = "\033[s"
|
|
19
|
-
restore_cursor: str = "\033[u"
|
|
20
|
-
up_line: str = "\033[1F"
|
|
21
16
|
reset_colour: str = "\033[0m"
|
|
22
17
|
dim_on: str = "\033[2m"
|
|
23
18
|
dim_off: str = "\033[22m"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
from pyallel import constants
|
|
9
|
+
from pyallel.colours import Colours
|
|
10
|
+
from pyallel.errors import InvalidLinesModifierError
|
|
11
|
+
from pyallel.parser import Arguments, create_parser
|
|
12
|
+
from pyallel.printer import (
|
|
13
|
+
InteractiveConsolePrinter,
|
|
14
|
+
NonInteractiveConsolePrinter,
|
|
15
|
+
Printer,
|
|
16
|
+
)
|
|
17
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def entry_point(*args: str) -> int:
|
|
21
|
+
args = args or tuple(sys.argv[1:])
|
|
22
|
+
parser = create_parser()
|
|
23
|
+
parsed_args = parser.parse_args(args=args, namespace=Arguments())
|
|
24
|
+
|
|
25
|
+
if parsed_args.version:
|
|
26
|
+
my_version = importlib.metadata.version("pyallel")
|
|
27
|
+
print(my_version)
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
if not parsed_args.commands:
|
|
31
|
+
parser.print_help()
|
|
32
|
+
return 2
|
|
33
|
+
|
|
34
|
+
colours = Colours.from_colour(parsed_args.colour)
|
|
35
|
+
printer: Printer
|
|
36
|
+
if not parsed_args.interactive or not constants.IN_TTY:
|
|
37
|
+
printer = NonInteractiveConsolePrinter(colours, timer=parsed_args.timer)
|
|
38
|
+
else:
|
|
39
|
+
printer = InteractiveConsolePrinter(colours, timer=parsed_args.timer)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
process_group_manager = ProcessGroupManager.from_args(*parsed_args.commands)
|
|
43
|
+
except InvalidLinesModifierError as e:
|
|
44
|
+
print(f"{colours.red_bold}Error: {str(e)}{colours.reset_colour}")
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
exit_code = run(process_group_manager, printer)
|
|
49
|
+
except Exception:
|
|
50
|
+
print(
|
|
51
|
+
f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}"
|
|
52
|
+
)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
if exit_code == 1:
|
|
56
|
+
print(f"{colours.red_bold}\nFailed!{colours.reset_colour}")
|
|
57
|
+
elif exit_code == 0:
|
|
58
|
+
print(f"{colours.green_bold}\nDone!{colours.reset_colour}")
|
|
59
|
+
|
|
60
|
+
return exit_code
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run(process_group_manager: ProcessGroupManager, printer: Printer) -> int:
|
|
64
|
+
process_group_manager.run()
|
|
65
|
+
while True:
|
|
66
|
+
process_group_manager.stream()
|
|
67
|
+
printer.print(process_group_manager)
|
|
68
|
+
|
|
69
|
+
poll = process_group_manager.poll()
|
|
70
|
+
if poll is not None:
|
|
71
|
+
# If we still have new output to print after the process group manager has completed,
|
|
72
|
+
# make sure to print it here before continuing
|
|
73
|
+
if process_group_manager.stream().has_output():
|
|
74
|
+
printer.print(process_group_manager)
|
|
75
|
+
|
|
76
|
+
if poll > 0:
|
|
77
|
+
return poll
|
|
78
|
+
|
|
79
|
+
process_group_manager.run()
|
|
80
|
+
if not process_group_manager.next():
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
time.sleep(0.1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
sys.exit(entry_point())
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from typing import Protocol
|
|
4
5
|
|
|
5
6
|
from pyallel import constants
|
|
6
7
|
from pyallel.colours import Colours
|
|
7
|
-
from pyallel.process import ProcessOutput
|
|
8
|
+
from pyallel.process import Process, ProcessOutput
|
|
8
9
|
from pyallel.process_group import ProcessGroupOutput
|
|
10
|
+
from pyallel.process_group_manager import ProcessGroupManager
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class Printer:
|
|
13
|
+
class Printer(Protocol):
|
|
14
|
+
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
15
|
+
"""Print output obtained from the provided process group manager.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
process_group_manager: manager to obtain output from
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConsolePrinter:
|
|
12
23
|
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
13
24
|
self._colours = colours or Colours()
|
|
14
25
|
self._timer = timer
|
|
15
26
|
self._prefix = f"{self._colours.dim_on}=>{self._colours.dim_off} "
|
|
16
27
|
self._icon = 0
|
|
17
|
-
self._last_printed: list[tuple[bool, str, str]] = []
|
|
18
28
|
self._to_print: list[tuple[bool, str, str]] = []
|
|
19
29
|
|
|
20
30
|
def write(
|
|
@@ -33,34 +43,10 @@ class Printer:
|
|
|
33
43
|
truncate_num = 6
|
|
34
44
|
if truncate:
|
|
35
45
|
columns = columns - truncate_num
|
|
36
|
-
if get_num_lines(line, columns) > 1:
|
|
37
|
-
line = truncate_line(line, columns)
|
|
46
|
+
if self.get_num_lines(line, columns) > 1:
|
|
47
|
+
line = self.truncate_line(line, columns)
|
|
38
48
|
print(f"{prefix}{line}", end=end, flush=flush)
|
|
39
49
|
|
|
40
|
-
def info(self, msg: str) -> None:
|
|
41
|
-
self.write(
|
|
42
|
-
f"{self._colours.white_bold}{msg}{self._colours.reset_colour}",
|
|
43
|
-
include_prefix=False,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def ok(self, msg: str) -> None:
|
|
47
|
-
self.write(
|
|
48
|
-
f"{self._colours.green_bold}{msg}{self._colours.reset_colour}",
|
|
49
|
-
include_prefix=False,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
def warn(self, msg: str) -> None:
|
|
53
|
-
self.write(
|
|
54
|
-
f"{self._colours.yellow_bold}{msg}{self._colours.reset_colour}",
|
|
55
|
-
include_prefix=False,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
def error(self, msg: str) -> None:
|
|
59
|
-
self.write(
|
|
60
|
-
f"{self._colours.red_bold}{msg}{self._colours.reset_colour}",
|
|
61
|
-
include_prefix=False,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
50
|
def generate_process_output(
|
|
65
51
|
self,
|
|
66
52
|
output: ProcessOutput,
|
|
@@ -154,12 +140,12 @@ class Printer:
|
|
|
154
140
|
if not output.process.end:
|
|
155
141
|
end = time.perf_counter()
|
|
156
142
|
elapsed = end - output.process.start
|
|
157
|
-
timer = f"({format_time_taken(elapsed)})"
|
|
143
|
+
timer = f"({self.format_time_taken(elapsed)})"
|
|
158
144
|
|
|
159
145
|
command = output.process.command
|
|
160
|
-
if get_num_lines(output.process.command) > 1:
|
|
146
|
+
if self.get_num_lines(output.process.command) > 1:
|
|
161
147
|
columns = constants.COLUMNS() - (len(msg) + len(timer) + 9)
|
|
162
|
-
command = truncate_line(command, columns)
|
|
148
|
+
command = self.truncate_line(command, columns)
|
|
163
149
|
|
|
164
150
|
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}"
|
|
165
151
|
|
|
@@ -174,7 +160,7 @@ class Printer:
|
|
|
174
160
|
interrupt_count: int = 0,
|
|
175
161
|
tail_output: bool = True,
|
|
176
162
|
) -> list[tuple[bool, str, str]]:
|
|
177
|
-
set_process_lines(output, interrupt_count)
|
|
163
|
+
self.set_process_lines(output, interrupt_count)
|
|
178
164
|
|
|
179
165
|
for out in output.processes:
|
|
180
166
|
self.generate_process_output(out, tail_output, append_newlines=True)
|
|
@@ -204,27 +190,124 @@ class Printer:
|
|
|
204
190
|
|
|
205
191
|
return self._to_print
|
|
206
192
|
|
|
207
|
-
def
|
|
193
|
+
def set_process_lines(
|
|
208
194
|
self,
|
|
209
|
-
output:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
include_output: bool = True,
|
|
213
|
-
include_progress: bool = True,
|
|
214
|
-
include_timer: bool | None = None,
|
|
195
|
+
output: ProcessGroupOutput,
|
|
196
|
+
interrupt_count: int = 0,
|
|
197
|
+
lines: int = 0,
|
|
215
198
|
) -> None:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
199
|
+
lines = lines or constants.LINES() - 1
|
|
200
|
+
if interrupt_count:
|
|
201
|
+
lines -= 2
|
|
202
|
+
|
|
203
|
+
# Allocate lines to processes that have a fixed percentage of lines set
|
|
204
|
+
allocated_process_lines = lines // len(output.processes)
|
|
205
|
+
processes_with_dynamic_lines: list[ProcessOutput] = []
|
|
206
|
+
used_lines = 0
|
|
207
|
+
for process_output in output.processes:
|
|
208
|
+
# This process output doesn't have percentage_lines set, so skip it
|
|
209
|
+
if not process_output.process.percentage_lines:
|
|
210
|
+
processes_with_dynamic_lines.append(process_output)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
process_output.process.lines = int(
|
|
214
|
+
lines * process_output.process.percentage_lines
|
|
215
|
+
)
|
|
216
|
+
used_lines += process_output.process.lines
|
|
217
|
+
|
|
218
|
+
# Remove the used lines from the total available lines
|
|
219
|
+
lines -= used_lines
|
|
220
|
+
|
|
221
|
+
while lines:
|
|
222
|
+
# Calculate how many lines each process should have based on how many processes and lines are left
|
|
223
|
+
num_processes = len(processes_with_dynamic_lines) or 1
|
|
224
|
+
allocated_process_lines = lines // num_processes
|
|
225
|
+
processes_with_excess_output: list[ProcessOutput] = []
|
|
226
|
+
recalculate_lines = False
|
|
227
|
+
for process_output in processes_with_dynamic_lines:
|
|
228
|
+
# If the number of lines in this process output is less than how many terminal lines we would allocate it,
|
|
229
|
+
# Set it's allocated terminal lines to the exact number of lines in its output and remove this number from
|
|
230
|
+
# the total available terminal lines
|
|
231
|
+
if process_output.lines < allocated_process_lines:
|
|
232
|
+
process_output.process.lines = process_output.lines
|
|
233
|
+
lines -= process_output.process.lines
|
|
234
|
+
recalculate_lines = True
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
processes_with_excess_output.append(process_output)
|
|
238
|
+
|
|
239
|
+
# We need to re-calcuate how many terminal lines we can allocate to each process if the output of at least one process
|
|
240
|
+
# contains less lines than what we would normally allocate it. This is done so we can allocate these extra lines to the
|
|
241
|
+
# other processes that contain more lines of output.
|
|
242
|
+
if recalculate_lines:
|
|
243
|
+
processes_with_dynamic_lines = processes_with_excess_output
|
|
244
|
+
else:
|
|
245
|
+
# All remaining processes exceed the number of terminal lines we will allocate them, so allocate them
|
|
246
|
+
# their terminal lines as normal and break out of the while loop
|
|
247
|
+
for process_output in processes_with_excess_output:
|
|
248
|
+
process_output.process.lines = allocated_process_lines
|
|
249
|
+
lines -= allocated_process_lines
|
|
250
|
+
|
|
251
|
+
# If there is any lines left, allocate them to the process that currently contains the most lines in its output, or
|
|
252
|
+
# allocate them to the first process if no process contains enough lines
|
|
253
|
+
if lines:
|
|
254
|
+
process_with_most_lines: ProcessOutput | None = None
|
|
255
|
+
most_lines = 0
|
|
256
|
+
for process_output in output.processes:
|
|
257
|
+
if process_output.process.lines > most_lines:
|
|
258
|
+
process_with_most_lines = process_output
|
|
259
|
+
most_lines = process_output.process.lines
|
|
260
|
+
|
|
261
|
+
if not process_with_most_lines:
|
|
262
|
+
output.processes[0].process.lines += lines
|
|
263
|
+
else:
|
|
264
|
+
process_with_most_lines.process.lines += lines
|
|
265
|
+
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
def get_num_lines(self, line: str, columns: int | None = None) -> int:
|
|
269
|
+
lines = 0
|
|
270
|
+
columns = columns or constants.COLUMNS()
|
|
271
|
+
line = constants.ANSI_ESCAPE.sub("", line)
|
|
272
|
+
length = len(line)
|
|
273
|
+
line_lines = 1
|
|
274
|
+
if length > columns:
|
|
275
|
+
line_lines = length // columns
|
|
276
|
+
remainder = length % columns
|
|
277
|
+
if remainder:
|
|
278
|
+
line_lines += 1
|
|
279
|
+
lines += 1 * line_lines
|
|
280
|
+
return lines
|
|
281
|
+
|
|
282
|
+
def truncate_line(self, line: str, columns: int | None = None) -> str:
|
|
283
|
+
columns = columns or constants.COLUMNS()
|
|
284
|
+
escaped_line = constants.ANSI_ESCAPE.sub("", line)
|
|
285
|
+
return "".join(escaped_line[:columns]) + "..."
|
|
286
|
+
|
|
287
|
+
def format_time_taken(self, time_taken: float) -> str:
|
|
288
|
+
time_taken = round(time_taken, 1)
|
|
289
|
+
seconds = time_taken % (24 * 3600)
|
|
290
|
+
|
|
291
|
+
return f"{seconds}s"
|
|
225
292
|
|
|
226
|
-
|
|
227
|
-
|
|
293
|
+
|
|
294
|
+
class InteractiveConsolePrinter(ConsolePrinter):
|
|
295
|
+
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
296
|
+
super().__init__(colours, timer)
|
|
297
|
+
self._last_printed: list[tuple[bool, str, str]] = []
|
|
298
|
+
|
|
299
|
+
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
300
|
+
output = process_group_manager.get_cur_process_group_output()
|
|
301
|
+
self.print_progress_group_output(output, process_group_manager._interrupt_count)
|
|
302
|
+
|
|
303
|
+
poll = process_group_manager.poll()
|
|
304
|
+
if poll is not None:
|
|
305
|
+
self.clear_last_printed_lines()
|
|
306
|
+
self.reset()
|
|
307
|
+
self.print_progress_group_output(
|
|
308
|
+
output, process_group_manager._interrupt_count, tail_output=False
|
|
309
|
+
)
|
|
310
|
+
self.reset()
|
|
228
311
|
|
|
229
312
|
def print_progress_group_output(
|
|
230
313
|
self,
|
|
@@ -289,104 +372,48 @@ class Printer:
|
|
|
289
372
|
self._to_print.clear()
|
|
290
373
|
|
|
291
374
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
continue
|
|
310
|
-
|
|
311
|
-
process_output.process.lines = int(
|
|
312
|
-
lines * process_output.process.percentage_lines
|
|
313
|
-
)
|
|
314
|
-
used_lines += process_output.process.lines
|
|
315
|
-
|
|
316
|
-
# Remove the used lines from the total available lines
|
|
317
|
-
lines -= used_lines
|
|
318
|
-
|
|
319
|
-
while lines:
|
|
320
|
-
# Calculate how many lines each process should have based on how many processes and lines are left
|
|
321
|
-
num_processes = len(processes_with_dynamic_lines) or 1
|
|
322
|
-
allocated_process_lines = lines // num_processes
|
|
323
|
-
processes_with_excess_output: list[ProcessOutput] = []
|
|
324
|
-
recalculate_lines = False
|
|
325
|
-
for process_output in processes_with_dynamic_lines:
|
|
326
|
-
# If the number of lines in this process output is less than how many terminal lines we would allocate it,
|
|
327
|
-
# Set it's allocated terminal lines to the exact number of lines in its output and remove this number from
|
|
328
|
-
# the total available terminal lines
|
|
329
|
-
if process_output.lines < allocated_process_lines:
|
|
330
|
-
process_output.process.lines = process_output.lines
|
|
331
|
-
lines -= process_output.process.lines
|
|
332
|
-
recalculate_lines = True
|
|
333
|
-
continue
|
|
334
|
-
|
|
335
|
-
processes_with_excess_output.append(process_output)
|
|
336
|
-
|
|
337
|
-
# We need to re-calcuate how many terminal lines we can allocate to each process if the output of at least one process
|
|
338
|
-
# contains less lines than what we would normally allocate it. This is done so we can allocate these extra lines to the
|
|
339
|
-
# other processes that contain more lines of output.
|
|
340
|
-
if recalculate_lines:
|
|
341
|
-
processes_with_dynamic_lines = processes_with_excess_output
|
|
342
|
-
else:
|
|
343
|
-
# All remaining processes exceed the number of terminal lines we will allocate them, so allocate them
|
|
344
|
-
# their terminal lines as normal and break out of the while loop
|
|
345
|
-
for process_output in processes_with_excess_output:
|
|
346
|
-
process_output.process.lines = allocated_process_lines
|
|
347
|
-
lines -= allocated_process_lines
|
|
348
|
-
|
|
349
|
-
# If there is any lines left, allocate them to the process that currently contains the most lines in its output, or
|
|
350
|
-
# allocate them to the first process if no process contains enough lines
|
|
351
|
-
if lines:
|
|
352
|
-
process_with_most_lines: ProcessOutput | None = None
|
|
353
|
-
most_lines = 0
|
|
354
|
-
for process_output in output.processes:
|
|
355
|
-
if process_output.process.lines > most_lines:
|
|
356
|
-
process_with_most_lines = process_output
|
|
357
|
-
most_lines = process_output.process.lines
|
|
358
|
-
|
|
359
|
-
if not process_with_most_lines:
|
|
360
|
-
output.processes[0].process.lines += lines
|
|
375
|
+
class NonInteractiveConsolePrinter(ConsolePrinter):
|
|
376
|
+
def __init__(self, colours: Colours | None = None, timer: bool = False) -> None:
|
|
377
|
+
super().__init__(colours, timer)
|
|
378
|
+
self._current_process: Process | None = None
|
|
379
|
+
|
|
380
|
+
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
381
|
+
outputs = process_group_manager.cur_output
|
|
382
|
+
for pg in outputs.process_group_outputs.values():
|
|
383
|
+
for output in pg.processes:
|
|
384
|
+
if self._current_process is None:
|
|
385
|
+
self._current_process = output.process
|
|
386
|
+
output = process_group_manager.get_process(output.id)
|
|
387
|
+
self.print_process_output(
|
|
388
|
+
output, include_progress=False, include_timer=False
|
|
389
|
+
)
|
|
390
|
+
elif self._current_process is not output.process:
|
|
391
|
+
continue
|
|
361
392
|
else:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
break
|
|
365
|
-
|
|
393
|
+
self.print_process_output(output, include_cmd=False)
|
|
366
394
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
line = constants.ANSI_ESCAPE.sub("", line)
|
|
371
|
-
length = len(line)
|
|
372
|
-
line_lines = 1
|
|
373
|
-
if length > columns:
|
|
374
|
-
line_lines = length // columns
|
|
375
|
-
remainder = length % columns
|
|
376
|
-
if remainder:
|
|
377
|
-
line_lines += 1
|
|
378
|
-
lines += 1 * line_lines
|
|
379
|
-
return lines
|
|
395
|
+
if output.process.poll() is not None:
|
|
396
|
+
self.print_process_output(output, include_output=False)
|
|
397
|
+
self._current_process = None
|
|
380
398
|
|
|
399
|
+
def print_process_output(
|
|
400
|
+
self,
|
|
401
|
+
output: ProcessOutput,
|
|
402
|
+
tail_output: bool = False,
|
|
403
|
+
include_cmd: bool = True,
|
|
404
|
+
include_output: bool = True,
|
|
405
|
+
include_progress: bool = True,
|
|
406
|
+
include_timer: bool | None = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
for include_prefix, line, end in self.generate_process_output(
|
|
409
|
+
output,
|
|
410
|
+
tail_output,
|
|
411
|
+
include_cmd,
|
|
412
|
+
include_output,
|
|
413
|
+
include_progress,
|
|
414
|
+
include_timer,
|
|
415
|
+
):
|
|
416
|
+
self.write(line, include_prefix, end)
|
|
381
417
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
escaped_line = constants.ANSI_ESCAPE.sub("", line)
|
|
385
|
-
return "".join(escaped_line[:columns]) + "..."
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def format_time_taken(time_taken: float) -> str:
|
|
389
|
-
time_taken = round(time_taken, 1)
|
|
390
|
-
seconds = time_taken % (24 * 3600)
|
|
391
|
-
|
|
392
|
-
return f"{seconds}s"
|
|
418
|
+
# Force a flush otherwise lines that don't end in a newline character will not get printed as they are read
|
|
419
|
+
print("", end="", flush=True)
|
|
@@ -24,6 +24,14 @@ class ProcessGroupManagerOutput:
|
|
|
24
24
|
else:
|
|
25
25
|
self.process_group_outputs[key] = value
|
|
26
26
|
|
|
27
|
+
def has_output(self) -> bool:
|
|
28
|
+
for pg in self.process_group_outputs.values():
|
|
29
|
+
for process in pg.processes:
|
|
30
|
+
if process.data:
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
return False
|
|
34
|
+
|
|
27
35
|
|
|
28
36
|
class ProcessGroupManager:
|
|
29
37
|
def __init__(self, process_groups: list[ProcessGroup]) -> None:
|
|
@@ -31,7 +39,7 @@ class ProcessGroupManager:
|
|
|
31
39
|
self._interrupt_count = 0
|
|
32
40
|
self._cur_process_group: ProcessGroup | None = None
|
|
33
41
|
self._process_groups = process_groups
|
|
34
|
-
self.
|
|
42
|
+
self._all_output = ProcessGroupManagerOutput(
|
|
35
43
|
process_group_outputs={
|
|
36
44
|
pg.id: ProcessGroupOutput(
|
|
37
45
|
id=pg.id,
|
|
@@ -40,6 +48,7 @@ class ProcessGroupManager:
|
|
|
40
48
|
for pg in self._process_groups
|
|
41
49
|
}
|
|
42
50
|
)
|
|
51
|
+
self.cur_output = ProcessGroupManagerOutput()
|
|
43
52
|
|
|
44
53
|
def run(self) -> None:
|
|
45
54
|
if self._process_groups:
|
|
@@ -62,18 +71,19 @@ class ProcessGroupManager:
|
|
|
62
71
|
},
|
|
63
72
|
)
|
|
64
73
|
|
|
65
|
-
self.
|
|
74
|
+
self._all_output.merge(output)
|
|
75
|
+
self.cur_output = output
|
|
66
76
|
|
|
67
77
|
return output
|
|
68
78
|
|
|
69
79
|
def get_cur_process_group_output(self) -> ProcessGroupOutput:
|
|
70
80
|
if self._cur_process_group:
|
|
71
|
-
return self.
|
|
81
|
+
return self._all_output.process_group_outputs[self._cur_process_group.id]
|
|
72
82
|
|
|
73
83
|
raise KeyError("no current process group output")
|
|
74
84
|
|
|
75
85
|
def get_process(self, id: int) -> ProcessOutput:
|
|
76
|
-
for pg in self.
|
|
86
|
+
for pg in self._all_output.process_group_outputs.values():
|
|
77
87
|
for process in pg.processes:
|
|
78
88
|
if process.id == id:
|
|
79
89
|
return process
|
|
@@ -1,137 +0,0 @@
|
|
|
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 InvalidLinesModifierError
|
|
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
|
-
output = process_group_manager.get_cur_process_group_output()
|
|
23
|
-
printer.print_progress_group_output(
|
|
24
|
-
output, process_group_manager._interrupt_count
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
poll = process_group_manager.poll()
|
|
28
|
-
if poll is not None:
|
|
29
|
-
printer.clear_last_printed_lines()
|
|
30
|
-
printer.reset()
|
|
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.reset()
|
|
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 InvalidLinesModifierError 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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|