shelltastic 0.4.8__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {shelltastic-0.4.8 → shelltastic-0.5.0}/PKG-INFO +1 -1
- {shelltastic-0.4.8 → shelltastic-0.5.0}/pyproject.toml +1 -1
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/common.py +72 -2
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/display.py +15 -9
- {shelltastic-0.4.8 → shelltastic-0.5.0}/LICENSE +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/README.md +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/__init__.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/__init__.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/base.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/__init__.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/local.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/backend/default/remote.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/enum.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/exception.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/git.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/scp.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/frontend/shell.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/host.py +0 -0
- {shelltastic-0.4.8 → shelltastic-0.5.0}/src/shelltastic/result.py +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import shlex
|
|
5
6
|
import subprocess
|
|
7
|
+
import time
|
|
6
8
|
from multiprocessing.pool import ThreadPool
|
|
7
9
|
from typing import IO, TYPE_CHECKING, Literal
|
|
8
10
|
|
|
@@ -17,6 +19,74 @@ if TYPE_CHECKING:
|
|
|
17
19
|
LOGGER = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
class IOTimeoutStream:
|
|
23
|
+
"""
|
|
24
|
+
Iterable for reading from an IO stream. Return lines but
|
|
25
|
+
if no bytes have been read since the given timeout, return
|
|
26
|
+
whatever is in the buffer.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self, io: IO[bytes], *, timeout: float = 0.1, batch_size: int = 1000
|
|
31
|
+
) -> None:
|
|
32
|
+
self.io = io
|
|
33
|
+
self.buffer = b""
|
|
34
|
+
self.batch_size = batch_size
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
|
|
37
|
+
def __iter__(self):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __next__(self) -> bytes:
|
|
41
|
+
last_read_time = time.time()
|
|
42
|
+
# Read into our buffer
|
|
43
|
+
while True:
|
|
44
|
+
# If we have no data in our buffer, then we block until we get data.
|
|
45
|
+
# Otherwise, we're waiting for end of line and should unblock.
|
|
46
|
+
if len(self.buffer) == 0:
|
|
47
|
+
os.set_blocking(self.io.fileno(), True)
|
|
48
|
+
batch_size = 1
|
|
49
|
+
else:
|
|
50
|
+
os.set_blocking(self.io.fileno(), False)
|
|
51
|
+
batch_size = self.batch_size
|
|
52
|
+
|
|
53
|
+
# Read from IO
|
|
54
|
+
batch = self.io.read(batch_size)
|
|
55
|
+
|
|
56
|
+
# Handle return batch
|
|
57
|
+
# None means no data to read, but NOT end of file
|
|
58
|
+
if batch is not None:
|
|
59
|
+
last_read_time = time.time()
|
|
60
|
+
self.buffer += batch
|
|
61
|
+
|
|
62
|
+
# If batch is not none,
|
|
63
|
+
# and the batch was 0 size,
|
|
64
|
+
# and our buffer is empty,
|
|
65
|
+
# we're at EOF
|
|
66
|
+
if len(self.buffer) == 0:
|
|
67
|
+
raise StopIteration()
|
|
68
|
+
|
|
69
|
+
# If buffer contains at least one line, return the first line, save the rest
|
|
70
|
+
if b"\n" in self.buffer:
|
|
71
|
+
buffer_lines = self.buffer.splitlines(keepends=True)
|
|
72
|
+
self.buffer = b""
|
|
73
|
+
for b in buffer_lines[1:]:
|
|
74
|
+
self.buffer += b
|
|
75
|
+
return buffer_lines[0]
|
|
76
|
+
|
|
77
|
+
# If we're at end of file, and we have no lines, just return what we have
|
|
78
|
+
if batch is not None and len(batch) == 0:
|
|
79
|
+
return_val = self.buffer
|
|
80
|
+
self.buffer = b""
|
|
81
|
+
return return_val
|
|
82
|
+
|
|
83
|
+
# Handle timeout
|
|
84
|
+
if time.time() - last_read_time >= self.timeout:
|
|
85
|
+
return_val = self.buffer
|
|
86
|
+
self.buffer = b""
|
|
87
|
+
return return_val
|
|
88
|
+
|
|
89
|
+
|
|
20
90
|
def _output_and_collect(
|
|
21
91
|
io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
|
|
22
92
|
) -> bytes | None:
|
|
@@ -26,14 +96,14 @@ def _output_and_collect(
|
|
|
26
96
|
if collect:
|
|
27
97
|
if display:
|
|
28
98
|
return_bytes: bytes = b""
|
|
29
|
-
for line in io:
|
|
99
|
+
for line in IOTimeoutStream(io):
|
|
30
100
|
return_bytes += line
|
|
31
101
|
display.printbytes(line)
|
|
32
102
|
return return_bytes
|
|
33
103
|
return io.read()
|
|
34
104
|
|
|
35
105
|
if display:
|
|
36
|
-
for line in io:
|
|
106
|
+
for line in IOTimeoutStream(io):
|
|
37
107
|
display.printbytes(line)
|
|
38
108
|
|
|
39
109
|
return None
|
|
@@ -13,30 +13,36 @@ _io_display_lock = threading.RLock()
|
|
|
13
13
|
class IODisplay(ABC):
|
|
14
14
|
def printbytes(self, line: bytes):
|
|
15
15
|
with _io_display_lock:
|
|
16
|
-
self.
|
|
16
|
+
self._emit("%s", line.decode(), end="")
|
|
17
17
|
|
|
18
18
|
def printline(self, msg: object, *args: object):
|
|
19
19
|
with _io_display_lock:
|
|
20
|
-
self.
|
|
20
|
+
self._emit(msg, *args)
|
|
21
21
|
|
|
22
22
|
@abstractmethod
|
|
23
|
-
def
|
|
23
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
24
24
|
raise NotImplementedError
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class DevNullDisplay(IODisplay):
|
|
28
|
-
def
|
|
28
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class StdoutDisplay(IODisplay):
|
|
33
|
-
def
|
|
34
|
-
|
|
33
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
34
|
+
if args:
|
|
35
|
+
print(str(msg) % args, flush=True, end=end)
|
|
36
|
+
return
|
|
37
|
+
print(str(msg), flush=True, end=end)
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
class StderrDisplay(IODisplay):
|
|
38
|
-
def
|
|
39
|
-
|
|
41
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
42
|
+
if args:
|
|
43
|
+
print(str(msg) % args, file=sys.stderr, flush=True, end=end)
|
|
44
|
+
return
|
|
45
|
+
print(str(msg), file=sys.stderr, flush=True, end=end)
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
class LogDisplay(IODisplay):
|
|
@@ -44,7 +50,7 @@ class LogDisplay(IODisplay):
|
|
|
44
50
|
super().__init__()
|
|
45
51
|
self.log_level = log_level
|
|
46
52
|
|
|
47
|
-
def
|
|
53
|
+
def _emit(self, msg: object, *args: object, end="\n"):
|
|
48
54
|
LOGGER.log(self.log_level, msg, *args, stacklevel=3)
|
|
49
55
|
|
|
50
56
|
|
|
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
|
|
File without changes
|
|
File without changes
|