Commit graph

74 commits

Author SHA1 Message Date
3c9ce19deb lib.ec.ssh.Paramiko: Fix exception logging
The catch-block around Paramiko's connect code throws another
exception, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-03 17:29:44 +02:00
7cfe2c4775 lib.App._run(): default_completer=NoopCompleter()
By default, argcomplete uses argcomplete.FilesCompleter as default
for every argument. This mixes accessible files into the list of
possible completions. For most of jw-pkg's commands, that's unwanted,
so turn it off by defining a NoopCompleter class which does nothing,
and by set every arguments's default completer to a NoopCompleter
instance. If desired, completing files can be restored for an
argument by

   parser.add_argument("some-arg").completer = FilesCompleter()

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-28 12:45:50 +01:00
ce1b8b6744 lib.App: Fully parse argparse tree if _ARGCOMPLETE
Every module derived from lib.Cmd implements its own
parser.add_argument() logic. As a consequence, all Cmd derived
modules need to be loaded to have the full argument tree available.
This is avoided during normal program startup because it takes some
time. It's not necessary during normal program execution, nor for
showing help messages. It is, however, needed for argcomplete to do
its thing, so fully parse the command line if the program runs in
argcomplete mode, as determined by checking if _ARGCOMPLETE is
present in the environment.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-27 09:16:14 +01:00
9b208ecc1e lib.ExecContext.log_delim(): No interactive footer
lib.ExecContext.log_delim() logs a header not designed for enclosing
command output, and, hence, no footer should be output. This commit
suppresses it.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-27 09:16:14 +01:00
9b6ec109a1 cmds and lib: Don't print() log messages
print() should be used to output information requested by a certain
command, but not for logging the process to achieve it. log() should
be used for the latter. The current code has the distinction not down
clearly, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-27 09:16:14 +01:00
6d876e88f6 lib.util.run_sudo(): Pass argument list on unchanged
run_sudo() is a thin wrapper around ExecContext.sudo(), so don't try
to make sense more arguments than necessary.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:37:56 +00:00
b21d2d1c21 lib.ec.ssh.AsyncSSH: Add interactivity
Request a remote PTY from AsyncSSH, and wire the local terminal's
stdin up with it if interactive == True. This gives a real
interactive session if local stdin belongs to a terminal. Also,
thanks to AsyncSSH understanding that, forward terminal size changes
to the remote end.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
737cbc3e24 lib.ec.ssh.AsyncSSH: Add class
Add a SSHClient implementation using AsyncSSH. This is the first and
currently only class derived from SSHClient which implements
SSHClient.Cap.LogOutput, designed to consume and log command output
as it streams in. It felt like the lower hanging fruit not to do that
with Paramiko: Paramiko doesn't provide a native async API, so it
would need to spawn additional worker threads. I think.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
279b7789e2 lib.ec.SSHClient: Add property port
Add a port property to SSHClient, parsed from the ctor's URL, to
supply the obvious information.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
3a84408436 lib.ec.SSHClient.__init__(): Add parameter caps
Add an optional caps ("capabilities") argument to the constructor of
SSHClient. It is meant to be used by derived classes in order to
declare that they don't want the base class to handle a default
behaviour for a certain capability, but that they want to implement
it themselves instead.

Also, give the _run_ssh() callbacks the necessary info as parameters,
so that the derived classes have the means to do so.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
b2e1e411f1 lib.pm.*.query_packages(): Make it non-interactive
lib.pm.query_packages() uses a TTY for doing its thing and outputs
half-digested stuff to the terminal, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
0b05eb4c37 ExecContext: Make mode:xxx an enum
This commit introduces two new types, Input and InputMode. They
replace the more error-prone special strings cmd_input could be used
with. InputMode is an Enum, and Input can be either IntputMode, a
string or None.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:45 +01:00
bb13aea694 lib.ExecContext: Fix ignored --interactive
Whether or not the CallContext.interactive property should be True or
False, and hence, a call should be processed interactively, depends
on multiple factors, constituting matrix of options with multiple
preferences.

  --interactive is the application default and can be true, false,
    or auto

  - A call can be explicitly invoked as interactive, non-interactive
    or auto via the cmd_input parameter to ExecContext.run()

This commit adds more "mode:" options to make the latter more
explicit. It takes preference over the global --interactive
parameter: Global --interactive is only given a chance to decide if
cmd_input is None (default) or mode:opt-interactive.

