shelltastic 0.4.9__tar.gz → 0.6.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.
- {shelltastic-0.4.9 → shelltastic-0.6.0}/PKG-INFO +1 -1
- {shelltastic-0.4.9 → shelltastic-0.6.0}/pyproject.toml +13 -1
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/backend/base.py +47 -6
- shelltastic-0.6.0/src/shelltastic/backend/default/common.py +429 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/display.py +11 -11
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/exception.py +6 -3
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/frontend/shell.py +29 -2
- shelltastic-0.6.0/src/shelltastic/io.py +59 -0
- shelltastic-0.6.0/src/shelltastic/result.py +47 -0
- shelltastic-0.4.9/src/shelltastic/backend/default/common.py +0 -201
- shelltastic-0.4.9/src/shelltastic/result.py +0 -16
- {shelltastic-0.4.9 → shelltastic-0.6.0}/LICENSE +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/README.md +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/backend/__init__.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/backend/default/local.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/backend/default/remote.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.4.9 → shelltastic-0.6.0}/src/shelltastic/host.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shelltastic"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "A fantastic shell command runner for python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -43,3 +43,15 @@ extend-select = [
|
|
|
43
43
|
|
|
44
44
|
[tool.ruff.lint.pycodestyle]
|
|
45
45
|
max-line-length = 100
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
{include-group = "lint"},
|
|
50
|
+
{include-group = "test"}
|
|
51
|
+
]
|
|
52
|
+
lint = [
|
|
53
|
+
"ruff"
|
|
54
|
+
]
|
|
55
|
+
test = [
|
|
56
|
+
"pytest"
|
|
57
|
+
]
|
|
@@ -5,22 +5,26 @@ from functools import wraps
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
+
from io import RawIOBase
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from shelltastic.display import IODisplay
|
|
11
12
|
from shelltastic.enum import CaptureMode, DisplayMode
|
|
12
13
|
from shelltastic.host import Host
|
|
13
|
-
from shelltastic.result import ShellResult
|
|
14
|
+
from shelltastic.result import ShellResult, ShellResultAsync
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class BackendShimResult:
|
|
18
|
+
__slots__ = ("args", "kwargs")
|
|
19
|
+
|
|
17
20
|
def __init__(self, *args, **kwargs) -> None:
|
|
18
21
|
self.args = args
|
|
19
22
|
self.kwargs = kwargs
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class BackendShim(ABC):
|
|
23
|
-
|
|
26
|
+
__slots__ = ()
|
|
27
|
+
|
|
24
28
|
def run_shim(
|
|
25
29
|
self,
|
|
26
30
|
backend_type: type,
|
|
@@ -31,15 +35,33 @@ class BackendShim(ABC):
|
|
|
31
35
|
"""
|
|
32
36
|
Shim for run() method of `ShellBackend`
|
|
33
37
|
|
|
38
|
+
Use `self.result(...)` to create and return a `BackendShimResult`
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
return self.result(*args, **kwargs)
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
return self.result(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
def run_async_shim(
|
|
47
|
+
self,
|
|
48
|
+
backend_type: type,
|
|
49
|
+
/,
|
|
50
|
+
*args,
|
|
51
|
+
**kwargs,
|
|
52
|
+
) -> BackendShimResult:
|
|
53
|
+
"""
|
|
54
|
+
Shim for run() method of `ShellBackend`
|
|
55
|
+
|
|
34
56
|
Use `self.run(...)` to create and return a `BackendShimResult`
|
|
35
57
|
|
|
36
58
|
```python
|
|
37
59
|
return self.run(*args, **kwargs)
|
|
38
60
|
```
|
|
39
61
|
"""
|
|
40
|
-
|
|
62
|
+
return self.result(*args, **kwargs)
|
|
41
63
|
|
|
42
|
-
def
|
|
64
|
+
def result(
|
|
43
65
|
self,
|
|
44
66
|
cmd: str | list[str],
|
|
45
67
|
*,
|
|
@@ -80,7 +102,9 @@ def _shim_run(f, backend_type: type):
|
|
|
80
102
|
def wrapper(*args, **kwargs):
|
|
81
103
|
result: BackendShimResult = BackendShimResult(*args, **kwargs)
|
|
82
104
|
for shim in _backend_shims:
|
|
83
|
-
result = shim.run_shim(
|
|
105
|
+
result = shim.run_shim(
|
|
106
|
+
backend_type, f.__name__, *result.args, **result.kwargs
|
|
107
|
+
)
|
|
84
108
|
return f(*result.args, **result.kwargs)
|
|
85
109
|
|
|
86
110
|
return wrapper
|
|
@@ -90,6 +114,7 @@ class _InstallShims(ABCMeta):
|
|
|
90
114
|
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
|
91
115
|
instance = super().__call__(*args, **kwds)
|
|
92
116
|
instance.run = _shim_run(instance.run, instance.__class__)
|
|
117
|
+
instance.run_async = _shim_run(instance.run_async, instance.__class__)
|
|
93
118
|
return instance
|
|
94
119
|
|
|
95
120
|
|
|
@@ -109,7 +134,23 @@ class ShellBackend(metaclass=_InstallShims):
|
|
|
109
134
|
cwd: str | Path | None = None,
|
|
110
135
|
echo_cmd: DisplayMode | IODisplay | bool | None = None,
|
|
111
136
|
**kwargs,
|
|
112
|
-
) -> ShellResult:
|
|
137
|
+
) -> ShellResult[bytes | None, bytes | None]:
|
|
138
|
+
raise NotImplementedError()
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def run_async(
|
|
142
|
+
self,
|
|
143
|
+
cmd: str | list[str],
|
|
144
|
+
*,
|
|
145
|
+
check: bool = True,
|
|
146
|
+
stdout: CaptureMode | None = None,
|
|
147
|
+
stderr: CaptureMode | None = None,
|
|
148
|
+
stdout_display: DisplayMode | IODisplay | None = None,
|
|
149
|
+
stderr_display: DisplayMode | IODisplay | None = None,
|
|
150
|
+
cwd: str | Path | None = None,
|
|
151
|
+
echo_cmd: DisplayMode | IODisplay | bool | None = None,
|
|
152
|
+
**kwargs,
|
|
153
|
+
) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
|
|
113
154
|
raise NotImplementedError()
|
|
114
155
|
|
|
115
156
|
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from io import RawIOBase
|
|
9
|
+
from multiprocessing import Pipe
|
|
10
|
+
from multiprocessing.pool import ThreadPool
|
|
11
|
+
from typing import IO, TYPE_CHECKING, Literal
|
|
12
|
+
|
|
13
|
+
from shelltastic import display
|
|
14
|
+
from shelltastic.backend.base import ShellBackend
|
|
15
|
+
from shelltastic.enum import CaptureMode, DisplayMode
|
|
16
|
+
from shelltastic.io import PipeReader
|
|
17
|
+
from shelltastic.result import ShellResult, ShellResultAsync
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from multiprocessing.connection import Connection
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
LOGGER = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IOTimeoutStream:
|
|
27
|
+
"""
|
|
28
|
+
Iterable for reading from an IO stream. Return lines but
|
|
29
|
+
if no bytes have been read since the given timeout, return
|
|
30
|
+
whatever is in the buffer.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self, io: IO[bytes], *, timeout: float = 0.1, batch_size: int = 1000
|
|
35
|
+
) -> None:
|
|
36
|
+
self.io = io
|
|
37
|
+
self.buffer = b""
|
|
38
|
+
self.batch_size = batch_size
|
|
39
|
+
self.timeout = timeout
|
|
40
|
+
|
|
41
|
+
def __iter__(self):
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __next__(self) -> bytes:
|
|
45
|
+
last_read_time = time.time()
|
|
46
|
+
# Read into our buffer
|
|
47
|
+
while True:
|
|
48
|
+
# If we have no data in our buffer, then we block until we get data.
|
|
49
|
+
# Otherwise, we're waiting for end of line and should unblock.
|
|
50
|
+
if len(self.buffer) == 0:
|
|
51
|
+
os.set_blocking(self.io.fileno(), True)
|
|
52
|
+
batch_size = 1
|
|
53
|
+
else:
|
|
54
|
+
os.set_blocking(self.io.fileno(), False)
|
|
55
|
+
batch_size = self.batch_size
|
|
56
|
+
|
|
57
|
+
# Read from IO
|
|
58
|
+
batch = self.io.read(batch_size)
|
|
59
|
+
|
|
60
|
+
# Handle return batch
|
|
61
|
+
# None means no data to read, but NOT end of file
|
|
62
|
+
if batch is not None:
|
|
63
|
+
last_read_time = time.time()
|
|
64
|
+
self.buffer += batch
|
|
65
|
+
|
|
66
|
+
# If batch is not none,
|
|
67
|
+
# and the batch was 0 size,
|
|
68
|
+
# and our buffer is empty,
|
|
69
|
+
# we're at EOF
|
|
70
|
+
if len(self.buffer) == 0:
|
|
71
|
+
raise StopIteration()
|
|
72
|
+
|
|
73
|
+
# If buffer contains at least one line, return the first line, save the rest
|
|
74
|
+
if b"\n" in self.buffer:
|
|
75
|
+
buffer_lines = self.buffer.splitlines(keepends=True)
|
|
76
|
+
self.buffer = b""
|
|
77
|
+
for b in buffer_lines[1:]:
|
|
78
|
+
self.buffer += b
|
|
79
|
+
return buffer_lines[0]
|
|
80
|
+
|
|
81
|
+
# If we're at end of file, and we have no lines, just return what we have
|
|
82
|
+
if batch is not None and len(batch) == 0:
|
|
83
|
+
return_val = self.buffer
|
|
84
|
+
self.buffer = b""
|
|
85
|
+
return return_val
|
|
86
|
+
|
|
87
|
+
# Handle timeout
|
|
88
|
+
if time.time() - last_read_time >= self.timeout:
|
|
89
|
+
return_val = self.buffer
|
|
90
|
+
self.buffer = b""
|
|
91
|
+
return return_val
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _output_and_collect(
|
|
95
|
+
io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
|
|
96
|
+
) -> bytes | None:
|
|
97
|
+
if io is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
if collect:
|
|
101
|
+
if display:
|
|
102
|
+
return_bytes: bytes = b""
|
|
103
|
+
for line in IOTimeoutStream(io):
|
|
104
|
+
return_bytes += line
|
|
105
|
+
display.printbytes(line)
|
|
106
|
+
return return_bytes
|
|
107
|
+
return io.read()
|
|
108
|
+
|
|
109
|
+
if display:
|
|
110
|
+
for line in IOTimeoutStream(io):
|
|
111
|
+
display.printbytes(line)
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _output_and_pipe(
|
|
117
|
+
io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
|
|
118
|
+
) -> None:
|
|
119
|
+
if io is None:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if pipe:
|
|
123
|
+
if display:
|
|
124
|
+
for line in IOTimeoutStream(io):
|
|
125
|
+
pipe.send_bytes(line)
|
|
126
|
+
display.printbytes(line)
|
|
127
|
+
for line in IOTimeoutStream(io):
|
|
128
|
+
pipe.send_bytes(line)
|
|
129
|
+
|
|
130
|
+
if display:
|
|
131
|
+
for line in IOTimeoutStream(io):
|
|
132
|
+
display.printbytes(line)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _determine_subprocess_output_mode(
|
|
136
|
+
capture_mode: CaptureMode | None,
|
|
137
|
+
display_mode: DisplayMode | display.IODisplay | None,
|
|
138
|
+
output_name: Literal["stdout", "stderr"],
|
|
139
|
+
) -> int:
|
|
140
|
+
# If DisplayMode is None, we act like subprocess does
|
|
141
|
+
if display_mode is None:
|
|
142
|
+
if capture_mode == CaptureMode.PIPE:
|
|
143
|
+
return subprocess.PIPE
|
|
144
|
+
if capture_mode == CaptureMode.STDOUT:
|
|
145
|
+
if output_name == "stdout":
|
|
146
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
147
|
+
return subprocess.STDOUT
|
|
148
|
+
if capture_mode == CaptureMode.DEVNULL:
|
|
149
|
+
return subprocess.DEVNULL
|
|
150
|
+
return subprocess.PIPE
|
|
151
|
+
|
|
152
|
+
# Don't print
|
|
153
|
+
if display_mode == DisplayMode.DEVNULL:
|
|
154
|
+
if capture_mode == CaptureMode.PIPE:
|
|
155
|
+
return subprocess.PIPE
|
|
156
|
+
if capture_mode == CaptureMode.STDOUT:
|
|
157
|
+
if output_name == "stdout":
|
|
158
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
159
|
+
# stderr
|
|
160
|
+
return subprocess.STDOUT
|
|
161
|
+
if capture_mode == CaptureMode.DEVNULL:
|
|
162
|
+
return subprocess.DEVNULL
|
|
163
|
+
assert capture_mode is None
|
|
164
|
+
return subprocess.DEVNULL
|
|
165
|
+
|
|
166
|
+
# Print Output
|
|
167
|
+
# display_mode == STDOUT, STDERR, LOG, or is an IODisplay
|
|
168
|
+
if capture_mode == CaptureMode.PIPE:
|
|
169
|
+
return subprocess.PIPE
|
|
170
|
+
if capture_mode == CaptureMode.STDOUT:
|
|
171
|
+
if output_name == "stdout":
|
|
172
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
173
|
+
return subprocess.STDOUT
|
|
174
|
+
if capture_mode == CaptureMode.DEVNULL:
|
|
175
|
+
# We need to PIPE to be able to print the output
|
|
176
|
+
return subprocess.PIPE
|
|
177
|
+
assert capture_mode is None
|
|
178
|
+
# We need to PIPE to be able to print the output
|
|
179
|
+
return subprocess.PIPE
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
cmd: str | list[str],
|
|
186
|
+
popen: subprocess.Popen,
|
|
187
|
+
stdout_display: display.IODisplay | None,
|
|
188
|
+
stderr_display: display.IODisplay | None,
|
|
189
|
+
) -> None:
|
|
190
|
+
stdout_rcv, stdout_snd = Pipe(False)
|
|
191
|
+
stderr_rcv, stderr_snd = Pipe(False)
|
|
192
|
+
super().__init__(cmd, PipeReader(stdout_rcv), PipeReader(stderr_rcv))
|
|
193
|
+
self._popen = popen
|
|
194
|
+
self._display_pool = ThreadPool(2)
|
|
195
|
+
|
|
196
|
+
# Print out and capture stdout
|
|
197
|
+
self._stdout_result = self._display_pool.apply_async(
|
|
198
|
+
_output_and_pipe,
|
|
199
|
+
args=[
|
|
200
|
+
popen.stdout,
|
|
201
|
+
stdout_display,
|
|
202
|
+
stdout_snd,
|
|
203
|
+
],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Print out and capture stderr
|
|
207
|
+
self._stderr_result = self._display_pool.apply_async(
|
|
208
|
+
_output_and_pipe,
|
|
209
|
+
args=[
|
|
210
|
+
popen.stderr,
|
|
211
|
+
stderr_display,
|
|
212
|
+
stderr_snd,
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def __exit__(self, exc_type, exc, tb):
|
|
217
|
+
self._popen.__exit__(exc_type, exc, tb)
|
|
218
|
+
self._display_pool.__exit__(exc_type, exc, tb)
|
|
219
|
+
|
|
220
|
+
def close(self) -> None:
|
|
221
|
+
self.__exit__(None, None, None)
|
|
222
|
+
|
|
223
|
+
def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
|
|
224
|
+
return ShellResult(
|
|
225
|
+
cmd=self.cmd,
|
|
226
|
+
returncode=self._popen.wait(),
|
|
227
|
+
stdout=self.stdout,
|
|
228
|
+
stderr=self.stderr,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class CommonDefaultBackend(ShellBackend):
|
|
233
|
+
__slots__ = ()
|
|
234
|
+
|
|
235
|
+
def run(
|
|
236
|
+
self,
|
|
237
|
+
cmd: str | list[str],
|
|
238
|
+
*,
|
|
239
|
+
check: bool = True,
|
|
240
|
+
stdout: CaptureMode | None = None,
|
|
241
|
+
stderr: CaptureMode | None = None,
|
|
242
|
+
stdout_display: DisplayMode | display.IODisplay | None = None,
|
|
243
|
+
stderr_display: DisplayMode | display.IODisplay | None = None,
|
|
244
|
+
cwd: str | Path | None = None,
|
|
245
|
+
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
246
|
+
**kwargs,
|
|
247
|
+
) -> ShellResult[bytes | None, bytes | None]:
|
|
248
|
+
if kwargs:
|
|
249
|
+
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
250
|
+
|
|
251
|
+
# If cmd is a str, run as shell
|
|
252
|
+
shell = isinstance(cmd, str)
|
|
253
|
+
|
|
254
|
+
# Echo command based on setting
|
|
255
|
+
# Defaults to DEBUG_LOG
|
|
256
|
+
# False to disable
|
|
257
|
+
if isinstance(echo_cmd, bool):
|
|
258
|
+
echo_cmd = DisplayMode.STDOUT if echo_cmd else DisplayMode.DEVNULL
|
|
259
|
+
|
|
260
|
+
if echo_cmd is None:
|
|
261
|
+
echo_cmd = DisplayMode.DEBUG_LOG
|
|
262
|
+
|
|
263
|
+
if isinstance(echo_cmd, display.IODisplay):
|
|
264
|
+
echo_display = echo_cmd
|
|
265
|
+
else:
|
|
266
|
+
echo_display = display.from_mode(echo_cmd)
|
|
267
|
+
|
|
268
|
+
if isinstance(cmd, str):
|
|
269
|
+
echo_display.printline("%s> %s", cwd or "", cmd)
|
|
270
|
+
else:
|
|
271
|
+
echo_display.printline("%s> %s", cwd or "", shlex.join(cmd))
|
|
272
|
+
|
|
273
|
+
# Determine PIPE mode for stdout
|
|
274
|
+
stdout_sub_mode = _determine_subprocess_output_mode(
|
|
275
|
+
stdout, stdout_display, "stdout"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Determine PIPE mode for stderr
|
|
279
|
+
stderr_sub_mode = _determine_subprocess_output_mode(
|
|
280
|
+
stderr, stderr_display, "stderr"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if stdout_display is None:
|
|
284
|
+
stdout_display = (
|
|
285
|
+
DisplayMode.STDOUT if stdout is None else DisplayMode.DEVNULL
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if stderr_display is None:
|
|
289
|
+
stderr_display = (
|
|
290
|
+
DisplayMode.STDERR if stderr is None else DisplayMode.DEVNULL
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Run subprocess command
|
|
294
|
+
with (
|
|
295
|
+
subprocess.Popen(
|
|
296
|
+
cmd,
|
|
297
|
+
shell=shell,
|
|
298
|
+
stdout=stdout_sub_mode,
|
|
299
|
+
stderr=stderr_sub_mode,
|
|
300
|
+
cwd=cwd,
|
|
301
|
+
) as popen,
|
|
302
|
+
ThreadPool(2) as display_pool,
|
|
303
|
+
):
|
|
304
|
+
# Print out and capture stdout
|
|
305
|
+
stdout_result = display_pool.apply_async(
|
|
306
|
+
_output_and_collect,
|
|
307
|
+
args=[
|
|
308
|
+
popen.stdout,
|
|
309
|
+
(
|
|
310
|
+
stdout_display
|
|
311
|
+
if isinstance(stdout_display, display.IODisplay)
|
|
312
|
+
else display.from_mode(stdout_display)
|
|
313
|
+
),
|
|
314
|
+
stdout is not None and stdout != CaptureMode.DEVNULL,
|
|
315
|
+
],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Print out and capture stderr
|
|
319
|
+
stderr_result = display_pool.apply_async(
|
|
320
|
+
_output_and_collect,
|
|
321
|
+
args=[
|
|
322
|
+
popen.stderr,
|
|
323
|
+
(
|
|
324
|
+
stderr_display
|
|
325
|
+
if isinstance(stderr_display, display.IODisplay)
|
|
326
|
+
else display.from_mode(stderr_display)
|
|
327
|
+
),
|
|
328
|
+
stderr is not None and stderr != CaptureMode.DEVNULL,
|
|
329
|
+
],
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Wait on command completion
|
|
333
|
+
returncode = popen.wait()
|
|
334
|
+
|
|
335
|
+
# Build our result
|
|
336
|
+
result = ShellResult(
|
|
337
|
+
cmd=cmd,
|
|
338
|
+
stdout=stdout_result.get(),
|
|
339
|
+
stderr=stderr_result.get(),
|
|
340
|
+
returncode=returncode,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# If check is True, raise exception if returncode is not 0
|
|
344
|
+
if check:
|
|
345
|
+
result.check_returncode()
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
def run_async(
|
|
350
|
+
self,
|
|
351
|
+
cmd: str | list[str],
|
|
352
|
+
*,
|
|
353
|
+
check: bool = True,
|
|
354
|
+
stdout: CaptureMode | None = None,
|
|
355
|
+
stderr: CaptureMode | None = None,
|
|
356
|
+
stdout_display: DisplayMode | display.IODisplay | None = None,
|
|
357
|
+
stderr_display: DisplayMode | display.IODisplay | None = None,
|
|
358
|
+
cwd: str | Path | None = None,
|
|
359
|
+
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
360
|
+
**kwargs,
|
|
361
|
+
) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
|
|
362
|
+
if kwargs:
|
|
363
|
+
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
364
|
+
|
|
365
|
+
# If cmd is a str, run as shell
|
|
366
|
+
shell = isinstance(cmd, str)
|
|
367
|
+
|
|
368
|
+
# Echo command based on setting
|
|
369
|
+
# Defaults to DEBUG_LOG
|
|
370
|
+
# False to disable
|
|
371
|
+
if isinstance(echo_cmd, bool):
|
|
372
|
+
echo_cmd = DisplayMode.STDOUT if echo_cmd else DisplayMode.DEVNULL
|
|
373
|
+
|
|
374
|
+
if echo_cmd is None:
|
|
375
|
+
echo_cmd = DisplayMode.DEBUG_LOG
|
|
376
|
+
|
|
377
|
+
if isinstance(echo_cmd, display.IODisplay):
|
|
378
|
+
echo_display = echo_cmd
|
|
379
|
+
else:
|
|
380
|
+
echo_display = display.from_mode(echo_cmd)
|
|
381
|
+
|
|
382
|
+
if isinstance(cmd, str):
|
|
383
|
+
echo_display.printline("%s> %s", cwd or "", cmd)
|
|
384
|
+
else:
|
|
385
|
+
echo_display.printline("%s> %s", cwd or "", shlex.join(cmd))
|
|
386
|
+
|
|
387
|
+
# Determine PIPE mode for stdout
|
|
388
|
+
stdout_sub_mode = _determine_subprocess_output_mode(
|
|
389
|
+
stdout, stdout_display, "stdout"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Determine PIPE mode for stderr
|
|
393
|
+
stderr_sub_mode = _determine_subprocess_output_mode(
|
|
394
|
+
stderr, stderr_display, "stderr"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if stdout_display is None:
|
|
398
|
+
stdout_display = (
|
|
399
|
+
DisplayMode.STDOUT if stdout is None else DisplayMode.DEVNULL
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if stderr_display is None:
|
|
403
|
+
stderr_display = (
|
|
404
|
+
DisplayMode.STDERR if stderr is None else DisplayMode.DEVNULL
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Run subprocess command
|
|
408
|
+
popen = subprocess.Popen(
|
|
409
|
+
cmd,
|
|
410
|
+
shell=shell,
|
|
411
|
+
stdout=stdout_sub_mode,
|
|
412
|
+
stderr=stderr_sub_mode,
|
|
413
|
+
cwd=cwd,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return CommonShellAcyncResult(
|
|
417
|
+
cmd,
|
|
418
|
+
popen,
|
|
419
|
+
(
|
|
420
|
+
stdout_display
|
|
421
|
+
if isinstance(stdout_display, display.IODisplay)
|
|
422
|
+
else display.from_mode(stdout_display)
|
|
423
|
+
),
|
|
424
|
+
(
|
|
425
|
+
stderr_display
|
|
426
|
+
if isinstance(stderr_display, display.IODisplay)
|
|
427
|
+
else display.from_mode(stderr_display)
|
|
428
|
+
),
|
|
429
|
+
)
|
|
@@ -13,36 +13,36 @@ _io_display_lock = threading.RLock()
|
|
|
13
13
|
class IODisplay(ABC):
|
|
14
14
|
def printbytes(self, line: bytes):
|
|
15
15
|
with _io_display_lock:
|
|
16
|
-
self.
|
|
16
|
+
self._emit("%s", line.decode(), end="")
|
|
17
17
|
|
|
18
18
|
def printline(self, msg: object, *args: object):
|
|
19
19
|
with _io_display_lock:
|
|
20
|
-
self.
|
|
20
|
+
self._emit(msg, *args)
|
|
21
21
|
|
|
22
22
|
@abstractmethod
|
|
23
|
-
def
|
|
23
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
24
24
|
raise NotImplementedError
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class DevNullDisplay(IODisplay):
|
|
28
|
-
def
|
|
28
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class StdoutDisplay(IODisplay):
|
|
33
|
-
def
|
|
33
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
34
34
|
if args:
|
|
35
|
-
print(str(msg) % args, flush=True)
|
|
35
|
+
print(str(msg) % args, flush=True, end=end)
|
|
36
36
|
return
|
|
37
|
-
print(str(msg), flush=True)
|
|
37
|
+
print(str(msg), flush=True, end=end)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class StderrDisplay(IODisplay):
|
|
41
|
-
def
|
|
41
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
42
42
|
if args:
|
|
43
|
-
print(str(msg) % args, file=sys.stderr, flush=True)
|
|
43
|
+
print(str(msg) % args, file=sys.stderr, flush=True, end=end)
|
|
44
44
|
return
|
|
45
|
-
print(str(msg), file=sys.stderr, flush=True)
|
|
45
|
+
print(str(msg), file=sys.stderr, flush=True, end=end)
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class LogDisplay(IODisplay):
|
|
@@ -50,7 +50,7 @@ class LogDisplay(IODisplay):
|
|
|
50
50
|
super().__init__()
|
|
51
51
|
self.log_level = log_level
|
|
52
52
|
|
|
53
|
-
def
|
|
53
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
54
54
|
LOGGER.log(self.log_level, msg, *args, stacklevel=3)
|
|
55
55
|
|
|
56
56
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from io import RawIOBase
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class SSHConnectionError(Exception):
|
|
2
5
|
def __init__(
|
|
3
6
|
self, hostname: str, returncode: int, stdout: bytes | None, stderr: bytes | None
|
|
@@ -12,13 +15,13 @@ class SSHConnectionError(Exception):
|
|
|
12
15
|
self.stderr = stderr
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
class ShellException(Exception):
|
|
18
|
+
class ShellException[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None](Exception):
|
|
16
19
|
def __init__(
|
|
17
20
|
self,
|
|
18
21
|
cmd: str | list[str],
|
|
19
22
|
returncode: int,
|
|
20
|
-
stdout:
|
|
21
|
-
stderr:
|
|
23
|
+
stdout: SO,
|
|
24
|
+
stderr: SE,
|
|
22
25
|
):
|
|
23
26
|
super().__init__(f"Shell execution failed with returncode {returncode}")
|
|
24
27
|
self.cmd = cmd
|
|
@@ -9,10 +9,12 @@ from shelltastic.enum import CaptureMode, DisplayMode, SystemType
|
|
|
9
9
|
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
+
from io import RawIOBase
|
|
13
|
+
|
|
12
14
|
from shelltastic.backend.base import ShellBackend
|
|
13
15
|
from shelltastic.display import IODisplay
|
|
14
16
|
from shelltastic.host import Host
|
|
15
|
-
from shelltastic.result import ShellResult
|
|
17
|
+
from shelltastic.result import ShellResult, ShellResultAsync
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class ShellFrontend:
|
|
@@ -74,7 +76,7 @@ class ShellFrontend:
|
|
|
74
76
|
cwd: str | Path | None = None,
|
|
75
77
|
echo_cmd: DisplayMode | IODisplay | bool | None = None,
|
|
76
78
|
**kwargs,
|
|
77
|
-
) -> ShellResult:
|
|
79
|
+
) -> ShellResult[bytes | None, bytes | None]:
|
|
78
80
|
"""Run a shell command"""
|
|
79
81
|
return self._backend.run(
|
|
80
82
|
cmd,
|
|
@@ -88,6 +90,31 @@ class ShellFrontend:
|
|
|
88
90
|
**kwargs,
|
|
89
91
|
)
|
|
90
92
|
|
|
93
|
+
def run_async(
|
|
94
|
+
self,
|
|
95
|
+
cmd: str | list[str],
|
|
96
|
+
*,
|
|
97
|
+
check: bool = True,
|
|
98
|
+
stdout: CaptureMode | None = None,
|
|
99
|
+
stderr: CaptureMode | None = None,
|
|
100
|
+
stdout_display: DisplayMode | IODisplay | None = None,
|
|
101
|
+
stderr_display: DisplayMode | IODisplay | None = None,
|
|
102
|
+
cwd: str | Path | None = None,
|
|
103
|
+
echo_cmd: DisplayMode | IODisplay | bool | None = None,
|
|
104
|
+
**kwargs,
|
|
105
|
+
) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
|
|
106
|
+
return self._backend.run_async(
|
|
107
|
+
cmd,
|
|
108
|
+
check=check,
|
|
109
|
+
stdout=stdout,
|
|
110
|
+
stderr=stderr,
|
|
111
|
+
stdout_display=stdout_display,
|
|
112
|
+
stderr_display=stderr_display,
|
|
113
|
+
cwd=cwd,
|
|
114
|
+
echo_cmd=echo_cmd,
|
|
115
|
+
**kwargs,
|
|
116
|
+
)
|
|
117
|
+
|
|
91
118
|
|
|
92
119
|
class RemoteShellFrontend(ShellFrontend, RemoteFrontend):
|
|
93
120
|
__slots__ = ()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from io import RawIOBase
|
|
2
|
+
from multiprocessing.connection import Connection
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PipeReader(RawIOBase):
|
|
6
|
+
def __init__(self, receiver: Connection) -> None:
|
|
7
|
+
super().__init__()
|
|
8
|
+
if not receiver.readable:
|
|
9
|
+
raise TypeError("Receiver Connection must be readable")
|
|
10
|
+
self.receiver = receiver
|
|
11
|
+
self._last_byte_read: bytes | None = None
|
|
12
|
+
|
|
13
|
+
def readinto(self, buffer) -> int:
|
|
14
|
+
# Fill buffer or flush last read
|
|
15
|
+
with memoryview(buffer) as buf:
|
|
16
|
+
# If there's nothing in our buffer, read bytes
|
|
17
|
+
if self._last_byte_read is None:
|
|
18
|
+
try:
|
|
19
|
+
self._last_byte_read = self.receiver.recv_bytes()
|
|
20
|
+
except EOFError:
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
if len(buf) >= len(self._last_byte_read):
|
|
24
|
+
# Buffer is bigger than read bytes
|
|
25
|
+
size = len(self._last_byte_read)
|
|
26
|
+
buf[0:size] = self._last_byte_read
|
|
27
|
+
self._last_byte_read = None
|
|
28
|
+
return size
|
|
29
|
+
|
|
30
|
+
# More bytes read than size of buffer
|
|
31
|
+
size = len(buf)
|
|
32
|
+
buf[0:size] = self._last_byte_read[0:size]
|
|
33
|
+
self._last_byte_read = self._last_byte_read[size:]
|
|
34
|
+
return size
|
|
35
|
+
|
|
36
|
+
def readall(self) -> bytes:
|
|
37
|
+
# Read until EOF
|
|
38
|
+
if self._last_byte_read is None:
|
|
39
|
+
self._last_byte_read = b""
|
|
40
|
+
while True:
|
|
41
|
+
try:
|
|
42
|
+
self._last_byte_read = self._last_byte_read + self.receiver.recv_bytes()
|
|
43
|
+
except EOFError:
|
|
44
|
+
return self._last_byte_read
|
|
45
|
+
|
|
46
|
+
def readable(self) -> bool:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def writable(self) -> bool:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def seekable(self) -> bool:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def fileno(self) -> int:
|
|
56
|
+
return self.receiver.fileno()
|
|
57
|
+
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
return self.receiver.close()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from io import RawIOBase
|
|
4
|
+
|
|
5
|
+
from shelltastic.exception import ShellException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
|
|
10
|
+
cmd: str | list[str]
|
|
11
|
+
returncode: int
|
|
12
|
+
stdout: SO
|
|
13
|
+
stderr: SE
|
|
14
|
+
|
|
15
|
+
def check_returncode(self):
|
|
16
|
+
"""If returncode is not 0, raise ShellException"""
|
|
17
|
+
if self.returncode != 0:
|
|
18
|
+
raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
|
|
22
|
+
def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
|
|
23
|
+
self.cmd = cmd
|
|
24
|
+
self.stdout = stdout
|
|
25
|
+
self.stderr = stderr
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, exc_type, exc, tb):
|
|
31
|
+
self.close()
|
|
32
|
+
|
|
33
|
+
def close(self) -> None:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def result(self) -> ShellResult[SO, SE]:
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
def wait(self) -> None:
|
|
41
|
+
self.result()
|
|
42
|
+
|
|
43
|
+
def returncode(self) -> int:
|
|
44
|
+
return self.result().returncode
|
|
45
|
+
|
|
46
|
+
def check_returncode(self):
|
|
47
|
+
self.result().check_returncode()
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import shlex
|
|
5
|
-
import subprocess
|
|
6
|
-
from multiprocessing.pool import ThreadPool
|
|
7
|
-
from typing import IO, TYPE_CHECKING, Literal
|
|
8
|
-
|
|
9
|
-
from shelltastic import display
|
|
10
|
-
from shelltastic.backend.base import ShellBackend
|
|
11
|
-
from shelltastic.enum import CaptureMode, DisplayMode
|
|
12
|
-
from shelltastic.result import ShellResult
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
LOGGER = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _output_and_collect(
|
|
21
|
-
io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
|
|
22
|
-
) -> bytes | None:
|
|
23
|
-
if io is None:
|
|
24
|
-
return None
|
|
25
|
-
|
|
26
|
-
if collect:
|
|
27
|
-
if display:
|
|
28
|
-
return_bytes: bytes = b""
|
|
29
|
-
for line in io:
|
|
30
|
-
return_bytes += line
|
|
31
|
-
display.printbytes(line)
|
|
32
|
-
return return_bytes
|
|
33
|
-
return io.read()
|
|
34
|
-
|
|
35
|
-
if display:
|
|
36
|
-
for line in io:
|
|
37
|
-
display.printbytes(line)
|
|
38
|
-
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _determine_subprocess_output_mode(
|
|
43
|
-
capture_mode: CaptureMode | None,
|
|
44
|
-
display_mode: DisplayMode | display.IODisplay | None,
|
|
45
|
-
output_name: Literal["stdout", "stderr"],
|
|
46
|
-
) -> int:
|
|
47
|
-
# If DisplayMode is None, we act like subprocess does
|
|
48
|
-
if display_mode is None:
|
|
49
|
-
if capture_mode == CaptureMode.PIPE:
|
|
50
|
-
return subprocess.PIPE
|
|
51
|
-
if capture_mode == CaptureMode.STDOUT:
|
|
52
|
-
return subprocess.STDOUT
|
|
53
|
-
if capture_mode == CaptureMode.DEVNULL:
|
|
54
|
-
return subprocess.DEVNULL
|
|
55
|
-
return subprocess.PIPE
|
|
56
|
-
|
|
57
|
-
# Don't print
|
|
58
|
-
if display_mode == DisplayMode.DEVNULL:
|
|
59
|
-
if capture_mode == CaptureMode.PIPE:
|
|
60
|
-
return subprocess.PIPE
|
|
61
|
-
if capture_mode == CaptureMode.STDOUT:
|
|
62
|
-
if output_name == "stdout":
|
|
63
|
-
# stdout
|
|
64
|
-
return subprocess.PIPE
|
|
65
|
-
# stderr
|
|
66
|
-
return subprocess.STDOUT
|
|
67
|
-
if capture_mode == CaptureMode.DEVNULL:
|
|
68
|
-
return subprocess.DEVNULL
|
|
69
|
-
assert capture_mode is None
|
|
70
|
-
return subprocess.DEVNULL
|
|
71
|
-
|
|
72
|
-
# Print Output
|
|
73
|
-
# display_mode == STDOUT, STDERR, LOG, or is an IODisplay
|
|
74
|
-
if capture_mode == CaptureMode.PIPE:
|
|
75
|
-
return subprocess.PIPE
|
|
76
|
-
if capture_mode == CaptureMode.STDOUT:
|
|
77
|
-
return subprocess.STDOUT
|
|
78
|
-
if capture_mode == CaptureMode.DEVNULL:
|
|
79
|
-
# We need to PIPE to be able to print the output
|
|
80
|
-
return subprocess.PIPE
|
|
81
|
-
assert capture_mode is None
|
|
82
|
-
# We need to PIPE to be able to print the output
|
|
83
|
-
return subprocess.PIPE
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class CommonDefaultBackend(ShellBackend):
|
|
87
|
-
__slots__ = ()
|
|
88
|
-
|
|
89
|
-
def run(
|
|
90
|
-
self,
|
|
91
|
-
cmd: str | list[str],
|
|
92
|
-
*,
|
|
93
|
-
check: bool = True,
|
|
94
|
-
stdout: CaptureMode | None = None,
|
|
95
|
-
stderr: CaptureMode | None = None,
|
|
96
|
-
stdout_display: DisplayMode | display.IODisplay | None = None,
|
|
97
|
-
stderr_display: DisplayMode | display.IODisplay | None = None,
|
|
98
|
-
cwd: str | Path | None = None,
|
|
99
|
-
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
100
|
-
**kwargs,
|
|
101
|
-
) -> ShellResult:
|
|
102
|
-
if kwargs:
|
|
103
|
-
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
104
|
-
|
|
105
|
-
# If cmd is a str, run as shell
|
|
106
|
-
shell = isinstance(cmd, str)
|
|
107
|
-
|
|
108
|
-
# Echo command based on setting
|
|
109
|
-
# Defaults to DEBUG_LOG
|
|
110
|
-
# False to disable
|
|
111
|
-
if isinstance(echo_cmd, bool):
|
|
112
|
-
echo_cmd = DisplayMode.STDOUT if echo_cmd else DisplayMode.DEVNULL
|
|
113
|
-
|
|
114
|
-
if echo_cmd is None:
|
|
115
|
-
echo_cmd = DisplayMode.DEBUG_LOG
|
|
116
|
-
|
|
117
|
-
if isinstance(echo_cmd, display.IODisplay):
|
|
118
|
-
echo_display = echo_cmd
|
|
119
|
-
else:
|
|
120
|
-
echo_display = display.from_mode(echo_cmd)
|
|
121
|
-
|
|
122
|
-
if isinstance(cmd, str):
|
|
123
|
-
echo_display.printline("%s> %s", cwd or "", cmd)
|
|
124
|
-
else:
|
|
125
|
-
echo_display.printline("%s> %s", cwd or "", shlex.join(cmd))
|
|
126
|
-
|
|
127
|
-
# Determine PIPE mode for stdout
|
|
128
|
-
stdout_sub_mode = _determine_subprocess_output_mode(
|
|
129
|
-
stdout, stdout_display, "stdout"
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
# Determine PIPE mode for stderr
|
|
133
|
-
stderr_sub_mode = _determine_subprocess_output_mode(
|
|
134
|
-
stderr, stderr_display, "stderr"
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
if stdout_display is None:
|
|
138
|
-
stdout_display = (
|
|
139
|
-
DisplayMode.STDOUT if stdout is None else DisplayMode.DEVNULL
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
if stderr_display is None:
|
|
143
|
-
stderr_display = (
|
|
144
|
-
DisplayMode.STDERR if stderr is None else DisplayMode.DEVNULL
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
# Run subprocess command
|
|
148
|
-
with (
|
|
149
|
-
subprocess.Popen(
|
|
150
|
-
cmd,
|
|
151
|
-
shell=shell,
|
|
152
|
-
stdout=stdout_sub_mode,
|
|
153
|
-
stderr=stderr_sub_mode,
|
|
154
|
-
cwd=cwd,
|
|
155
|
-
) as popen,
|
|
156
|
-
ThreadPool(2) as display_pool,
|
|
157
|
-
):
|
|
158
|
-
# Print out and capture stdout
|
|
159
|
-
stdout_result = display_pool.apply_async(
|
|
160
|
-
_output_and_collect,
|
|
161
|
-
args=[
|
|
162
|
-
popen.stdout,
|
|
163
|
-
(
|
|
164
|
-
stdout_display
|
|
165
|
-
if isinstance(stdout_display, display.IODisplay)
|
|
166
|
-
else display.from_mode(stdout_display)
|
|
167
|
-
),
|
|
168
|
-
stdout is not None and stdout != CaptureMode.DEVNULL,
|
|
169
|
-
],
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# Print out and capture stderr
|
|
173
|
-
stderr_result = display_pool.apply_async(
|
|
174
|
-
_output_and_collect,
|
|
175
|
-
args=[
|
|
176
|
-
popen.stderr,
|
|
177
|
-
(
|
|
178
|
-
stderr_display
|
|
179
|
-
if isinstance(stderr_display, display.IODisplay)
|
|
180
|
-
else display.from_mode(stderr_display)
|
|
181
|
-
),
|
|
182
|
-
stderr is not None and stderr != CaptureMode.DEVNULL,
|
|
183
|
-
],
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# Wait on command completion
|
|
187
|
-
returncode = popen.wait()
|
|
188
|
-
|
|
189
|
-
# Build our result
|
|
190
|
-
result = ShellResult(
|
|
191
|
-
cmd=cmd,
|
|
192
|
-
stdout=stdout_result.get(),
|
|
193
|
-
stderr=stderr_result.get(),
|
|
194
|
-
returncode=returncode,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
# If check is True, raise exception if returncode is not 0
|
|
198
|
-
if check:
|
|
199
|
-
result.check_returncode()
|
|
200
|
-
|
|
201
|
-
return result
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
|
|
3
|
-
from shelltastic.exception import ShellException
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class ShellResult:
|
|
8
|
-
cmd: str | list[str]
|
|
9
|
-
returncode: int
|
|
10
|
-
stdout: bytes | None
|
|
11
|
-
stderr: bytes | None
|
|
12
|
-
|
|
13
|
-
def check_returncode(self):
|
|
14
|
-
"""If returncode is not 0, raise ShellException"""
|
|
15
|
-
if self.returncode != 0:
|
|
16
|
-
raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|