shelltastic 0.5.0__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.5.0 → shelltastic-0.6.0}/PKG-INFO +1 -1
- {shelltastic-0.5.0 → shelltastic-0.6.0}/pyproject.toml +13 -1
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/backend/base.py +47 -6
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/backend/default/common.py +162 -4
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/exception.py +6 -3
- {shelltastic-0.5.0 → 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.5.0/src/shelltastic/result.py +0 -16
- {shelltastic-0.5.0 → shelltastic-0.6.0}/LICENSE +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/README.md +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/backend/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/backend/default/local.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/backend/default/remote.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/display.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.5.0 → shelltastic-0.6.0}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.5.0 → 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
|
|
|
@@ -5,15 +5,19 @@ import os
|
|
|
5
5
|
import shlex
|
|
6
6
|
import subprocess
|
|
7
7
|
import time
|
|
8
|
+
from io import RawIOBase
|
|
9
|
+
from multiprocessing import Pipe
|
|
8
10
|
from multiprocessing.pool import ThreadPool
|
|
9
11
|
from typing import IO, TYPE_CHECKING, 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.
|
|
16
|
+
from shelltastic.io import PipeReader
|
|
17
|
+
from shelltastic.result import ShellResult, ShellResultAsync
|
|
15
18
|
|
|
16
19
|
if TYPE_CHECKING:
|
|
20
|
+
from multiprocessing.connection import Connection
|
|
17
21
|
from pathlib import Path
|
|
18
22
|
|
|
19
23
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -109,6 +113,25 @@ def _output_and_collect(
|
|
|
109
113
|
return None
|
|
110
114
|
|
|
111
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
|
+
|
|
112
135
|
def _determine_subprocess_output_mode(
|
|
113
136
|
capture_mode: CaptureMode | None,
|
|
114
137
|
display_mode: DisplayMode | display.IODisplay | None,
|
|
@@ -119,6 +142,8 @@ def _determine_subprocess_output_mode(
|
|
|
119
142
|
if capture_mode == CaptureMode.PIPE:
|
|
120
143
|
return subprocess.PIPE
|
|
121
144
|
if capture_mode == CaptureMode.STDOUT:
|
|
145
|
+
if output_name == "stdout":
|
|
146
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
122
147
|
return subprocess.STDOUT
|
|
123
148
|
if capture_mode == CaptureMode.DEVNULL:
|
|
124
149
|
return subprocess.DEVNULL
|
|
@@ -130,8 +155,7 @@ def _determine_subprocess_output_mode(
|
|
|
130
155
|
return subprocess.PIPE
|
|
131
156
|
if capture_mode == CaptureMode.STDOUT:
|
|
132
157
|
if output_name == "stdout":
|
|
133
|
-
|
|
134
|
-
return subprocess.PIPE
|
|
158
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
135
159
|
# stderr
|
|
136
160
|
return subprocess.STDOUT
|
|
137
161
|
if capture_mode == CaptureMode.DEVNULL:
|
|
@@ -144,6 +168,8 @@ def _determine_subprocess_output_mode(
|
|
|
144
168
|
if capture_mode == CaptureMode.PIPE:
|
|
145
169
|
return subprocess.PIPE
|
|
146
170
|
if capture_mode == CaptureMode.STDOUT:
|
|
171
|
+
if output_name == "stdout":
|
|
172
|
+
raise ValueError("stdout cannot be piped to stdout")
|
|
147
173
|
return subprocess.STDOUT
|
|
148
174
|
if capture_mode == CaptureMode.DEVNULL:
|
|
149
175
|
# We need to PIPE to be able to print the output
|
|
@@ -153,6 +179,56 @@ def _determine_subprocess_output_mode(
|
|
|
153
179
|
return subprocess.PIPE
|
|
154
180
|
|
|
155
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
|
+
|
|
156
232
|
class CommonDefaultBackend(ShellBackend):
|
|
157
233
|
__slots__ = ()
|
|
158
234
|
|
|
@@ -168,7 +244,7 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
168
244
|
cwd: str | Path | None = None,
|
|
169
245
|
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
170
246
|
**kwargs,
|
|
171
|
-
) -> ShellResult:
|
|
247
|
+
) -> ShellResult[bytes | None, bytes | None]:
|
|
172
248
|
if kwargs:
|
|
173
249
|
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
174
250
|
|
|
@@ -269,3 +345,85 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
269
345
|
result.check_returncode()
|
|
270
346
|
|
|
271
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
|
+
)
|
|
@@ -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,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
|
|
File without changes
|