mirror of
https://pagure.io/fm-orchestrator.git
synced 2026-04-26 19:51:49 +08:00
MBSConsoleHandler: show status of ongoing repository downloads
When downloading files from Koji to make a local repository, display a temporary status of which files are being displayed to the console appended after any log messages. Updates are done by erasing the status that was written, adding a log message, then writing the status again.
This commit is contained in:
@@ -24,6 +24,8 @@ import os
|
||||
import logging
|
||||
import logging.handlers
|
||||
import inspect
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
|
||||
levels = {
|
||||
@@ -204,6 +206,95 @@ class ModuleBuildLogs(object):
|
||||
del self.handlers[build.id]
|
||||
|
||||
|
||||
class LocalRepo(object):
|
||||
def __init__(self, koji_tag):
|
||||
self.koji_tag = koji_tag
|
||||
self.current_downloads = set()
|
||||
self.total_downloads = 0
|
||||
self.completed_downloads = 0
|
||||
self.status = ""
|
||||
|
||||
def start_downloads(self, total):
|
||||
self.status = "Downloading packages"
|
||||
self.total_downloads = total
|
||||
|
||||
def start_download(self, url):
|
||||
self.current_downloads.add(url)
|
||||
|
||||
def done_download(self, url):
|
||||
self.current_downloads.remove(url)
|
||||
self.completed_downloads += 1
|
||||
|
||||
def show_status(self, stream, style):
|
||||
if self.total_downloads > 0:
|
||||
count = " {}/{}".format(self.completed_downloads, self.total_downloads)
|
||||
else:
|
||||
count = ""
|
||||
|
||||
print("{}: {}{}".format(style(self.koji_tag, bold=True), self.status, count),
|
||||
file=stream)
|
||||
for url in self.current_downloads:
|
||||
print(" {}".format(os.path.basename(url)), file=stream)
|
||||
|
||||
|
||||
# Used to split aaa\nbbbb\n to ('aaa', '\n', 'bbb', '\n')
|
||||
NL_DELIMITER = re.compile('(\n)')
|
||||
# Matches *common* ANSI control sequences
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
|
||||
CSI_SEQUENCE = re.compile('\033[0-9;]*[A-Za-z]')
|
||||
|
||||
|
||||
class EraseableStream(object):
|
||||
"""
|
||||
Wrapper around a terminal stream for writing output that can be
|
||||
erased.
|
||||
"""
|
||||
|
||||
def __init__(self, target):
|
||||
self.target = target
|
||||
self.lines_written = 0
|
||||
# We assume that the EraseableStream starts writing at column 0
|
||||
self.column = 0
|
||||
self.resize()
|
||||
|
||||
def resize(self):
|
||||
self.size = os.get_terminal_size(self.target.fileno())
|
||||
|
||||
def write(self, string):
|
||||
# We want to track how many lines we've written so that we can
|
||||
# back up and erase them. Tricky thing is handling wrapping.
|
||||
|
||||
# Strip control sequences
|
||||
plain = CSI_SEQUENCE.sub('', string)
|
||||
|
||||
for piece in NL_DELIMITER.split(plain):
|
||||
if piece == '\n':
|
||||
self.column = 0
|
||||
self.lines_written += 1
|
||||
else:
|
||||
self.column = self.column + len(piece)
|
||||
# self.column == self.size.column doesn't wrap -
|
||||
# normal modern terminals wrap when a character is written
|
||||
# that would be off-screen, not immediately when the
|
||||
# line is full.
|
||||
while self.column > self.size.columns:
|
||||
self.column -= self.size.columns
|
||||
self.lines_written += 1
|
||||
|
||||
self.target.write(string)
|
||||
|
||||
def erase(self):
|
||||
if self.column > 0:
|
||||
# move cursor to the beginning of line and delete whole line
|
||||
self.target.write("\033[0G\033[2K")
|
||||
for i in range(0, self.lines_written):
|
||||
# move up cursor and delete whole line
|
||||
self.target.write("\033[1A\033[2K")
|
||||
|
||||
self.lines_written = 0
|
||||
self.column = 0
|
||||
|
||||
|
||||
FG_COLORS = {
|
||||
'green': '32',
|
||||
'red': '91',
|
||||
@@ -238,8 +329,13 @@ class MBSConsoleHandler(logging.Handler):
|
||||
self.stream = stream
|
||||
|
||||
self.tty = self.stream.isatty()
|
||||
if self.tty:
|
||||
self.status_stream = EraseableStream(self.stream)
|
||||
else:
|
||||
self.status_stream = None
|
||||
|
||||
self.long_running = None
|
||||
self.repos = {}
|
||||
|
||||
self.debug_formatter = logging.Formatter(log_format)
|
||||
self.info_formatter = logging.Formatter("%(message)s")
|
||||
@@ -286,6 +382,12 @@ class MBSConsoleHandler(logging.Handler):
|
||||
self.long_running = None
|
||||
|
||||
print(formatted, file=self.stream)
|
||||
|
||||
if self.tty:
|
||||
if self.repos:
|
||||
print('------------------------------', file=self.status_stream)
|
||||
for repo in self.repos.values():
|
||||
repo.show_status(self.status_stream, self.style)
|
||||
finally:
|
||||
self.release()
|
||||
|
||||
@@ -296,6 +398,10 @@ class MBSConsoleHandler(logging.Handler):
|
||||
|
||||
return decorate
|
||||
|
||||
def resize(self):
|
||||
if self.status_stream:
|
||||
self.status_stream.resize()
|
||||
|
||||
@console_message("%s ...")
|
||||
def long_running_start(self, msg):
|
||||
if self.long_running:
|
||||
@@ -315,6 +421,37 @@ class MBSConsoleHandler(logging.Handler):
|
||||
self.long_running = None
|
||||
return "{} ... done".format(msg)
|
||||
|
||||
@console_message("%s: Making local repository for Koji tag")
|
||||
def local_repo_start(self, koji_tag):
|
||||
repo = LocalRepo(koji_tag)
|
||||
self.repos[koji_tag] = repo
|
||||
|
||||
return "{}: Making local repository for Koji tag".format(
|
||||
self.style(koji_tag, bold=True))
|
||||
|
||||
@console_message("%s: %s")
|
||||
def local_repo_done(self, koji_tag, message):
|
||||
del self.repos[koji_tag]
|
||||
|
||||
return "{}: {}".format(
|
||||
self.style(koji_tag, bold=True),
|
||||
self.style(message, fg='green', bold=True))
|
||||
|
||||
@console_message("%s: Downloading %d packages from Koji tag to %s")
|
||||
def local_repo_start_downloads(self, koji_tag, num_packages, repo_dir):
|
||||
repo = self.repos[koji_tag]
|
||||
repo.start_downloads(num_packages)
|
||||
|
||||
@console_message("%s: Downloading %s")
|
||||
def local_repo_start_download(self, koji_tag, url):
|
||||
repo = self.repos[koji_tag]
|
||||
repo.start_download(url)
|
||||
|
||||
@console_message("%s: Done downloading %s")
|
||||
def local_repo_done_download(self, koji_tag, url):
|
||||
repo = self.repos[koji_tag]
|
||||
repo.done_download(url)
|
||||
|
||||
@classmethod
|
||||
def _setup_console_messages(cls):
|
||||
for value in cls.__dict__.values():
|
||||
@@ -434,6 +571,11 @@ def init_logging(conf):
|
||||
root_logger.setLevel(conf.log_level)
|
||||
handler = MBSConsoleHandler()
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
def handle_sigwinch(*args):
|
||||
handler.resize()
|
||||
|
||||
signal.signal(signal.SIGWINCH, handle_sigwinch)
|
||||
else:
|
||||
logging.basicConfig(level=conf.log_level, format=log_format)
|
||||
|
||||
|
||||
@@ -606,6 +606,10 @@ class ModuleBuild(MBSBase):
|
||||
def nvr_string(self):
|
||||
return kobo.rpmlib.make_nvr(self.nvr)
|
||||
|
||||
@property
|
||||
def nsvc(self):
|
||||
return "{}:{}:{}:{}".format(self.name, self.stream, self.version, self.context)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
|
||||
Reference in New Issue
Block a user