This commit also fixes a bug: --interactive is ignored because the
interactive argument passed to ExecContext's constructor is ignored
later on in calls to the wrapped _run() and _sudo() methods.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:45 +01:00
3bda9bc826 lib.ExecContext.sudo(): Default None mod_env to {}
mod_env can be None. Make it an empty dict in that case to take a
little burden off the implementations in the derived classes.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:45 +01:00
21e67291b5 Fix: Decode run_cmd() result
Since commit 02697af5, ExecContext.run() returns bytes for stdout and
stderr and fixes that in calling code. The thing it did not fix was
the code calling run_cmd(), which also made return bytes. This commit
catches up on that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:45 +01:00
fd35fa0871 lib.Distro: Add missing async
Distro's sudo() and run() wrappers are not flagged async. It still
works, because throughout jw-pkg all callers expect a coroutine
return value, but flagging them as async makes the return value
obvious.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-23 11:33:35 +01:00
f4c76ebab9 lib.ec.SSHClientInternal|SSHClientCmd: Own .py
Move the code of SSHClientInternal and SSCClientCmd into lib.ec.ssh,
as "Paramiko" and "Exec", respectively. This makes the class layout
a little more modular, and along the way fixes a bug where
SSHClientInternal could be instantiated but was unusable (if the
Paramiko is not installed).

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 13:35:50 +01:00
f37f025b17 lib.SSHClient: Move to lib.ec
SSHClient in an ExecContext, hence it's better off in lib.ec, move it
there and adapt the references.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 13:35:11 +01:00
02697af568 lib.ExecContext: Align .sudo() prototype to .run()
ExecContext's .sudo() omits many of run()'s parameters, and this
commit adds them. To avoid redundancy around repeating and massaging
the long parameter list of both functions and their return values, it
also adds some deeper changes:

  - Make run(), _run(), sudo() and _sudo() always return instances of
    Result. Before it was allowed to return a triplet of stdout,
    stderr, and exit status.

  - Have ExecContext stay out of the business of decoding the result
    entirely. Result provides a convenience method .decode()
    operating on stdout and stderr and leaves the decision to the
    caller.

    This entails miniscule adaptations in calling code, namely in
    App.os_release, util.get_profile_env() and CmdListRepos._run().

  - Wrap the _run() and _sudo() callbacks in a context manager object
    of type CallContext to avoid code duplication.

  - Consistently name the first argument to run(), _run(), sudo() and
    _sudo() "cmd", not "args". The latter suggests that the caller is
    omitting the executable, which is not the case.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
37af0a05e9 lib.SSHClient: Implement verbose logging
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
df40af9fc3 lib.App.call_async(): Add method
Use the AsyncRunner class introduced in the previous commit to add a
call_async() method, allowing to run async functions from sync
functions by spawning an extra event loop.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
67e51cf07c lib.AsyncRunner: Add class
Add class AsyncRunner. This is a wrapper around the ceremony needed
to spawn an extra event loop in a synchronous function which wants to
call an async function.

Guido van Rossum considers it bad design that such a function exists
in the first place. While that may be true in the long run also for
jw-pkg, at this point I'm unwilling to flag every lazyly initialized
App property as async. It's not clear, yet, which will be async and
which not, and I dread the churn. So I will accept this as a
minimally invasive helper for now. When the API has stabilized, a
design without it may be better.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
95a384bfff lib.App.eloop: Add property
Expose App's __eloop member containing the application's main event
loop to allow outside async event loop trickery.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
52dd3b8f21 lib.ExecContext.run(): Push code up into base class
Take implementation burden from the derived classes _run() callback
by moving the respective code into the run() wrapper methods of the
base class.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
f2ffe85b61 lib.SSHClientInternal: No key_filename in connect()
Remove the key_filename parameter from the call to Paramiko's
connect(). It's user-dependent, and the current DevOps implementation
relies on having a SSH_AUTH_SOCK in the environment, anyway.

Signed-off-by: janware DevOps <devops@janware.com>
2026-03-20 10:30:25 +01:00
49daa86696 lib.SSHClient: Log more details around exceptions
Add a wrapper around urlparse() and Paramiko's connect() function, in
order to log some more info in case an exception is thrown.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
76b702f5b4 lib.ExecContext.create(): Add method
Add a class method to instantiate an ExecContext by its URI.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
5f6ec5a182 lib.SSHClient: Retire non-EC API
Remove .run_cmd(), forcing future clients to use the ExecContext
aligned API.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
888c1e7f16 lib.ExecContext.__init__(): Add parameter uri
Take a positional uri argument to the constructor of ExecContext,
forcing SSHClient to follow suit. The latter was instantiated with a
hostname as only argument up to now, which still works as a special
case of an uri.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
a76a6c9316 lib.SSHClient: Move public methods down
Code beautification chore: Move the public methods of SSHClients to
the bottom of the class to be consistent with other classes.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
284eb30ecf lib.SSHClient: Derive from ExecContext
Make SSHClient an ExecContext by implementing _run() and _sudo().

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
989e2c93e3 lib.SSHClient.run_cmd(): Align prototype with EC
Align the prototype of SSHClient.run_cmd() to ExecContext.run(). This
is a push towards making the SSHClient code an ExceContext, too. Some
arguments still log a warning or outright raise NotImplementedError.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
d0776db01f lib.SSHClient.run_cmd(): Accept cmd: list[str]
Make SSHClient accept a list of strings for the cmd argument to align
with the other run_cmd() functions in jw-pkg.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-17 16:23:34 +01:00
58142a1115 lib.Distro.pkg_files(): Fix argument name
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-17 07:35:21 +01:00
824de4eca4 lib.distros.*.Distro: Align PM prototypes
Make all backend package manager prototypes have the same arguments:

	yum(self, args: list[str], verbose: bool=True, sudo: bool=True)

