Skip to content

Commit

Permalink
Update the status table to as cool as the setup table
Browse files Browse the repository at this point in the history
Gets rid of some dead code and gives the status table applicable
versions of the inline stderr/stdout view.
  • Loading branch information
naddeoa committed Jan 1, 2024
1 parent 2c96a65 commit 6147422
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 118 deletions.
109 changes: 63 additions & 46 deletions booty/app.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
from dataclasses import dataclass, field
import time
import subprocess
from pprint import pprint
from typing import Dict, List, Literal

from rich.box import SIMPLE
from rich.text import Text
from rich.tree import Tree
from rich.table import Table
from rich.console import Group
from rich.live import Live
from rich.progress import Progress

from booty.ast_util import get_dependencies, get_executable_index, get_recipe_definition_index
from booty.execute import BootyData, CommandExecutor, check_target_status
from booty.execute import BootyData, CommandExecutor, get_commands
from booty.graph import DependencyGraph
from booty.parser import parse
from booty.target_logger import TargetLogger
from booty.types import Executable, RecipeInvocation
from booty.ui import StdTree
from booty.validation import validate
from booty.lang.stdlib import stdlib


_REFRESH_RATE = 8


@dataclass
class StatusResult:
missing: List[str] = field(default_factory=list)
Expand Down Expand Up @@ -67,6 +71,30 @@ def setup(self, debug: bool) -> BootyData:
validate(conf)
return conf

def check_sudo_usage(self) -> None:
sudo_targets: List[str] = []

for target, commands in get_commands(self.data, "is_setup").items():
for cmd in commands:
if "sudo" in cmd:
sudo_targets.append(target)

for target, commands in get_commands(self.data, "setup").items():
for cmd in commands:
if "sudo" in cmd:
sudo_targets.append(target)

if sudo_targets:
# run sudo -v to make sure the user has sudo access
targets = ", ".join(sudo_targets)
print(
f"""
Detected sudo in targets: '{targets}'.
Running `sudo -v` to cache sudo credentials. You can disable this behavior with `booty --no-sudo`. See `booty --help` for details.
"""
)
subprocess.run(["sudo", "-v"], check=True)

