aboutsummaryrefslogtreecommitdiff
path: root/aptdaemon/progress.py
diff options
context:
space:
mode:
Diffstat (limited to 'aptdaemon/progress.py')
-rw-r--r--aptdaemon/progress.py829
1 files changed, 829 insertions, 0 deletions
diff --git a/aptdaemon/progress.py b/aptdaemon/progress.py
new file mode 100644
index 0000000..5312c5a
--- /dev/null
+++ b/aptdaemon/progress.py
@@ -0,0 +1,829 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Progress handlers for APT operations"""
+# Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com>
+#
+# Licensed under the GNU General Public License Version 2
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+__author__ = "Sebastian Heinlein <devel@glatzor.de>"
+
+__all__ = ("DaemonAcquireProgress", "DaemonOpenProgress",
+ "DaemonInstallProgress", "DaemonDpkgInstallProgress",
+ "DaemonForkProgress", "DaemonDpkgRecoverProgress")
+
+import locale
+import logging
+import os
+import platform
+import re
+import signal
+import sys
+import termios
+import time
+import traceback
+import tty
+
+import apt_pkg
+import apt.progress.base
+import apt.debfile
+from gi.repository import GLib
+
+from . import enums
+from . import lock
+from .loop import mainloop
+from .utils import IsoCodes
+
+# Required to get translatable strings extraced by xgettext
+_ = lambda s: s
+
+log = logging.getLogger("AptDaemon.Worker")
+log_terminal = logging.getLogger("AptDaemon.Worker.Terminal")
+
+INSTALL_TIMEOUT = 10 * 60
+
+MAP_DPKG_STAGE = {"install": enums.PKG_INSTALLING,
+ "configure": enums.PKG_CONFIGURING,
+ "remove": enums.PKG_REMOVING,
+ "trigproc": enums.PKG_RUNNING_TRIGGER,
+ "purge": enums.PKG_PURGING,
+ "disappear": enums.PKG_DISAPPEARING,
+ "upgrade": enums.PKG_UPGRADING}
+
+REGEX_ANSI_ESCAPE_CODE = chr(27) + "\[[;?0-9]*[A-Za-z]"
+
+
+class DaemonOpenProgress(apt.progress.base.OpProgress):
+
+ """Handles the progress of the cache opening."""
+
+ def __init__(self, transaction, begin=0, end=100, quiet=False):
+ """Initialize a new DaemonOpenProgress instance.
+
+ Keyword arguments:
+ transaction -- corresponding transaction D-Bus object
+ begin -- begin of the progress range (defaults to 0)
+ end -- end of the progress range (defaults to 100)
+ quiet -- do not emit any progress information for the transaction
+ """
+ apt.progress.base.OpProgress.__init__(self)
+ self._transaction = transaction
+ self.steps = [begin + (end - begin) * modifier
+ # the final 1.00 will not be used but we still
+ # need it here for the final pop()
+ for modifier in [0.25, 0.50, 0.75, 1.00, 1.00]]
+ self.progress_begin = float(begin)
+ self.progress_end = self.steps.pop(0)
+ self.progress = 0
+ self.quiet = quiet
+
+ def update(self, percent=None):
+ """Callback for progress updates.
+
+ Keyword argument:
+ percent - current progress in percent
+ """
+ # python-apt 0.8 does not include "percent" anymore in the call
+ percent = percent or self.percent
+ if percent < 101:
+ progress = int(self.progress_begin + (percent / 100) *
+ (self.progress_end - self.progress_begin))
+ if self.progress == progress:
+ return
+ else:
+ progress = 101
+ self.progress = progress
+ if not self.quiet:
+ self._transaction.progress = progress
+
+ def done(self):
+ """Callback after completing a step.
+
+ Sets the progress range to the next interval."""
+ # ensure that progress is updated
+ self.progress = self.progress_end
+ # switch to new progress_{begin, end}
+ self.progress_begin = self.progress_end
+ try:
+ self.progress_end = self.steps.pop(0)
+ except:
+ log.warning("An additional step to open the cache is required")
+
+
+class DaemonAcquireProgress(apt.progress.base.AcquireProgress):
+ '''
+ Handle the package download process
+ '''
+ def __init__(self, transaction, begin=0, end=100):
+ apt.progress.base.AcquireProgress.__init__(self)
+ self.transaction = transaction
+ self.progress_end = end
+ self.progress_begin = begin
+ self.progress = 0
+
+ def _emit_acquire_item(self, item, total_size=0, current_size=0):
+ if item.owner.status == apt_pkg.AcquireItem.STAT_DONE:
+ status = enums.DOWNLOAD_DONE
+ # Workaround for a bug in python-apt, see lp: #581886
+ current_size = item.owner.filesize
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_AUTH_ERROR:
+ status = enums.DOWNLOAD_AUTH_ERROR
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_FETCHING:
+ status = enums.DOWNLOAD_FETCHING
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_ERROR:
+ status = enums.DOWNLOAD_ERROR
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_IDLE:
+ status = enums.DOWNLOAD_IDLE
+ else:
+ # Workaround: The StatTransientNetworkError status isn't mapped
+ # by python-apt, see LP #602578
+ status = enums.DOWNLOAD_NETWORK_ERROR
+ if (item.owner.status != apt_pkg.AcquireItem.STAT_DONE and
+ item.owner.error_text):
+ msg = item.owner.error_text
+ elif item.owner.mode:
+ msg = item.owner.mode
+ else:
+ msg = ""
+ self.transaction.progress_download = (
+ item.uri, status, item.shortdesc,
+ total_size | item.owner.filesize,
+ current_size | item.owner.partialsize,
+ msg)
+
+ def _emit_status_details(self, items):
+ """Emit the transaction status details."""
+ names = set()
+ for item in items:
+ if item.owner.id:
+ names.add(item.owner.id)
+ else:
+ names.add(item.shortdesc)
+ if names:
+ # TRANSLATORS: %s is a list of package names
+ msg = self.transaction.ngettext("Downloading %(files)s",
+ "Downloading %(files)s",
+ len(items)) % {"files":
+ " ".join(names)}
+ self.transaction.status_details = msg
+
+ def done(self, item):
+ """Invoked when an item is successfully and completely fetched."""
+ self._emit_acquire_item(item)
+
+ def fail(self, item):
+ """Invoked when an item could not be fetched."""
+ self._emit_acquire_item(item)
+
+ def fetch(self, item):
+ """Invoked when some of the item's data is fetched."""
+ self._emit_acquire_item(item)
+
+ def ims_hit(self, item):
+ """Invoked when an item is confirmed to be up-to-date.
+
+ Invoked when an item is confirmed to be up-to-date. For instance,
+ when an HTTP download is informed that the file on the server was
+ not modified.
+ """
+ self._emit_acquire_item(item)
+
+ def pulse(self, owner):
+ """Callback to update progress information"""
+ if self.transaction.cancelled:
+ return False
+ self.transaction.progress_details = (self.current_items,
+ self.total_items,
+ self.current_bytes,
+ self.total_bytes,
+ self.current_cps,
+ self.elapsed_time)
+ percent = (((self.current_bytes + self.current_items) * 100.0) /
+ float(self.total_bytes + self.total_items))
+ progress = int(self.progress_begin + percent / 100 *
+ (self.progress_end - self.progress_begin))
+ # If the progress runs backwards emit an illegal progress value
+ # e.g. during cache updates.
+ if self.progress > progress:
+ self.transaction.progress = 101
+ else:
+ self.transaction.progress = progress
+ self.progress = progress
+ # Show all currently downloaded files
+ items = []
+ for worker in owner.workers:
+ if not worker.current_item:
+ continue
+ self._emit_acquire_item(worker.current_item,
+ worker.total_size,
+ worker.current_size)
+ items.append(worker.current_item)
+ self._emit_status_details(items)
+ while GLib.main_context_default().pending():
+ GLib.main_context_default().iteration()
+ return True
+
+ def start(self):
+ """Callback at the beginning of the operation"""
+ self.transaction.status = enums.STATUS_DOWNLOADING
+ self.transaction.cancellable = True
+
+ def stop(self):
+ """Callback at the end of the operation"""
+ self.transaction.progress_details = (0, 0, 0, 0, 0.0, 0)
+ self.transaction.progress = self.progress_end
+ self.transaction.cancellable = False
+
+ def media_change(self, medium, drive):
+ """Callback for media changes"""
+ self.transaction.required_medium = medium, drive
+ self.transaction.paused = True
+ self.transaction.status = enums.STATUS_WAITING_MEDIUM
+ while self.transaction.paused:
+ GLib.main_context_default().iteration()
+ self.transaction.status = enums.STATUS_DOWNLOADING
+ if self.transaction.cancelled:
+ return False
+ return True
+
+
+class DaemonAcquireRepoProgress(DaemonAcquireProgress):
+
+ """Handle the repository information download"""
+
+ def __init__(self, transaction, begin=0, end=100):
+ DaemonAcquireProgress.__init__(self, transaction, begin, end)
+ self.languages = IsoCodes("iso_639", tag="iso_639_1_code",
+ fallback_tag="iso_639_2T_code")
+ self.regions = IsoCodes("iso_3166", "alpha_2_code")
+ self.progress = 101
+
+ def start(self):
+ """Callback at the beginning of the operation"""
+ self.transaction.status = enums.STATUS_DOWNLOADING_REPO
+ self.transaction.cancellable = True
+
+ def _emit_status_details(self, items):
+ """Emit the transaction status details."""
+ repos = set()
+ for item in items:
+ # We are only interested in the hostname currently
+ try:
+ repos.add(item.description.split()[0].split("://")[-1])
+ except IndexError:
+ # TRANSLATORS: the string is used as a fallback if we cannot
+ # get the URI of a local repository
+ repos.add(self.transaction.gettext("local repository"))
+ if repos:
+ # TRANSLATORS: %s is a list of repository names
+ msg = self.transaction.ngettext("Downloading from %s",
+ "Downloading from %s",
+ len(repos)) % " ".join(repos)
+ self.transaction.status_details = msg
+
+ def _emit_acquire_item(self, item, total_size=0, current_size=0):
+ if item.owner.status == apt_pkg.AcquireItem.STAT_DONE:
+ status = enums.DOWNLOAD_DONE
+ # Workaround for a bug in python-apt, see lp: #581886
+ current_size = item.owner.filesize
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_AUTH_ERROR:
+ status = enums.DOWNLOAD_AUTH_ERROR
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_FETCHING:
+ status = enums.DOWNLOAD_FETCHING
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_ERROR:
+ status = enums.DOWNLOAD_ERROR
+ elif item.owner.status == apt_pkg.AcquireItem.STAT_IDLE:
+ status = enums.DOWNLOAD_IDLE
+ else:
+ # Workaround: The StatTransientNetworkError status isn't mapped
+ # by python-apt, see LP #602578
+ status = enums.DOWNLOAD_NETWORK_ERROR
+ if (item.owner.status != apt_pkg.AcquireItem.STAT_DONE and
+ item.owner.error_text):
+ msg = item.owner.error_text
+ elif item.owner.mode:
+ msg = item.owner.mode
+ else:
+ msg = ""
+ # Get a better description than e.g. Packages or Sources
+ host, dist = item.description.split()[0:2]
+ try:
+ host = host.split("://")[1]
+ except IndexError:
+ # TRANSLATORS: the string is used as a fallback if we cannot
+ # get the URI of a local repository
+ desc = self.transaction.gettext("local repository")
+ repo = "%s %s" % (host, dist)
+ if item.shortdesc == "InRelease":
+ # TRANSLATORS: repo is the name of a repository
+ desc = self.transaction.gettext("Structure of %s") % repo
+ elif item.shortdesc == "Release":
+ # TRANSLATORS: repo is the name of a repository
+ desc = self.transaction.gettext("Description of %s") % repo
+ elif item.shortdesc == "Release.gpg":
+ # TRANSLATORS: repo is the name of a repository
+ desc = self.transaction.gettext("Description signature "
+ "of %s") % repo
+ elif item.shortdesc.startswith("Packages"):
+ # TRANSLATORS: repo is the name of a repository
+ desc = self.transaction.gettext(
+ "Available packages from %s") % repo
+ elif item.shortdesc.startswith("Sources"):
+ # TRANSLATORS: repo is the name of a repository
+ desc = self.transaction.gettext(
+ "Available sources from %s") % repo
+ elif item.shortdesc == "TranslationIndex":
+ # TRANSLATORS: repo is the name of a repository
+ desc = self.transaction.gettext("Available translations from "
+ "%s") % repo
+ elif item.shortdesc.startswith("Translation-"):
+ lang_code = item.shortdesc.split("-", 1)[-1]
+ try:
+ lang_code, region_code = lang_code.split("_")
+ except ValueError:
+ region_code = None
+ lang = self.languages.get_localised_name(lang_code,
+ self.transaction.locale)
+ region = self.regions.get_localised_name(region_code,
+ self.transaction.locale)
+ if lang and region:
+ # TRANSLATORS: The first %s is the name of a language. The
+ # second one the name of the region/country. Th
+ # third %s is the name of the repository
+ desc = self.transaction.gettext(
+ "Translations for %s (%s) from %s") % (lang, region, repo)
+ elif lang:
+ # TRANSLATORS: %s is the name of a language. The second one is
+ # the name of the repository
+ desc = self.transaction.gettext("Translations for %s from "
+ "%s") % (lang, repo)
+ else:
+ # TRANSLATORS: %s is the code of a language, e.g. ru_RU.
+ # The second one is the name of the repository
+ desc = self.transaction.gettext("Translations (%s) from "
+ "%s") % (lang_code, repo)
+ else:
+ desc = item.shortdesc
+ self.transaction.progress_download = (
+ item.uri, status, desc, total_size | item.owner.filesize,
+ current_size | item.owner.partialsize, msg)
+
+
+class DaemonForkProgress(object):
+
+ """Forks and executes a given method in the child process while
+ monitoring the output and return state.
+
+ During the run() call the mainloop will be iterated.
+
+ Furthermore a status file descriptor is available to communicate
+ with the child process.
+ """
+
+ def __init__(self, transaction, begin=50, end=100):
+ self.transaction = transaction
+ self.status = ""
+ self.progress = 0
+ self.progress_begin = begin
+ self.progress_end = end
+ self._child_exit = -1
+ self.last_activity = 0
+ self.child_pid = 0
+ self.status_parent_fd, self.status_child_fd = os.pipe()
+ if hasattr(os, "set_inheritable"):
+ os.set_inheritable(self.status_parent_fd, True)
+ os.set_inheritable(self.status_child_fd, True)
+ self.output = ""
+ self._line_buffer = ""
+
+ def __enter__(self):
+ self.start_update()
+ return self
+
+ def __exit__(self, etype, evalue, etb):
+ self.finish_update()
+
+ def start_update(self):
+ log.debug("Start update")
+ self.transaction.status = enums.STATUS_COMMITTING
+ self.transaction.term_attached = True
+ self.last_activity = time.time()
+ self.start_time = time.time()
+
+ def finish_update(self):
+ """Callback at the end of the operation"""
+ self.transaction.term_attached = False
+
+ def _child(self, method, *args):
+ """Call the given method or function with the
+ corrsponding arguments in the child process.
+
+ This method should be replace in subclasses.
+ """
+ method(*args)
+ time.sleep(0.5)
+ os._exit(0)
+
+ def run(self, *args, **kwargs):
+ """Setup monitoring, fork and call the self._child() method in the
+ child process with the given arguments.
+ """
+ log.debug("Run")
+ terminal_fd = None
+ if self.transaction.terminal:
+ try:
+ # Save the settings of the transaction terminal and set to
+ # raw mode
+ terminal_fd = os.open(self.transaction.terminal,
+ os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
+ terminal_attr = termios.tcgetattr(terminal_fd)
+ tty.setraw(terminal_fd, termios.TCSANOW)
+ except (OSError, termios.error):
+ # Switch to non-interactive
+ self.transaction.terminal = ""
+ pid = self._fork()
+ if pid == 0:
+ os.close(self.status_parent_fd)
+ try:
+ self._setup_child()
+ self._child(*args, **kwargs)
+ except Exception:
+ traceback.print_exc()
+ finally:
+ # Give the parent process enough time to catch the output
+ time.sleep(1)
+ # Abort the subprocess immediatelly on any unhandled
+ # failure - otherwise the atexit methods would
+ # be called, e.g. the frozen status decorator
+ os._exit(apt_pkg.PackageManager.RESULT_FAILED)
+ else:
+ self.child_pid = pid
+ os.close(self.status_child_fd)
+ log.debug("Child pid: %s", pid)
+ watchers = []
+ flags = GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP
+ if self.transaction.terminal:
+ # Setup copying of i/o between the controlling terminals
+ watchers.append(GLib.io_add_watch(terminal_fd,
+ GLib.PRIORITY_HIGH_IDLE,
+ flags,
+ self._copy_io))
+ watchers.append(GLib.io_add_watch(self.master_fd,
+ GLib.PRIORITY_HIGH_IDLE, flags,
+ self._copy_io_master, terminal_fd))
+ # Monitor the child process
+ watchers.append(
+ GLib.child_watch_add(GLib.PRIORITY_HIGH_IDLE,
+ pid, self._on_child_exit))
+ # Watch for status updates
+ watchers.append(GLib.io_add_watch(self.status_parent_fd,
+ GLib.PRIORITY_HIGH_IDLE,
+ GLib.IO_IN,
+ self._on_status_update))
+ while self._child_exit == -1:
+ GLib.main_context_default().iteration()
+ for id in watchers:
+ GLib.source_remove(id)
+ # Restore the settings of the transaction terminal
+ if terminal_fd:
+ try:
+ termios.tcsetattr(terminal_fd, termios.TCSADRAIN,
+ terminal_attr)
+ except termios.error:
+ pass
+ # Make sure all file descriptors are closed
+ for fd in [self.master_fd, self.status_parent_fd, terminal_fd]:
+ try:
+ os.close(fd)
+ except (OSError, TypeError):
+ pass
+ return os.WEXITSTATUS(self._child_exit)
+
+ def _on_child_exit(self, pid, condition):
+ log.debug("Child exited: %s", condition)
+ self._child_exit = condition
+ return False
+
+ def _on_status_update(self, source, condition):
+ """Callback for changes on the status file descriptor.
+
+ The method has to return True to keep the monitoring alive. If
+ it returns False the monitoring will stop.
+
+ Replace this method in your subclass if you use the status fd.
+ """
+ return False
+
+ def _fork(self):
+ """Fork and create a master/slave pty pair by which the forked process
+ can be controlled.
+ """
+ pid, self.master_fd = os.forkpty()
+ return pid
+
+ def _setup_child(self):
+ """Setup the environment of the child process."""
+ def interrupt_handler(signum, frame):
+ # Exit the child immediately if we receive the interrupt
+ # signal or a Ctrl+C - otherwise the atexit methods would
+ # be called, e.g. the frozen status decorator
+ os._exit(apt_pkg.PackageManager.RESULT_FAILED)
+ signal.signal(signal.SIGINT, interrupt_handler)
+ # Make sure that exceptions of the child are not caught by apport
+ sys.excepthook = sys.__excepthook__
+
+ mainloop.quit()
+ # force terminal messages in dpkg to be untranslated, the
+ # status-fd or debconf prompts will not be affected
+ os.environ["DPKG_UNTRANSLATED_MESSAGES"] = "1"
+ # We also want untranslated status messages from apt
+ locale.setlocale(locale.LC_ALL, "C")
+ # Switch to the language of the user
+ if self.transaction.locale:
+ os.putenv("LANG", self.transaction.locale)
+ # Either connect to the controllong terminal or switch to
+ # non-interactive mode
+ if not self.transaction.terminal:
+ # FIXME: we should check for "mail" or "gnome" here
+ # and not unset in this case
+ os.putenv("APT_LISTCHANGES_FRONTEND", "none")
+ os.putenv("APT_LISTBUGS_FRONTEND", "none")
+ else:
+ os.putenv("TERM", "linux")
+ # Run debconf through a proxy if available
+ if self.transaction.debconf:
+ os.putenv("DEBCONF_PIPE", self.transaction.debconf)
+ os.putenv("DEBIAN_FRONTEND", "passthrough")
+ if log.level == logging.DEBUG:
+ os.putenv("DEBCONF_DEBUG", ".")
+ elif not self.transaction.terminal:
+ os.putenv("DEBIAN_FRONTEND", "noninteractive")
+ # Proxy configuration
+ if self.transaction.http_proxy:
+ apt_pkg.config.set("Acquire::http::Proxy",
+ self.transaction.http_proxy)
+ # Mark changes as being make by aptdaemon
+ cmd = "aptdaemon role='%s' sender='%s'" % (self.transaction.role,
+ self.transaction.sender)
+ apt_pkg.config.set("CommandLine::AsString", cmd)
+
+ def _copy_io_master(self, source, condition, target):
+ if condition == GLib.IO_IN:
+ self.last_activity = time.time()
+ try:
+ char_byte = os.read(source, 1)
+ except OSError:
+ log.debug("Faild to read from master")
+ return True
+ # Write all the output from dpkg to a log
+ char = char_byte.decode("UTF-8", "ignore")
+ if char == "\n":
+ # Skip ANSI characters from the console output
+ line = re.sub(REGEX_ANSI_ESCAPE_CODE, "", self._line_buffer)
+ if line:
+ log_terminal.debug(line)
+ self.output += line + "\n"
+ self._line_buffer = ""
+ else:
+ self._line_buffer += char
+ if target:
+ try:
+ os.write(target, char_byte)
+ except OSError:
+ log.debug("Failed to write to controlling terminal")
+ return True
+ try:
+ os.close(source)
+ except OSError:
+ # Could already be closed by the clean up in run()
+ pass
+ return False
+
+ def _copy_io(self, source, condition):
+ if condition == GLib.IO_IN:
+ try:
+ char = os.read(source, 1)
+ os.write(self.master_fd, char)
+ except OSError:
+ pass
+ else:
+ # Detect config file prompt answers on the console
+ if (self.transaction.paused and
+ self.transaction.config_file_conflict):
+ self.transaction.config_file_conflict_resolution = None
+ self.transaction.paused = False
+ return True
+ os.close(source)
+ return False
+
+
+class DaemonInstallProgress(DaemonForkProgress):
+
+ """Progress to execute APT package operations in a child process."""
+
+ def start_update(self):
+ DaemonForkProgress.start_update(self)
+ lock.status_lock.release()
+
+ def finish_update(self):
+ """Callback at the end of the operation"""
+ DaemonForkProgress.finish_update(self)
+ lock.wait_for_lock(self.transaction, lock.status_lock)
+
+ def _child(self, pm):
+ try:
+ res = pm.do_install(self.status_child_fd)
+ except:
+ os._exit(apt_pkg.PackageManager.RESULT_FAILED)
+ else:
+ os._exit(res)
+
+ def _on_status_update(self, source, condition):
+ """Parse messages from APT on the status fd."""
+ log.debug("UpdateInterface")
+ status_msg = ""
+ try:
+ while not status_msg.endswith("\n"):
+ self.last_activity = time.time()
+ status_msg += os.read(source, 1).decode("UTF-8", "ignore")
+ except:
+ return False
+ try:
+ (status, pkg, percent, message_raw) = status_msg.split(":", 3)
+ except ValueError:
+ # silently ignore lines that can't be parsed
+ return True
+ message = message_raw.strip()
+ # print "percent: %s %s" % (pkg, float(percent)/100.0)
+ if status == "pmerror":
+ self._error(pkg, message)
+ elif status == "pmconffile":
+ # we get a string like this:
+ # 'current-conffile' 'new-conffile' useredited distedited
+ match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", message_raw)
+ if match:
+ new, old = match.group(1), match.group(2)
+ self._conffile(new, old)
+ elif status == "pmstatus":
+ if message.startswith("Installing"):
+ status_enum = enums.PKG_INSTALLING
+ elif message.startswith("Installed"):
+ status_enum = enums.PKG_INSTALLED
+ elif message.startswith("Configuring"):
+ status_enum = enums.PKG_CONFIGURING
+ elif message.startswith("Preparing to configure"):
+ status_enum = enums.PKG_PREPARING_CONFIGURE
+ elif message.startswith("Preparing for removal of"):
+ status_enum = enums.PKG_PREPARING_REMOVE
+ elif message.startswith("Removing"):
+ status_enum = enums.PKG_REMOVING
+ elif message.startswith("Removed"):
+ status_enum = enums.PKG_REMOVED
+ elif message.startswith("Preparing to completely remove"):
+ status_enum = enums.PKG_PREPARING_PURGE
+ elif message.startswith("Completely removing"):
+ status_enum = enums.PKG_PURGING
+ elif message.startswith("Completely removed"):
+ status_enum = enums.PKG_PURGED
+ elif message.startswith("Unpacking"):
+ status_enum = enums.PKG_UNPACKING
+ elif message.startswith("Preparing"):
+ status_enum = enums.PKG_PREPARING_INSTALL
+ elif message.startswith("Noting disappearance of"):
+ status_enum = enums.PKG_DISAPPEARING
+ elif message.startswith("Running"):
+ status_enum = enums.PKG_RUNNING_TRIGGER
+ else:
+ status_enum = enums.PKG_UNKNOWN
+ self._status_changed(pkg, float(percent), status_enum)
+ # catch a time out by sending crtl+c
+ if (self.last_activity + INSTALL_TIMEOUT < time.time() and
+ self.child_pid):
+ log.critical("Killing child since timeout of %s s",
+ INSTALL_TIMEOUT)
+ os.kill(self.child_pid, 15)
+ return True
+
+ def _status_changed(self, pkg, percent, status_enum):
+ """Callback to update status information"""
+ log.debug("APT status: %s, %s, %s", pkg, percent, status_enum)
+ progress = self.progress_begin + percent / 100 * (self.progress_end -
+ self.progress_begin)
+ if self.progress < progress:
+ self.transaction.progress = int(progress)
+ self.progress = progress
+ # We use untranslated messages from apt.
+ # So convert them to an enum to allow translations, see LP #641262
+ # The strings are taken from apt-pkg/deb/dpkgpm.cc
+ desc = enums.get_package_status_from_enum(status_enum)
+ msg = self.transaction.gettext(desc) % pkg
+ self.transaction.status_details = msg
+ self.transaction.progress_package = (pkg, status_enum)
+
+ def _conffile(self, current, new):
+ """Callback for a config file conflict"""
+ log.warning("Config file prompt: '%s' (%s)" % (current, new))
+ self.transaction.config_file_conflict = (current, new)
+ self.transaction.paused = True
+ self.transaction.status = enums.STATUS_WAITING_CONFIG_FILE_PROMPT
+ while self.transaction.paused:
+ GLib.main_context_default().iteration()
+ log.debug("Sending config file answer: %s",
+ self.transaction.config_file_conflict_resolution)
+ if self.transaction.config_file_conflict_resolution == "replace":
+ os.write(self.master_fd, b"y\n")
+ elif self.transaction.config_file_conflict_resolution == "keep":
+ os.write(self.master_fd, b"n\n")
+ self.transaction.config_file_conflict_resolution = None
+ self.transaction.config_file_conflict = None
+ self.transaction.status = enums.STATUS_COMMITTING
+ return True
+
+ def _error(self, pkg, msg):
+ """Callback for an error"""
+ log.critical("%s: %s" % (pkg, msg))
+
+
+class DaemonDpkgInstallProgress(DaemonInstallProgress):
+
+ """Progress handler for a local Debian package installation."""
+
+ def __init__(self, transaction, begin=101, end=101):
+ DaemonInstallProgress.__init__(self, transaction, begin, end)
+
+ def _child(self, debfile):
+ args = [apt_pkg.config["Dir::Bin::DPkg"], "--status-fd",
+ str(self.status_child_fd)]
+ args.extend(apt_pkg.config.value_list("DPkg::Options"))
+ if not self.transaction.terminal:
+ args.extend(["--force-confdef", "--force-confold"])
+ args.extend(["-i", debfile])
+ os.execlp(apt_pkg.config["Dir::Bin::DPkg"], *args)
+
+ def _on_status_update(self, source, condition):
+ log.debug("UpdateInterface")
+ status_raw = ""
+ try:
+ while not status_raw.endswith("\n"):
+ status_raw += os.read(source, 1).decode("UTF-8", "ignore")
+ except:
+ return False
+ try:
+ status = [s.strip() for s in status_raw.split(":", 3)]
+ except ValueError:
+ # silently ignore lines that can't be parsed
+ return True
+ # Parse the status message. It can be of the following types:
+ # - "status: PACKAGE: STATUS"
+ # - "status: PACKAGE: error: MESSAGE"
+ # - "status: FILE: conffile: 'OLD' 'NEW' useredited distedited"
+ # - "processing: STAGE: PACKAGE" with STAGE is one of upgrade,
+ # install, configure, trigproc, remove, purge
+ if status[0] == "status":
+ # FIXME: Handle STATUS
+ if status[2] == "error":
+ self._error(status[1], status[3])
+ elif status[2] == "conffile":
+ match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status[3])
+ if match:
+ new, old = match.group(1), match.group(2)
+ self._conffile(new, old)
+ elif status[0] == "processing":
+ try:
+ status_enum = MAP_DPKG_STAGE[status[1]]
+ except KeyError:
+ status_enum = enums.PKG_UNKONWN
+ self._status_changed(status[2], 101, status_enum)
+ return True
+
+
+class DaemonDpkgRecoverProgress(DaemonDpkgInstallProgress):
+
+ """Progress handler for dpkg --confiure -a call."""
+
+ def _child(self):
+ args = [apt_pkg.config["Dir::Bin::Dpkg"], "--status-fd",
+ str(self.status_child_fd), "--configure", "-a"]
+ args.extend(apt_pkg.config.value_list("Dpkg::Options"))
+ if not self.transaction.terminal:
+ args.extend(["--force-confdef", "--force-confold"])
+ os.execlp(apt_pkg.config["Dir::Bin::DPkg"], *args)
+
+
+class DaemonDpkgReconfigureProgress(DaemonDpkgInstallProgress):
+
+ """Progress handler for dpkg-reconfigure call."""
+
+ def _child(self, packages, priority, ):
+ args = ["/usr/sbin/dpkg-reconfigure"]
+ if priority != "default":
+ args.extend(["--priority", priority])
+ args.extend(packages)
+ os.execlp("/usr/sbin/dpkg-reconfigure", *args)
+
+
+# vim:ts=4:sw=4:et