lib.ExecApp: Add class #76
Loading…
Reference in a new issue
No description provided.
Delete branch "jan/feature/20260822-lib-execapp-add-class"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
py-defs.mk: Honor PY_INSTALL_INIT_PY = false
PY_ALL_PY is derived from PY_SRC_PY, which wildcards *.py in the module directory, so a checked-in init.py enters the install list unconditionally. PY_INSTALL_INIT_PY only gates the later append of init.py, which covers files generated at build time. Setting it to false thus had no effect on an existing init.py, which was still installed together with its .pyc.
Filter init.py out of PY_ALL_PY. When PY_INSTALL_INIT_PY is true, the existing append adds it back, so generated and checked-in init.py files are installed as before. When it is false, an existing init.py is now excluded from PY_ALL_PY and therefore also from PY_INSTALLED_PY and PY_PYC.
App.__get_project_refs(): Skip children for Scope.Self
__get_project_refs() appends the values of a node to the result buffer when the scope is Scope.Self. That is wrong: Scope.Self means "no children", so a node visited with Scope.Self is supposed to contribute only itself (if add_self is set), never its children. A previous refactor that turned the scope handling into a match/case block accidentally changed that, and as a consequence get_project_refs() with scope=Scope.One now returns the direct dependencies plus their dependencies. The second-level entries are raw values: they are not stripped of whitespace, and they are not reduced to module names even when names_only is True. The spurious self-edges this injects into the dependency graph built by __read_dep_graph() make find_circular_deps() report cycles that do not exist in the direct dependency graph.
Restore the original semantics by not recursing into children when the scope is Scope.Self.
App.__find_circular_deps(): Fix cycle path
__find_circular_deps_recursive() builds the cycle path by inserting each visited dependency at the front of the list on the way back up, and __find_circular_deps() appends the project where the cycle was detected at the end. The path therefore starts at the first child of the DFS root instead of at the project that closes the cycle, and the closing project appears twice: for a dependency cycle between projects A and B, find_circular_deps() reports "B -> A -> A" instead of "A -> B -> A".
Keep the DFS stack as an ordered list, and when a dependency is already on the stack, return the part of the stack from that dependency to the top, plus the dependency itself, which starts and ends at the same project. Reverse the result before returning, because an edge a -> b in the flipped graph means that b depends on a, so the cycle is reported in the original dependency direction.
App.__read_dep_graph(): Remove redundant loop
__read_dep_graph() iterates over the given sections (flavours), but the loop body does not use the loop variable: it passes the entire sections list to get_project_refs() on every iteration, so the same lookup is repeated for each section, and the recursion into the found dependencies is re-triggered (and skipped) for each of them.
Remove the loop and do the lookup once per project.
App.__format_topdir(): Mention relative in error
__format_topdir() accepts "absolute", "relative", "unaltered", and "make:", but the error message it raises for anything else only lists "unaltered", "absolute", and "make:", leaving out "relative".
Add "relative" to the list of valid formats in the error message.
lib.Uri: Fix path join for empty authority
__new_with_path() joins base and path with exactly one '/', assuming a trailing '/' in base is a path separator. That assumption breaks for URIs with empty authority, where scheme_plus_authority ends in '://' (e.g. 'file:///tmp/x'): new_replace_path('/etc/hosts') drops the leading slash of the path and produces 'file://etc/hosts', which parses with hostname 'etc' and path '/hosts' instead of path '/etc/hosts'.
Treat a base ending in '://' separately and keep a leading slash in the path, adding one if it is missing.
lib.FileContext: Interpolate path in _is_dir()
_is_dir() logs a DEBUG message when _stat() is not implemented and it must guess from the trailing slash whether a path is a directory. The second part of that message is a plain string rather than an f-string, so '{path}' is logged verbatim instead of the path.
Make it an f-string so the path is interpolated.
lib.ExecContext: Clean up temp file in _put()
_put() writes the content into a temporary file with tee and, when atomic is set, moves it to the target path with a final mv. The loop over the command list sets tmp_file to None after each command, on the assumption stated in the comment that the file has been moved at that point - which is only true for the last command. When a chown, chmod, or mv after the tee fails, the finally block finds tmp_file is None and leaves the temporary file behind.
Reset tmp_file only after all commands have completed, so that the finally block erases the temporary file whenever a step fails.
lib.ExecContext: Fix error message in _put()
_put() catches a failure of the remote command sequence and logs "Failed to get from ", a phrase copied over from the get() path. This path, however, pushes content, so the message describes the wrong operation.
Log a put-oriented message instead.
lib.ec.ssh.AsyncSSH: Actually hide password
_connect_kwargs(hide_secrets = True) is used to log the connection parameters when a connection fails, without leaking the password. The filtered dictionary is built before the password is replaced with '', and the replacement is applied to the local kwargs dictionary afterwards, after the filtered copy has already been made. The dictionary that ends up in the log therefore still contains the real password.
Hide the password before building the filtered dictionary.
lib.App: Restore caller's event loop in run()
run() remembers the thread's current event loop before creating a new one only when the app does not own a loop yet. The finally block, however, always restores the remembered loop or unsets the loop when there was none. So when a caller passes its own event loop to the App constructor and has set it as the thread's current loop, run() unsets the thread's loop on the way out, although it never changed it.
Remember the thread's current event loop unconditionally and restore it in the finally block; for a caller-provided loop that is a no-op.
lib.pm.dpkg: Pass context to run_sudo()
_run() forwards the execution context to run_cmd() when sudo is not requested, but calls run_sudo(cmd) without the context in the sudo case. run_sudo() then falls back to a fresh local context, so sudoed dpkg and dpkg-query commands run on the local machine instead of on the given context, e.g. Distro._delete() on a remote host.
Pass the context and non-interactive stdin to run_sudo() as well.
lib.ec.ssh.Paramiko: Pass port and password
__client() connects using only the URI's hostname and username. The URI's port is ignored, so ssh://host:2222/... ends up connecting to port 22, and a password carried in the URI is never passed to paramiko, so URI-based password authentication cannot work. The Exec and AsyncSSH clients both honor the port and the password.
Pass the port and the password to connect(). The port argument is omitted entirely when the URI carries no port, because getaddrinfo() would interpret a None port as service port 0; without the argument, paramiko falls back to its default of 22.
lib.ec.ssh.Paramiko: Close remote stdin
_run_ssh() writes cmd_input to the remote stdin channel but never closes the write side. The channel stays open until the client process exits, so a remote command that reads stdin (cat, a login shell, ...) never sees EOF and blocks forever - including for cmd_input = None, which is supposed to mean non-interactive with stdin from /dev/null.
Call shutdown_write() on the channel after writing the input, or immediately when there is none, so the remote command gets EOF.
lib.ec.ssh.Paramiko: Avoid recv deadlock
_run_ssh() waits for the remote process's exit status before reading stdout and stderr. When a command produces more output than the channel's flow-control window can hold, the server stops sending, the remote process blocks on its write and never exits, and recv_exit_status() blocks forever.
Drain stdout and stderr to EOF first - the channels close when the process exits, so reading them also implies completion - and only then query the exit status.
lib.ec.ssh.Exec: Drop askpass newline
__init_askpass() generates an askpass script whose echo statement embeds a newline inside the quoted password: echo -n "PASSWORD ". Bash happily spans the quote over the newline, and the script therefore prints the password followed by a trailing newline. run_askpass() returns that output verbatim, so the password passed on carries a newline and authentication fails.
Write the script so it prints the password and nothing else.
lib.TarIo: Fix missing paren in _read_filtered()
_read_filtered() logs an error when reading the tar file fails, but the opening parenthesis of the error detail after the path is never closed.
Close it.
lib.ExecApp: Add class
__new_with_path() joins base and path with exactly one '/', assuming a trailing '/' in base is a path separator. That assumption breaks for URIs with empty authority, where scheme_plus_authority ends in '://' (e.g. 'file:///tmp/x'): new_replace_path('/etc/hosts') drops the leading slash of the path and produces 'file://etc/hosts', which parses with hostname 'etc' and path '/hosts' instead of path '/etc/hosts'. Treat a base ending in '://' separately and keep a leading slash in the path, adding one if it is missing. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann <jan@janware.com>_is_dir() logs a DEBUG message when _stat() is not implemented and it must guess from the trailing slash whether a path is a directory. The second part of that message is a plain string rather than an f-string, so '{path}' is logged verbatim instead of the path. Make it an f-string so the path is interpolated. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann <jan@janware.com>Pull request closed