#!/usr/bin/env python
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
# (c) 2005-2012 Canonical, GPL
# (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>
from __future__ import print_function
from gi.repository import Gtk
from aptdaemon import client, errors
from defer import inline_callbacks
from aptdaemon.gtk3widgets import (AptCancelButton,
AptConfigFileConflictDialog,
AptDetailsExpander,
AptMediumRequiredDialog,
AptProgressBar)
from aptdaemon.enums import (EXIT_SUCCESS,
EXIT_FAILED,
STATUS_COMMITTING,
STATUS_DOWNLOADING,
STATUS_DOWNLOADING_REPO,
STATUS_FINISHED,
get_error_description_from_enum,
get_error_string_from_enum,
get_status_string_from_enum)
from UpdateManager.backend import InstallBackend
from UpdateManager.UnitySupport import UnitySupport
from UpdateManager.Dialogs import BuilderDialog
from gettext import gettext as _
import dbus
import os
class UpdateManagerExpander(AptDetailsExpander):
"""An AptDetailsExpander which can be used with multiple terminals.
The default AptDetailsExpander will shrink/hide when its transaction
finishes. But here we want to support "chaining" transactions. So we
override the status-changed handler to only do that when we are
running the final transaction."""
def __init__(self, transaction, terminal=True, final=False):
super().__init__(transaction, terminal)
self.final = final
def _on_status_changed(self, trans, status):
if status in (STATUS_DOWNLOADING, STATUS_DOWNLOADING_REPO):
self.set_sensitive(True)
self.download_scrolled.show()
if self.terminal:
self.terminal.hide()
elif status == STATUS_COMMITTING:
self.download_scrolled.hide()
if self.terminal:
self.terminal.show()
self.set_sensitive(True)
elif self.final:
self.set_expanded(False)
self.set_sensitive(False)
elif self.final and status == STATUS_FINISHED:
self.download_scrolled.hide()
if self.terminal:
self.terminal.hide()
self.set_sensitive(False)
self.set_expanded(False)
class AptStackedProgressBar(Gtk.ProgressBar):
""" A GtkProgressBar which represents the state of many aptdaemon
transactions.
aptdaemon provides AptProgressBar for the state of *one* transaction to
be represented in a progress bar. This widget creates one of those per
containing transaction, and scales its progress to the given ratio, so
one progress bar can show the state of many transactions."""
def __init__(self, unity):
self.current_max_progress = 0
self.progress_bars = []
self.unity = unity
super().__init__()
def add_transaction(self, trans, max_progress):
assert 0 <= max_progress <= 1
progress = AptProgressBar(trans)
self.progress_bars.append(progress)
progress.min = self.current_max_progress
self.current_max_progress += max_progress
if self.current_max_progress > 1:
self.current_max_progress = 1
progress.max = self.current_max_progress
progress.connect("notify::fraction", self._update_progress)
progress.connect("notify::text", self._update_text)
def _update_progress(self, inner_progress, data):
delta = inner_progress.max - inner_progress.min
position_in_delta = delta * inner_progress.get_fraction()
new_progress = inner_progress.min + position_in_delta
self.set_fraction(new_progress)
self.unity.set_progress(new_progress * 100)
def _update_text(self, inner_progress, data):
self.set_text(inner_progress.get_text())
class InstallBackendAptdaemon(InstallBackend, BuilderDialog):
"""Makes use of aptdaemon to refresh the cache and to install updates."""
def __init__(self, window_main, action):
InstallBackend.__init__(self, window_main, action)
ui_path = os.path.join(window_main.datadir,
"gtkbuilder/UpdateProgress.ui")
BuilderDialog.__init__(self, window_main, ui_path,
"pane_update_progress")
self.client = client.AptClient()
self.unity = UnitySupport()
self._expanded_size = None
self.button_cancel = None
self.trans_failed_msg = None
self.progressbar = None
self._active_transaction = None
self._expander = None
def close(self):
if self.button_cancel and self.button_cancel.get_sensitive():
try:
self.button_cancel.clicked()
except Exception:
# there is not much left to do if the transaction can't be
# canceled
pass
return True
else:
return False
@inline_callbacks
def update(self):
"""Refresh the package list"""
try:
trans = yield self.client.update_cache(defer=True)
yield self._show_transaction(trans, self.ACTION_UPDATE,
_("Checking for updates…"), False)
except errors.NotAuthorizedError:
self._action_done(self.ACTION_UPDATE,
authorized=False, success=False,
error_string=None, error_desc=None)
except Exception:
self._action_done(self.ACTION_UPDATE,
authorized=True, success=False,
error_string=None, error_desc=None)
raise
def _show_transaction_error(self, trans, action):
error_string = get_error_string_from_enum(trans.error.code)
error_desc = get_error_description_from_enum(trans.error.code)
if self.trans_failed_msg:
trans_failed = True
error_desc = error_desc + "\n" + self.trans_failed_msg
else:
trans_failed = None
self._action_done(action,
authorized=True, success=False,
error_string=error_string,
error_desc=error_desc,
trans_failed=trans_failed)
def _update_next_package(self, trans, status, action):
if status == EXIT_FAILED:
self._show_transaction_error(trans, action)
return
self._apt_update_oem()
@inline_callbacks
def _apt_update_oem(self):
assert self._oem_packages_to_update
elem = self._oem_packages_to_update.pop()
sources_list_file = f'/etc/apt/sources.list.d/{elem}.list'
try:
if os.path.exists(sources_list_file):
trans = yield self.client.update_cache(
sources_list=sources_list_file
)
if self._oem_packages_to_update:
finished_handler = self._update_next_package
else:
finished_handler = self._on_finished
yield self._show_transaction(
trans,
self.ACTION_PRE_INSTALL,
_("Installing updates…"), True,
on_finished_handler=finished_handler,
progress_bar_max=0.1 / self._len_oem_updates
)
except errors.NotAuthorizedError:
self._action_done(self.ACTION_PRE_INSTALL,
authorized=False, success=False,
error_string=None, error_desc=None)
except errors.TransactionFailed as e:
self.trans_failed_msg = str(e)
except dbus.DBusException as e:
if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
raise
self._action_done(self.ACTION_PRE_INSTALL,
authorized=False, success=False,
error_string=None, error_desc=None)
except Exception:
self._action_done(self.ACTION_PRE_INSTALL,
authorized=True, success=False,
error_string=None, error_desc=None)
raise
def _update_oem(self, trans, status, action):
# This is the "finished" handler of installing an oem metapackage
# What we do now is:
# 1. update_cache() for the new sources.lists only
if status == EXIT_FAILED:
self._show_transaction_error(trans, action)
return
(install, _, _, _, _, _) = trans.packages
self._oem_packages_to_update = set(install)
self._len_oem_updates = len(install)
self._apt_update_oem()
@inline_callbacks
def commit_oem(self, pkgs_install_oem, pkgs_upgrade_oem):
self.all_oem_packages = set(pkgs_install_oem) | set(pkgs_upgrade_oem)
# Nothing to do? Go to the regular updates.
try:
if not pkgs_install_oem and not pkgs_upgrade_oem:
self._action_done(self.ACTION_PRE_INSTALL,
authorized=True, success=True,
error_string=None, error_desc=None,
trans_failed=None)
return
if pkgs_install_oem:
trans = yield self.client.install_packages(pkgs_install_oem,
defer=True)
yield self._show_transaction(
trans,
self.ACTION_PRE_INSTALL,
_("Installing updates…"), True,
on_finished_handler=self._update_oem,
progress_bar_max=0.1
)
except errors.NotAuthorizedError:
self._action_done(self.ACTION_PRE_INSTALL,
authorized=False, success=False,
error_string=None, error_desc=None)
except errors.TransactionFailed as e:
self.trans_failed_msg = str(e)
except dbus.DBusException as e:
if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
raise
self._action_done(self.ACTION_PRE_INSTALL,
authorized=False, success=False,
error_string=None, error_desc=None)
except Exception:
self._action_done(self.ACTION_PRE_INSTALL,
authorized=True, success=False,
error_string=None, error_desc=None)
raise
@inline_callbacks
def commit(self, pkgs_install, pkgs_upgrade, pkgs_remove):
"""Commit a list of package adds and removes"""
try:
reinstall = purge = downgrade = []
trans = yield self.client.commit_packages(
pkgs_install, reinstall, pkgs_remove, purge, pkgs_upgrade,
downgrade, defer=True)
yield self._show_transaction(trans, self.ACTION_INSTALL,
_("Installing updates…"), True)
except errors.NotAuthorizedError:
self._action_done(self.ACTION_INSTALL,
authorized=False, success=False,
error_string=None, error_desc=None)
except errors.TransactionFailed as e:
self.trans_failed_msg = str(e)
except dbus.DBusException as e:
#print(e, e.get_dbus_name())
if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
raise
self._action_done(self.ACTION_INSTALL,
authorized=False, success=False,
error_string=None, error_desc=None)
except Exception:
self._action_done(self.ACTION_INSTALL,
authorized=True, success=False,
error_string=None, error_desc=None)
raise
def _on_details_changed(self, trans, details, label_details):
label_details.set_label(details)
def _on_status_changed(self, trans, status, label_details):
label_details.set_label(get_status_string_from_enum(status))
# Also resize the window if we switch from download details to
# the terminal window
if status == STATUS_COMMITTING and self._expander \
and self._expander.terminal.get_visible():
self._resize_to_show_details(self._expander)
@inline_callbacks
def _show_transaction(self, trans, action, header, show_details,
progress_bar_max=1,
on_finished_handler=None):
if on_finished_handler is None:
on_finished_handler = self._on_finished
self.label_header.set_label(header)
if not self.progressbar:
self.progressbar = AptStackedProgressBar(self.unity)
self.progressbar.show()
self.progressbar_slot.add(self.progressbar)
self.progressbar.add_transaction(trans, progress_bar_max)
if self.button_cancel:
self.button_cancel.set_transaction(trans)
else:
self.button_cancel = AptCancelButton(trans)
self.button_cancel.show()
self.button_cancel_slot.add(self.button_cancel)
if action == self.ACTION_UPDATE:
self.button_cancel.set_label(Gtk.STOCK_STOP)
if show_details:
if not self._expander:
self._expander = UpdateManagerExpander(trans)
self._expander.set_vexpand(True)
self._expander.set_hexpand(True)
self._expander.show_all()
self._expander.connect("notify::expanded",
self._on_expanded)
self.expander_slot.add(self._expander)
self.expander_slot.show()
else:
self._expander.set_transaction(trans)
self._expander.final = action != self.ACTION_PRE_INSTALL
elif self._expander:
self._expander_slot.hide()
trans.connect("status-details-changed", self._on_details_changed,
self.label_details)
trans.connect("status-changed", self._on_status_changed,
self.label_details)
trans.connect("finished", on_finished_handler, action)
trans.connect("medium-required", self._on_medium_required)
trans.connect("config-file-conflict", self._on_config_file_conflict)
yield trans.set_debconf_frontend("gnome")
yield trans.run()
def _on_expanded(self, expander, param):
# Make the dialog resizable if the expander is expanded
# try to restore a previous size
if not expander.get_expanded():
self._expanded_size = (expander.terminal.get_visible(),
self.window_main.get_size())
self.window_main.end_user_resizable()
elif self._expanded_size:
term_visible, (stored_width, stored_height) = self._expanded_size
# Check if the stored size was for the download details or
# the terminal widget
if term_visible != expander.terminal.get_visible():
# The stored size was for the download details, so we need
# get a new size for the terminal widget
self._resize_to_show_details(expander)
else:
self.window_main.begin_user_resizable(stored_width,
stored_height)
else:
self._resize_to_show_details(expander)
def _resize_to_show_details(self, expander):
"""Resize the window to show the expanded details.
Unfortunately the expander only expands to the preferred size of the
child widget (e.g showing all 80x24 chars of the Vte terminal) if
the window is rendered the first time and the terminal is also visible.
If the expander is expanded afterwards the window won't change its
size anymore. So we have to do this manually. See LP#840942
"""
if expander.get_expanded():
win_width, win_height = self.window_main.get_size()
exp_width = expander.get_allocation().width
exp_height = expander.get_allocation().height
if expander.terminal.get_visible():
terminal_width = expander.terminal.get_char_width() * 80
terminal_height = expander.terminal.get_char_height() * 24
new_width = terminal_width - exp_width + win_width
new_height = terminal_height - exp_height + win_height
else:
new_width = win_width + 100
new_height = win_height + 200
self.window_main.begin_user_resizable(new_width, new_height)
def _on_medium_required(self, transaction, medium, drive):
dialog = AptMediumRequiredDialog(medium, drive, self.window_main)
res = dialog.run()
dialog.hide()
if res == Gtk.ResponseType.OK:
transaction.provide_medium(medium)
else:
transaction.cancel()
def _on_config_file_conflict(self, transaction, old, new):
dialog = AptConfigFileConflictDialog(old, new, self.window_main)
res = dialog.run()
dialog.hide()
if res == Gtk.ResponseType.YES:
transaction.resolve_config_file_conflict(old, "replace")
else:
transaction.resolve_config_file_conflict(old, "keep")
def _on_finished(self, trans, status, action):
error_string = None
error_desc = None
trans_failed = False
if status == EXIT_FAILED:
error_string = get_error_string_from_enum(trans.error.code)
error_desc = get_error_description_from_enum(trans.error.code)
if self.trans_failed_msg:
trans_failed = True
error_desc = error_desc + "\n" + self.trans_failed_msg
# tell unity to hide the progress again
self.unity.set_progress(-1)
is_success = (status == EXIT_SUCCESS)
try:
self._action_done(action,
authorized=True, success=is_success,
error_string=error_string, error_desc=error_desc,
trans_failed=trans_failed)
except TypeError:
# this module used to be be lazily imported and in older code
# trans_failed= is not accepted
# TODO: this workaround can be dropped in Ubuntu 20.10
self._action_done(action,
authorized=True, success=is_success,
error_string=error_string, error_desc=error_desc)
if __name__ == "__main__":
import mock
options = mock.Mock()
data_dir = "/usr/share/update-manager"
from UpdateManager.UpdateManager import UpdateManager
app = UpdateManager(data_dir, options)
b = InstallBackendAptdaemon(app, None)
b.commit(["2vcard"], [], [])
Gtk.main()