def status(self) -> StatusResult:
"""
List the install status of each target
Expand All @@ -80,7 +108,7 @@ def status(self) -> StatusResult:
table.add_column("Target", no_wrap=True, width=20)
table.add_column("Dependencies", width=20)
table.add_column("Status", width=20)
table.add_column("Details", width=70)
table.add_column("Details", width=70, no_wrap=True)
table.add_column("Time", justify="right", width=10)

overall_progress = Progress()
Expand All @@ -90,38 +118,45 @@ def status(self) -> StatusResult:
total_time = 0.0

group = Group(table, overall_progress)
with Live(group, refresh_per_second=4):
with Live(group, refresh_per_second=_REFRESH_RATE):
for target in self.data.G.iterator():
deps_string = dependency_strings[target]

target_text = Text(target)
dependency_text = Text(deps_string)
status_text = Text("🟡 Checking...")
details_text = Text(self._display_is_setup(self.data.execution_index[target]))

tree = StdTree(self._display_setup(self.data.execution_index[target]))

time_text = Text("") # Make update in real time

table.add_row(target_text, dependency_text, status_text, details_text, time_text)
table.add_row(target_text, dependency_text, status_text, tree.tree, time_text)

start_time = time.perf_counter()
result = check_target_status(self.data, target)
_time = time.perf_counter() - start_time
total_time += _time
time_text.plain = f"{_time:.2f}s"
cmd = CommandExecutor(self.data, target, "is_setup")
for _ in cmd.execute():
time_text.plain = f"{time.perf_counter() - start_time:.2f}s"

target_time = time.perf_counter() - start_time
total_time += target_time
time_text.plain = f"{target_time:.2f}s"

if result is None:
if cmd.code == 0:
status_result.installed.append(target)
status_text.plain = "🟢 Installed"
tree.reset()
else:
if result.returncode == 1:
tree.set_stdout(cmd.latest_stdout())
tree.set_stderr(cmd.latest_stderr())

if cmd.code == 1:
status_result.missing.append(target)
status_text.plain = "🟡 Not installed"
details_text.plain = result.stdout if result.stdout else details_text.plain
self.logger.log_is_setup(target, result.stdout, result.stderr)
self.logger.log_is_setup(target, cmd.all_stdout(), cmd.all_stderr())
else:
status_result.errors.append(target)
status_text.plain = "🔴 Error"
details_text.plain = result.stderr.strip()
self.logger.log_is_setup(target, result.stdout, result.stderr)
self.logger.log_is_setup(target, cmd.all_stdout(), cmd.all_stderr())

overall_progress.advance(overall_id)

Expand Down Expand Up @@ -150,7 +185,7 @@ def install_missing(self, status_result: StatusResult) -> StatusResult:
total_time = 0.0
status_result = StatusResult()
gen = self.data.G.bfs()
with Live(group, refresh_per_second=4):
with Live(group, refresh_per_second=_REFRESH_RATE):
try:
next(gen) # Skip the first fake target
target = gen.send(True)
Expand All @@ -163,32 +198,17 @@ def install_missing(self, status_result: StatusResult) -> StatusResult:
target_text = Text(target)
status_text = Text("🟡 Installing...")

detail_tree = Tree("", hide_root=True)
current_cmd_display = self._display_setup(self.data.execution_index[target])
detail_tree.add(current_cmd_display)
stdout_branch = detail_tree.add("stdout")
stderr_branch = detail_tree.add("stderr")

details_stdout = Text("", style="dim")
stdout_branch.add(details_stdout)

details_stderr = Text("", style="red")
stderr_branch.add(details_stderr)

tree = StdTree(self._display_setup(self.data.execution_index[target]))
time_text = Text("") # Make update in real time
table.add_row(target_text, status_text, detail_tree, time_text)

table.add_row(target_text, status_text, tree.tree, time_text)
target_text.plain = target

# details_text.plain = self._display_setup(self.data.execution_index[target])

start_time = time.perf_counter()

cmd = CommandExecutor(self.data, target, "setup")
for _ in cmd.execute():
time_text.plain = f"{time.perf_counter() - start_time:.2f}s"
details_stdout.plain = cmd.latest_stdout()
details_stderr.plain = cmd.latest_stderr()
tree.set_stdout(cmd.latest_stdout())
tree.set_stderr(cmd.latest_stderr())

cmd_time = time.perf_counter() - start_time
time_text.plain = f"{cmd_time:.2f}s"
Expand All @@ -197,8 +217,7 @@ def install_missing(self, status_result: StatusResult) -> StatusResult:
if cmd.code == 0:
status_result.installed.append(target)
status_text.plain = "🟢 Installed"
detail_tree.children = []
detail_tree.add(current_cmd_display)
tree.reset()

else:
status_text.plain = "🔴 Error"
Expand Down Expand Up @@ -243,17 +262,15 @@ def _display(self, it: Dict[str, List[Executable]], method: Literal["setup", "is
if isinstance(executable, RecipeInvocation):
recipe = self.data.recipe_index[executable.name]
if method == "setup":
commands.append("\n".join(recipe.get_setup_commands(executable.args, self.data.recipe_index)))
cmds = [it.replace("\n", "\\n ") for it in recipe.get_setup_commands(executable.args, self.data.recipe_index)]
commands.append("\\n".join(cmds))
else:
commands.append("\n".join(recipe.get_is_setup_commands(executable.args, self.data.recipe_index)))
cmds = [it.replace("\n", "\\n ") for it in recipe.get_is_setup_commands(executable.args, self.data.recipe_index)]
commands.append("\\n ".join(cmds))
else:
commands.append(executable.command)
commands.append(executable.command.replace("\n", "\\n"))

# what's the best way to display multiple commands? They're bound to be too long. For now
# I'll just show the first one and maybe append a "..." if there are more.
has_multiple_commands = len(commands) > 1 or "\n" in commands[0]
first_command = commands[0].strip().replace("\n", "\\n")[:60]
return f"{first_command}{'...' if has_multiple_commands else ''}"
return "\\n ".join(commands[:3])

def _display_is_setup(self, it: Dict[str, List[Executable]]) -> str:
return self._display(it, method="is_setup")
Expand Down
22 changes: 19 additions & 3 deletions booty/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,31 @@
@click.option("-i", "--install", type=bool, is_flag=True, required=False, help="Install all uninstalled targets")
@click.option("-d", "--debug", type=bool, is_flag=True, required=False, help="See the AST of the config file")
@click.option("-l", "--log-dir", type=str, required=False, help="Where to store logs. Defaults to ./logs", default="./logs")
@click.option(
"-s",
"--no-sudo",
type=bool,
is_flag=True,
required=False,
help="""
Don't allow booty to prompt with sudo -v. Instead, you can manually run sudo -v
before using booty to cache credentials for any targets that use sudo. By
default, booty runs sudo -v upfront if you use sudo in any targets.
""",
default=False,
)
@click.option("-y", "--yes", type=bool, is_flag=True, required=False, help="Don't prompt for confirmation")
def cli(config: str, yes: bool, log_dir: str, status: bool = True, install: bool = False, debug: bool = False):
def cli(config: str, yes: bool, log_dir: str, no_sudo: bool, status: bool = True, install: bool = False, debug: bool = False):
# Make sure config exists
if not pathlib.Path(config).exists():
click.echo(f"Config file {config} does not exist. Use -c to specify the location of an install.booty file.")
sys.exit(1)

app = App(config, TargetLogger(log_dir), debug=debug)

if no_sudo is False:
app.check_sudo_usage()

if not install and not status:
install = True

Expand All @@ -41,12 +57,12 @@ def cli(config: str, yes: bool, log_dir: str, status: bool = True, install: bool
install_result = app.install_missing(status_result)
if install_result.errors:
# Don't consider `missing` to be an error. Some status checks may require logging in/out.
click.echo("There were errors. See above.")
click.echo(f"There were errors. See logs in {log_dir} for more information")
sys.exit(1)

elif status:
if app.status().errors:
click.echo("There were errors. See above.")
click.echo(f"There were errors. See logs in {log_dir} for more information")
sys.exit(1)


Expand Down
75 changes: 17 additions & 58 deletions booty/execute.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from subprocess import CalledProcessError, Popen, PIPE
from subprocess import Popen, PIPE
from select import select
import shutil
from typing import Callable, Dict, Generator, List, Literal, Optional, Tuple
from typing import Dict, Generator, List, Literal, Optional, Sequence, Tuple
from lark import ParseTree
from dataclasses import dataclass, field
from booty.ast_util import ExecutableIndex, RecipeDefinitionIndex, DependencyIndex
Expand All @@ -18,54 +18,22 @@ class BootyData:
ast: ParseTree


def check_status_all(data: BootyData) -> Dict[str, Optional[CalledProcessError]]:
results: Dict[str, Optional[CalledProcessError]] = {}
for target in list(data.G.iterator()):
# print(target)
exec = data.execution_index[target]

# print(exec)
commands = _get_is_setup(data, exec)
for command in commands:
try:
command()
results[target] = None
except CalledProcessError as e:
results[target] = e

return results


def check_target_status(data: BootyData, target: str) -> Optional[CalledProcessError]:
exec = data.execution_index[target]
commands = _get_is_setup(data, exec)
for command in commands:
try:
command()
except CalledProcessError as e:
return e


def _get_is_setup(data: BootyData, it: Dict[str, List[Executable]]) -> List[Callable[[], None]]:
exec = it["is_setup"] if "is_setup" in it else it["recipe"]
@dataclass
class ExecuteError(Exception):
code: int

execs: List[Callable[[], None]] = []
for e in exec:
if isinstance(e, ShellCommand):
a = e # whut
execs.append(lambda: a.execute())
else:
recipe_name = e.name
args = e.args
recipe_definition = data.recipe_index[recipe_name]
execs.append(lambda: recipe_definition.is_setup(args, data.recipe_index))

return execs
def get_commands(data: BootyData, method: Literal["setup", "is_setup"]) -> Dict[str, Sequence[str]]:
commands: Dict[str, Sequence[str]] = {}

for target in data.execution_index:
exec = data.execution_index[target]
cmds = _get_setup_commands(data, exec) if method == "setup" else _get_is_setup_commands(data, exec)
current_cmds = commands.get(target, [])
new_cmds = [*current_cmds, *cmds]
commands[target] = new_cmds

@dataclass
class ExecuteError(Exception):
code: int
return commands


def execute(
Expand Down Expand Up @@ -97,7 +65,9 @@ def execute(
yield (out, err)

if proc.poll() is not None:
# also check if stdout and stderr are empty
out = proc.stdout.readline().strip()
err = proc.stderr.readline().strip()
yield (out, err)
break

if proc.returncode != 0:
Expand Down Expand Up @@ -180,14 +150,3 @@ def latest_stdout(self, tail_n: int = 5) -> str:

def latest_stderr(self, tail_n: int = 5) -> str:
return "\n".join(self.stderr[-tail_n:])

def latest(self, tail_n: int = 5) -> str:
out = self.latest_stdout(tail_n)
err = self.latest_stderr(tail_n)
return r"""
stdout:
{out}
stderr:
{err}
""".format(out=out, err=err)
11 changes: 3 additions & 8 deletions booty/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
class ShellCommand:
command: str

def execute(self) -> None:
subprocess.run(["bash", "-c", self.command], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)


@dataclass
class RecipeInvocation:
Expand All @@ -30,9 +27,6 @@ class TargetDefinition:
# TODO put a bunch of stuff in here that I've been putting into types.py, execute.py, and app.py


# TargetDefinition = Union[Dict[str, List[Executable]], RecipeInvocation]


TargetNames = str


Expand All @@ -51,9 +45,10 @@ def compact_shell_executables(executables: List[Executable]) -> List[Executable]
compacted_executables.append(executable)
else:
if len(compacted_executables) == 0 or isinstance(compacted_executables[-1], RecipeInvocation):
compacted_executables.append(executable)
first_cmd = ShellCommand(executable.command.strip())
compacted_executables.append(first_cmd)
else:
compacted_executables[-1].command += "\n" + executable.command
compacted_executables[-1].command += "\n" + executable.command.strip()

return compacted_executables

Expand Down
Loading

0 comments on commit 6147422

Please sign in to comment.