Naively join()ing a command list to be executed remotely via SSH also quotes shell operators which doesn't work, of course. Work around that. The workaround will not always work but covers lots of cases.
Instantiating a SSHClient-derived class with an invalid or missing uri parameter is accepted and fails later down the road. Raise an Exception early on to make the error log more comprehensible.
The SSHClient classes Paramiko and Exec are exported via # export. This is a bad idea, because if Paramiko is not installed, none of the other's can be instantiated either: On the attempt to load them, __init__.py is loaded first and fails. SSHClient.ssh_client() knows what to do, no need to auto-import them into the lib.ec.ssh module.
run_curl() has no clear API of whether or not the return values should be decoded. It has parse_json, which should imply decoding, but there's no way to specify that explicitly. Moreover, when it tries to decode, it decodes on the coroutine returned from run_cmd(), not the awaited coroutine return value.
Add a decode parameter, defaulting to False, change the parse_json parameter's default from True to False, and fix the run_cmd() return value evaluation.
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
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.
lib.ExecContext.log_delim() logs a header not designed for enclosing command output, and, hence, no footer should be output. This commit suppresses it.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.