sshscript 3.1.4__py3-none-any.whl → 4.0.1__py3-none-any.whl
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.
- _sshscript_cli.py +19 -0
- sshscript/__init__.py +19 -2
- sshscript/__init__.pyi +15 -0
- sshscript/_runtime.py +14 -0
- sshscript/_version.py +1 -1
- sshscript/channelgeneric.py +48 -16
- sshscript/channelssh.py +8 -7
- sshscript/channelsubprocess.py +6 -6
- sshscript/channelutils.py +389 -118
- sshscript/commandjob.py +475 -0
- sshscript/commandjob.pyi +45 -0
- sshscript/commandresult.py +31 -0
- sshscript/commandresult.pyi +18 -0
- sshscript/consolejob.py +309 -0
- sshscript/dollar.py +67 -35
- sshscript/dollarchanger.py +15 -3
- sshscript/dollarparser.py +4 -0
- sshscript/patching.py +3 -1
- sshscript/py.typed +0 -0
- sshscript/session.py +391 -77
- sshscript/session.pyi +73 -0
- sshscript/sessionsettings.py +93 -0
- sshscript/sessionwrapper.py +141 -11
- sshscript/sessionwrapper.pyi +24 -0
- sshscript/sshconfig.py +127 -0
- sshscript/sshscript.py +51 -16
- sshscript/tokenparser.py +56 -1
- sshscript-4.0.1.dist-info/METADATA +551 -0
- sshscript-4.0.1.dist-info/RECORD +36 -0
- sshscript-4.0.1.dist-info/entry_points.txt +2 -0
- sshscript-4.0.1.dist-info/top_level.txt +2 -0
- sshscript-3.1.4.dist-info/METADATA +0 -178
- sshscript-3.1.4.dist-info/RECORD +0 -23
- sshscript-3.1.4.dist-info/entry_points.txt +0 -2
- sshscript-3.1.4.dist-info/top_level.txt +0 -1
- {sshscript-3.1.4.dist-info → sshscript-4.0.1.dist-info}/WHEEL +0 -0
- {sshscript-3.1.4.dist-info → sshscript-4.0.1.dist-info}/licenses/LICENSE.txt +0 -0
_sshscript_cli.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Minimal console bootstrap; must remain parseable on older Python."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
# Importing a package submodule executes __init__ first, so this check
|
|
7
|
+
# must be independent of the package's runtime guard.
|
|
8
|
+
if sys.version_info < (3, 11):
|
|
9
|
+
raise SystemExit(
|
|
10
|
+
"SSHScript requires Python 3.11 or newer.\n"
|
|
11
|
+
"Detected Python %s at %s."
|
|
12
|
+
% ('.'.join(map(str, sys.version_info[:3])), sys.executable)
|
|
13
|
+
)
|
|
14
|
+
from sshscript.sshscript import main as run
|
|
15
|
+
return run()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == '__main__':
|
|
19
|
+
main()
|
sshscript/__init__.py
CHANGED
|
@@ -26,15 +26,22 @@ Example::
|
|
|
26
26
|
from sshscript import Session
|
|
27
27
|
|
|
28
28
|
with Session() as local:
|
|
29
|
-
stdout, stderr = local("hostname")
|
|
29
|
+
stdout, stderr, exitcode = local("hostname")
|
|
30
30
|
with local.connect("user@host") as remote:
|
|
31
|
-
stdout, stderr = remote("uname -s")
|
|
31
|
+
stdout, stderr, exitcode = remote("uname -s")
|
|
32
32
|
print(stdout.strip())
|
|
33
33
|
|
|
34
34
|
Use run_file(path) for a script file, run_script(source) for source text,
|
|
35
35
|
and spy_imports() to temporarily enable Python imports of .spy modules.
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
+
if __package__:
|
|
39
|
+
from ._runtime import require_python as _require_python
|
|
40
|
+
else:
|
|
41
|
+
from _runtime import require_python as _require_python
|
|
42
|
+
|
|
43
|
+
_require_python()
|
|
44
|
+
|
|
38
45
|
if __package__:
|
|
39
46
|
from ._version import __version__
|
|
40
47
|
from . import sshscript
|
|
@@ -49,8 +56,13 @@ else:
|
|
|
49
56
|
from spyimporter import spy_imports
|
|
50
57
|
|
|
51
58
|
run_file = sshscript.run_file
|
|
59
|
+
check_file = sshscript.check_file
|
|
52
60
|
run_script = sshscript.run_script
|
|
53
61
|
Session = session.Session
|
|
62
|
+
CommandResult = session.CommandResult
|
|
63
|
+
CommandJob = session.CommandJob
|
|
64
|
+
CommandTimeoutError = session.CommandTimeoutError
|
|
65
|
+
JobResult = session.JobResult
|
|
54
66
|
|
|
55
67
|
set_logger = errorutils.set_logger
|
|
56
68
|
get_logger = errorutils.get_logger
|
|
@@ -73,8 +85,13 @@ SSHScriptException = errorutils.SSHScriptException
|
|
|
73
85
|
|
|
74
86
|
__all__ = [
|
|
75
87
|
'run_file',
|
|
88
|
+
'check_file',
|
|
76
89
|
'run_script',
|
|
77
90
|
'Session',
|
|
91
|
+
'CommandResult',
|
|
92
|
+
'CommandJob',
|
|
93
|
+
'CommandTimeoutError',
|
|
94
|
+
'JobResult',
|
|
78
95
|
'get_logger',
|
|
79
96
|
'set_logger',
|
|
80
97
|
'spy_imports',
|
sshscript/__init__.pyi
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, ContextManager
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from .session import Session as Session
|
|
4
|
+
from .commandresult import CommandResult as CommandResult
|
|
5
|
+
from .commandjob import CommandJob as CommandJob, JobResult as JobResult, CommandTimeoutError as CommandTimeoutError
|
|
6
|
+
from .errorutils import SSHScriptException as SSHScriptException
|
|
7
|
+
|
|
8
|
+
__version__: str
|
|
9
|
+
|
|
10
|
+
def check_file(script_path: str | PathLike[str]) -> int: ...
|
|
11
|
+
def run_file(script_path: str | PathLike[str], vars: dict[str, Any] | None = ..., showScript: bool = ...) -> int: ...
|
|
12
|
+
def run_script(script: str, varGlobals: dict[str, Any] | None = ..., showScript: bool = ...) -> dict[str, Any]: ...
|
|
13
|
+
def spy_imports() -> ContextManager[None]: ...
|
|
14
|
+
def get_logger() -> Any: ...
|
|
15
|
+
def set_logger(userlogger: Any = ..., logname: str | None = ...) -> Any: ...
|
sshscript/_runtime.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Reject unsupported interpreters before importing SSHScript features."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def require_python(cli=False):
|
|
6
|
+
if sys.version_info < (3, 11):
|
|
7
|
+
message = (
|
|
8
|
+
"SSHScript requires Python 3.11 or newer.\n"
|
|
9
|
+
"Detected Python %s at %s."
|
|
10
|
+
% ('.'.join(map(str, sys.version_info[:3])), sys.executable)
|
|
11
|
+
)
|
|
12
|
+
if cli:
|
|
13
|
+
raise SystemExit(message)
|
|
14
|
+
raise ImportError(message)
|
sshscript/_version.py
CHANGED
sshscript/channelgeneric.py
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import threading, os, sys, re
|
|
20
20
|
import time
|
|
21
|
+
import uuid
|
|
21
22
|
import logging
|
|
22
23
|
import paramiko
|
|
23
24
|
import asyncio
|
|
@@ -67,6 +68,7 @@ class GenericChannel(object):
|
|
|
67
68
|
|
|
68
69
|
## an instance of Dollar
|
|
69
70
|
self.owner = owner
|
|
71
|
+
self.logger = getattr(owner, "logger", logger)
|
|
70
72
|
|
|
71
73
|
## see hijack() for details
|
|
72
74
|
self.hijacked = False
|
|
@@ -80,12 +82,13 @@ class GenericChannel(object):
|
|
|
80
82
|
## 如果沒有,則送出一個dumyEcho
|
|
81
83
|
#self._exitcode_session_id = 0
|
|
82
84
|
#self._command_session_id = 0
|
|
83
|
-
|
|
85
|
+
settings = getattr(owner, 'settings', {})
|
|
86
|
+
if settings.get('verbose', bool(os.environ.get('VERBOSE'))):
|
|
84
87
|
## verbose-related
|
|
85
88
|
self.dump2sys = (1,1)
|
|
86
89
|
self.stdoutPrefix = os.environ.get('VERBOSE_STDOUT_PREFIX','🟩').encode('utf8')
|
|
87
90
|
self.stderrPrefix = os.environ.get('VERBOSE_STDERR_PREFIX','🟨').encode('utf8')
|
|
88
|
-
elif os.environ.get('VERBOSE_STDERR'):
|
|
91
|
+
elif settings.get('verbose_stderr', bool(os.environ.get('VERBOSE_STDERR'))):
|
|
89
92
|
self.dump2sys = (0,1)
|
|
90
93
|
self.stdoutPrefix = b''
|
|
91
94
|
self.stderrPrefix = os.environ.get('VERBOSE_STDERR_PREFIX','🟨').encode('utf8')
|
|
@@ -141,6 +144,8 @@ class GenericChannel(object):
|
|
|
141
144
|
self._send_call_lock = threading.Lock()
|
|
142
145
|
self._async_send_lock = None
|
|
143
146
|
self.send_timeout = 30
|
|
147
|
+
self._console_job = None
|
|
148
|
+
self._console_job_lock = threading.Lock()
|
|
144
149
|
self.command_timeout = 60
|
|
145
150
|
|
|
146
151
|
@property
|
|
@@ -286,7 +291,7 @@ class GenericChannel(object):
|
|
|
286
291
|
basetime = self.lastIOAtTime[:]
|
|
287
292
|
timeouttime = (time.time() + timeout) if timeout else 0
|
|
288
293
|
ret = True
|
|
289
|
-
logger.debug(
|
|
294
|
+
self.logger.debug(
|
|
290
295
|
'%s Waiting for channel output (timeout=%s, silent=%s)',
|
|
291
296
|
self.prefixOfLog,
|
|
292
297
|
timeout or None,
|
|
@@ -314,7 +319,7 @@ class GenericChannel(object):
|
|
|
314
319
|
else:
|
|
315
320
|
raise TimeoutError(f'wait_for_output exceeded {timeout}')
|
|
316
321
|
|
|
317
|
-
logger.debug(
|
|
322
|
+
self.logger.debug(
|
|
318
323
|
'%s Channel output wait completed (received=%s)',
|
|
319
324
|
self.prefixOfLog,
|
|
320
325
|
ret,
|
|
@@ -395,7 +400,7 @@ class GenericChannel(object):
|
|
|
395
400
|
"""Return the recorded status, fetching it if needed; -1 while interactive."""
|
|
396
401
|
if self.hijacked:
|
|
397
402
|
#raise ValueError('exitcode is not available in current state')
|
|
398
|
-
logger.warning(
|
|
403
|
+
self.logger.warning(
|
|
399
404
|
'%s Exit status is unavailable while the channel is in interactive mode',
|
|
400
405
|
self.prefixOfLog,
|
|
401
406
|
)
|
|
@@ -410,7 +415,7 @@ class GenericChannel(object):
|
|
|
410
415
|
"""Log with channel context, preserving format arguments and logging keywords."""
|
|
411
416
|
if isinstance(level, int) and 0 < level < logging.DEBUG:
|
|
412
417
|
level = logging.DEBUG
|
|
413
|
-
logger.log(level, f'{self.prefixOfLog} {msg}', *args, **kwargs)
|
|
418
|
+
getattr(self, 'logger', logger).log(level, f'{self.prefixOfLog} {msg}', *args, **kwargs)
|
|
414
419
|
|
|
415
420
|
def expect(
|
|
416
421
|
self,
|
|
@@ -1401,7 +1406,7 @@ class GenericChannel(object):
|
|
|
1401
1406
|
|
|
1402
1407
|
:text: Text to send as input
|
|
1403
1408
|
"""
|
|
1404
|
-
logger.debug(
|
|
1409
|
+
self.logger.debug(
|
|
1405
1410
|
'%s Sending channel input (length=%d, interactive=%s)',
|
|
1406
1411
|
self.prefixOfLog,
|
|
1407
1412
|
len(text),
|
|
@@ -1482,6 +1487,11 @@ class GenericChannel(object):
|
|
|
1482
1487
|
)
|
|
1483
1488
|
|
|
1484
1489
|
def input(self, text, timeout=60):
|
|
1490
|
+
"""Append one newline and wait for interactive readiness, exit, or silence.
|
|
1491
|
+
|
|
1492
|
+
Existing newlines are retained. Use send() to write exact text instead.
|
|
1493
|
+
A readiness/silence status does not establish command success.
|
|
1494
|
+
"""
|
|
1485
1495
|
if not isinstance(text, str):
|
|
1486
1496
|
raise TypeError('input text must be str')
|
|
1487
1497
|
|
|
@@ -1724,6 +1734,8 @@ class GenericChannel(object):
|
|
|
1724
1734
|
stdout = None
|
|
1725
1735
|
stderr = None
|
|
1726
1736
|
completed = threading.Event()
|
|
1737
|
+
stderr_completed = threading.Event()
|
|
1738
|
+
stderr_marker = None
|
|
1727
1739
|
|
|
1728
1740
|
try:
|
|
1729
1741
|
self._exitcode = EXITCODE_DEFAULT
|
|
@@ -1754,9 +1766,16 @@ class GenericChannel(object):
|
|
|
1754
1766
|
'-@_@-',
|
|
1755
1767
|
)
|
|
1756
1768
|
|
|
1769
|
+
# Separate pipe readers can observe stdout completion before
|
|
1770
|
+
# earlier stderr has been drained. Fence both streams before
|
|
1771
|
+
# returning a completed command's buffers to the public wrapper.
|
|
1772
|
+
trailer = 'echo _TT$?_-@_@-'
|
|
1773
|
+
if not self.owner.get_pty:
|
|
1774
|
+
stderr_marker = '-@stderr-' + uuid.uuid4().hex + '\n'
|
|
1775
|
+
stderr.set_callback(stderr_completed.set, stderr_marker)
|
|
1776
|
+
trailer += "; printf '%s\\n' " + repr(stderr_marker.rstrip('\n')) + ' >&2'
|
|
1757
1777
|
self.send(
|
|
1758
|
-
f'{{ {command}; }} ; '
|
|
1759
|
-
'echo _TT$?_-@_@-\n',
|
|
1778
|
+
f'{{ {command}; }} ; ' + trailer + '\n',
|
|
1760
1779
|
timeout=self._remaining(deadline),
|
|
1761
1780
|
)
|
|
1762
1781
|
|
|
@@ -1794,9 +1813,12 @@ class GenericChannel(object):
|
|
|
1794
1813
|
'command completion',
|
|
1795
1814
|
)
|
|
1796
1815
|
|
|
1816
|
+
if stderr_marker is not None:
|
|
1817
|
+
self._wait_event(stderr_completed, deadline, 'stderr completion')
|
|
1818
|
+
|
|
1797
1819
|
if not self.prompt:
|
|
1798
1820
|
match = re.compile(
|
|
1799
|
-
r'_TT(\d+)_'
|
|
1821
|
+
r'_TT(\d+)_\r?\n?$'
|
|
1800
1822
|
).search(str(stdout))
|
|
1801
1823
|
|
|
1802
1824
|
if match is None:
|
|
@@ -1826,7 +1848,7 @@ class GenericChannel(object):
|
|
|
1826
1848
|
|
|
1827
1849
|
:return: Tuple of (stdout, stderr)
|
|
1828
1850
|
"""
|
|
1829
|
-
logger.debug(
|
|
1851
|
+
self.logger.debug(
|
|
1830
1852
|
'%s Dispatching command (interactive=%s, expectations=%d) %s',
|
|
1831
1853
|
self.prefixOfLog,
|
|
1832
1854
|
bool(self.prompt),
|
|
@@ -1874,7 +1896,7 @@ class GenericChannel(object):
|
|
|
1874
1896
|
if m:
|
|
1875
1897
|
self._exitcode = int(m.group(1))
|
|
1876
1898
|
else:
|
|
1877
|
-
logger.warning(
|
|
1899
|
+
self.logger.warning(
|
|
1878
1900
|
'%s Unable to parse command exit status',
|
|
1879
1901
|
self.prefixOfLog,
|
|
1880
1902
|
)
|
|
@@ -1891,7 +1913,7 @@ class GenericChannel(object):
|
|
|
1891
1913
|
## so that when next line is getting $.exitcode, it would get the corrent exitcode
|
|
1892
1914
|
## not the exitcode of our modified command.
|
|
1893
1915
|
while self.executing_lock.locked(): time.sleep(0.01)
|
|
1894
|
-
logger.debug(
|
|
1916
|
+
self.logger.debug(
|
|
1895
1917
|
'%s Command completed (interactive=%s, exit_status=%s)',
|
|
1896
1918
|
self.prefixOfLog,
|
|
1897
1919
|
bool(self.prompt),
|
|
@@ -1921,7 +1943,7 @@ class GenericChannel(object):
|
|
|
1921
1943
|
if self.closed: return
|
|
1922
1944
|
self.touchIO(True)
|
|
1923
1945
|
try:
|
|
1924
|
-
self._stdout.append(newbytes.decode('utf8'),True)
|
|
1946
|
+
self._stdout.append(self._console_job._decoders[0].decode(newbytes) if self._console_job is not None else newbytes.decode('utf8'),True)
|
|
1925
1947
|
except UnicodeDecodeError:
|
|
1926
1948
|
self._stdout.append(newbytes.decode('utf8','replace'),True)
|
|
1927
1949
|
if self.dump2sys[0]:
|
|
@@ -1938,7 +1960,7 @@ class GenericChannel(object):
|
|
|
1938
1960
|
if self.closed: return
|
|
1939
1961
|
self.touchIO(True)
|
|
1940
1962
|
try:
|
|
1941
|
-
self._stderr.append(newbytes.decode('utf8'),True)
|
|
1963
|
+
self._stderr.append(self._console_job._decoders[1].decode(newbytes) if self._console_job is not None else newbytes.decode('utf8'),True)
|
|
1942
1964
|
except UnicodeDecodeError:
|
|
1943
1965
|
self._stderr.append(newbytes.decode('utf8','replace'),True)
|
|
1944
1966
|
if self.dump2sys[1]:
|
|
@@ -2000,13 +2022,23 @@ class GenericChannel(object):
|
|
|
2000
2022
|
await handler[x](newbytes)
|
|
2001
2023
|
self._dumpBuf.clear()
|
|
2002
2024
|
except Exception:
|
|
2003
|
-
logger.exception('%s Channel output forwarding failed', self.prefixOfLog)
|
|
2025
|
+
self.logger.exception('%s Channel output forwarding failed', self.prefixOfLog)
|
|
2004
2026
|
raise
|
|
2005
2027
|
async def _dump_stdout_err(self):
|
|
2006
2028
|
handler = [self._dump_stdout,self._dump_stderr]
|
|
2007
2029
|
for x,newbytes in self._dumpBuf:
|
|
2008
2030
|
await handler[x](newbytes)
|
|
2009
2031
|
self._dumpBuf.clear()
|
|
2032
|
+
# One-shot completion must also forward a final unterminated line.
|
|
2033
|
+
for name, stream, prefix in (
|
|
2034
|
+
('_stdoutDumpBuf', sys.stdout, self.stdoutPrefix),
|
|
2035
|
+
('_stderrDumpBuf', sys.stderr, self.stderrPrefix),
|
|
2036
|
+
):
|
|
2037
|
+
pending = getattr(self, name)
|
|
2038
|
+
if pending:
|
|
2039
|
+
stream.buffer.write(prefix + self._native_id.encode() + b':' + pending)
|
|
2040
|
+
stream.buffer.flush()
|
|
2041
|
+
setattr(self, name, b'')
|
|
2010
2042
|
|
|
2011
2043
|
def reset_buffer(self,reason=None):
|
|
2012
2044
|
"""Replace the current stdout/stderr pair; return (new_pair, previous_pair)."""
|
sshscript/channelssh.py
CHANGED
|
@@ -36,7 +36,8 @@ class ParamikoChannel(object):
|
|
|
36
36
|
def __init__(self,sshchannel,get_pty,channel=None):
|
|
37
37
|
if not isinstance(sshchannel, SSHChannel):
|
|
38
38
|
raise TypeError('sshchannel must be SSHChannel')
|
|
39
|
-
self.sshchannel = sshchannel
|
|
39
|
+
self.sshchannel = sshchannel
|
|
40
|
+
self.logger = sshchannel.logger
|
|
40
41
|
self.get_pty = get_pty
|
|
41
42
|
self.suspending = False
|
|
42
43
|
if channel is None:
|
|
@@ -78,7 +79,7 @@ class ParamikoChannel(object):
|
|
|
78
79
|
|
|
79
80
|
## two-dollars has no self.command
|
|
80
81
|
if self.sshchannel.owner.command:
|
|
81
|
-
logger.debug(
|
|
82
|
+
self.logger.debug(
|
|
82
83
|
'[SSHChannel] Executing remote command (host=%s, pty=%s)',
|
|
83
84
|
self.sshchannel.owner.session.host,
|
|
84
85
|
self.get_pty,
|
|
@@ -118,7 +119,7 @@ class ParamikoChannel(object):
|
|
|
118
119
|
except (SSHException,ValueError):
|
|
119
120
|
if self.channel.closed or self.channel.exit_status_ready():
|
|
120
121
|
break
|
|
121
|
-
logger.exception(
|
|
122
|
+
self.logger.exception(
|
|
122
123
|
'[SSHChannel] SSH channel read failed (host=%s)',
|
|
123
124
|
self.sshchannel.owner.session.host,
|
|
124
125
|
)
|
|
@@ -126,7 +127,7 @@ class ParamikoChannel(object):
|
|
|
126
127
|
await asyncio.sleep(0.1)
|
|
127
128
|
except OSError as e:
|
|
128
129
|
if e.errno != errno.EIO and not self.channel.closed:
|
|
129
|
-
logger.exception(
|
|
130
|
+
self.logger.exception(
|
|
130
131
|
'[SSHChannel] SSH channel reader failed (host=%s)',
|
|
131
132
|
self.sshchannel.owner.session.host,
|
|
132
133
|
)
|
|
@@ -166,7 +167,7 @@ class ParamikoChannel(object):
|
|
|
166
167
|
command result is kept separately from the SSH channel exit status.
|
|
167
168
|
"""
|
|
168
169
|
host = self.sshchannel.owner.session.host
|
|
169
|
-
logger.debug('[SSHChannel] Closing SSH channel (host=%s)', host)
|
|
170
|
+
self.logger.debug('[SSHChannel] Closing SSH channel (host=%s)', host)
|
|
170
171
|
## automatically send exit to shell
|
|
171
172
|
try:
|
|
172
173
|
if not self.channel.exit_status_ready():
|
|
@@ -176,7 +177,7 @@ class ParamikoChannel(object):
|
|
|
176
177
|
deadline = time.monotonic() + timeout_seconds
|
|
177
178
|
while True:
|
|
178
179
|
if time.monotonic() > deadline:
|
|
179
|
-
logger.warning(
|
|
180
|
+
self.logger.warning(
|
|
180
181
|
'[SSHChannel] Exit status was not received before close timeout; '
|
|
181
182
|
'forcing channel close (host=%s, timeout=%ss)',
|
|
182
183
|
host,
|
|
@@ -191,7 +192,7 @@ class ParamikoChannel(object):
|
|
|
191
192
|
else:
|
|
192
193
|
time.sleep(0.1)
|
|
193
194
|
except (SSHException, OSError):
|
|
194
|
-
logger.exception('[SSHChannel] Failed to close SSH channel (host=%s)', host)
|
|
195
|
+
self.logger.exception('[SSHChannel] Failed to close SSH channel (host=%s)', host)
|
|
195
196
|
raise
|
|
196
197
|
finally:
|
|
197
198
|
self.channel.close()
|
sshscript/channelsubprocess.py
CHANGED
|
@@ -189,7 +189,7 @@ class POpenChannel(GenericChannel):
|
|
|
189
189
|
for key, mask in events:
|
|
190
190
|
# key.fileobj 是原始的 pipe 物件
|
|
191
191
|
# key.data 是我們剛才註冊的自定義字串
|
|
192
|
-
await callback[key.fileobj](os.read(key.fileobj,1024))
|
|
192
|
+
await callback[key.fileobj](os.read(key.fileobj, 65536 if self._console_job is not None else 1024))
|
|
193
193
|
await asyncio.sleep(interval)
|
|
194
194
|
except OSError as e:
|
|
195
195
|
## when subprocess exited, the file descriptor would be closed, and os.read() would raise OSError with errno.EIO
|
|
@@ -200,7 +200,7 @@ class POpenChannel(GenericChannel):
|
|
|
200
200
|
sel.close()
|
|
201
201
|
POpenChannel.count += 1
|
|
202
202
|
await _reading()
|
|
203
|
-
logger.debug(
|
|
203
|
+
self.logger.debug(
|
|
204
204
|
'[POpen] Subprocess reader stopped (pid=%s, returncode=%s)',
|
|
205
205
|
self.cp.pid,
|
|
206
206
|
self.cp.poll(),
|
|
@@ -248,7 +248,7 @@ class POpenChannel(GenericChannel):
|
|
|
248
248
|
pid = self.cp.pid
|
|
249
249
|
if self.cp.poll() is None:
|
|
250
250
|
timeout = 2
|
|
251
|
-
logger.debug(
|
|
251
|
+
self.logger.debug(
|
|
252
252
|
'[POpen] Waiting for subprocess to exit (pid=%s, timeout=%ss)',
|
|
253
253
|
pid,
|
|
254
254
|
timeout,
|
|
@@ -256,7 +256,7 @@ class POpenChannel(GenericChannel):
|
|
|
256
256
|
try:
|
|
257
257
|
self.cp.wait(timeout)
|
|
258
258
|
except subprocess.TimeoutExpired:
|
|
259
|
-
logger.warning(
|
|
259
|
+
self.logger.warning(
|
|
260
260
|
'[POpen] Subprocess did not exit before close timeout; '
|
|
261
261
|
'terminating (pid=%s, timeout=%ss)',
|
|
262
262
|
pid,
|
|
@@ -266,7 +266,7 @@ class POpenChannel(GenericChannel):
|
|
|
266
266
|
try:
|
|
267
267
|
self.cp.wait(timeout)
|
|
268
268
|
except subprocess.TimeoutExpired:
|
|
269
|
-
logger.warning(
|
|
269
|
+
self.logger.warning(
|
|
270
270
|
'[POpen] Subprocess did not terminate before timeout; '
|
|
271
271
|
'killing (pid=%s, timeout=%ss)',
|
|
272
272
|
pid,
|
|
@@ -284,7 +284,7 @@ class POpenChannel(GenericChannel):
|
|
|
284
284
|
else:
|
|
285
285
|
fd.close()
|
|
286
286
|
|
|
287
|
-
logger.debug(
|
|
287
|
+
self.logger.debug(
|
|
288
288
|
'[POpen] Subprocess channel closed (pid=%s, returncode=%s)',
|
|
289
289
|
pid,
|
|
290
290
|
self.cp.poll(),
|