jw.pkg: Fix "make check" static code check fallout
The previous commits have put rules for linting and formatting via ruff, yapf, mypy and pyright into place. They are checked with the make check target, and this commit adds the fixes for the target to succeed.
It does some refactoring where type checking dug up dirty bits, and also adds lots of churn in the Python code. To a good deal, that's owed to mere formatting changes. It would have been better to seperate those from syntax and refactoring fixes into multiple commits, so that the interesting changes don't drown in the formatting nose. However, that would have been a lot of additional work only to be thrown away by later commits, hence this commit has a big diff in one piece. The size of the diff is regrettable but hopefully a one-off: What it buys is automatic format checking for CI and predictble formats for smaller diffs in the future.
Rules that "make check" enforces are, in the following order
- Syntax checkers:
- ruff check . - mypy . - pyright- Format check:
- yapf --diff --recursive .The refactoring includes:
- Turn the Result class into a more elaborate object, capable of doing more heavy lifting around stderr and stdout decoding, summarizing outcome, and matching error strings.Aside from fixing broken type checks, this also removes lots of boilerplate calling code which is currently used for handling possible call outcome scenarios. Trying to access an inexistent, decoded string should raise a meaningful exception by itself now, which removes lots of code with case distinctions.- Fix Cmd type hierarchy:
- Add the AbstractCmd class above Cmd. This is necessary because the checker rightfully complains it can't instantiate a Cmd instance where constructor arguments were needed. They never were, but the type used at the instantiating code's location in jw.pkg.App so claims.- Lots of sub- and sub-subcommands are derived from the base class of the invoking command. That provides some properties shared across the ancestor hierarchy of a command, but is semantically unsound. Fix that by introducing jw.pkg.BaseCmd class as a place to provide basic helpers shared across all commands used in a jw.pkg.App's context, and derive all command classes from that afresh. The parent command is still reachable via a common parent property.Formatting changes are conforming to PEP-8, mostly, with minor tweaks. All in all they include the following changes.
- Remove # -*- coding: utf-8 -*-
The line was needed by Python 2 which is not supported anylonger. For Python 3, the default encoding is UTF-8, anyway.- Allow to run "make py-format" without having it produce any changes. It's basically "yapf --in-place --recursive ." with some code style settings, see conf/topdir/pyproject.toml. The settings may be debatable. I've had custom tweaks in place on that target, too, but then again, IDEs would have more hassle to integrate that.- Introduce a 88 character line length limit
- One import per line, reshuffle them semantically, see [tool.isort] in pyproject.toml.- Hide imports needed for type-checking only behind
if TYPE_CHECKING- Spaces around assignments accounts for much churn. Having having no spaces in inline parameter list assignments and default parameter values would arguably be more compact where it's useful. On the other hand, I have not found a code formatter which allows spaces around assignments in parameter lists broken into one per line and that's often better than a wall of text.- Add two spaces before # export, as this seems to be mandated by PEP-8- Use single quotes by default
Signed-off-by: Jan Lindemann <jan@janware.com>
|
|
@ -1,32 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..FileContext import FileContext as Base
|
||||
from ..base import Result
|
||||
from ..FileContext import FileContext as Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..ExecContext import ExecContext
|
||||
from ..Uri import Uri
|
||||
from .Local import Local
|
||||
|
||||
class Curl(Base):
|
||||
|
||||
def __init__(self, uri: str|Uri, *args, ec: ExecContext|None=None, **kwargs) -> None:
|
||||
super().__init__(uri=uri, *args, **kwargs)
|
||||
self.__ec: ExecContext|None = ec
|
||||
if ec is None:
|
||||
def __init__(
|
||||
self, uri: str | Uri, *args, ec: ExecContext | None = None, **kwargs
|
||||
) -> None:
|
||||
|
||||
def __local() -> Local:
|
||||
from .Local import Local
|
||||
self.__ec = Local(interactive=False, *args, **kwargs)
|
||||
|
||||
return Local(interactive = False, *args, **kwargs)
|
||||
|
||||
# MyPy complains for reasons I don't understand:
|
||||
# E: "__init__" of "FileContext" gets multiple values for keyword # argument
|
||||
# "uri" [misc]
|
||||
super().__init__(uri = uri, *args, **kwargs) # type: ignore[misc]
|
||||
|
||||
self.__ec = ec if ec else __local()
|
||||
|
||||
async def _get(
|
||||
self,
|
||||
path: str,
|
||||
wd: str|None,
|
||||
throw: bool,
|
||||
verbose: bool|None,
|
||||
title: str
|
||||
self, path: str, wd: str | None, throw: bool, verbose: bool | None, title: str
|
||||
) -> Result:
|
||||
cmd = ['curl']
|
||||
if verbose is None:
|
||||
|
|
@ -38,5 +41,5 @@ class Curl(Base):
|
|||
path = wd + '/' + path
|
||||
if not len(path) or path[0] != '/':
|
||||
path = '/' + path
|
||||
cmd.append(self.url.to_string + self._chroot(path))
|
||||
return await self.__ec.run(cmd, throw=throw, verbose=verbose)
|
||||
cmd.append(self.uri.to_string + self._chroot(path))
|
||||
return await self.__ec.run(cmd, throw = throw, verbose = verbose)
|
||||
|
|
|
|||
|
|
@ -1,91 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import os, sys, subprocess, asyncio, pwd, grp, stat
|
||||
import asyncio
|
||||
import grp
|
||||
import os
|
||||
import pwd
|
||||
import sys
|
||||
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..ExecContext import ExecContext as Base
|
||||
from ..base import Result, StatResult
|
||||
|
||||
from ..log import *
|
||||
from ..util import pretty_cmd
|
||||
from ..ExecContext import ExecContext as Base
|
||||
from ..log import ERR, NOTICE, log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..Uri import Uri
|
||||
|
||||
class Local(Base):
|
||||
|
||||
def __init__(self, uri: str|Uri='local', *args, **kwargs) -> None:
|
||||
def __init__(self, uri: str | Uri = 'local', *args, **kwargs) -> None:
|
||||
super().__init__(uri, *args, **kwargs)
|
||||
|
||||
@cache
|
||||
def _username(self) -> str:
|
||||
return pwd.getpwuid(os.getuid()).pw_name,
|
||||
return pwd.getpwuid(os.getuid()).pw_name
|
||||
|
||||
async def _run(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str|None,
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes|None,
|
||||
mod_env: dict[str, str]|None,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
|
||||
def __log(prio, *args, verbose=verbose):
|
||||
def __log(prio, *args, verbose = verbose):
|
||||
if verbose:
|
||||
log(prio, log_prefix, *args)
|
||||
|
||||
def __make_pty_reader(collector: list[bytes], enc_for_verbose: str):
|
||||
|
||||
def _read(fd):
|
||||
ret = os.read(fd, 1024)
|
||||
if not ret:
|
||||
return ret
|
||||
collector.append(ret)
|
||||
return ret
|
||||
|
||||
return _read
|
||||
|
||||
cwd: str|None = None
|
||||
cwd: str | None = None
|
||||
if wd is not None:
|
||||
cwd = os.getcwd()
|
||||
os.chdir(wd)
|
||||
|
||||
try:
|
||||
|
||||
# -- interactive mode
|
||||
|
||||
if interactive:
|
||||
|
||||
import pty
|
||||
|
||||
def _spawn():
|
||||
# Apply env in PTY mode by temporarily updating os.environ around spawn.
|
||||
# Apply env in PTY mode by temporarily updating os.environ
|
||||
# around spawn.
|
||||
if mod_env:
|
||||
old_env = os.environ.copy()
|
||||
try:
|
||||
os.environ.update(mod_env)
|
||||
return pty.spawn(cmd, master_read=reader)
|
||||
return pty.spawn(cmd, master_read = reader)
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(old_env)
|
||||
return pty.spawn(cmd, master_read=reader)
|
||||
return pty.spawn(cmd, master_read = reader)
|
||||
|
||||
stdout_chunks: list[bytes] = []
|
||||
enc_for_verbose = sys.stdout.encoding or "utf-8"
|
||||
enc_for_verbose = sys.stdout.encoding or 'utf-8'
|
||||
reader = __make_pty_reader(stdout_chunks, enc_for_verbose)
|
||||
|
||||
exit_code = await asyncio.to_thread(_spawn)
|
||||
|
||||
# PTY merges stdout/stderr
|
||||
stdout = b"".join(stdout_chunks) if stdout_chunks else None
|
||||
stdout = b''.join(stdout_chunks) if stdout_chunks else None
|
||||
return Result(stdout, None, exit_code)
|
||||
|
||||
# -- non-interactive mode
|
||||
stdin = asyncio.subprocess.DEVNULL if cmd_input is None else asyncio.subprocess.PIPE
|
||||
|
||||
stdin = (
|
||||
asyncio.subprocess.DEVNULL
|
||||
if cmd_input is None else asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
if mod_env:
|
||||
new_env = os.environ.copy()
|
||||
|
|
@ -94,21 +99,21 @@ class Local(Base):
|
|||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=stdin,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
env=mod_env,
|
||||
stdin = stdin,
|
||||
stdout = asyncio.subprocess.PIPE,
|
||||
stderr = asyncio.subprocess.PIPE,
|
||||
env = mod_env,
|
||||
)
|
||||
|
||||
stdout_parts: list[bytes] = []
|
||||
stderr_parts: list[bytes] = []
|
||||
|
||||
# -- decoding for verbose output in pipe mode
|
||||
stdout_log_enc = sys.stdout.encoding or "utf-8"
|
||||
stderr_log_enc = sys.stderr.encoding or "utf-8"
|
||||
stdout_log_enc = sys.stdout.encoding or 'utf-8'
|
||||
stderr_log_enc = sys.stderr.encoding or 'utf-8'
|
||||
|
||||
async def read_stream(stream, prio, collector: list[bytes], log_enc: str):
|
||||
buf = b""
|
||||
buf = b''
|
||||
while True:
|
||||
chunk = await stream.read(4096)
|
||||
if not chunk:
|
||||
|
|
@ -116,12 +121,12 @@ class Local(Base):
|
|||
collector.append(chunk)
|
||||
if verbose:
|
||||
buf += chunk
|
||||
while b"\n" in buf:
|
||||
line, buf = buf.split(b"\n", 1)
|
||||
__log(prio, line.decode(log_enc, errors="replace"))
|
||||
while b'\n' in buf:
|
||||
line, buf = buf.split(b'\n', 1)
|
||||
__log(prio, line.decode(log_enc, errors = 'replace'))
|
||||
if verbose and buf:
|
||||
# flush trailing partial line (no newline)
|
||||
__log(prio, buf.decode(log_enc, errors="replace"))
|
||||
__log(prio, buf.decode(log_enc, errors = 'replace'))
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(
|
||||
|
|
@ -132,7 +137,8 @@ class Local(Base):
|
|||
),
|
||||
]
|
||||
|
||||
if stdin is asyncio.subprocess.PIPE:
|
||||
if (cmd_input is not None and stdin is asyncio.subprocess.PIPE
|
||||
and proc.stdin is not None):
|
||||
proc.stdin.write(cmd_input)
|
||||
await proc.stdin.drain()
|
||||
proc.stdin.close()
|
||||
|
|
@ -140,8 +146,8 @@ class Local(Base):
|
|||
exit_code = await proc.wait()
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
stdout = b"".join(stdout_parts) if stdout_parts else None
|
||||
stderr = b"".join(stderr_parts) if stderr_parts else None
|
||||
stdout = b''.join(stdout_parts) if stdout_parts else None
|
||||
stderr = b''.join(stderr_parts) if stderr_parts else None
|
||||
|
||||
return Result(stdout, stderr, exit_code)
|
||||
|
||||
|
|
@ -153,7 +159,9 @@ class Local(Base):
|
|||
os.unlink(path)
|
||||
|
||||
async def _erase(self, path: str) -> None:
|
||||
if os.isdir(path):
|
||||
if os.path.isdir(path):
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(path)
|
||||
return
|
||||
os.unlink(path)
|
||||
|
|
@ -165,12 +173,12 @@ class Local(Base):
|
|||
os.mkdir(name, mode)
|
||||
|
||||
async def _stat(self, path: str, follow_symlinks: bool) -> StatResult:
|
||||
return StatResult.from_os(os.stat(path, follow_symlinks=follow_symlinks))
|
||||
return StatResult.from_os(os.stat(path, follow_symlinks = follow_symlinks))
|
||||
|
||||
async def _file_exists(self, path: str) -> bool:
|
||||
return os.path.exists(path)
|
||||
|
||||
async def _chown(self, path: str, owner: str|None, group: str|None) -> None:
|
||||
async def _chown(self, path: str, owner: str | None, group: str | None) -> None:
|
||||
uid = pwd.getpwnam(owner).pw_uid if owner else -1
|
||||
gid = grp.getgrnam(group).gr_gid if group else -1
|
||||
os.chown(path, uid, gid)
|
||||
|
|
@ -179,6 +187,6 @@ class Local(Base):
|
|||
os.chmod(path, mode)
|
||||
|
||||
async def _is_dir(self, path: str, follow_symlinks: bool) -> bool:
|
||||
if (not follow_symlinks) and os.islink(path):
|
||||
if (not follow_symlinks) and os.path.islink(path):
|
||||
return False
|
||||
return os.path.isdir(path)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
import abc
|
||||
import os
|
||||
import pwd
|
||||
import sys
|
||||
|
||||
import os, abc, sys, pwd
|
||||
from enum import Flag, auto
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..util import pretty_cmd
|
||||
from ..log import *
|
||||
from ..base import Result
|
||||
from ..ExecContext import ExecContext
|
||||
from ..log import DEBUG, ERR, INFO, NOTICE, WARNING, log
|
||||
from ..Uri import Uri
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -24,48 +24,50 @@ class SSHClient(ExecContext):
|
|||
ModEnv = auto()
|
||||
Wd = auto()
|
||||
|
||||
def __init__(self, uri: Uri|str, caps: Caps=Caps(0), *args, **kwargs) -> None:
|
||||
def __init__(self, uri: Uri | str, caps: Caps = Caps(0), *args, **kwargs) -> None:
|
||||
uri = Uri.pimp(uri)
|
||||
if uri.username is None:
|
||||
uri.set_username(pwd.getpwuid(os.getuid()).pw_name)
|
||||
super().__init__(uri=uri, *args, **kwargs)
|
||||
super().__init__(uri = uri, *args, **kwargs)
|
||||
self.__caps = caps
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _run_ssh(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str|None,
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes|None,
|
||||
mod_env: dict[str, str]|None,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
pass
|
||||
|
||||
async def _run(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str|None,
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes|None,
|
||||
mod_env: dict[str, str]|None,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
|
||||
def __log(prio: int, *args):
|
||||
log(prio, log_prefix, *args)
|
||||
|
||||
def __log_block(prio: int, title: str, block: str):
|
||||
def __log_block(prio: int, title: str, block: bytes | str | None):
|
||||
if self.__caps & self.Caps.LogOutput:
|
||||
return
|
||||
if not block:
|
||||
return
|
||||
encoding = sys.stdout.encoding or 'utf-8'
|
||||
block = block.decode(encoding).strip()
|
||||
if not block:
|
||||
return
|
||||
if isinstance(block, bytes):
|
||||
encoding = sys.stdout.encoding or 'utf-8'
|
||||
block = block.decode(encoding).strip()
|
||||
# Needed to pacify pyright: block can't be anything else at this point
|
||||
assert isinstance(block, str)
|
||||
delim = f'---- {title} ----'
|
||||
__log(prio, f',{delim}')
|
||||
for line in block.splitlines():
|
||||
|
|
@ -79,44 +81,49 @@ class SSHClient(ExecContext):
|
|||
raise NotImplementedError('Interactive SSH is not yet implemented')
|
||||
|
||||
if mod_env is not None and not self.__caps & self.Caps.ModEnv:
|
||||
raise NotImplementedError('Passing an environment to SSH commands is not yet implemented')
|
||||
raise NotImplementedError(
|
||||
'Passing an environment to SSH commands is not yet implemented'
|
||||
)
|
||||
|
||||
ret = await self._run_ssh(
|
||||
cmd=cmd,
|
||||
wd=wd,
|
||||
verbose=verbose,
|
||||
cmd_input=cmd_input,
|
||||
mod_env=mod_env,
|
||||
interactive=interactive,
|
||||
log_prefix=log_prefix
|
||||
cmd = cmd,
|
||||
wd = wd,
|
||||
verbose = verbose,
|
||||
cmd_input = cmd_input,
|
||||
mod_env = mod_env,
|
||||
interactive = interactive,
|
||||
log_prefix = log_prefix,
|
||||
)
|
||||
|
||||
if verbose:
|
||||
__log_block(NOTICE, 'stdout', ret.stdout)
|
||||
__log_block(NOTICE, 'stderr', ret.stderr)
|
||||
__log_block(NOTICE, 'stdout', ret.stdout_str_or_none)
|
||||
__log_block(NOTICE, 'stderr', ret.stderr_str_or_none)
|
||||
if ret.status != 0:
|
||||
__log(WARNING, f'Exit code {ret.status}')
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def hostname(self) -> str|None:
|
||||
def hostname(self) -> str | None:
|
||||
return self.uri.hostname
|
||||
|
||||
@property
|
||||
def port(self) -> int|None:
|
||||
def port(self) -> int | None:
|
||||
return self.uri.port
|
||||
|
||||
@property
|
||||
def username(self) -> str|None:
|
||||
def username(self) -> str | None:
|
||||
return self.uri.username
|
||||
|
||||
@property
|
||||
def password(self) -> str|None:
|
||||
def password(self) -> str | None:
|
||||
return self.uri.password
|
||||
|
||||
def ssh_client(*args, type: str|list[str]|None=None, **kwargs) -> SSHClient: # export
|
||||
def ssh_client(
|
||||
*args, type: str | list[str] | None = None, **kwargs
|
||||
) -> SSHClient: # export
|
||||
from importlib import import_module
|
||||
|
||||
errors: list[str] = []
|
||||
if type is None:
|
||||
val = os.getenv('JW_DEFAULT_SSH_CLIENT')
|
||||
|
|
@ -128,11 +135,12 @@ def ssh_client(*args, type: str|list[str]|None=None, **kwargs) -> SSHClient: # e
|
|||
type = [type]
|
||||
for name in type:
|
||||
try:
|
||||
ret = getattr(import_module(f'jw.pkg.lib.ec.ssh.{name}'), name)(*args, **kwargs)
|
||||
ret = getattr(import_module(f'jw.pkg.lib.ec.ssh.{name}'),
|
||||
name)(*args, **kwargs)
|
||||
log(INFO, f'Using SSH-client "{name}"')
|
||||
return ret
|
||||
except Exception as e:
|
||||
msg = f'Can\'t instantiate SSH client class {name} ({str(e)})'
|
||||
msg = f"Can't instantiate SSH client class {name} ({str(e)})"
|
||||
errors.append(msg)
|
||||
log(DEBUG, f'{msg}, trying next')
|
||||
msg = f'No working SSH clients for {" ".join([str(arg) for arg in args])}'
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import os, sys, shlex, asyncio, asyncssh, shutil, signal
|
||||
import asyncssh
|
||||
|
||||
from ...log import *
|
||||
from ...base import Result
|
||||
from ...log import DEBUG, ERR, NOTICE, log
|
||||
from ..SSHClient import SSHClient as Base
|
||||
|
||||
from .util import join_cmd
|
||||
|
||||
_USE_DEFAULT_KNOWN_HOSTS = object()
|
||||
|
|
@ -25,15 +29,18 @@ class AsyncSSH(Base):
|
|||
|
||||
super().__init__(
|
||||
uri,
|
||||
caps = self.Caps.LogOutput | self.Caps.Wd | self.Caps.Interactive | self.Caps.ModEnv,
|
||||
**kwargs
|
||||
caps = self.Caps.LogOutput
|
||||
| self.Caps.Wd
|
||||
| self.Caps.Interactive
|
||||
| self.Caps.ModEnv,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.__client_keys = client_keys
|
||||
self.__known_hosts = known_hosts
|
||||
self.__term_type = term_type or os.environ.get('TERM', 'xterm')
|
||||
self.__connect_timeout = connect_timeout
|
||||
self.__conn: asyncssh.SSHClientConnection|None = None
|
||||
self.__conn: asyncssh.SSHClientConnection | None = None
|
||||
|
||||
async def _open(self) -> None:
|
||||
await super()._open()
|
||||
|
|
@ -48,7 +55,7 @@ class AsyncSSH(Base):
|
|||
log(DEBUG, f'Failed to close connection ({str(e)}, ignored)')
|
||||
self.__conn = None
|
||||
|
||||
def _connect_kwargs(self, hide_secrets: bool=False) -> dict:
|
||||
def _connect_kwargs(self, hide_secrets: bool = False) -> dict:
|
||||
kwargs: dict = {
|
||||
'host': self.hostname,
|
||||
'port': self.port,
|
||||
|
|
@ -72,7 +79,7 @@ class AsyncSSH(Base):
|
|||
except Exception as e:
|
||||
msg = f'-------------------- Failed to connect ({str(e)})'
|
||||
log(ERR, ',', msg)
|
||||
for key, val in self._connect_kwargs(hide_secrets=True).items():
|
||||
for key, val in self._connect_kwargs(hide_secrets = True).items():
|
||||
log(ERR, f'| {key:<20} = {val}')
|
||||
log(ERR, '`', msg)
|
||||
raise
|
||||
|
|
@ -94,10 +101,13 @@ class AsyncSSH(Base):
|
|||
|
||||
@staticmethod
|
||||
def _get_local_term_size() -> tuple[int, int, int, int]:
|
||||
cols, rows = shutil.get_terminal_size(fallback=(80, 24))
|
||||
cols, rows = shutil.get_terminal_size(fallback = (80, 24))
|
||||
xpixel = ypixel = 0
|
||||
try:
|
||||
import fcntl, termios, struct
|
||||
import fcntl
|
||||
import struct
|
||||
import termios
|
||||
|
||||
packed = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b'\0' * 8)
|
||||
rows2, cols2, xpixel, ypixel = struct.unpack('HHHH', packed)
|
||||
if cols2 > 0 and rows2 > 0:
|
||||
|
|
@ -126,9 +136,9 @@ class AsyncSSH(Base):
|
|||
buf += chunk
|
||||
while b'\n' in buf:
|
||||
line, buf = buf.split(b'\n', 1)
|
||||
log(prio, log_prefix, line.decode(log_enc, errors='replace'))
|
||||
log(prio, log_prefix, line.decode(log_enc, errors = 'replace'))
|
||||
if verbose and buf:
|
||||
log(prio, log_prefix, buf.decode(log_enc, errors='replace'))
|
||||
log(prio, log_prefix, buf.decode(log_enc, errors = 'replace'))
|
||||
|
||||
async def _run_interactive_on_conn(
|
||||
self,
|
||||
|
|
@ -222,7 +232,8 @@ class AsyncSSH(Base):
|
|||
sys.stderr.flush()
|
||||
|
||||
try:
|
||||
import termios, tty
|
||||
import termios
|
||||
import tty
|
||||
|
||||
old_tty_state = termios.tcgetattr(stdin_fd)
|
||||
tty.setraw(stdin_fd)
|
||||
|
|
@ -257,7 +268,9 @@ class AsyncSSH(Base):
|
|||
|
||||
exit_code = completed.exit_status
|
||||
if exit_code is None:
|
||||
exit_code = completed.returncode if completed.returncode is not None else -1
|
||||
exit_code = (
|
||||
completed.returncode if completed.returncode is not None else -1
|
||||
)
|
||||
|
||||
stdout = b''.join(stdout_parts) if stdout_parts else None
|
||||
return Result(stdout, None, exit_code)
|
||||
|
|
@ -278,6 +291,7 @@ class AsyncSSH(Base):
|
|||
if old_tty_state is not None:
|
||||
try:
|
||||
import termios
|
||||
|
||||
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty_state)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -331,7 +345,7 @@ class AsyncSSH(Base):
|
|||
await proc.stdin.drain()
|
||||
proc.stdin.write_eof()
|
||||
|
||||
completed = await proc.wait(check=False)
|
||||
completed = await proc.wait(check = False)
|
||||
await task
|
||||
|
||||
exit_code = completed.exit_status
|
||||
|
|
@ -346,14 +360,13 @@ class AsyncSSH(Base):
|
|||
cmd: list[str],
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: str | None,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
|
||||
try:
|
||||
|
||||
if interactive:
|
||||
if self._has_local_tty():
|
||||
return await self._run_interactive_on_conn(
|
||||
|
|
@ -421,7 +434,7 @@ class AsyncSSH(Base):
|
|||
await proc.stdin.drain()
|
||||
proc.stdin.write_eof()
|
||||
|
||||
completed = await proc.wait(check=False)
|
||||
completed = await proc.wait(check = False)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
stdout = b''.join(stdout_parts) if stdout_parts else None
|
||||
|
|
@ -429,7 +442,9 @@ class AsyncSSH(Base):
|
|||
|
||||
exit_code = completed.exit_status
|
||||
if exit_code is None:
|
||||
exit_code = completed.returncode if completed.returncode is not None else -1
|
||||
exit_code = (
|
||||
completed.returncode if completed.returncode is not None else -1
|
||||
)
|
||||
|
||||
return Result(stdout, stderr, exit_code)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...base import InputMode
|
||||
|
|
@ -8,18 +10,14 @@ from ..SSHClient import SSHClient as Base
|
|||
from .util import join_cmd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...base import Result
|
||||
from ...base import Input, Result
|
||||
|
||||
class Exec(Base):
|
||||
|
||||
def __init__(self, uri, *args, **kwargs) -> None:
|
||||
self.__askpass: str|None = None
|
||||
self.__askpass_orig: dict[str, str|None] = dict()
|
||||
super().__init__(
|
||||
uri = uri,
|
||||
caps = self.Caps.ModEnv,
|
||||
**kwargs
|
||||
)
|
||||
self.__askpass: str | None = None
|
||||
self.__askpass_orig: dict[str, str | None] = dict()
|
||||
super().__init__(uri = uri, caps = self.Caps.ModEnv, **kwargs)
|
||||
|
||||
def __del__(self):
|
||||
for key, val in self.__askpass_orig.items():
|
||||
|
|
@ -32,36 +30,53 @@ class Exec(Base):
|
|||
|
||||
def __init_askpass(self):
|
||||
if self.__askpass is None and self.password is not None:
|
||||
import sys, tempfile
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
prefix = os.path.basename(sys.argv[0]) + '-'
|
||||
f = tempfile.NamedTemporaryFile(mode='w+t', prefix=prefix, delete=False)
|
||||
f = tempfile.NamedTemporaryFile(
|
||||
mode = 'w+t', prefix = prefix, delete = False
|
||||
)
|
||||
os.chmod(f.name, 0o0700)
|
||||
self.__askpass = f.name
|
||||
f.write(f'#!/bin/bash\n\necho -n "{self.password}\n"')
|
||||
f.close()
|
||||
for key, val in {'SSH_ASKPASS': self.__askpass, 'SSH_ASKPASS_REQUIRE': 'force'}.items():
|
||||
for key, val in {
|
||||
'SSH_ASKPASS': self.__askpass,
|
||||
'SSH_ASKPASS_REQUIRE': 'force',
|
||||
}.items():
|
||||
self.__askpass_orig[key] = os.getenv(key)
|
||||
os.environ[key] = val
|
||||
|
||||
async def _run_ssh(
|
||||
self,
|
||||
cmd: list[str],
|
||||
wd: str|None,
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: bytes|None,
|
||||
mod_env: dict[str, str]|None,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
|
||||
def __pub_cmd_input(cmd_input: bytes | None) -> Input:
|
||||
if cmd_input is None:
|
||||
if interactive:
|
||||
return InputMode.Interactive
|
||||
return InputMode.NonInteractive
|
||||
return cmd_input
|
||||
|
||||
self.__init_askpass()
|
||||
if cmd_input is None:
|
||||
cmd_input = InputMode.Interactive if interactive else InputMode.NonInteractive
|
||||
opts: dict[str, str] = []
|
||||
opts: list[str] = []
|
||||
if mod_env:
|
||||
for key, val in mod_env.items():
|
||||
opts.extend(['-o', f'SetEnv {key}="{val}"'])
|
||||
if self.username:
|
||||
opts.extend(['-l', self.username])
|
||||
if self.port is not None:
|
||||
pots.extend(['-p', str(self.port)])
|
||||
return await run_cmd(['ssh', *opts, self.hostname, join_cmd(cmd)], cmd_input=cmd_input, throw=False)
|
||||
opts.extend(['-p', str(self.port)])
|
||||
return await run_cmd(
|
||||
['ssh', *opts, self.hostname, join_cmd(cmd)],
|
||||
cmd_input = __pub_cmd_input(cmd_input),
|
||||
throw = False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,44 +2,46 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import paramiko # type: ignore # error: Library stubs not installed for "paramiko"
|
||||
import paramiko # type: ignore[import-untyped] # error: Library stubs not installed for "paramiko"
|
||||
|
||||
from ...log import *
|
||||
from ...base import Result
|
||||
from ...log import ERR, log
|
||||
from ..SSHClient import SSHClient as Base
|
||||
|
||||
from .util import join_cmd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import paramiko.agent # type: ignore[import-untyped]
|
||||
import paramiko.SCPClient # type: ignore[import-untyped]
|
||||
|
||||
class Paramiko(Base):
|
||||
|
||||
def __init__(self, uri, *args, **kwargs) -> None:
|
||||
super().__init__(
|
||||
uri,
|
||||
*args,
|
||||
caps = self.Caps.ModEnv,
|
||||
**kwargs
|
||||
)
|
||||
self.__timeout: float|None = None # Untested
|
||||
self.___client: Any|None = None
|
||||
kwargs['caps'] = (self.Caps.ModEnv, )
|
||||
super().__init__(uri, *args, **kwargs)
|
||||
self.__timeout: float | None = None # Untested
|
||||
self.___client: Any | None = None
|
||||
|
||||
@property
|
||||
def __client(self) -> Any:
|
||||
if self.___client is None:
|
||||
ret = paramiko.SSHClient()
|
||||
ret.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
hostname = self.hostname
|
||||
if hostname is None:
|
||||
raise Exception('Tried to run connect without target hostname')
|
||||
try:
|
||||
ret.connect(
|
||||
hostname = self.hostname,
|
||||
username = self.username,
|
||||
allow_agent = True
|
||||
hostname = hostname, username = self.username, allow_agent = True
|
||||
)
|
||||
except Exception as e:
|
||||
log(ERR, f'Failed to connect to {self.hostname} ({str(e)})')
|
||||
raise
|
||||
s = ret.get_transport().open_session()
|
||||
transport = ret.get_transport()
|
||||
if transport is None:
|
||||
raise Exception(f'Failed to get SSH transport for {hostname}')
|
||||
s = transport.open_session()
|
||||
# set up the agent request handler to handle agent requests from the server
|
||||
paramiko.agent.AgentRequestHandler(s)
|
||||
self.___client = ret
|
||||
|
|
@ -47,7 +49,7 @@ class Paramiko(Base):
|
|||
|
||||
@property
|
||||
def __scp(self) -> Any:
|
||||
return SCPClient(self.__client.get_transport())
|
||||
return paramiko.SCPClient(self.__client.get_transport())
|
||||
|
||||
async def _open(self) -> None:
|
||||
await super()._open()
|
||||
|
|
@ -63,13 +65,13 @@ class Paramiko(Base):
|
|||
cmd: list[str],
|
||||
wd: str | None,
|
||||
verbose: bool,
|
||||
cmd_input: str | None,
|
||||
cmd_input: bytes | None,
|
||||
mod_env: dict[str, str] | None,
|
||||
interactive: bool,
|
||||
log_prefix: str,
|
||||
) -> Result:
|
||||
try:
|
||||
kwargs: [str, Any] = {}
|
||||
kwargs: dict[str, Any] = {}
|
||||
if mod_env is not None:
|
||||
kwargs['environment'] = mod_env
|
||||
stdin, stdout, stderr = self.__client.exec_command(
|
||||
|
|
@ -78,7 +80,7 @@ class Paramiko(Base):
|
|||
**kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
log(ERR, f'Command failed for {self.uri}: "{join_cmd(cmd)}"')
|
||||
log(ERR, f'Command failed for {self.uri}: "{join_cmd(cmd)}" ({str(e)})')
|
||||
raise
|
||||
if cmd_input is not None:
|
||||
stdin.write(cmd_input)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import shlex
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
import shlex
|
||||
|
||||
DEFAULT_SHELL_OPERATORS = {
|
||||
# redirections
|
||||
">", ">>", "<", "<<", "<<-", "<&", ">&", "<>", ">|",
|
||||
"1>", "1>>", "2>", "2>>",
|
||||
|
||||
# pipelines / control
|
||||
"|", "||", "&", "&&", ";",
|
||||
|
||||
# grouping
|
||||
"(", ")",
|
||||
'>',
|
||||
'>>',
|
||||
'<',
|
||||
'<<',
|
||||
'<<-',
|
||||
'<&',
|
||||
'>&',
|
||||
'<>',
|
||||
'>|',
|
||||
'1>',
|
||||
'1>>',
|
||||
'2>',
|
||||
'2>>', # pipelines / control
|
||||
'|',
|
||||
'||',
|
||||
'&',
|
||||
'&&',
|
||||
';', # grouping
|
||||
'(',
|
||||
')',
|
||||
}
|
||||
|
||||
def join_cmd(
|
||||
|
|
|
|||