Merge pull request #3 from kiviktnm/highlight-pacman-output

Add feature to print pacman output highlights
This commit is contained in:
Kivi Kaitaniemi
2024-07-06 18:23:13 +03:00
committed by GitHub
3 changed files with 146 additions and 19 deletions
+12
View File
@@ -132,6 +132,18 @@ decman.config.suppress_command_output = True
# Make output less verbose. Summaries are still printed. # Make output less verbose. Summaries are still printed.
decman.config.quiet_output = False decman.config.quiet_output = False
# Decman captures pacman command output, and any line (and adjacent lines) that contains any of
# the following keywords (case-insensetive) will be printed after the pacman command finishes.
decman.config.pacman_output_keywords = [
"warning",
"error",
"note",
"pacsave",
"pacnew",
]
# If you don't want to print lines that contain keywords, set this to False
decman.config.print_pacman_output_highlights = True
# The user which builds aur and user packages. # The user which builds aur and user packages.
# decman.config.makepkg_user = "nobody" # This was set in a previous example. Let's not override it. # decman.config.makepkg_user = "nobody" # This was set in a previous example. Let's not override it.
+9
View File
@@ -199,6 +199,15 @@ valid_pkgexts: list[str] = [
".pkg.tar.Z", ".pkg.tar.Z",
] ]
pacman_output_keywords: list[str] = [
"warning",
"error",
"note",
"pacsave",
"pacnew",
]
print_pacman_output_highlights: bool = True
makepkg_user: str = "nobody" makepkg_user: str = "nobody"
build_dir: str = "/tmp/decman/build" build_dir: str = "/tmp/decman/build"
pkg_cache_dir: str = "/var/cache/decman" pkg_cache_dir: str = "/var/cache/decman"
+125 -19
View File
@@ -2,7 +2,8 @@
Library module for decman. Library module for decman.
""" """
import pwd import threading
import sys
import shutil import shutil
import subprocess import subprocess
import json import json
@@ -755,14 +756,23 @@ class Pacman:
if not packages: if not packages:
return return
returncode, output = echo_and_capture_command(
conf.commands.install_pkgs(packages))
if returncode != 0:
raise err.UserFacingError(
f"Failed to install packages using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
try: try:
subprocess.run(conf.commands.install_pkgs(packages), check=True)
subprocess.run(conf.commands.set_as_explicitly_installed(packages), subprocess.run(conf.commands.set_as_explicitly_installed(packages),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to install packages using pacman.") from error "Failed to set packages as explicitly installed using pacman."
) from error
def install_dependencies(self, deps: list[str]): def install_dependencies(self, deps: list[str]):
""" """
@@ -771,12 +781,14 @@ class Pacman:
if not deps: if not deps:
return return
try: returncode, output = echo_and_capture_command(
subprocess.run(conf.commands.install_deps(deps), check=True) conf.commands.install_deps(deps))
except subprocess.CalledProcessError as error: if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to install packages as dependencies using pacman." f"Failed to install packages as dependencies using pacman. Process exited with code {returncode}."
) from error )
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def install_files(self, files: list[str], as_explicit: list[str]): def install_files(self, files: list[str], as_explicit: list[str]):
""" """
@@ -786,9 +798,16 @@ class Pacman:
if not files: if not files:
return return
try: returncode, output = echo_and_capture_command(
subprocess.run(conf.commands.install_files(files), check=True) conf.commands.install_files(files))
if returncode != 0:
raise err.UserFacingError(
f"Failed to install package files using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
try:
if as_explicit: if as_explicit:
subprocess.run( subprocess.run(
conf.commands.set_as_explicitly_installed(as_explicit), conf.commands.set_as_explicitly_installed(as_explicit),
@@ -799,17 +818,20 @@ class Pacman:
print_error("Output:") print_error("Output:")
print_continuation(error.output) print_continuation(error.output)
raise err.UserFacingError( raise err.UserFacingError(
"Failed to install package files using pacman.") from error "Failed to set packages as explicitly installed using pacman."
) from error
def upgrade(self): def upgrade(self):
""" """
Upgrades all packages. Upgrades all packages.
""" """
try: returncode, output = echo_and_capture_command(conf.commands.upgrade())
subprocess.run(conf.commands.upgrade(), check=True) if returncode != 0:
except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to upgrade packages using pacman.") from error f"Failed to upgrade packages using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def remove(self, packages: list[str]): def remove(self, packages: list[str]):
""" """
@@ -817,11 +839,95 @@ class Pacman:
""" """
if not packages: if not packages:
return return
try:
subprocess.run(conf.commands.remove(packages), check=True) returncode, output = echo_and_capture_command(
except subprocess.CalledProcessError as error: conf.commands.remove(packages))
if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to remove packages using pacman.") from error f"Failed to remove packages using pacman. Process exited with code {returncode}."
)
if conf.print_pacman_output_highlights:
print_highlighted_pacman_messages(output)
def print_highlighted_pacman_messages(output: str):
"""
Prints lines that contain pacman output keywords.
"""
print_summary("Pacman output highlights:")
lines = output.split("\n")
for index, line in enumerate(lines):
for keyword in conf.pacman_output_keywords:
if keyword.lower() in line.lower():
print_summary(f"lines: {index}-{index+2}")
if index >= 1:
print_continuation(lines[index - 1])
print_continuation(line)
if index + 1 < len(lines):
print_continuation(lines[index + 1])
print_continuation("")
# Break, as to not print the same line again if it contains multiple keywords
break
def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
"""
Runs the given CLI program and arguments.
Returns a tuple containing the return code of the program as well as all output of the program.
"""
with subprocess.Popen(program,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) as process:
os.set_blocking(process.stdout.fileno(), False)
output_thread = _OutputCapturingThread(process.stdout)
output_thread.start()
os.set_blocking(sys.stdin.fileno(), False)
# Capture stdin and forward it to the process in a non-blocking manner
while process.poll() is None:
inp = sys.stdin.readline()
if inp:
process.stdin.write(inp.encode())
process.stdin.flush()
time.sleep(0.1)
os.set_blocking(sys.stdin.fileno(), True)
output_thread.done = True
output_thread.join()
# Capture any output that may not have been yet captured
output = output_thread.output
missing_output = process.stdout.read()
if missing_output:
output += missing_output.decode()
return (process.returncode, output)
class _OutputCapturingThread(threading.Thread):
def __init__(self, stream):
super().__init__()
self._stream = stream
self.output = ""
self.done = False
def run(self):
while not self.done and not self._stream.closed:
output = self._stream.read()
if output:
output = output.decode()
self.output += output
print(output, end="")
time.sleep(0.1)
class Systemd: class Systemd: