shelltastic 0.5.0__tar.gz → 0.7.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.5.0 → shelltastic-0.7.0}/PKG-INFO +1 -1
- {shelltastic-0.5.0 → shelltastic-0.7.0}/pyproject.toml +13 -1
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/backend/base.py +47 -6
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/common.py +200 -79
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/local.py +0 -2
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/remote.py +3 -9
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/exception.py +6 -3
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/frontend/shell.py +29 -2
- shelltastic-0.7.0/src/shelltastic/io.py +59 -0
- shelltastic-0.7.0/src/shelltastic/result.py +101 -0
- shelltastic-0.5.0/src/shelltastic/result.py +0 -16
- {shelltastic-0.5.0 → shelltastic-0.7.0}/LICENSE +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/README.md +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/backend/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/display.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.7.0}/src/shelltastic/host.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shelltastic"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.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
|
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import logging
|
|
4
2
|
import os
|
|
5
3
|
import shlex
|
|
6
4
|
import subprocess
|
|
7
5
|
import time
|
|
6
|
+
from io import RawIOBase
|
|
7
|
+
from multiprocessing import Pipe
|
|
8
|
+
from multiprocessing.connection import Connection
|
|
8
9
|
from multiprocessing.pool import ThreadPool
|
|
9
|
-
from
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import IO, Literal
|
|
10
12
|
|
|
11
13
|
from shelltastic import display
|
|
12
14
|
from shelltastic.backend.base import ShellBackend
|
|
13
15
|
from shelltastic.enum import CaptureMode, DisplayMode
|
|
14
|
-
from shelltastic.
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from pathlib import Path
|
|
16
|
+
from shelltastic.io import PipeReader
|
|
17
|
+
from shelltastic.result import ShellResult, ShellResultAsync
|
|
18
18
|
|
|
19
19
|
LOGGER = logging.getLogger(__name__)
|
|
20
20
|
|
|
@@ -87,26 +87,27 @@ class IOTimeoutStream:
|
|
|
87
87
|
return return_val
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def
|
|
91
|
-
io: IO[bytes] | None, display: display.IODisplay | None,
|
|
92
|
-
) ->
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
def _output_and_pipe(
|
|
91
|
+
io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
|
|
92
|
+
) -> None:
|
|
93
|
+
try:
|
|
94
|
+
if io is None:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
if pipe:
|
|
98
|
+
if display:
|
|
99
|
+
for line in IOTimeoutStream(io):
|
|
100
|
+
pipe.send_bytes(line)
|
|
101
|
+
display.printbytes(line)
|
|
102
|
+
for line in IOTimeoutStream(io):
|
|
103
|
+
pipe.send_bytes(line)
|
|
95
104
|
|
|
96
|
-
if collect:
|
|
97
105
|
if display:
|
|
98
|
-
return_bytes: bytes = b""
|
|
99
106
|
for line in IOTimeoutStream(io):
|
|
100
|
-
return_bytes += line
|
|
101
107
|
display.printbytes(line)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if display:
|
|
106
|
-
for line in IOTimeoutStream(io):
|
|
107
|
-
display.printbytes(line)
|
|
108
|
-
|
|
109
|
-
return None
|
|
108
|
+
finally:
|
|
109
|
+
if pipe:
|
|
110
|
+
pipe.close()
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
def _determine_subprocess_output_mode(
|
|
@@ -119,6 +120,8 @@ def _determine_subprocess_output_mode(
|
|
|
119
120
|
if capture_mode == CaptureMode.PIPE:
|
|
120
121
|
return subprocess.PIPE
|
|
121
122
|
if capture_mode == CaptureMode.STDOUT:
|
|
123
|
+
if output_name == "stdout":
|
|
124
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
122
125
|
return subprocess.STDOUT
|
|
123
126
|
if capture_mode == CaptureMode.DEVNULL:
|
|
124
127
|
return subprocess.DEVNULL
|
|
@@ -130,8 +133,7 @@ def _determine_subprocess_output_mode(
|
|
|
130
133
|
return subprocess.PIPE
|
|
131
134
|
if capture_mode == CaptureMode.STDOUT:
|
|
132
135
|
if output_name == "stdout":
|
|
133
|
-
|
|
134
|
-
return subprocess.PIPE
|
|
136
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
135
137
|
# stderr
|
|
136
138
|
return subprocess.STDOUT
|
|
137
139
|
if capture_mode == CaptureMode.DEVNULL:
|
|
@@ -144,6 +146,8 @@ def _determine_subprocess_output_mode(
|
|
|
144
146
|
if capture_mode == CaptureMode.PIPE:
|
|
145
147
|
return subprocess.PIPE
|
|
146
148
|
if capture_mode == CaptureMode.STDOUT:
|
|
149
|
+
if output_name == "stdout":
|
|
150
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
147
151
|
return subprocess.STDOUT
|
|
148
152
|
if capture_mode == CaptureMode.DEVNULL:
|
|
149
153
|
# We need to PIPE to be able to print the output
|
|
@@ -153,6 +157,126 @@ def _determine_subprocess_output_mode(
|
|
|
153
157
|
return subprocess.PIPE
|
|
154
158
|
|
|
155
159
|
|
|
160
|
+
class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
cmd: str | list[str],
|
|
164
|
+
popen: subprocess.Popen,
|
|
165
|
+
stdout_capture: CaptureMode | None,
|
|
166
|
+
stderr_capture: CaptureMode | None,
|
|
167
|
+
stdout_display: display.IODisplay | None,
|
|
168
|
+
stderr_display: display.IODisplay | None,
|
|
169
|
+
check: bool,
|
|
170
|
+
) -> None:
|
|
171
|
+
_display_pool = None
|
|
172
|
+
_stdout_async_result = None
|
|
173
|
+
_stderr_async_result = None
|
|
174
|
+
|
|
175
|
+
if popen.stdout:
|
|
176
|
+
# If stdout is PIPE, then setup reader
|
|
177
|
+
if stdout_capture == CaptureMode.PIPE:
|
|
178
|
+
stdout_rcv, stdout_snd = Pipe(False)
|
|
179
|
+
stdout = PipeReader(stdout_rcv)
|
|
180
|
+
else:
|
|
181
|
+
stdout_snd = None
|
|
182
|
+
stdout = None
|
|
183
|
+
|
|
184
|
+
if stdout_snd or stdout_display:
|
|
185
|
+
if _display_pool is None:
|
|
186
|
+
_display_pool = ThreadPool(2)
|
|
187
|
+
|
|
188
|
+
_stdout_async_result=_display_pool.apply_async(
|
|
189
|
+
_output_and_pipe,
|
|
190
|
+
args=[
|
|
191
|
+
popen.stdout,
|
|
192
|
+
stdout_display,
|
|
193
|
+
stdout_snd,
|
|
194
|
+
],
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
stdout = None
|
|
198
|
+
|
|
199
|
+
if popen.stderr:
|
|
200
|
+
if stderr_capture == CaptureMode.PIPE:
|
|
201
|
+
stderr_rcv, stderr_snd = Pipe(False)
|
|
202
|
+
stderr = PipeReader(stderr_rcv)
|
|
203
|
+
else:
|
|
204
|
+
stderr_snd = None
|
|
205
|
+
stderr = None
|
|
206
|
+
|
|
207
|
+
if stderr_snd or stderr_display:
|
|
208
|
+
if _display_pool is None:
|
|
209
|
+
_display_pool = ThreadPool(2)
|
|
210
|
+
|
|
211
|
+
_stderr_async_result = _display_pool.apply_async(
|
|
212
|
+
_output_and_pipe,
|
|
213
|
+
args=[
|
|
214
|
+
popen.stderr,
|
|
215
|
+
stderr_display,
|
|
216
|
+
stderr_snd,
|
|
217
|
+
],
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
stderr = None
|
|
221
|
+
|
|
222
|
+
super().__init__(cmd, stdout, stderr)
|
|
223
|
+
|
|
224
|
+
self._display_pool = _display_pool
|
|
225
|
+
self._stdout_async_result = _stdout_async_result
|
|
226
|
+
self._stderr_async_result = _stderr_async_result
|
|
227
|
+
self._popen = popen
|
|
228
|
+
self._check = check
|
|
229
|
+
|
|
230
|
+
def __exit__(self, exc_type, exc, tb):
|
|
231
|
+
# Wait for command to finish before closing everything
|
|
232
|
+
self.wait()
|
|
233
|
+
|
|
234
|
+
print("closing popen")
|
|
235
|
+
# Close popen
|
|
236
|
+
self._popen.__exit__(exc_type, exc, tb)
|
|
237
|
+
|
|
238
|
+
if self._display_pool:
|
|
239
|
+
print("closing _display_pool")
|
|
240
|
+
self._display_pool.__exit__(exc_type, exc, tb)
|
|
241
|
+
|
|
242
|
+
# Close stdout and stderr
|
|
243
|
+
if self.stdout:
|
|
244
|
+
print("closing stdout")
|
|
245
|
+
self.stdout.close()
|
|
246
|
+
if self.stderr:
|
|
247
|
+
print("closing stderr")
|
|
248
|
+
self.stderr.close()
|
|
249
|
+
|
|
250
|
+
def close(self) -> None:
|
|
251
|
+
self.__exit__(None, None, None)
|
|
252
|
+
|
|
253
|
+
def terminate(self) -> None:
|
|
254
|
+
self._popen.terminate()
|
|
255
|
+
self.close()
|
|
256
|
+
|
|
257
|
+
def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
|
|
258
|
+
# Get result
|
|
259
|
+
result = ShellResult(
|
|
260
|
+
cmd=self.cmd,
|
|
261
|
+
returncode=self._popen.wait(),
|
|
262
|
+
stdout=self.stdout,
|
|
263
|
+
stderr=self.stderr,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Wait for our async reader to be complete
|
|
267
|
+
if self._stdout_async_result:
|
|
268
|
+
self._stdout_async_result.wait()
|
|
269
|
+
|
|
270
|
+
if self._stderr_async_result:
|
|
271
|
+
self._stderr_async_result.wait()
|
|
272
|
+
|
|
273
|
+
# Check if set
|
|
274
|
+
if self._check:
|
|
275
|
+
result.check_returncode()
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
|
|
156
280
|
class CommonDefaultBackend(ShellBackend):
|
|
157
281
|
__slots__ = ()
|
|
158
282
|
|
|
@@ -168,7 +292,33 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
168
292
|
cwd: str | Path | None = None,
|
|
169
293
|
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
170
294
|
**kwargs,
|
|
171
|
-
) -> ShellResult:
|
|
295
|
+
) -> ShellResult[bytes | None, bytes | None]:
|
|
296
|
+
with self.run_async(
|
|
297
|
+
cmd,
|
|
298
|
+
check=check,
|
|
299
|
+
stdout=stdout,
|
|
300
|
+
stderr=stderr,
|
|
301
|
+
stdout_display=stdout_display,
|
|
302
|
+
stderr_display=stderr_display,
|
|
303
|
+
cwd=cwd,
|
|
304
|
+
echo_cmd=echo_cmd,
|
|
305
|
+
**kwargs,
|
|
306
|
+
) as running_cmd:
|
|
307
|
+
return running_cmd.result_bytes()
|
|
308
|
+
|
|
309
|
+
def run_async(
|
|
310
|
+
self,
|
|
311
|
+
cmd: str | list[str],
|
|
312
|
+
*,
|
|
313
|
+
check: bool = True,
|
|
314
|
+
stdout: CaptureMode | None = None,
|
|
315
|
+
stderr: CaptureMode | None = None,
|
|
316
|
+
stdout_display: DisplayMode | display.IODisplay | None = None,
|
|
317
|
+
stderr_display: DisplayMode | display.IODisplay | None = None,
|
|
318
|
+
cwd: str | Path | None = None,
|
|
319
|
+
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
320
|
+
**kwargs,
|
|
321
|
+
) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
|
|
172
322
|
if kwargs:
|
|
173
323
|
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
174
324
|
|
|
@@ -215,57 +365,28 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
215
365
|
)
|
|
216
366
|
|
|
217
367
|
# Run subprocess command
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
) as popen,
|
|
226
|
-
ThreadPool(2) as display_pool,
|
|
227
|
-
):
|
|
228
|
-
# Print out and capture stdout
|
|
229
|
-
stdout_result = display_pool.apply_async(
|
|
230
|
-
_output_and_collect,
|
|
231
|
-
args=[
|
|
232
|
-
popen.stdout,
|
|
233
|
-
(
|
|
234
|
-
stdout_display
|
|
235
|
-
if isinstance(stdout_display, display.IODisplay)
|
|
236
|
-
else display.from_mode(stdout_display)
|
|
237
|
-
),
|
|
238
|
-
stdout is not None and stdout != CaptureMode.DEVNULL,
|
|
239
|
-
],
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
# Print out and capture stderr
|
|
243
|
-
stderr_result = display_pool.apply_async(
|
|
244
|
-
_output_and_collect,
|
|
245
|
-
args=[
|
|
246
|
-
popen.stderr,
|
|
247
|
-
(
|
|
248
|
-
stderr_display
|
|
249
|
-
if isinstance(stderr_display, display.IODisplay)
|
|
250
|
-
else display.from_mode(stderr_display)
|
|
251
|
-
),
|
|
252
|
-
stderr is not None and stderr != CaptureMode.DEVNULL,
|
|
253
|
-
],
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
# Wait on command completion
|
|
257
|
-
returncode = popen.wait()
|
|
258
|
-
|
|
259
|
-
# Build our result
|
|
260
|
-
result = ShellResult(
|
|
261
|
-
cmd=cmd,
|
|
262
|
-
stdout=stdout_result.get(),
|
|
263
|
-
stderr=stderr_result.get(),
|
|
264
|
-
returncode=returncode,
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
# If check is True, raise exception if returncode is not 0
|
|
268
|
-
if check:
|
|
269
|
-
result.check_returncode()
|
|
368
|
+
popen = subprocess.Popen(
|
|
369
|
+
cmd,
|
|
370
|
+
shell=shell,
|
|
371
|
+
stdout=stdout_sub_mode,
|
|
372
|
+
stderr=stderr_sub_mode,
|
|
373
|
+
cwd=cwd,
|
|
374
|
+
)
|
|
270
375
|
|
|
271
|
-
return
|
|
376
|
+
return CommonShellAcyncResult(
|
|
377
|
+
cmd=cmd,
|
|
378
|
+
popen=popen,
|
|
379
|
+
stdout_capture=stdout,
|
|
380
|
+
stderr_capture=stderr,
|
|
381
|
+
stdout_display=(
|
|
382
|
+
stdout_display
|
|
383
|
+
if isinstance(stdout_display, display.IODisplay)
|
|
384
|
+
else display.from_mode(stdout_display)
|
|
385
|
+
),
|
|
386
|
+
stderr_display=(
|
|
387
|
+
stderr_display
|
|
388
|
+
if isinstance(stderr_display, display.IODisplay)
|
|
389
|
+
else display.from_mode(stderr_display)
|
|
390
|
+
),
|
|
391
|
+
check=check,
|
|
392
|
+
)
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import logging
|
|
4
2
|
import shlex
|
|
5
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
6
4
|
|
|
7
5
|
from shelltastic.backend.base import RemoteShellBackend
|
|
8
6
|
from shelltastic.backend.default.common import CommonDefaultBackend
|
|
9
7
|
from shelltastic.enum import CaptureMode
|
|
10
8
|
from shelltastic.exception import ShellException, SSHConnectionError
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
from shelltastic.host import Host
|
|
16
|
-
from shelltastic.result import ShellResult
|
|
9
|
+
from shelltastic.host import Host
|
|
10
|
+
from shelltastic.result import ShellResult
|
|
17
11
|
|
|
18
12
|
LOGGER = logging.getLogger(__name__)
|
|
19
13
|
|
|
@@ -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,101 @@
|
|
|
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
|
+
"""
|
|
23
|
+
An asynchronsly running shell command.
|
|
24
|
+
|
|
25
|
+
cmd: String for the running shell command.
|
|
26
|
+
|
|
27
|
+
stdout: Bytes stream for stdout
|
|
28
|
+
|
|
29
|
+
stderr: Bytes stream for stderr
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
|
|
33
|
+
self.cmd = cmd
|
|
34
|
+
"""The running command"""
|
|
35
|
+
|
|
36
|
+
self.stdout = stdout
|
|
37
|
+
"""Byte based IO stream for stdout"""
|
|
38
|
+
|
|
39
|
+
self.stderr = stderr
|
|
40
|
+
"""Byte based IO stream for stderr"""
|
|
41
|
+
|
|
42
|
+
def __enter__(self):
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def __exit__(self, exc_type, exc, tb):
|
|
46
|
+
self.close()
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
"""Close the running command."""
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
def terminate(self) -> None:
|
|
53
|
+
"""Terminate the running command"""
|
|
54
|
+
raise NotImplementedError()
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def result(self) -> ShellResult[SO, SE]:
|
|
58
|
+
"""
|
|
59
|
+
Blocking
|
|
60
|
+
|
|
61
|
+
Wait for the command to finish and return a ShellResult with the outcome.
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError()
|
|
64
|
+
|
|
65
|
+
def wait(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Blocking
|
|
68
|
+
|
|
69
|
+
Wait for the command to finish.
|
|
70
|
+
"""
|
|
71
|
+
self.result()
|
|
72
|
+
|
|
73
|
+
def returncode(self) -> int:
|
|
74
|
+
"""
|
|
75
|
+
Blocking
|
|
76
|
+
|
|
77
|
+
Get the returncode of the command.
|
|
78
|
+
"""
|
|
79
|
+
return self.result().returncode
|
|
80
|
+
|
|
81
|
+
def check_returncode(self):
|
|
82
|
+
"""
|
|
83
|
+
Blocking
|
|
84
|
+
|
|
85
|
+
Raise a ShellException if the returncode is not zero.
|
|
86
|
+
"""
|
|
87
|
+
self.result().check_returncode()
|
|
88
|
+
|
|
89
|
+
def result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
|
|
90
|
+
result = self.result()
|
|
91
|
+
stdout = result.stdout
|
|
92
|
+
stdout_bytes = stdout.readall() if stdout is not None else None
|
|
93
|
+
stderr = result.stderr
|
|
94
|
+
stderr_bytes = stderr.readall() if stderr is not None else None
|
|
95
|
+
|
|
96
|
+
return ShellResult(
|
|
97
|
+
cmd=result.cmd,
|
|
98
|
+
returncode=result.returncode,
|
|
99
|
+
stdout=stdout_bytes,
|
|
100
|
+
stderr=stderr_bytes,
|
|
101
|
+
)
|
|
@@ -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
|