shelltastic 0.6.0__tar.gz → 0.8.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.6.0 → shelltastic-0.8.0}/PKG-INFO +1 -1
- {shelltastic-0.6.0 → shelltastic-0.8.0}/pyproject.toml +1 -1
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/default/common.py +130 -171
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/default/local.py +0 -2
- shelltastic-0.8.0/src/shelltastic/backend/default/remote.py +147 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/exception.py +3 -6
- shelltastic-0.8.0/src/shelltastic/result.py +111 -0
- shelltastic-0.6.0/src/shelltastic/backend/default/remote.py +0 -75
- shelltastic-0.6.0/src/shelltastic/result.py +0 -47
- {shelltastic-0.6.0 → shelltastic-0.8.0}/LICENSE +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/README.md +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/__init__.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/base.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/display.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/shell.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/host.py +0 -0
- {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/io.py +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import logging
|
|
4
2
|
import os
|
|
5
3
|
import shlex
|
|
@@ -7,8 +5,10 @@ import subprocess
|
|
|
7
5
|
import time
|
|
8
6
|
from io import RawIOBase
|
|
9
7
|
from multiprocessing import Pipe
|
|
8
|
+
from multiprocessing.connection import Connection
|
|
10
9
|
from multiprocessing.pool import ThreadPool
|
|
11
|
-
from
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import IO, Literal
|
|
12
12
|
|
|
13
13
|
from shelltastic import display
|
|
14
14
|
from shelltastic.backend.base import ShellBackend
|
|
@@ -16,10 +16,6 @@ from shelltastic.enum import CaptureMode, DisplayMode
|
|
|
16
16
|
from shelltastic.io import PipeReader
|
|
17
17
|
from shelltastic.result import ShellResult, ShellResultAsync
|
|
18
18
|
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from multiprocessing.connection import Connection
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
19
|
LOGGER = logging.getLogger(__name__)
|
|
24
20
|
|
|
25
21
|
|
|
@@ -91,45 +87,27 @@ class IOTimeoutStream:
|
|
|
91
87
|
return return_val
|
|
92
88
|
|
|
93
89
|
|
|
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
90
|
def _output_and_pipe(
|
|
117
91
|
io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
|
|
118
92
|
) -> None:
|
|
119
|
-
|
|
120
|
-
|
|
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)
|
|
121
104
|
|
|
122
|
-
if pipe:
|
|
123
105
|
if display:
|
|
124
106
|
for line in IOTimeoutStream(io):
|
|
125
|
-
pipe.send_bytes(line)
|
|
126
107
|
display.printbytes(line)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if display:
|
|
131
|
-
for line in IOTimeoutStream(io):
|
|
132
|
-
display.printbytes(line)
|
|
108
|
+
finally:
|
|
109
|
+
if pipe:
|
|
110
|
+
pipe.close()
|
|
133
111
|
|
|
134
112
|
|
|
135
113
|
def _determine_subprocess_output_mode(
|
|
@@ -179,55 +157,121 @@ def _determine_subprocess_output_mode(
|
|
|
179
157
|
return subprocess.PIPE
|
|
180
158
|
|
|
181
159
|
|
|
182
|
-
class
|
|
160
|
+
class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
|
|
183
161
|
def __init__(
|
|
184
162
|
self,
|
|
185
163
|
cmd: str | list[str],
|
|
186
164
|
popen: subprocess.Popen,
|
|
165
|
+
stdout_capture: CaptureMode | None,
|
|
166
|
+
stderr_capture: CaptureMode | None,
|
|
187
167
|
stdout_display: display.IODisplay | None,
|
|
188
168
|
stderr_display: display.IODisplay | None,
|
|
169
|
+
check: bool,
|
|
189
170
|
) -> None:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
205
198
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
215
229
|
|
|
216
230
|
def __exit__(self, exc_type, exc, tb):
|
|
231
|
+
# Wait for command to finish before closing everything
|
|
232
|
+
self.wait()
|
|
233
|
+
|
|
234
|
+
# Close popen
|
|
217
235
|
self._popen.__exit__(exc_type, exc, tb)
|
|
218
|
-
|
|
236
|
+
|
|
237
|
+
if self._display_pool:
|
|
238
|
+
self._display_pool.__exit__(exc_type, exc, tb)
|
|
239
|
+
|
|
240
|
+
# Close stdout and stderr
|
|
241
|
+
if self.stdout is not None:
|
|
242
|
+
self.stdout.close()
|
|
243
|
+
if self.stderr is not None:
|
|
244
|
+
self.stderr.close()
|
|
219
245
|
|
|
220
246
|
def close(self) -> None:
|
|
221
247
|
self.__exit__(None, None, None)
|
|
222
248
|
|
|
249
|
+
def terminate(self) -> None:
|
|
250
|
+
self._popen.terminate()
|
|
251
|
+
self.close()
|
|
252
|
+
|
|
223
253
|
def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
|
|
224
|
-
|
|
254
|
+
# Get result
|
|
255
|
+
result = ShellResult(
|
|
225
256
|
cmd=self.cmd,
|
|
226
257
|
returncode=self._popen.wait(),
|
|
227
258
|
stdout=self.stdout,
|
|
228
259
|
stderr=self.stderr,
|
|
229
260
|
)
|
|
230
261
|
|
|
262
|
+
# Wait for our async readers to be complete
|
|
263
|
+
if self._stdout_async_result:
|
|
264
|
+
self._stdout_async_result.wait()
|
|
265
|
+
|
|
266
|
+
if self._stderr_async_result:
|
|
267
|
+
self._stderr_async_result.wait()
|
|
268
|
+
|
|
269
|
+
# Check if set
|
|
270
|
+
if self._check:
|
|
271
|
+
result.check_returncode()
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
|
|
231
275
|
|
|
232
276
|
class CommonDefaultBackend(ShellBackend):
|
|
233
277
|
__slots__ = ()
|
|
@@ -245,106 +289,18 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
245
289
|
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
246
290
|
**kwargs,
|
|
247
291
|
) -> ShellResult[bytes | None, bytes | None]:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
292
|
+
with self.run_async(
|
|
293
|
+
cmd,
|
|
294
|
+
check=check,
|
|
295
|
+
stdout=stdout,
|
|
296
|
+
stderr=stderr,
|
|
297
|
+
stdout_display=stdout_display,
|
|
298
|
+
stderr_display=stderr_display,
|
|
299
|
+
cwd=cwd,
|
|
300
|
+
echo_cmd=echo_cmd,
|
|
301
|
+
**kwargs,
|
|
302
|
+
) as running_cmd:
|
|
303
|
+
return running_cmd.result_bytes()
|
|
348
304
|
|
|
349
305
|
def run_async(
|
|
350
306
|
self,
|
|
@@ -413,17 +369,20 @@ class CommonDefaultBackend(ShellBackend):
|
|
|
413
369
|
cwd=cwd,
|
|
414
370
|
)
|
|
415
371
|
|
|
416
|
-
return
|
|
417
|
-
cmd,
|
|
418
|
-
popen,
|
|
419
|
-
|
|
372
|
+
return CommonShellAsyncResult(
|
|
373
|
+
cmd=cmd,
|
|
374
|
+
popen=popen,
|
|
375
|
+
stdout_capture=stdout,
|
|
376
|
+
stderr_capture=stderr,
|
|
377
|
+
stdout_display=(
|
|
420
378
|
stdout_display
|
|
421
379
|
if isinstance(stdout_display, display.IODisplay)
|
|
422
380
|
else display.from_mode(stdout_display)
|
|
423
381
|
),
|
|
424
|
-
(
|
|
382
|
+
stderr_display=(
|
|
425
383
|
stderr_display
|
|
426
384
|
if isinstance(stderr_display, display.IODisplay)
|
|
427
385
|
else display.from_mode(stderr_display)
|
|
428
386
|
),
|
|
387
|
+
check=check,
|
|
429
388
|
)
|
|
@@ -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
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from io import RawIOBase
|
|
6
|
+
|
|
7
|
+
from shelltastic.exception import ShellException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
|
|
12
|
+
cmd: str | list[str]
|
|
13
|
+
returncode: int
|
|
14
|
+
stdout: SO
|
|
15
|
+
stderr: SE
|
|
16
|
+
|
|
17
|
+
def check_returncode(self):
|
|
18
|
+
"""If returncode is not 0, raise ShellException"""
|
|
19
|
+
if self.returncode != 0:
|
|
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)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
|
|
32
|
+
"""
|
|
33
|
+
An asynchronsly running shell command.
|
|
34
|
+
|
|
35
|
+
cmd: String for the running shell command.
|
|
36
|
+
|
|
37
|
+
stdout: Bytes stream for stdout
|
|
38
|
+
|
|
39
|
+
stderr: Bytes stream for stderr
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
|
|
43
|
+
self.cmd = cmd
|
|
44
|
+
"""The running command"""
|
|
45
|
+
|
|
46
|
+
self.stdout = stdout
|
|
47
|
+
"""Byte based IO stream for stdout"""
|
|
48
|
+
|
|
49
|
+
self.stderr = stderr
|
|
50
|
+
"""Byte based IO stream for stderr"""
|
|
51
|
+
|
|
52
|
+
def __enter__(self):
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, exc_type, exc, tb):
|
|
56
|
+
self.close()
|
|
57
|
+
|
|
58
|
+
def close(self) -> None:
|
|
59
|
+
"""Close the running command."""
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
def terminate(self) -> None:
|
|
63
|
+
"""Terminate the running command"""
|
|
64
|
+
raise NotImplementedError()
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def result(self) -> ShellResult[SO, SE]:
|
|
68
|
+
"""
|
|
69
|
+
Blocking
|
|
70
|
+
|
|
71
|
+
Wait for the command to finish and return a ShellResult with the outcome.
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError()
|
|
74
|
+
|
|
75
|
+
def wait(self) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Blocking
|
|
78
|
+
|
|
79
|
+
Wait for the command to finish.
|
|
80
|
+
"""
|
|
81
|
+
self.result()
|
|
82
|
+
|
|
83
|
+
def returncode(self) -> int:
|
|
84
|
+
"""
|
|
85
|
+
Blocking
|
|
86
|
+
|
|
87
|
+
Get the returncode of the command.
|
|
88
|
+
"""
|
|
89
|
+
return self.result().returncode
|
|
90
|
+
|
|
91
|
+
def check_returncode(self):
|
|
92
|
+
"""
|
|
93
|
+
Blocking
|
|
94
|
+
|
|
95
|
+
Raise a ShellException if the returncode is not zero.
|
|
96
|
+
"""
|
|
97
|
+
self.result().check_returncode()
|
|
98
|
+
|
|
99
|
+
def result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
|
|
100
|
+
result = self.result()
|
|
101
|
+
stdout = result.stdout
|
|
102
|
+
stdout_bytes = stdout.readall() if stdout is not None else None
|
|
103
|
+
stderr = result.stderr
|
|
104
|
+
stderr_bytes = stderr.readall() if stderr is not None else None
|
|
105
|
+
|
|
106
|
+
return ShellResult(
|
|
107
|
+
cmd=result.cmd,
|
|
108
|
+
returncode=result.returncode,
|
|
109
|
+
stdout=stdout_bytes,
|
|
110
|
+
stderr=stderr_bytes,
|
|
111
|
+
)
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import shlex
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
from shelltastic.backend.base import RemoteShellBackend
|
|
8
|
-
from shelltastic.backend.default.common import CommonDefaultBackend
|
|
9
|
-
from shelltastic.enum import CaptureMode
|
|
10
|
-
from shelltastic.exception import ShellException, SSHConnectionError
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
from shelltastic.host import Host
|
|
16
|
-
from shelltastic.result import ShellResult
|
|
17
|
-
|
|
18
|
-
LOGGER = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
|
|
22
|
-
def __init__(self, host: Host) -> None:
|
|
23
|
-
super().__init__()
|
|
24
|
-
self.host: Host = host
|
|
25
|
-
|
|
26
|
-
@staticmethod
|
|
27
|
-
def for_host(host: Host) -> RemoteShellBackend:
|
|
28
|
-
return DefaultRemoteBackend(host)
|
|
29
|
-
|
|
30
|
-
def run(
|
|
31
|
-
self,
|
|
32
|
-
cmd: str | list[str],
|
|
33
|
-
*,
|
|
34
|
-
subshell: bool = True,
|
|
35
|
-
cwd: str | Path | None = None,
|
|
36
|
-
**kwargs,
|
|
37
|
-
) -> ShellResult:
|
|
38
|
-
"""
|
|
39
|
-
Execute a remote command using SSH.
|
|
40
|
-
"""
|
|
41
|
-
if isinstance(cmd, list):
|
|
42
|
-
cmd = shlex.join(cmd)
|
|
43
|
-
|
|
44
|
-
# Wrap user command in sh -c call
|
|
45
|
-
if subshell:
|
|
46
|
-
cmd = shlex.join(["bash", "-c", cmd])
|
|
47
|
-
|
|
48
|
-
# If we're redirecting stderr to stdout,
|
|
49
|
-
# add the same redirect inside the ssh command.
|
|
50
|
-
if kwargs.get("stderr") == CaptureMode.STDOUT:
|
|
51
|
-
cmd = cmd + " 2>&1"
|
|
52
|
-
|
|
53
|
-
# cd into cwd first if cwd is set
|
|
54
|
-
if cwd:
|
|
55
|
-
cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
|
|
56
|
-
|
|
57
|
-
port_flag = ["-p", str(self.host.port)] if self.host.port else []
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
result = super().run(
|
|
61
|
-
["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
|
|
62
|
-
)
|
|
63
|
-
if (
|
|
64
|
-
result.returncode == 255
|
|
65
|
-
): # 255 is what ssh returns if it has an error with ssh itself
|
|
66
|
-
raise SSHConnectionError(
|
|
67
|
-
self.host.hostname, result.returncode, result.stdout, result.stderr
|
|
68
|
-
)
|
|
69
|
-
return result
|
|
70
|
-
except ShellException as ex:
|
|
71
|
-
if ex.returncode == 255:
|
|
72
|
-
raise SSHConnectionError(
|
|
73
|
-
self.host.hostname, ex.returncode, ex.stdout, ex.stderr
|
|
74
|
-
) from ex
|
|
75
|
-
raise
|
|
@@ -1,47 +0,0 @@
|
|
|
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()
|
|
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
|
|
File without changes
|