2026-03-21 04:29:58 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
import os, sys, shlex, asyncio, asyncssh, shutil, signal
|
2026-03-21 04:29:58 +01:00
|
|
|
|
|
|
|
|
from ...log import *
|
2026-04-16 11:23:05 +02:00
|
|
|
from ...base import Result
|
2026-03-21 04:29:58 +01:00
|
|
|
from ..SSHClient import SSHClient as Base
|
|
|
|
|
|
2026-04-10 14:58:29 +02:00
|
|
|
from .util import join_cmd
|
|
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
_USE_DEFAULT_KNOWN_HOSTS = object()
|
|
|
|
|
|
|
|
|
|
class AsyncSSH(Base):
|
2026-04-15 21:49:40 +02:00
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
uri: str,
|
|
|
|
|
*,
|
|
|
|
|
client_keys: list[str] | None = None,
|
2026-04-19 14:04:27 +02:00
|
|
|
known_hosts = _USE_DEFAULT_KNOWN_HOSTS,
|
2026-03-21 04:29:58 +01:00
|
|
|
term_type: str | None = None,
|
|
|
|
|
connect_timeout: float | None = 30.0,
|
|
|
|
|
**kwargs,
|
|
|
|
|
) -> None:
|
|
|
|
|
|
|
|
|
|
super().__init__(
|
|
|
|
|
uri,
|
2026-04-19 14:04:27 +02:00
|
|
|
caps = self.Caps.LogOutput | self.Caps.Wd | self.Caps.Interactive | self.Caps.Env,
|
2026-03-21 04:29:58 +01:00
|
|
|
**kwargs
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.client_keys = client_keys
|
|
|
|
|
self.known_hosts = known_hosts
|
2026-04-19 14:04:27 +02:00
|
|
|
self.term_type = term_type or os.environ.get('TERM', 'xterm')
|
2026-03-21 04:29:58 +01:00
|
|
|
self.connect_timeout = connect_timeout
|
|
|
|
|
|
2026-04-19 14:04:35 +02:00
|
|
|
def _connect_kwargs(self, hide_secrets: bool=False) -> dict:
|
|
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
kwargs: dict = {
|
2026-04-19 14:04:27 +02:00
|
|
|
'host': self.hostname,
|
|
|
|
|
'port': self.port,
|
|
|
|
|
'username': self.username,
|
|
|
|
|
'password': self.password,
|
|
|
|
|
'client_keys': self.client_keys,
|
|
|
|
|
'connect_timeout': self.connect_timeout,
|
2026-03-21 04:29:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.known_hosts is not _USE_DEFAULT_KNOWN_HOSTS:
|
2026-04-19 14:04:27 +02:00
|
|
|
kwargs['known_hosts'] = self.known_hosts
|
2026-03-21 04:29:58 +01:00
|
|
|
|
2026-04-19 14:04:35 +02:00
|
|
|
ret = {k: v for k, v in kwargs.items() if v is not None}
|
|
|
|
|
if hide_secrets and 'password' in kwargs:
|
|
|
|
|
kwargs['password'] = '<hidden>'
|
|
|
|
|
return ret
|
2026-03-21 04:29:58 +01:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _build_remote_command(cmd: list[str], wd: str | None) -> str:
|
|
|
|
|
|
|
|
|
|
if not cmd:
|
2026-04-19 14:04:27 +02:00
|
|
|
raise ValueError('cmd must not be empty')
|
2026-03-21 04:29:58 +01:00
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
inner = f'exec {join_cmd(cmd)}'
|
2026-03-21 04:29:58 +01:00
|
|
|
if wd is not None:
|
2026-04-19 14:04:27 +02:00
|
|
|
inner = f'cd {shlex.quote(wd)} && {inner}'
|
2026-03-21 04:29:58 +01:00
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
return f'/bin/sh -lc {shlex.quote(inner)}'
|
2026-03-21 04:29:58 +01:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
@staticmethod
|
|
|
|
|
def _has_local_tty() -> bool:
|
|
|
|
|
try:
|
|
|
|
|
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _get_local_term_size() -> tuple[int, int, int, int]:
|
|
|
|
|
cols, rows = shutil.get_terminal_size(fallback=(80, 24))
|
|
|
|
|
xpixel = ypixel = 0
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
try:
|
|
|
|
|
import fcntl, termios, struct
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
packed = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b'\0' * 8)
|
|
|
|
|
rows2, cols2, xpixel, ypixel = struct.unpack('HHHH', packed)
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
if cols2 > 0 and rows2 > 0:
|
|
|
|
|
cols, rows = cols2, rows2
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
return (cols, rows, xpixel, ypixel)
|
|
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
async def _read_stream(
|
|
|
|
|
self,
|
|
|
|
|
stream,
|
|
|
|
|
prio,
|
|
|
|
|
collector: list[bytes],
|
|
|
|
|
*,
|
|
|
|
|
verbose: bool,
|
|
|
|
|
log_prefix: str,
|
|
|
|
|
log_enc: str,
|
|
|
|
|
) -> None:
|
2026-04-19 14:04:27 +02:00
|
|
|
buf = b''
|
2026-03-21 04:29:58 +01:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
chunk = await stream.read(4096)
|
|
|
|
|
if not chunk:
|
|
|
|
|
break
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
collector.append(chunk)
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
if verbose:
|
|
|
|
|
buf += chunk
|
2026-04-19 14:04:27 +02:00
|
|
|
while b'\n' in buf:
|
|
|
|
|
line, buf = buf.split(b'\n', 1)
|
|
|
|
|
log(prio, log_prefix, line.decode(log_enc, errors='replace'))
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
if verbose and buf:
|
2026-04-19 14:04:27 +02:00
|
|
|
log(prio, log_prefix, buf.decode(log_enc, errors='replace'))
|
2026-03-21 04:29:58 +01:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
async def _run_interactive_on_conn(
|
|
|
|
|
self,
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
conn: asyncssh.SSHClientConnection,
|
2026-03-21 11:34:35 +01:00
|
|
|
cmd: list[str],
|
|
|
|
|
wd: str | None,
|
2026-04-15 14:02:44 +02:00
|
|
|
cmd_input: bytes | None,
|
2026-04-19 14:04:35 +02:00
|
|
|
mod_env: dict[str, str] | None,
|
2026-03-21 11:34:35 +01:00
|
|
|
) -> Result:
|
2026-04-19 14:04:35 +02:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
command = self._build_remote_command(cmd, wd)
|
|
|
|
|
stdout_parts: list[bytes] = []
|
|
|
|
|
|
|
|
|
|
proc = await conn.create_process(
|
2026-04-19 14:04:27 +02:00
|
|
|
command = command,
|
|
|
|
|
env = mod_env,
|
|
|
|
|
stdin = asyncssh.PIPE,
|
|
|
|
|
stdout = asyncssh.PIPE,
|
|
|
|
|
stderr = asyncssh.STDOUT,
|
|
|
|
|
encoding = None,
|
|
|
|
|
request_pty = 'force',
|
|
|
|
|
term_type = self.term_type,
|
|
|
|
|
term_size = self._get_local_term_size(),
|
2026-03-21 11:34:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
stdin_fd = sys.stdin.fileno()
|
|
|
|
|
|
|
|
|
|
stdin_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
|
|
|
|
|
old_tty_state = None
|
|
|
|
|
old_winch_handler = None
|
|
|
|
|
stdin_reader_installed = False
|
|
|
|
|
|
|
|
|
|
def _write_local(data: bytes) -> None:
|
|
|
|
|
try:
|
|
|
|
|
sys.stdout.buffer.write(data)
|
|
|
|
|
sys.stdout.buffer.flush()
|
|
|
|
|
except AttributeError:
|
|
|
|
|
os.write(sys.stdout.fileno(), data)
|
|
|
|
|
|
|
|
|
|
def _on_stdin_ready() -> None:
|
|
|
|
|
try:
|
|
|
|
|
data = os.read(stdin_fd, 4096)
|
|
|
|
|
except OSError:
|
2026-04-19 14:04:27 +02:00
|
|
|
data = b''
|
2026-03-21 11:34:35 +01:00
|
|
|
|
|
|
|
|
if data:
|
|
|
|
|
stdin_queue.put_nowait(data)
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
loop.remove_reader(stdin_fd)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
stdin_queue.put_nowait(None)
|
|
|
|
|
|
|
|
|
|
async def _pump_stdin() -> None:
|
|
|
|
|
if cmd_input is not None and proc.stdin is not None:
|
2026-04-15 14:02:44 +02:00
|
|
|
proc.stdin.write(cmd_input)
|
2026-03-21 11:34:35 +01:00
|
|
|
await proc.stdin.drain()
|
|
|
|
|
while True:
|
|
|
|
|
data = await stdin_queue.get()
|
|
|
|
|
if data is None:
|
|
|
|
|
if proc.stdin is not None:
|
|
|
|
|
try:
|
|
|
|
|
proc.stdin.write_eof()
|
|
|
|
|
except (BrokenPipeError, OSError):
|
|
|
|
|
pass
|
|
|
|
|
return
|
|
|
|
|
if proc.stdin is None:
|
|
|
|
|
return
|
|
|
|
|
proc.stdin.write(data)
|
|
|
|
|
await proc.stdin.drain()
|
|
|
|
|
|
|
|
|
|
async def _pump_stdout() -> None:
|
|
|
|
|
while True:
|
|
|
|
|
chunk = await proc.stdout.read(4096)
|
|
|
|
|
if not chunk:
|
|
|
|
|
break
|
|
|
|
|
stdout_parts.append(chunk)
|
|
|
|
|
_write_local(chunk)
|
|
|
|
|
|
|
|
|
|
def _on_winch(*_args) -> None:
|
2026-04-15 21:49:40 +02:00
|
|
|
|
2026-03-21 11:34:35 +01:00
|
|
|
try:
|
|
|
|
|
proc.change_terminal_size(*self._get_local_term_size())
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
sys.stderr.flush()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import termios, tty
|
|
|
|
|
|
|
|
|
|
old_tty_state = termios.tcgetattr(stdin_fd)
|
|
|
|
|
tty.setraw(stdin_fd)
|
|
|
|
|
except Exception:
|
|
|
|
|
old_tty_state = None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
loop.add_reader(stdin_fd, _on_stdin_ready)
|
|
|
|
|
stdin_reader_installed = True
|
|
|
|
|
except (NotImplementedError, RuntimeError):
|
|
|
|
|
stdin_queue.put_nowait(None)
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
if hasattr(signal, 'SIGWINCH'):
|
2026-03-21 11:34:35 +01:00
|
|
|
try:
|
|
|
|
|
old_winch_handler = signal.getsignal(signal.SIGWINCH)
|
|
|
|
|
signal.signal(signal.SIGWINCH, _on_winch)
|
|
|
|
|
except Exception:
|
|
|
|
|
old_winch_handler = None
|
|
|
|
|
|
|
|
|
|
stdin_task = asyncio.create_task(_pump_stdin())
|
|
|
|
|
stdout_task = asyncio.create_task(_pump_stdout())
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
completed = await proc.wait(check = False)
|
2026-03-21 11:34:35 +01:00
|
|
|
await stdout_task
|
|
|
|
|
|
|
|
|
|
if not stdin_task.done():
|
|
|
|
|
stdin_task.cancel()
|
|
|
|
|
try:
|
|
|
|
|
await stdin_task
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
exit_code = completed.exit_status
|
|
|
|
|
if exit_code is None:
|
|
|
|
|
exit_code = completed.returncode if completed.returncode is not None else -1
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
stdout = b''.join(stdout_parts) if stdout_parts else None
|
2026-03-21 11:34:35 +01:00
|
|
|
return Result(stdout, None, exit_code)
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
if stdin_reader_installed:
|
|
|
|
|
try:
|
|
|
|
|
loop.remove_reader(stdin_fd)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
if old_winch_handler is not None and hasattr(signal, 'SIGWINCH'):
|
2026-03-21 11:34:35 +01:00
|
|
|
try:
|
|
|
|
|
signal.signal(signal.SIGWINCH, old_winch_handler)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if old_tty_state is not None:
|
|
|
|
|
try:
|
|
|
|
|
import termios
|
|
|
|
|
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty_state)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
sys.stderr.flush()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def _run_captured_pty_on_conn(
|
|
|
|
|
self,
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
conn: asyncssh.SSHClientConnection,
|
2026-03-21 11:34:35 +01:00
|
|
|
cmd: list[str],
|
|
|
|
|
wd: str | None,
|
|
|
|
|
verbose: bool,
|
2026-04-15 14:02:44 +02:00
|
|
|
cmd_input: bytes | None,
|
2026-04-19 14:04:35 +02:00
|
|
|
mod_env: dict[str, str] | None,
|
2026-03-21 11:34:35 +01:00
|
|
|
log_prefix: str,
|
|
|
|
|
) -> Result:
|
|
|
|
|
command = self._build_remote_command(cmd, wd)
|
|
|
|
|
|
|
|
|
|
stdout_parts: list[bytes] = []
|
2026-04-19 14:04:27 +02:00
|
|
|
stdout_log_enc = sys.stdout.encoding or 'utf-8'
|
2026-03-21 11:34:35 +01:00
|
|
|
|
|
|
|
|
proc = await conn.create_process(
|
2026-04-19 14:04:27 +02:00
|
|
|
command = command,
|
|
|
|
|
env = mod_env,
|
|
|
|
|
stdin = asyncssh.PIPE if cmd_input is not None else asyncssh.DEVNULL,
|
|
|
|
|
stdout = asyncssh.PIPE,
|
|
|
|
|
stderr = asyncssh.STDOUT,
|
|
|
|
|
encoding = None,
|
|
|
|
|
request_pty = 'force',
|
|
|
|
|
term_type = self.term_type,
|
2026-03-21 11:34:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
task = asyncio.create_task(
|
|
|
|
|
self._read_stream(
|
|
|
|
|
proc.stdout,
|
|
|
|
|
NOTICE,
|
|
|
|
|
stdout_parts,
|
2026-04-19 14:04:27 +02:00
|
|
|
verbose = verbose,
|
|
|
|
|
log_prefix = log_prefix,
|
|
|
|
|
log_enc = stdout_log_enc,
|
2026-03-21 11:34:35 +01:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if cmd_input is not None and proc.stdin is not None:
|
2026-04-15 14:02:44 +02:00
|
|
|
proc.stdin.write(cmd_input)
|
2026-03-21 11:34:35 +01:00
|
|
|
await proc.stdin.drain()
|
|
|
|
|
proc.stdin.write_eof()
|
|
|
|
|
|
|
|
|
|
completed = await proc.wait(check=False)
|
|
|
|
|
await task
|
|
|
|
|
|
|
|
|
|
exit_code = completed.exit_status
|
|
|
|
|
if exit_code is None:
|
|
|
|
|
exit_code = completed.returncode if completed.returncode is not None else -1
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
stdout = b''.join(stdout_parts) if stdout_parts else None
|
2026-03-21 11:34:35 +01:00
|
|
|
return Result(stdout, None, exit_code)
|
|
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
async def _run_on_conn(
|
|
|
|
|
self,
|
lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a3be98192cc75e8c364cab700b3f9fb.
Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.
The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
return await self._run_on_conn(
^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
proc = await conn.create_process(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<7 lines>...
)
^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
chan, process = await self.create_session(
^^^^^^^^^^^^^^^^^^^^^^^^^^
SSHClientProcess, *args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
session = await chan.create(session_factory, command, subsystem,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
bool(self._agent_forward_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
packet = await self._open(b'session')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
return await self._open_waiter
^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 15:42:20 +02:00
|
|
|
conn: asyncssh.SSHClientConnection,
|
2026-03-21 04:29:58 +01:00
|
|
|
cmd: list[str],
|
|
|
|
|
wd: str | None,
|
|
|
|
|
verbose: bool,
|
2026-04-15 14:02:44 +02:00
|
|
|
cmd_input: bytes | None,
|
2026-04-19 14:04:35 +02:00
|
|
|
mod_env: dict[str, str] | None,
|
2026-03-21 04:29:58 +01:00
|
|
|
interactive: bool,
|
|
|
|
|
log_prefix: str,
|
|
|
|
|
) -> Result:
|
2026-03-21 11:34:35 +01:00
|
|
|
if interactive:
|
|
|
|
|
if self._has_local_tty():
|
|
|
|
|
return await self._run_interactive_on_conn(
|
2026-04-19 14:04:27 +02:00
|
|
|
conn = conn,
|
|
|
|
|
cmd = cmd,
|
|
|
|
|
wd = wd,
|
|
|
|
|
cmd_input = cmd_input,
|
|
|
|
|
mod_env = mod_env,
|
2026-03-21 11:34:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return await self._run_captured_pty_on_conn(
|
2026-04-19 14:04:27 +02:00
|
|
|
conn = conn,
|
|
|
|
|
cmd = cmd,
|
|
|
|
|
wd = wd,
|
|
|
|
|
verbose = verbose,
|
|
|
|
|
cmd_input = cmd_input,
|
|
|
|
|
mod_env = mod_env,
|
|
|
|
|
log_prefix = log_prefix,
|
2026-03-21 11:34:35 +01:00
|
|
|
)
|
|
|
|
|
|
2026-03-21 04:29:58 +01:00
|
|
|
command = self._build_remote_command(cmd, wd)
|
|
|
|
|
|
|
|
|
|
stdout_parts: list[bytes] = []
|
|
|
|
|
stderr_parts: list[bytes] = []
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
stdout_log_enc = sys.stdout.encoding or 'utf-8'
|
|
|
|
|
stderr_log_enc = sys.stderr.encoding or 'utf-8'
|
2026-03-21 04:29:58 +01:00
|
|
|
|
|
|
|
|
stdin_mode = asyncssh.PIPE if cmd_input is not None else asyncssh.DEVNULL
|
|
|
|
|
|
|
|
|
|
proc = await conn.create_process(
|
2026-04-19 14:04:27 +02:00
|
|
|
command = command,
|
|
|
|
|
env = mod_env,
|
|
|
|
|
stdin = stdin_mode,
|
|
|
|
|
stdout = asyncssh.PIPE,
|
|
|
|
|
stderr = asyncssh.PIPE,
|
|
|
|
|
encoding = None,
|
|
|
|
|
request_pty = False,
|
2026-03-21 04:29:58 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tasks = [
|
|
|
|
|
asyncio.create_task(
|
|
|
|
|
self._read_stream(
|
|
|
|
|
proc.stdout,
|
|
|
|
|
NOTICE,
|
|
|
|
|
stdout_parts,
|
2026-04-19 14:04:27 +02:00
|
|
|
verbose = verbose,
|
|
|
|
|
log_prefix = log_prefix,
|
|
|
|
|
log_enc = stdout_log_enc,
|
2026-03-21 04:29:58 +01:00
|
|
|
)
|
2026-03-21 11:34:35 +01:00
|
|
|
),
|
|
|
|
|
asyncio.create_task(
|
|
|
|
|
self._read_stream(
|
|
|
|
|
proc.stderr,
|
|
|
|
|
ERR,
|
|
|
|
|
stderr_parts,
|
2026-04-19 14:04:27 +02:00
|
|
|
verbose = verbose,
|
|
|
|
|
log_prefix = log_prefix,
|
|
|
|
|
log_enc = stderr_log_enc,
|
2026-03-21 04:29:58 +01:00
|
|
|
)
|
2026-03-21 11:34:35 +01:00
|
|
|
),
|
|
|
|
|
]
|
2026-03-21 04:29:58 +01:00
|
|
|
|
|
|
|
|
if cmd_input is not None and proc.stdin is not None:
|
2026-04-15 14:02:44 +02:00
|
|
|
proc.stdin.write(cmd_input)
|
2026-03-21 04:29:58 +01:00
|
|
|
await proc.stdin.drain()
|
|
|
|
|
proc.stdin.write_eof()
|
|
|
|
|
|
|
|
|
|
completed = await proc.wait(check=False)
|
|
|
|
|
await asyncio.gather(*tasks)
|
|
|
|
|
|
2026-04-19 14:04:27 +02:00
|
|
|
stdout = b''.join(stdout_parts) if stdout_parts else None
|
|
|
|
|
stderr = b''.join(stderr_parts) if stderr_parts else None
|
2026-03-21 04:29:58 +01:00
|
|
|
|
|
|
|
|
exit_code = completed.exit_status
|
|
|
|
|
if exit_code is None:
|
|
|
|
|
exit_code = completed.returncode if completed.returncode is not None else -1
|
|
|
|
|
|
|
|
|
|
return Result(stdout, stderr, exit_code)
|
|
|
|
|
|
|
|
|
|
async def _run_ssh(
|
|
|
|
|
self,
|
|
|
|
|
cmd: list[str],
|
|
|
|
|
wd: str | None,
|
|
|
|
|
verbose: bool,
|
|
|
|
|
cmd_input: str | None,
|
2026-04-19 14:04:35 +02:00
|
|
|
mod_env: dict[str, str] | None,
|
2026-03-21 04:29:58 +01:00
|
|
|
interactive: bool,
|
|
|
|
|
log_prefix: str,
|
|
|
|
|
) -> Result:
|
2026-04-19 14:04:35 +02:00
|
|
|
try:
|
|
|
|
|
async with asyncssh.connect(**self._connect_kwargs()) as conn:
|
|
|
|
|
return await self._run_on_conn(
|
2026-04-19 14:04:27 +02:00
|
|
|
conn = conn,
|
|
|
|
|
cmd = cmd,
|
|
|
|
|
wd = wd,
|
|
|
|
|
verbose = verbose,
|
|
|
|
|
cmd_input = cmd_input,
|
|
|
|
|
mod_env = mod_env,
|
|
|
|
|
interactive = interactive,
|
|
|
|
|
log_prefix = log_prefix,
|
2026-04-19 14:04:35 +02:00
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
msg = f'-------------------- Failed to run command {" ".join(cmd)} ({e})'
|
|
|
|
|
log(ERR, ',', msg)
|
|
|
|
|
for key, val in self._connect_kwargs(hide_secrets=True).items():
|
|
|
|
|
log(ERR, f'| {key:<20} = {val}')
|
|
|
|
|
log(ERR, '`', msg)
|
|
|
|
|
raise
|