lib.ExecApp: Add class #76
11 changed files with 161 additions and 67 deletions
|
|
@ -61,7 +61,8 @@ class App(Base):
|
|||
if m is None:
|
||||
raise Exception(
|
||||
f'Can\'t interpret "{fmt}" as valid topdir reference, '
|
||||
'expecting "unaltered", "absolute", or "make:<variable-name>"'
|
||||
'expecting "absolute", "relative", "unaltered", '
|
||||
'or "make:<variable-name>"'
|
||||
)
|
||||
return '$(' + m.group(1) + ')'
|
||||
|
||||
|
|
@ -199,10 +200,9 @@ class App(Base):
|
|||
),
|
||||
)
|
||||
vals_list = vals.split(',') if vals else []
|
||||
match scope:
|
||||
case Scope.Self:
|
||||
buf += vals_list
|
||||
case Scope.One | Scope.Subtree:
|
||||
if scope != Scope.Self:
|
||||
# Scope.Self adds the node itself (if add_self), but not its
|
||||
# children, so stop recursing here
|
||||
subscope = scope.Self if scope == Scope.One else scope
|
||||
for val in vals_list:
|
||||
val = val.strip()
|
||||
|
|
@ -232,7 +232,6 @@ class App(Base):
|
|||
for project in projects:
|
||||
if project in graph:
|
||||
continue
|
||||
for section in sections:
|
||||
deps = self.get_project_refs(
|
||||
[project],
|
||||
['pkg.requires.jw'],
|
||||
|
|
@ -259,45 +258,43 @@ class App(Base):
|
|||
project: str,
|
||||
graph: Graph,
|
||||
unvisited: list[str],
|
||||
temp: set[str],
|
||||
path: list[str],
|
||||
) -> str | None:
|
||||
if project in temp:
|
||||
stack: list[str],
|
||||
) -> list[str] | None:
|
||||
if project in stack:
|
||||
log(DEBUG, 'found circular dependency at project', project)
|
||||
return project
|
||||
idx = stack.index(project)
|
||||
return stack[idx:] + [project]
|
||||
if project not in unvisited:
|
||||
return None
|
||||
temp.add(project)
|
||||
stack.append(project)
|
||||
if project in graph:
|
||||
for dep in graph[project]:
|
||||
last = self.__find_circular_deps_recursive(
|
||||
dep, graph, unvisited, temp, path
|
||||
cycle = self.__find_circular_deps_recursive(
|
||||
dep, graph, unvisited, stack
|
||||
)
|
||||
if last is not None:
|
||||
path.insert(0, dep)
|
||||
return last
|
||||
if cycle is not None:
|
||||
return cycle
|
||||
unvisited.remove(project)
|
||||
temp.remove(project)
|
||||
stack.pop()
|
||||
return None
|
||||
|
||||
def __find_circular_deps(self, projects: list[str],
|
||||
flavours: list[str]) -> list[str]:
|
||||
graph: Graph = {}
|
||||
ret: list[str] = []
|
||||
self.__read_dep_graph(projects, flavours, graph)
|
||||
unvisited = list(graph.keys())
|
||||
temp: set[str] = set()
|
||||
flipped = self.__flip_dep_graph(graph)
|
||||
while unvisited:
|
||||
project = unvisited[0]
|
||||
log(DEBUG, 'Checking circular dependency of', project)
|
||||
last = self.__find_circular_deps_recursive(
|
||||
project, flipped, unvisited, temp, ret
|
||||
)
|
||||
if last is not None:
|
||||
log(DEBUG, f'Found circular dependency below {project}, last is {last}')
|
||||
ret.append(last)
|
||||
return ret
|
||||
cycle = self.__find_circular_deps_recursive(project, flipped, unvisited, [])
|
||||
if cycle is not None:
|
||||
# An edge a -> b in the flipped graph means that b
|
||||
# depends on a, so reverse to report the cycle in the
|
||||
# original direction
|
||||
cycle = list(reversed(cycle))
|
||||
log(DEBUG, f'Found circular dependency: {" -> ".join(cycle)}')
|
||||
return cycle
|
||||
return []
|
||||
|
||||
def __init__(self, distro: Distro | None = None) -> None:
|
||||
|
|
|
|||
|
|
@ -513,9 +513,8 @@ class App: # export
|
|||
return self.__parser
|
||||
|
||||
def run(self, argv: list[str] | None = None) -> None:
|
||||
previous_eloop: asyncio.AbstractEventLoop | None = None
|
||||
if self.__eloop is None:
|
||||
previous_eloop = _get_current_event_loop()
|
||||
if self.__eloop is None:
|
||||
eloop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(eloop)
|
||||
self.__eloop = eloop
|
||||
|
|
|
|||
84
src/python/jw/pkg/lib/ExecApp.py
Normal file
84
src/python/jw/pkg/lib/ExecApp.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from .App import App as Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from .ExecContext import ExecContext
|
||||
|
||||
class ExecApp(Base): # export
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.__opt_interactive: bool | None = None
|
||||
self.__opt_verbose: bool | None = None
|
||||
self.__exec_context: ExecContext | None = None
|
||||
|
||||
@override
|
||||
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
||||
if self.__exec_context is not None:
|
||||
await self.__exec_context.close()
|
||||
self.__exec_context = None
|
||||
|
||||
@override
|
||||
def _add_arguments(self, parser: ArgumentParser) -> None:
|
||||
super()._add_arguments(parser)
|
||||
parser.add_argument(
|
||||
'--interactive',
|
||||
choices = ['true', 'false', 'auto'],
|
||||
default = 'true',
|
||||
help = 'Wait for user input or try to proceed unattended',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action = 'store_true',
|
||||
default = False,
|
||||
help = "Be verbose on stderr about what's being done on the distro level",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--target', default = 'local', help = 'Run commands on this host'
|
||||
)
|
||||
|
||||
@property
|
||||
def interactive(self) -> bool:
|
||||
if self.__opt_interactive is None:
|
||||
match self.args.interactive:
|
||||
case 'true':
|
||||
self.__opt_interactive = True
|
||||
case 'false':
|
||||
self.__opt_interactive = False
|
||||
case 'auto':
|
||||
self.__opt_interactive = sys.stdin.isatty()
|
||||
case _:
|
||||
raise ValueError(
|
||||
f'Unknown --interactive value: {self.args.interactive}'
|
||||
)
|
||||
# Not logically possible to fail, but this keeps pyright happy
|
||||
assert self.__opt_interactive is not None
|
||||
return self.__opt_interactive
|
||||
|
||||
@property
|
||||
def verbose(self) -> bool:
|
||||
if self.__opt_verbose is None:
|
||||
self.__opt_verbose = self.args.verbose
|
||||
# Not logically possible to fail, but this keeps pyright happy
|
||||
assert self.__opt_verbose is not None
|
||||
return self.__opt_verbose
|
||||
|
||||
@property
|
||||
def exec_context(self) -> ExecContext:
|
||||
if self.__exec_context is None:
|
||||
from .ExecContext import ExecContext
|
||||
|
||||
self.__exec_context = ExecContext.create(
|
||||
self.args.target,
|
||||
interactive = self.interactive,
|
||||
verbose_default = self.verbose,
|
||||
)
|
||||
return self.__exec_context
|
||||
|
|
@ -553,14 +553,14 @@ class ExecContext(Base):
|
|||
for cmd in cmds:
|
||||
log(DEBUG, f'{self.log_name}: Running {pretty_cmd(cmd.cmd, wd)}')
|
||||
ret = await __run(cmd.cmd, cmd_input = cmd.cmd_input)
|
||||
tmp_file = None # Has been successfully moved at this point
|
||||
tmp_file = None # All commands, including the final mv, succeeded
|
||||
return ret
|
||||
finally:
|
||||
if tmp_file is not None:
|
||||
await self.erase(tmp_file)
|
||||
await self.close()
|
||||
except Exception as e:
|
||||
msg = f'Failed to get {path} from {self.root} ({str(e)})'
|
||||
msg = f'Failed to put content to {path} on {self.root} ({str(e)})'
|
||||
if throw:
|
||||
raise Exception(msg)
|
||||
log(ERR, msg)
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ class FileContext(abc.ABC):
|
|||
DEBUG,
|
||||
(
|
||||
f"{self.log_name} doesn't implement stat(), judging by trailing "
|
||||
'slash if {path} is a directory'
|
||||
f'slash if {path} is a directory'
|
||||
),
|
||||
)
|
||||
return path[-1] == '/'
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class TarIo(CopyContext):
|
|||
try:
|
||||
blob = (await self.src.get(path)).stdout
|
||||
except Exception as e:
|
||||
log(ERR, f'Failed to read tar file "{path}" ({str(e)}')
|
||||
log(ERR, f'Failed to read tar file "{path}" ({str(e)})')
|
||||
raise
|
||||
return self._filter_tar_file(blob, path_filter, matched = matched)
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class Uri:
|
|||
ret.__password = None
|
||||
if not path:
|
||||
return ret
|
||||
if ret.__string.endswith('://'):
|
||||
# -- Empty authority: the trailing '/' is part of the '://'
|
||||
# separator, so a leading slash in path must be kept to stay
|
||||
# an absolute path
|
||||
ret.__string += path if path.startswith('/') else '/' + path
|
||||
return ret
|
||||
if ret.__string[-1] == '/':
|
||||
if path[0] == '/':
|
||||
ret.__string += path[1:]
|
||||
|
|
|
|||
|
|
@ -72,10 +72,9 @@ class AsyncSSH(Base):
|
|||
}
|
||||
if self.__known_hosts is not _USE_DEFAULT_KNOWN_HOSTS:
|
||||
kwargs['known_hosts'] = self.__known_hosts
|
||||
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
|
||||
return {k: v for k, v in kwargs.items() if v is not None}
|
||||
|
||||
@property
|
||||
async def _conn(self) -> asyncssh.SSHClientConnection:
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class Exec(Base):
|
|||
)
|
||||
os.chmod(f.name, 0o0700)
|
||||
self.__askpass = f.name
|
||||
f.write(f'#!/bin/bash\n\necho -n "{self.password}\n"')
|
||||
f.write(f'#!/bin/bash\n\necho -n "{self.password}"\n')
|
||||
f.close()
|
||||
for key, val in {
|
||||
'SSH_ASKPASS': self.__askpass,
|
||||
|
|
|
|||
|
|
@ -32,10 +32,16 @@ class Paramiko(Base):
|
|||
hostname = self.hostname
|
||||
if hostname is None:
|
||||
raise Exception('Tried to run connect without target hostname')
|
||||
kwargs: dict[str, Any] = {
|
||||
'hostname': hostname,
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'allow_agent': True,
|
||||
}
|
||||
if self.port is not None:
|
||||
kwargs['port'] = self.port
|
||||
try:
|
||||
ret.connect(
|
||||
hostname = hostname, username = self.username, allow_agent = True
|
||||
)
|
||||
ret.connect(**kwargs)
|
||||
except Exception as e:
|
||||
log(ERR, f'Failed to connect to {self.hostname} ({str(e)})')
|
||||
raise
|
||||
|
|
@ -88,5 +94,8 @@ class Paramiko(Base):
|
|||
raise
|
||||
if cmd_input is not None:
|
||||
stdin.write(cmd_input)
|
||||
stdin.channel.shutdown_write()
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
return Result(stdout.read(), stderr.read(), exit_status, cmd = cmd)
|
||||
return Result(stdout_data, stderr_data, exit_status, cmd = cmd)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ async def _run(
|
|||
cmd: list[str], sudo: bool = False, ec: ExecContext | None = None
|
||||
) -> str:
|
||||
return (
|
||||
await run_sudo(cmd)
|
||||
await run_sudo(cmd, ec = ec, cmd_input = InputMode.NonInteractive)
|
||||
if sudo else await run_cmd(cmd, ec = ec, cmd_input = InputMode.NonInteractive)
|
||||
).stdout_str
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue