shelltastic 0.7.0__tar.gz → 0.8.1__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.7.0 → shelltastic-0.8.1}/PKG-INFO +1 -1
- {shelltastic-0.7.0 → shelltastic-0.8.1}/pyproject.toml +1 -1
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/__init__.py +2 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/base.py +18 -2
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/default/common.py +6 -10
- shelltastic-0.8.1/src/shelltastic/backend/default/remote.py +147 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/exception.py +3 -6
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/result.py +11 -1
- shelltastic-0.7.0/src/shelltastic/backend/default/remote.py +0 -69
- {shelltastic-0.7.0 → shelltastic-0.8.1}/LICENSE +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/README.md +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/backend/default/local.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/display.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/frontend/shell.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/host.py +0 -0
- {shelltastic-0.7.0 → shelltastic-0.8.1}/src/shelltastic/io.py +0 -0
|
@@ -4,6 +4,7 @@ from shelltastic.backend.base import (
|
|
|
4
4
|
RemoteShellBackend,
|
|
5
5
|
ShellBackend,
|
|
6
6
|
add_shim,
|
|
7
|
+
clear_shims,
|
|
7
8
|
)
|
|
8
9
|
from shelltastic.backend.default.local import DefaultLocalBackend
|
|
9
10
|
from shelltastic.backend.default.remote import DefaultRemoteBackend
|
|
@@ -17,6 +18,7 @@ __all__ = [
|
|
|
17
18
|
"register_default_local_backend",
|
|
18
19
|
"register_default_remote_backend",
|
|
19
20
|
"add_shim",
|
|
21
|
+
"clear_shims",
|
|
20
22
|
"get_local_backend",
|
|
21
23
|
"get_remote_backend",
|
|
22
24
|
]
|
|
@@ -90,6 +90,10 @@ class BackendShim(ABC):
|
|
|
90
90
|
|
|
91
91
|
_backend_shims: list[BackendShim] = []
|
|
92
92
|
|
|
93
|
+
def clear_shims():
|
|
94
|
+
"""Clear all shims"""
|
|
95
|
+
global _backend_shims
|
|
96
|
+
_backend_shims.clear()
|
|
93
97
|
|
|
94
98
|
def add_shim(shim: BackendShim):
|
|
95
99
|
"""Add a shim to manipulate `ShellBackend` params before any `ShellBackend` runs"""
|
|
@@ -103,7 +107,19 @@ def _shim_run(f, backend_type: type):
|
|
|
103
107
|
result: BackendShimResult = BackendShimResult(*args, **kwargs)
|
|
104
108
|
for shim in _backend_shims:
|
|
105
109
|
result = shim.run_shim(
|
|
106
|
-
backend_type,
|
|
110
|
+
backend_type, *result.args, **result.kwargs
|
|
111
|
+
)
|
|
112
|
+
return f(*result.args, **result.kwargs)
|
|
113
|
+
|
|
114
|
+
return wrapper
|
|
115
|
+
|
|
116
|
+
def _shim_run_async(f, backend_type: type):
|
|
117
|
+
@wraps(f)
|
|
118
|
+
def wrapper(*args, **kwargs):
|
|
119
|
+
result: BackendShimResult = BackendShimResult(*args, **kwargs)
|
|
120
|
+
for shim in _backend_shims:
|
|
121
|
+
result = shim.run_async_shim(
|
|
122
|
+
backend_type, *result.args, **result.kwargs
|
|
107
123
|
)
|
|
108
124
|
return f(*result.args, **result.kwargs)
|
|
109
125
|
|
|
@@ -114,7 +130,7 @@ class _InstallShims(ABCMeta):
|
|
|
114
130
|
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
|
115
131
|
instance = super().__call__(*args, **kwds)
|
|
116
132
|
instance.run = _shim_run(instance.run, instance.__class__)
|
|
117
|
-
instance.run_async =
|
|
133
|
+
instance.run_async = _shim_run_async(instance.run_async, instance.__class__)
|
|
118
134
|
return instance
|
|
119
135
|
|
|
120
136
|
|
|
@@ -157,7 +157,7 @@ def _determine_subprocess_output_mode(
|
|
|
157
157
|
return subprocess.PIPE
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
class
|
|
160
|
+
class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
|
|
161
161
|
def __init__(
|
|
162
162
|
self,
|
|
163
163
|
cmd: str | list[str],
|
|
@@ -185,7 +185,7 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
|
|
|
185
185
|
if _display_pool is None:
|
|
186
186
|
_display_pool = ThreadPool(2)
|
|
187
187
|
|
|
188
|
-
_stdout_async_result=_display_pool.apply_async(
|
|
188
|
+
_stdout_async_result = _display_pool.apply_async(
|
|
189
189
|
_output_and_pipe,
|
|
190
190
|
args=[
|
|
191
191
|
popen.stdout,
|
|
@@ -231,20 +231,16 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
|
|
|
231
231
|
# Wait for command to finish before closing everything
|
|
232
232
|
self.wait()
|
|
233
233
|
|
|
234
|
-
print("closing popen")
|
|
235
234
|
# Close popen
|
|
236
235
|
self._popen.__exit__(exc_type, exc, tb)
|
|
237
236
|
|
|
238
237
|
if self._display_pool:
|
|
239
|
-
print("closing _display_pool")
|
|
240
238
|
self._display_pool.__exit__(exc_type, exc, tb)
|
|
241
239
|
|
|
242
240
|
# Close stdout and stderr
|
|
243
|
-
if self.stdout:
|
|
244
|
-
print("closing stdout")
|
|
241
|
+
if self.stdout is not None:
|
|
245
242
|
self.stdout.close()
|
|
246
|
-
if self.stderr:
|
|
247
|
-
print("closing stderr")
|
|
243
|
+
if self.stderr is not None:
|
|
248
244
|
self.stderr.close()
|
|
249
245
|
|
|
250
246
|
def close(self) -> None:
|
|
@@ -263,7 +259,7 @@ class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None
|
|
|
263
259
|
stderr=self.stderr,
|
|
264
260
|
)
|
|
265
261
|
|
|
266
|
-
# Wait for our async
|
|
262
|
+
# Wait for our async readers to be complete
|
|
267
263
|
if self._stdout_async_result:
|
|
268
264
|
self._stdout_async_result.wait()
|
|
269
265
|
|
|
@@ -373,7 +369,7 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
373
369
|
cwd=cwd,
|
|
374
370
|
)
|
|
375
371
|
|
|
376
|
-
return
|
|
372
|
+
return CommonShellAsyncResult(
|
|
377
373
|
cmd=cmd,
|
|
378
374
|
popen=popen,
|
|
379
375
|
stdout_capture=stdout,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import shlex
|
|
3
|
+
from io import RawIOBase
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from subprocess import Popen
|
|
6
|
+
|
|
7
|
+
from shelltastic.backend.base import RemoteShellBackend
|
|
8
|
+
from shelltastic.backend.default.common import (
|
|
9
|
+
CommonDefaultBackend,
|
|
10
|
+
CommonShellAsyncResult,
|
|
11
|
+
)
|
|
12
|
+
from shelltastic.display import IODisplay
|
|
13
|
+
from shelltastic.enum import CaptureMode
|
|
14
|
+
from shelltastic.exception import ShellException, SSHConnectionError
|
|
15
|
+
from shelltastic.host import Host
|
|
16
|
+
from shelltastic.result import ShellResult, ShellResultAsync
|
|
17
|
+
|
|
18
|
+
LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _monkey_patch_async_result(
|
|
22
|
+
async_result: ShellResultAsync[RawIOBase | None, RawIOBase | None], host: Host
|
|
23
|
+
):
|
|
24
|
+
result_fn = async_result.result
|
|
25
|
+
|
|
26
|
+
def patched_result_fn():
|
|
27
|
+
# Extra check for SSH Errors
|
|
28
|
+
try:
|
|
29
|
+
result = result_fn()
|
|
30
|
+
if (
|
|
31
|
+
result.returncode == 255
|
|
32
|
+
): # 255 is what ssh returns if it has an error with ssh itself
|
|
33
|
+
result = result.to_result_bytes()
|
|
34
|
+
raise SSHConnectionError(
|
|
35
|
+
host.hostname, result.returncode, result.stdout, result.stderr
|
|
36
|
+
)
|
|
37
|
+
return result
|
|
38
|
+
except ShellException as ex:
|
|
39
|
+
if ex.returncode == 255:
|
|
40
|
+
raise SSHConnectionError(
|
|
41
|
+
host.hostname, ex.returncode, ex.stdout, ex.stderr
|
|
42
|
+
) from ex
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
async_result.result = patched_result_fn
|
|
46
|
+
|
|
47
|
+
return async_result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RemoteShellAcyncResult(CommonShellAsyncResult):
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
cmd: str | list[str],
|
|
54
|
+
popen: Popen,
|
|
55
|
+
stdout_capture: CaptureMode | None,
|
|
56
|
+
stderr_capture: CaptureMode | None,
|
|
57
|
+
stdout_display: IODisplay | None,
|
|
58
|
+
stderr_display: IODisplay | None,
|
|
59
|
+
check: bool,
|
|
60
|
+
host: Host,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(
|
|
63
|
+
cmd,
|
|
64
|
+
popen,
|
|
65
|
+
stdout_capture,
|
|
66
|
+
stderr_capture,
|
|
67
|
+
stdout_display,
|
|
68
|
+
stderr_display,
|
|
69
|
+
check,
|
|
70
|
+
)
|
|
71
|
+
self.host = host
|
|
72
|
+
|
|
73
|
+
def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
|
|
74
|
+
# Extra check for SSH Errors
|
|
75
|
+
try:
|
|
76
|
+
result = super().result()
|
|
77
|
+
if (
|
|
78
|
+
result.returncode == 255
|
|
79
|
+
): # 255 is what ssh returns if it has an error with ssh itself
|
|
80
|
+
result = result.to_result_bytes()
|
|
81
|
+
raise SSHConnectionError(
|
|
82
|
+
self.host.hostname, result.returncode, result.stdout, result.stderr
|
|
83
|
+
)
|
|
84
|
+
return result
|
|
85
|
+
except ShellException as ex:
|
|
86
|
+
if ex.returncode == 255:
|
|
87
|
+
raise SSHConnectionError(
|
|
88
|
+
self.host.hostname, ex.returncode, ex.stdout, ex.stderr
|
|
89
|
+
) from ex
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
|
|
94
|
+
def __init__(self, host: Host) -> None:
|
|
95
|
+
super().__init__()
|
|
96
|
+
self.host: Host = host
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def for_host(host: Host) -> RemoteShellBackend:
|
|
100
|
+
return DefaultRemoteBackend(host)
|
|
101
|
+
|
|
102
|
+
def run(
|
|
103
|
+
self,
|
|
104
|
+
cmd: str | list[str],
|
|
105
|
+
*,
|
|
106
|
+
subshell: bool = True,
|
|
107
|
+
cwd: str | Path | None = None,
|
|
108
|
+
**kwargs,
|
|
109
|
+
) -> ShellResult:
|
|
110
|
+
"""
|
|
111
|
+
Execute a remote command using SSH.
|
|
112
|
+
"""
|
|
113
|
+
with self.run_async(cmd, subshell=subshell, cwd=cwd, **kwargs) as running_cmd:
|
|
114
|
+
return running_cmd.result_bytes()
|
|
115
|
+
|
|
116
|
+
def run_async(
|
|
117
|
+
self,
|
|
118
|
+
cmd: str | list[str],
|
|
119
|
+
*,
|
|
120
|
+
subshell: bool = True,
|
|
121
|
+
cwd: str | Path | None = None,
|
|
122
|
+
**kwargs,
|
|
123
|
+
) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
|
|
124
|
+
if isinstance(cmd, list):
|
|
125
|
+
cmd = shlex.join(cmd)
|
|
126
|
+
|
|
127
|
+
# Wrap user command in sh -c call
|
|
128
|
+
if subshell:
|
|
129
|
+
cmd = shlex.join(["bash", "-c", cmd])
|
|
130
|
+
|
|
131
|
+
# If we're redirecting stderr to stdout,
|
|
132
|
+
# add the same redirect inside the ssh command.
|
|
133
|
+
if kwargs.get("stderr") == CaptureMode.STDOUT:
|
|
134
|
+
cmd = cmd + " 2>&1"
|
|
135
|
+
|
|
136
|
+
# cd into cwd first if cwd is set
|
|
137
|
+
if cwd:
|
|
138
|
+
cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
|
|
139
|
+
|
|
140
|
+
port_flag = ["-p", str(self.host.port)] if self.host.port else []
|
|
141
|
+
|
|
142
|
+
return _monkey_patch_async_result(
|
|
143
|
+
super().run_async(
|
|
144
|
+
["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
|
|
145
|
+
),
|
|
146
|
+
self.host,
|
|
147
|
+
)
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
from io import RawIOBase
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
class SSHConnectionError(Exception):
|
|
5
2
|
def __init__(
|
|
6
3
|
self, hostname: str, returncode: int, stdout: bytes | None, stderr: bytes | None
|
|
@@ -15,13 +12,13 @@ class SSHConnectionError(Exception):
|
|
|
15
12
|
self.stderr = stderr
|
|
16
13
|
|
|
17
14
|
|
|
18
|
-
class ShellException
|
|
15
|
+
class ShellException(Exception):
|
|
19
16
|
def __init__(
|
|
20
17
|
self,
|
|
21
18
|
cmd: str | list[str],
|
|
22
19
|
returncode: int,
|
|
23
|
-
stdout:
|
|
24
|
-
stderr:
|
|
20
|
+
stdout: bytes | None,
|
|
21
|
+
stderr: bytes | None,
|
|
25
22
|
):
|
|
26
23
|
super().__init__(f"Shell execution failed with returncode {returncode}")
|
|
27
24
|
self.cmd = cmd
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from abc import ABC, abstractmethod
|
|
2
4
|
from dataclasses import dataclass
|
|
3
5
|
from io import RawIOBase
|
|
@@ -15,7 +17,15 @@ class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
|
|
|
15
17
|
def check_returncode(self):
|
|
16
18
|
"""If returncode is not 0, raise ShellException"""
|
|
17
19
|
if self.returncode != 0:
|
|
18
|
-
|
|
20
|
+
result = self.to_result_bytes()
|
|
21
|
+
raise ShellException(self.cmd, self.returncode, result.stdout, result.stderr)
|
|
22
|
+
|
|
23
|
+
def to_result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
|
|
24
|
+
stdout = self.stdout.readall() if isinstance(self.stdout, RawIOBase) else self.stdout
|
|
25
|
+
|
|
26
|
+
stderr = self.stderr.readall() if isinstance(self.stderr, RawIOBase) else self.stderr
|
|
27
|
+
|
|
28
|
+
return ShellResult(self.cmd, self.returncode, stdout, stderr)
|
|
19
29
|
|
|
20
30
|
|
|
21
31
|
class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import shlex
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from shelltastic.backend.base import RemoteShellBackend
|
|
6
|
-
from shelltastic.backend.default.common import CommonDefaultBackend
|
|
7
|
-
from shelltastic.enum import CaptureMode
|
|
8
|
-
from shelltastic.exception import ShellException, SSHConnectionError
|
|
9
|
-
from shelltastic.host import Host
|
|
10
|
-
from shelltastic.result import ShellResult
|
|
11
|
-
|
|
12
|
-
LOGGER = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
|
|
16
|
-
def __init__(self, host: Host) -> None:
|
|
17
|
-
super().__init__()
|
|
18
|
-
self.host: Host = host
|
|
19
|
-
|
|
20
|
-
@staticmethod
|
|
21
|
-
def for_host(host: Host) -> RemoteShellBackend:
|
|
22
|
-
return DefaultRemoteBackend(host)
|
|
23
|
-
|
|
24
|
-
def run(
|
|
25
|
-
self,
|
|
26
|
-
cmd: str | list[str],
|
|
27
|
-
*,
|
|
28
|
-
subshell: bool = True,
|
|
29
|
-
cwd: str | Path | None = None,
|
|
30
|
-
**kwargs,
|
|
31
|
-
) -> ShellResult:
|
|
32
|
-
"""
|
|
33
|
-
Execute a remote command using SSH.
|
|
34
|
-
"""
|
|
35
|
-
if isinstance(cmd, list):
|
|
36
|
-
cmd = shlex.join(cmd)
|
|
37
|
-
|
|
38
|
-
# Wrap user command in sh -c call
|
|
39
|
-
if subshell:
|
|
40
|
-
cmd = shlex.join(["bash", "-c", cmd])
|
|
41
|
-
|
|
42
|
-
# If we're redirecting stderr to stdout,
|
|
43
|
-
# add the same redirect inside the ssh command.
|
|
44
|
-
if kwargs.get("stderr") == CaptureMode.STDOUT:
|
|
45
|
-
cmd = cmd + " 2>&1"
|
|
46
|
-
|
|
47
|
-
# cd into cwd first if cwd is set
|
|
48
|
-
if cwd:
|
|
49
|
-
cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
|
|
50
|
-
|
|
51
|
-
port_flag = ["-p", str(self.host.port)] if self.host.port else []
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
result = super().run(
|
|
55
|
-
["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
|
|
56
|
-
)
|
|
57
|
-
if (
|
|
58
|
-
result.returncode == 255
|
|
59
|
-
): # 255 is what ssh returns if it has an error with ssh itself
|
|
60
|
-
raise SSHConnectionError(
|
|
61
|
-
self.host.hostname, result.returncode, result.stdout, result.stderr
|
|
62
|
-
)
|
|
63
|
-
return result
|
|
64
|
-
except ShellException as ex:
|
|
65
|
-
if ex.returncode == 255:
|
|
66
|
-
raise SSHConnectionError(
|
|
67
|
-
self.host.hostname, ex.returncode, ex.stdout, ex.stderr
|
|
68
|
-
) from ex
|
|
69
|
-
raise
|
|
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
|