pyallel 0.13.0__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyallel-0.13.0 → pyallel-0.15.0}/PKG-INFO +5 -2
- {pyallel-0.13.0 → pyallel-0.15.0}/README.md +4 -1
- {pyallel-0.13.0 → pyallel-0.15.0}/pyproject.toml +1 -1
- pyallel-0.15.0/src/pyallel/colours.py +32 -0
- pyallel-0.15.0/src/pyallel/constants.py +21 -0
- {pyallel-0.13.0 → pyallel-0.15.0}/src/pyallel/main.py +15 -9
- {pyallel-0.13.0 → pyallel-0.15.0}/src/pyallel/parser.py +8 -0
- {pyallel-0.13.0 → pyallel-0.15.0}/src/pyallel/process.py +174 -134
- pyallel-0.13.0/src/pyallel/constants.py +0 -55
- {pyallel-0.13.0 → pyallel-0.15.0}/LICENSE +0 -0
- {pyallel-0.13.0 → pyallel-0.15.0}/src/pyallel/__init__.py +0 -0
- {pyallel-0.13.0 → pyallel-0.15.0}/src/pyallel/errors.py +0 -0
- {pyallel-0.13.0 → pyallel-0.15.0}/src/pyallel/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
5
5
|
Home-page: https://github.com/Danthewaann/pyallel
|
|
6
6
|
License: MIT
|
|
@@ -40,7 +40,7 @@ pip install pyallel
|
|
|
40
40
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
41
41
|
|
|
42
42
|
```
|
|
43
|
-
usage: pyallel [-h] [-t] [-n] [-V] [-v] [commands ...]
|
|
43
|
+
usage: pyallel [-h] [-t] [-n] [-V] [-v] [--colour {yes,no,auto}] [commands ...]
|
|
44
44
|
|
|
45
45
|
Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
46
46
|
|
|
@@ -58,6 +58,8 @@ options:
|
|
|
58
58
|
run in non-interactive mode
|
|
59
59
|
-V, --verbose run in verbose mode
|
|
60
60
|
-v, --version print version and exit
|
|
61
|
+
--colour {yes,no,auto}
|
|
62
|
+
colour terminal output, defaults to "auto"
|
|
61
63
|
```
|
|
62
64
|
|
|
63
65
|
Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
|
|
@@ -94,6 +96,7 @@ pyallel "MYPY_FORCE_COLOR=1 mypy ." \
|
|
|
94
96
|
- [x] Maybe make tail mode followed by an optional dump of all the command output once it
|
|
95
97
|
finishes the default behaviour?
|
|
96
98
|
- [x] Add graceful Ctrl-C interrupt handling to streamed modes
|
|
99
|
+
- [x] Add a `--colour` flag to configure pyallel print colours
|
|
97
100
|
- [ ] Setup build system to convert `pyallel` into a single file executable for ease of
|
|
98
101
|
use and distribution
|
|
99
102
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -19,7 +19,7 @@ pip install pyallel
|
|
|
19
19
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
usage: pyallel [-h] [-t] [-n] [-V] [-v] [commands ...]
|
|
22
|
+
usage: pyallel [-h] [-t] [-n] [-V] [-v] [--colour {yes,no,auto}] [commands ...]
|
|
23
23
|
|
|
24
24
|
Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
25
25
|
|
|
@@ -37,6 +37,8 @@ options:
|
|
|
37
37
|
run in non-interactive mode
|
|
38
38
|
-V, --verbose run in verbose mode
|
|
39
39
|
-v, --version print version and exit
|
|
40
|
+
--colour {yes,no,auto}
|
|
41
|
+
colour terminal output, defaults to "auto"
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
|
|
@@ -73,6 +75,7 @@ pyallel "MYPY_FORCE_COLOR=1 mypy ." \
|
|
|
73
75
|
- [x] Maybe make tail mode followed by an optional dump of all the command output once it
|
|
74
76
|
finishes the default behaviour?
|
|
75
77
|
- [x] Add graceful Ctrl-C interrupt handling to streamed modes
|
|
78
|
+
- [x] Add a `--colour` flag to configure pyallel print colours
|
|
76
79
|
- [ ] Setup build system to convert `pyallel` into a single file executable for ease of
|
|
77
80
|
use and distribution
|
|
78
81
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, fields
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from pyallel import constants
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Colours:
|
|
10
|
+
white_bold: str = "\033[1m"
|
|
11
|
+
green_bold: str = "\033[1;32m"
|
|
12
|
+
blue_bold: str = "\033[1;34m"
|
|
13
|
+
red_bold: str = "\033[1;31m"
|
|
14
|
+
yellow_bold: str = "\033[1;33m"
|
|
15
|
+
clear_line: str = "\033[2K"
|
|
16
|
+
clear_screen: str = "\033[2J"
|
|
17
|
+
save_cursor: str = "\033[s"
|
|
18
|
+
restore_cursor: str = "\033[u"
|
|
19
|
+
up_line: str = "\033[1F"
|
|
20
|
+
reset_colour: str = "\033[0m"
|
|
21
|
+
dim_on: str = "\033[2m"
|
|
22
|
+
dim_off: str = "\033[22m"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_colour(cls, colour: Literal["yes", "no", "auto"]) -> Colours:
|
|
26
|
+
colours = cls()
|
|
27
|
+
|
|
28
|
+
if colour == "no" or colour == "auto" and not constants.IN_TTY:
|
|
29
|
+
for field in fields(colours):
|
|
30
|
+
setattr(colours, field.name, "")
|
|
31
|
+
|
|
32
|
+
return colours
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
IN_TTY = sys.stdout.isatty()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if IN_TTY:
|
|
8
|
+
|
|
9
|
+
def LINES() -> int:
|
|
10
|
+
return os.get_terminal_size().lines
|
|
11
|
+
|
|
12
|
+
else:
|
|
13
|
+
|
|
14
|
+
def LINES() -> int:
|
|
15
|
+
return sys.maxsize
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ICONS = ("/", "-", "\\", "|")
|
|
19
|
+
# Unicode character bytes to render different symbols in the terminal
|
|
20
|
+
TICK = "\u2713"
|
|
21
|
+
X = "\u2717"
|
|
@@ -3,21 +3,26 @@ from __future__ import annotations
|
|
|
3
3
|
import sys
|
|
4
4
|
import traceback
|
|
5
5
|
import importlib.metadata
|
|
6
|
+
from pyallel.colours import Colours
|
|
6
7
|
from pyallel.errors import InvalidExecutableErrors
|
|
7
8
|
|
|
8
9
|
from pyallel.parser import Arguments, create_parser
|
|
9
10
|
from pyallel.process import ProcessGroup
|
|
10
|
-
from pyallel import constants
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def main_loop(
|
|
14
14
|
*commands: str,
|
|
15
|
+
colours: Colours,
|
|
15
16
|
interactive: bool = False,
|
|
16
17
|
timer: bool = False,
|
|
17
18
|
verbose: bool = False,
|
|
18
19
|
) -> int:
|
|
19
20
|
process_group = ProcessGroup.from_commands(
|
|
20
|
-
*commands,
|
|
21
|
+
*commands,
|
|
22
|
+
colours=colours,
|
|
23
|
+
interactive=interactive,
|
|
24
|
+
timer=timer,
|
|
25
|
+
verbose=verbose,
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
return process_group.stream()
|
|
@@ -36,10 +41,13 @@ def run(*args: str) -> int:
|
|
|
36
41
|
parser.print_help()
|
|
37
42
|
return 2
|
|
38
43
|
|
|
44
|
+
colours = Colours.from_colour(parsed_args.colour)
|
|
45
|
+
|
|
39
46
|
message = None
|
|
40
47
|
try:
|
|
41
48
|
exit_code = main_loop(
|
|
42
49
|
*parsed_args.commands,
|
|
50
|
+
colours=colours,
|
|
43
51
|
interactive=parsed_args.interactive,
|
|
44
52
|
timer=parsed_args.timer,
|
|
45
53
|
verbose=parsed_args.verbose,
|
|
@@ -51,15 +59,13 @@ def run(*args: str) -> int:
|
|
|
51
59
|
exit_code = 1
|
|
52
60
|
message = traceback.format_exc()
|
|
53
61
|
|
|
54
|
-
if exit_code ==
|
|
55
|
-
print(f"{constants.YELLOW_BOLD}Interrupt!{constants.RESET_COLOUR}")
|
|
56
|
-
elif exit_code == 1:
|
|
62
|
+
if exit_code == 1:
|
|
57
63
|
if not message:
|
|
58
|
-
print(f"{
|
|
64
|
+
print(f"{colours.red_bold}A command failed!{colours.reset_colour}")
|
|
59
65
|
else:
|
|
60
|
-
print(f"{
|
|
61
|
-
|
|
62
|
-
print(f"{
|
|
66
|
+
print(f"{colours.red_bold}Error: {message}{colours.reset_colour}")
|
|
67
|
+
elif exit_code == 0:
|
|
68
|
+
print(f"{colours.green_bold}Success!{colours.reset_colour}")
|
|
63
69
|
|
|
64
70
|
return exit_code
|
|
65
71
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class Arguments:
|
|
8
|
+
colour: Literal["yes", "no", "auto"]
|
|
7
9
|
commands: list[str]
|
|
8
10
|
interactive: bool
|
|
9
11
|
timer: bool
|
|
@@ -67,5 +69,11 @@ def create_parser() -> ArgumentParser:
|
|
|
67
69
|
action="store_true",
|
|
68
70
|
default=False,
|
|
69
71
|
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--colour",
|
|
74
|
+
help="colour terminal output, defaults to \"%(default)s\"",
|
|
75
|
+
choices=("yes", "no", "auto"),
|
|
76
|
+
default="auto",
|
|
77
|
+
)
|
|
70
78
|
|
|
71
79
|
return parser
|
|
@@ -9,23 +9,14 @@ import shlex
|
|
|
9
9
|
import shutil
|
|
10
10
|
import os
|
|
11
11
|
from uuid import UUID, uuid4
|
|
12
|
-
from typing import BinaryIO
|
|
12
|
+
from typing import Any, BinaryIO
|
|
13
13
|
from pyallel import constants
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
|
+
from pyallel.colours import Colours
|
|
16
17
|
from pyallel.errors import InvalidExecutableErrors, InvalidExecutableError
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def prefix(output: str, keepend: bool = True) -> str:
|
|
20
|
-
prefixed_output = "\n".join(
|
|
21
|
-
f"{constants.DIM_ON}=>{constants.DIM_OFF} {line}{constants.RESET_COLOUR}"
|
|
22
|
-
for line in output.splitlines()
|
|
23
|
-
)
|
|
24
|
-
if keepend and output and output[-1] == "\n":
|
|
25
|
-
prefixed_output += "\n"
|
|
26
|
-
return prefixed_output
|
|
27
|
-
|
|
28
|
-
|
|
29
20
|
def format_time_taken(time_taken: float) -> str:
|
|
30
21
|
time_taken = round(time_taken, 1)
|
|
31
22
|
seconds = time_taken % (24 * 3600)
|
|
@@ -33,47 +24,6 @@ def format_time_taken(time_taken: float) -> str:
|
|
|
33
24
|
return f"{seconds}s"
|
|
34
25
|
|
|
35
26
|
|
|
36
|
-
def get_command_status(
|
|
37
|
-
process: Process,
|
|
38
|
-
icon: str | None = None,
|
|
39
|
-
passed: bool | None = None,
|
|
40
|
-
verbose: bool = False,
|
|
41
|
-
timer: bool = False,
|
|
42
|
-
) -> str:
|
|
43
|
-
if passed is True:
|
|
44
|
-
colour = constants.GREEN_BOLD
|
|
45
|
-
msg = "done"
|
|
46
|
-
icon = icon or constants.TICK
|
|
47
|
-
elif passed is False:
|
|
48
|
-
colour = constants.RED_BOLD
|
|
49
|
-
msg = "failed"
|
|
50
|
-
icon = icon or constants.X
|
|
51
|
-
else:
|
|
52
|
-
colour = constants.WHITE_BOLD
|
|
53
|
-
msg = "running"
|
|
54
|
-
icon = icon or ""
|
|
55
|
-
if not icon:
|
|
56
|
-
msg += "..."
|
|
57
|
-
|
|
58
|
-
output = f"[{constants.BLUE_BOLD}{process.name}"
|
|
59
|
-
|
|
60
|
-
if verbose:
|
|
61
|
-
output += f" {' '.join(process.args)}"
|
|
62
|
-
|
|
63
|
-
output += f"{constants.RESET_COLOUR}]{colour} {msg} {icon}{constants.RESET_COLOUR}"
|
|
64
|
-
|
|
65
|
-
if timer:
|
|
66
|
-
end = process.end
|
|
67
|
-
if not process.end:
|
|
68
|
-
end = time.perf_counter()
|
|
69
|
-
elapsed = end - process.start
|
|
70
|
-
output += (
|
|
71
|
-
f" ({constants.DIM_ON}{format_time_taken(elapsed)}{constants.DIM_OFF})"
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
return output
|
|
75
|
-
|
|
76
|
-
|
|
77
27
|
@dataclass
|
|
78
28
|
class ProcessGroup:
|
|
79
29
|
processes: list[Process]
|
|
@@ -83,8 +33,11 @@ class ProcessGroup:
|
|
|
83
33
|
output: dict[UUID, list[str]] = field(default_factory=lambda: defaultdict(list))
|
|
84
34
|
process_lines: list[int] = field(default_factory=list)
|
|
85
35
|
completed_processes: set[UUID] = field(default_factory=set)
|
|
36
|
+
exit_code: int = 0
|
|
37
|
+
interrupt_count: int = 0
|
|
86
38
|
passed: bool = True
|
|
87
39
|
icon: int = 0
|
|
40
|
+
colours: Colours = field(default_factory=Colours)
|
|
88
41
|
|
|
89
42
|
def stream(self) -> int:
|
|
90
43
|
for process in self.processes:
|
|
@@ -93,110 +46,177 @@ class ProcessGroup:
|
|
|
93
46
|
if not self.interactive:
|
|
94
47
|
return self.stream_non_interactive()
|
|
95
48
|
|
|
96
|
-
interrupted = False
|
|
97
|
-
|
|
98
49
|
while True:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.icon = 0
|
|
50
|
+
output = self.complete_output()
|
|
51
|
+
self.icon += 1
|
|
52
|
+
if self.icon == len(constants.ICONS):
|
|
53
|
+
self.icon = 0
|
|
104
54
|
|
|
105
|
-
|
|
106
|
-
|
|
55
|
+
# Clear the screen and print the output
|
|
56
|
+
print(f"\033[H\033[0J{output}", end="")
|
|
107
57
|
|
|
108
|
-
|
|
109
|
-
|
|
58
|
+
# Clear the screen again
|
|
59
|
+
print("\033[H\033[0J", end="")
|
|
110
60
|
|
|
111
|
-
|
|
112
|
-
|
|
61
|
+
if len(self.completed_processes) == len(self.processes):
|
|
62
|
+
break
|
|
113
63
|
|
|
114
|
-
|
|
115
|
-
except KeyboardInterrupt:
|
|
116
|
-
interrupted = True
|
|
117
|
-
for process in self.processes:
|
|
118
|
-
process.interrupt()
|
|
119
|
-
process.wait()
|
|
64
|
+
time.sleep(0.1)
|
|
120
65
|
|
|
121
66
|
output = self.complete_output(all=True)
|
|
122
67
|
# Clear the screen one final time before printing the output
|
|
123
68
|
print(f"\033[3J{output}")
|
|
124
69
|
|
|
125
|
-
|
|
70
|
+
if not self.exit_code and not self.passed:
|
|
71
|
+
self.exit_code = 1
|
|
72
|
+
|
|
73
|
+
return self.exit_code
|
|
126
74
|
|
|
127
75
|
def stream_non_interactive(self) -> int:
|
|
128
76
|
running_process = None
|
|
129
77
|
interrupted = False
|
|
130
78
|
|
|
131
|
-
print(
|
|
79
|
+
print(
|
|
80
|
+
f"{self.colours.white_bold}Running commands...{self.colours.reset_colour}\n"
|
|
81
|
+
)
|
|
132
82
|
|
|
133
83
|
while True:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
84
|
+
output = ""
|
|
85
|
+
for process in self.processes:
|
|
86
|
+
if (
|
|
87
|
+
running_process is None
|
|
88
|
+
and process.id not in self.completed_processes
|
|
89
|
+
):
|
|
90
|
+
output += self._get_command_status(process, verbose=self.verbose)
|
|
91
|
+
output += "\n"
|
|
92
|
+
running_process = process
|
|
93
|
+
elif running_process is not process:
|
|
94
|
+
# Need to do this to properly keep track of how long all the other
|
|
95
|
+
# commands are taking
|
|
96
|
+
process.poll()
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
process_output = process.readline().decode()
|
|
100
|
+
|
|
101
|
+
if not self.output[process.id] and process_output:
|
|
102
|
+
process_output = self._prefix(process_output)
|
|
103
|
+
self.output[process.id].append(process_output)
|
|
104
|
+
output += process_output
|
|
105
|
+
elif process_output:
|
|
106
|
+
if self.output[process.id][-1][-1] != "\n":
|
|
107
|
+
self.output[process.id][-1] += process_output
|
|
108
|
+
else:
|
|
109
|
+
process_output = self._prefix(process_output)
|
|
110
|
+
self.output[process.id].append(process_output)
|
|
111
|
+
output += process_output
|
|
112
|
+
|
|
113
|
+
if process.poll() is not None:
|
|
114
|
+
if process.return_code() != 0:
|
|
115
|
+
self.passed = False
|
|
116
|
+
process_output = process.read().decode()
|
|
117
|
+
if process_output:
|
|
118
|
+
output += self._prefix(process_output)
|
|
119
|
+
|
|
120
|
+
output += self._get_command_status(
|
|
121
|
+
process,
|
|
122
|
+
passed=process.return_code() == 0,
|
|
123
|
+
verbose=self.verbose,
|
|
124
|
+
timer=self.timer,
|
|
125
|
+
)
|
|
126
|
+
output += "\n\n"
|
|
127
|
+
self.completed_processes.add(process.id)
|
|
128
|
+
running_process = None
|
|
129
|
+
|
|
130
|
+
if self.interrupt_count == 0:
|
|
131
|
+
pass
|
|
132
|
+
elif not interrupted and self.interrupt_count == 1:
|
|
133
|
+
if (output and output[-1] != "\n") or (
|
|
134
|
+
self.output[process.id]
|
|
135
|
+
and self.output[process.id][-1][-1] != "\n"
|
|
140
136
|
):
|
|
141
|
-
output += get_command_status(process, verbose=self.verbose)
|
|
142
137
|
output += "\n"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# Need to do this to properly keep track of how long all the other
|
|
146
|
-
# commands are taking
|
|
147
|
-
process.poll()
|
|
148
|
-
continue
|
|
138
|
+
output += f"\n{self.colours.yellow_bold}Interrupt!{self.colours.reset_colour}\n\n"
|
|
139
|
+
interrupted = True
|
|
149
140
|
|
|
150
|
-
|
|
141
|
+
if output:
|
|
142
|
+
print(output, end="")
|
|
151
143
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
144
|
+
if len(self.completed_processes) == len(self.processes):
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
time.sleep(0.01)
|
|
148
|
+
|
|
149
|
+
if self.interrupt_count == 2:
|
|
150
|
+
print(f"{self.colours.red_bold}Abort!{self.colours.reset_colour}")
|
|
151
|
+
|
|
152
|
+
if not self.exit_code and not self.passed:
|
|
153
|
+
self.exit_code = 1
|
|
154
|
+
|
|
155
|
+
return self.exit_code
|
|
156
|
+
|
|
157
|
+
def _prefix(self, output: str, keepend: bool = True) -> str:
|
|
158
|
+
prefixed_output = "\n".join(
|
|
159
|
+
f"{self.colours.dim_on}=>{self.colours.dim_off} {line}{self.colours.reset_colour}"
|
|
160
|
+
for line in output.splitlines()
|
|
161
|
+
)
|
|
162
|
+
if keepend and output and output[-1] == "\n":
|
|
163
|
+
prefixed_output += "\n"
|
|
164
|
+
return prefixed_output
|
|
165
|
+
|
|
166
|
+
def _get_command_status(
|
|
167
|
+
self,
|
|
168
|
+
process: Process,
|
|
169
|
+
icon: str | None = None,
|
|
170
|
+
passed: bool | None = None,
|
|
171
|
+
verbose: bool = False,
|
|
172
|
+
timer: bool = False,
|
|
173
|
+
) -> str:
|
|
174
|
+
if passed is True:
|
|
175
|
+
colour = self.colours.green_bold
|
|
176
|
+
msg = "done"
|
|
177
|
+
icon = icon or constants.TICK
|
|
178
|
+
elif passed is False:
|
|
179
|
+
colour = self.colours.red_bold
|
|
180
|
+
msg = "failed"
|
|
181
|
+
icon = icon or constants.X
|
|
182
|
+
else:
|
|
183
|
+
colour = self.colours.white_bold
|
|
184
|
+
msg = "running"
|
|
185
|
+
icon = icon or ""
|
|
186
|
+
if not icon:
|
|
187
|
+
msg += "..."
|
|
188
|
+
|
|
189
|
+
output = f"[{self.colours.blue_bold}{process.name}"
|
|
190
|
+
|
|
191
|
+
if verbose:
|
|
192
|
+
output += f" {' '.join(process.args)}"
|
|
193
|
+
|
|
194
|
+
output += f"{self.colours.reset_colour}]{colour} {msg} {icon}{self.colours.reset_colour}"
|
|
195
|
+
|
|
196
|
+
if timer:
|
|
197
|
+
end = process.end
|
|
198
|
+
if not process.end:
|
|
199
|
+
end = time.perf_counter()
|
|
200
|
+
elapsed = end - process.start
|
|
201
|
+
output += f" ({self.colours.dim_on}{format_time_taken(elapsed)}{self.colours.dim_off})"
|
|
202
|
+
|
|
203
|
+
return output
|
|
204
|
+
|
|
205
|
+
def _handle_signal(self, signum: int, _frame: Any) -> None:
|
|
206
|
+
for process in self.processes:
|
|
207
|
+
if self.interrupt_count == 0:
|
|
208
|
+
process.interrupt()
|
|
209
|
+
else:
|
|
210
|
+
process.kill()
|
|
211
|
+
|
|
212
|
+
self.exit_code = 128 + signum
|
|
213
|
+
self.interrupt_count += 1
|
|
195
214
|
|
|
196
215
|
@classmethod
|
|
197
216
|
def from_commands(
|
|
198
217
|
cls,
|
|
199
218
|
*commands: str,
|
|
219
|
+
colours: Colours,
|
|
200
220
|
interactive: bool = False,
|
|
201
221
|
timer: bool = False,
|
|
202
222
|
verbose: bool = False,
|
|
@@ -213,13 +233,19 @@ class ProcessGroup:
|
|
|
213
233
|
if errors:
|
|
214
234
|
raise InvalidExecutableErrors(*errors)
|
|
215
235
|
|
|
216
|
-
|
|
236
|
+
process_group = cls(
|
|
217
237
|
processes=processes,
|
|
218
238
|
interactive=interactive,
|
|
219
239
|
timer=timer,
|
|
220
240
|
verbose=verbose,
|
|
241
|
+
colours=colours,
|
|
221
242
|
)
|
|
222
243
|
|
|
244
|
+
signal.signal(signal.SIGINT, process_group._handle_signal)
|
|
245
|
+
signal.signal(signal.SIGTERM, process_group._handle_signal)
|
|
246
|
+
|
|
247
|
+
return process_group
|
|
248
|
+
|
|
223
249
|
def complete_output(self, tail: int = 20, all: bool = False) -> str:
|
|
224
250
|
num_processes = len(self.processes)
|
|
225
251
|
lines = constants.LINES() - (2 * num_processes)
|
|
@@ -243,7 +269,7 @@ class ProcessGroup:
|
|
|
243
269
|
self.completed_processes.add(process.id)
|
|
244
270
|
if process.return_code() != 0:
|
|
245
271
|
self.passed = False
|
|
246
|
-
output +=
|
|
272
|
+
output += self._get_command_status(
|
|
247
273
|
process,
|
|
248
274
|
passed=process.return_code() == 0,
|
|
249
275
|
verbose=self.verbose,
|
|
@@ -251,7 +277,7 @@ class ProcessGroup:
|
|
|
251
277
|
)
|
|
252
278
|
output += "\n"
|
|
253
279
|
else:
|
|
254
|
-
output +=
|
|
280
|
+
output += self._get_command_status(
|
|
255
281
|
process,
|
|
256
282
|
icon=constants.ICONS[self.icon],
|
|
257
283
|
verbose=self.verbose,
|
|
@@ -270,12 +296,22 @@ class ProcessGroup:
|
|
|
270
296
|
process_output.splitlines()[-self.process_lines[i - 1] :]
|
|
271
297
|
)
|
|
272
298
|
process_output += "\n"
|
|
273
|
-
output +=
|
|
299
|
+
output += self._prefix(process_output)
|
|
274
300
|
if output and output[-1] != "\n":
|
|
275
301
|
output += "\n"
|
|
276
302
|
if i != num_processes:
|
|
277
303
|
output += "\n"
|
|
278
304
|
|
|
305
|
+
if self.interrupt_count == 0:
|
|
306
|
+
return output
|
|
307
|
+
|
|
308
|
+
if self.interrupt_count == 1:
|
|
309
|
+
output += (
|
|
310
|
+
f"\n{self.colours.yellow_bold}Interrupt!{self.colours.reset_colour}"
|
|
311
|
+
)
|
|
312
|
+
elif self.interrupt_count == 2:
|
|
313
|
+
output += f"\n{self.colours.red_bold}Abort!{self.colours.reset_colour}"
|
|
314
|
+
|
|
279
315
|
return output
|
|
280
316
|
|
|
281
317
|
|
|
@@ -341,6 +377,10 @@ class Process:
|
|
|
341
377
|
if self._process:
|
|
342
378
|
self._process.send_signal(signal.SIGINT)
|
|
343
379
|
|
|
380
|
+
def kill(self) -> None:
|
|
381
|
+
if self._process:
|
|
382
|
+
self._process.send_signal(signal.SIGKILL)
|
|
383
|
+
|
|
344
384
|
def wait(self) -> int:
|
|
345
385
|
if self._process:
|
|
346
386
|
return self._process.wait()
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
IN_TTY = sys.stdout.isatty()
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if IN_TTY:
|
|
8
|
-
WHITE_BOLD = "\033[1m"
|
|
9
|
-
GREEN_BOLD = "\033[1;32m"
|
|
10
|
-
BLUE_BOLD = "\033[1;34m"
|
|
11
|
-
RED_BOLD = "\033[1;31m"
|
|
12
|
-
YELLOW_BOLD = "\033[1;33m"
|
|
13
|
-
CLEAR_LINE = "\033[2K"
|
|
14
|
-
CLEAR_SCREEN = "\033[2J"
|
|
15
|
-
SAVE_CURSOR = "\033[s"
|
|
16
|
-
RESTORE_CURSOR = "\033[u"
|
|
17
|
-
UP_LINE = "\033[1F"
|
|
18
|
-
RESET_COLOUR = "\033[0m"
|
|
19
|
-
CARRIAGE_RETURN = "\r"
|
|
20
|
-
DIM_ON = "\033[2m"
|
|
21
|
-
DIM_OFF = "\033[22m"
|
|
22
|
-
|
|
23
|
-
def COLUMNS() -> int:
|
|
24
|
-
return os.get_terminal_size().columns
|
|
25
|
-
|
|
26
|
-
def LINES() -> int:
|
|
27
|
-
return os.get_terminal_size().lines
|
|
28
|
-
|
|
29
|
-
else:
|
|
30
|
-
WHITE_BOLD = ""
|
|
31
|
-
GREEN_BOLD = ""
|
|
32
|
-
BLUE_BOLD = ""
|
|
33
|
-
RED_BOLD = ""
|
|
34
|
-
YELLOW_BOLD = ""
|
|
35
|
-
CLEAR_LINE = ""
|
|
36
|
-
CLEAR_SCREEN = ""
|
|
37
|
-
SAVE_CURSOR = ""
|
|
38
|
-
RESTORE_CURSOR = ""
|
|
39
|
-
UP_LINE = ""
|
|
40
|
-
RESET_COLOUR = ""
|
|
41
|
-
CARRIAGE_RETURN = ""
|
|
42
|
-
DIM_ON = ""
|
|
43
|
-
DIM_OFF = ""
|
|
44
|
-
|
|
45
|
-
def COLUMNS() -> int:
|
|
46
|
-
return sys.maxsize
|
|
47
|
-
|
|
48
|
-
def LINES() -> int:
|
|
49
|
-
return sys.maxsize
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
ICONS = ("/", "-", "\\", "|")
|
|
53
|
-
# Unicode character bytes to render different symbols in the terminal
|
|
54
|
-
TICK = "\u2713"
|
|
55
|
-
X = "\u2717"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|