This also implies having them behave equally verbose, unless
otherwise specified by the caller. This changes the default for
Debian.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-16 14:53:58 +01:00
cff63786e9 lib.Distro: Allow empty packages list
In commands taking lists of packages, namely install, delete and
pkg_files, don't bother asking the backend. Uniformly log a warning
and return successfully.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-15 16:41:46 +01:00
b2847809c1 lib.Types: Make debug logging optional
lib.Types class detection is too chatty. Make that a ctor option.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-15 10:42:24 +01:00
72bd5e3555 lib.Local.run(): Be less dramatic about exit != 0
Don't mention "error" in log message for exit codes > 0 from spawned
processes, because sometimes they don't mean an error.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-09 20:03:01 +01:00
6df4c86fc5 lib.App: Add property cmdline
Add the property App.cmdline, containing the invoking command line as
a string.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-09 20:00:30 +01:00
bd38700f67 lib.Distro: Add .id
Allow to query the distribution ID a Distro was instantiated with via
the .id property.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-07 14:35:54 +01:00
67a2931f5e App: Support --verbose
Add the --verbose global option, which is made available as the
App.verbose property.

Some functions still take a verbose parameter, but the type of these
parameters is converted from bool to bool|None.  The idea is that, if
they are None, their verbosity falls back to the global default.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 19:02:22 +01:00
525fa34387 lib.pm: Add parameter ec: ExecContext to functions
Functions in lib.pm (i.e. run_dpkg(), run_rpm() and friends) also get
an ExecContext-type parameter. Use them in lib/distros/*/Distro.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 19:02:22 +01:00
1325222fbd lib.ExecContext,Local: Remove callback default params
Remove defaults from protected callback function parameters. They
have to be decided by the base class's public API.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 19:02:22 +01:00
7fdba1b5db lib.util: Add ec: ExecContext to all subprocesses
Allow to pass an optional execution context to all functions spawning
a subprocess defined in lib.util.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 15:14:13 +01:00
fadf1bca49 lib.util.run_cmd(): Add parameter ec: ExecContext
Allow to specify the ExecContext in a call to run_cmd(). This
effectively makes run_cmd() an thin wrapper around ExecContext.run(),
which is what's going to be used in the future. The wrapper is for
backwards-compatibility.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 15:14:13 +01:00
3e897f4df8 lib.Distro, ExecContext: Add classes, refactor lib.distro
The code below lib.distro, as left behind by the previous commit, is
geared towards being directly used as a command-line API. This commit
introduces the abstract base class Distro, a proxy for
distribution-specific interactions. The proxy abstracts distro
specifics into an API with proper method prototypes, not
argparse.Namespace contents, and can thus be more easily driven by
arbitrary code.

The Distro class is initialized with a member variable of type
ExecContext, another new class introduced by this commit. It is
designed to abstract the communication channel to the distribution
instance.  Currently only one specialization exists, Local, which
interacts with the distribution and root file system it is running
in, but is planned to be subclassed to support interaction via SSH,
serial, chroot, or chains thereof.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 14:56:46 +01:00
7e7cee6d11 cmds.distro: Move all modules to lib
Functions abstracting the distribution are not only needed in the
context of the distro subcommand, but also by other code, so make the
bulk of the code abstracting the distribution available in some place
more universally useful than below cmds.distro.

This commit leaves the source files mostly unchanged. They are only
patched to fix import paths, so that functionality is preserved.
Refactoring the code from command-line API to library API will be
done by the next commit.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 12:06:28 +01:00
ae902250bd jw/pkg/lib/util.run_cmd(): Chunked read_stream()
jw-pkg distro dup got hung in a chroot environment. strace shows that
write(2) into a pipe is the hanging syscall, with the write buffer
hinting at zypper dup output.

I strongly suspect that run_cmd() tries to write stdout into the pipe
which read_stream() fails to empty. So, make read_stream() more
resilient by using read(4096) instead of readline(), which I suspect
to be prone to hang on overlong lines.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-05 06:42:57 +00:00
0c1c2b9351 lib.util.run_cmd(): Reduce interactive logging
run_cmd() with cmd_input == mode:interactive  and verbose == true
logs output too often. First, __log() is called, then pty.spawn()
writes everything it reads from the PTY master to the terminal.

The fix it to not call __log() from _read() for the PTY reader.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-04 16:02:07 +00:00
8bc22a3a68 lib.util.run_cmd(): Fix docstring
The docstring of run_cmd()'s signature documents a wrong return
value, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-04 14:48:26 +01:00