From 5afbd53a5743fe37947d0ebd282111addd325615 Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Sun, 8 Mar 2009 18:13:48 +0100 Subject: * apt/progress/: New package, replaces apt.progress and apt.gtk - apt/progress/gtk2.py: Moved here from apt/gtk/widgets.py - apt/progress/__init__.py: Move here from apt/progress.py --- apt/gtk/__init__.py | 0 apt/gtk/widgets.py | 443 --------------------------------------- apt/progress.py | 356 ------------------------------- apt/progress/__init__.py | 356 +++++++++++++++++++++++++++++++ apt/progress/gtk2.py | 443 +++++++++++++++++++++++++++++++++++++++ debian/changelog | 3 + doc/examples/gui-inst.py | 4 +- doc/source/apt/gtk.widgets.rst | 29 --- doc/source/apt/progress.gtk2.rst | 29 +++ doc/source/examples/apt-gtk.py | 4 +- 10 files changed, 835 insertions(+), 832 deletions(-) delete mode 100644 apt/gtk/__init__.py delete mode 100644 apt/gtk/widgets.py delete mode 100644 apt/progress.py create mode 100644 apt/progress/__init__.py create mode 100644 apt/progress/gtk2.py delete mode 100644 doc/source/apt/gtk.widgets.rst create mode 100644 doc/source/apt/progress.gtk2.rst diff --git a/apt/gtk/__init__.py b/apt/gtk/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apt/gtk/widgets.py b/apt/gtk/widgets.py deleted file mode 100644 index 435265d4..00000000 --- a/apt/gtk/widgets.py +++ /dev/null @@ -1,443 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2004-2005 Canonical -# -# Authors: Michael Vogt -# Sebastian Heinlein -# Julian Andres Klode -# -# 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. -# -# his program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -# USA -"""GObject-powered progress classes and a GTK+ status widget.""" - -from gettext import gettext as _ -import os -import time - -import pygtk -pygtk.require('2.0') -import gtk -try: - import glib -except ImportError: - import gobject as glib -import gobject -import pango -import vte - -import apt -import apt_pkg - - -def mksig(params=(), run=gobject.SIGNAL_RUN_FIRST, rettype=gobject.TYPE_NONE): - """Simplified Create a gobject signal. - - This allows us to write signals easier, because we just need to define the - type of the parameters (in most cases). - - ``params`` is a tuple which defines the types of the arguments. - """ - return (run, rettype, params) - - -class GOpProgress(gobject.GObject, apt.progress.OpProgress): - """Operation progress with GObject signals. - - Signals: - - * status-changed(str: operation, int: percent) - * status-started() - Not Implemented yet - * status-finished() - - """ - - __gsignals__ = {"status-changed": mksig((str, int)), - "status-started": mksig(), - "status-finished": mksig()} - - def __init__(self): - apt.progress.OpProgress.__init__(self) - gobject.GObject.__init__(self) - self._context = glib.main_context_default() - - def update(self, percent): - """Called to update the percentage done""" - self.emit("status-changed", self.op, percent) - while self._context.pending(): - self._context.iteration() - - def done(self): - """Called when all operation have finished.""" - self.emit("status-finished") - - -class GInstallProgress(gobject.GObject, apt.progress.InstallProgress): - """Installation progress with GObject signals. - - Signals: - - * status-changed(str: status, int: percent) - * status-started() - * status-finished() - * status-timeout() - * status-error() - * status-conffile() - - """ - # Seconds until a maintainer script will be regarded as hanging - INSTALL_TIMEOUT = 5 * 60 - - __gsignals__ = {"status-changed": mksig((str, int)), - "status-started": mksig(), - "status-timeout": mksig(), - "status-error": mksig(), - "status-conffile": mksig(), - "status-finished": mksig()} - - def __init__(self, term): - apt.progress.InstallProgress.__init__(self) - gobject.GObject.__init__(self) - self.finished = False - self.time_last_update = time.time() - self.term = term - reaper = vte.reaper_get() - reaper.connect("child-exited", self.childExited) - self.env = ["VTE_PTY_KEEP_FD=%s" % self.writefd, - "DEBIAN_FRONTEND=gnome", - "APT_LISTCHANGES_FRONTEND=gtk"] - self._context = glib.main_context_default() - - def childExited(self, term, pid, status): - """Called when a child process exits""" - self.apt_status = os.WEXITSTATUS(status) - self.finished = True - - def error(self, pkg, errormsg): - """Called when an error happens. - - Emits: status-error() - """ - self.emit("status-error") - - def conffile(self, current, new): - """Called during conffile. - - Emits: status-conffile() - """ - self.emit("status-conffile") - - def startUpdate(self): - """Called when the update starts. - - Emits: status-started() - """ - self.emit("status-started") - - def finishUpdate(self): - """Called when the update finished. - - Emits: status-finished() - """ - self.emit("status-finished") - - def statusChange(self, pkg, percent, status): - """Called when the status changed. - - Emits: status-changed(status, percent) - """ - self.time_last_update = time.time() - self.emit("status-changed", status, percent) - - def updateInterface(self): - """Called periodically to update the interface. - - Emits: status-timeout() [When a timeout happens] - """ - apt.progress.InstallProgress.updateInterface(self) - while self._context.pending(): - self._context.iteration() - if self.time_last_update + self.INSTALL_TIMEOUT < time.time(): - self.emit("status-timeout") - - def fork(self): - """Fork the process.""" - return self.term.forkpty(envv=self.env) - - def waitChild(self): - """Wait for the child process to exit.""" - while not self.finished: - self.updateInterface() - return self.apt_status - - -class GDpkgInstallProgress(apt.progress.DpkgInstallProgress, GInstallProgress): - """An InstallProgress for local installations. - - Signals: - - * status-changed(str: status, int: percent) - * status-started() - Not Implemented yet - * status-finished() - * status-timeout() - When the maintainer script hangs - * status-error() - When an error happens - * status-conffile() - On Conffile - """ - - def run(self, debfile): - """Install the given package.""" - apt.progress.DpkgInstallProgress.run(self, debfile) - - def updateInterface(self): - """Called periodically to update the interface. - - Emits: status-timeout() [When a timeout happens]""" - apt.progress.DpkgInstallProgress.updateInterface(self) - if self.time_last_update + self.INSTALL_TIMEOUT < time.time(): - self.emit("status-timeout") - - -class GFetchProgress(gobject.GObject, apt.progress.FetchProgress): - """A Fetch Progress with GObject signals. - - Signals: - - * status-changed(str: description, int: percent) - * status-started() - * status-finished() - """ - - __gsignals__ = {"status-changed": mksig((str, int)), - "status-started": mksig(), - "status-finished": mksig()} - - def __init__(self): - apt.progress.FetchProgress.__init__(self) - gobject.GObject.__init__(self) - self._continue = True - self._context = glib.main_context_default() - - def start(self): - self.emit("status-started") - - def stop(self): - self.emit("status-finished") - - def cancel(self): - self._continue = False - - def pulse(self): - apt.progress.FetchProgress.pulse(self) - currentItem = self.currentItems + 1 - if currentItem > self.totalItems: - currentItem = self.totalItems - if self.currentCPS > 0: - text = (_("Downloading file %(current)li of %(total)li with " - "%(speed)s/s") % \ - {"current": currentItem, - "total": self.totalItems, - "speed": apt_pkg.SizeToStr(self.currentCPS)}) - else: - text = (_("Downloading file %(current)li of %(total)li") % \ - {"current": currentItem, - "total": self.totalItems}) - self.emit("status-changed", text, self.percent) - while self._context.pending(): - self._context.iteration() - return self._continue - - -class GtkAptProgress(gtk.VBox): - """Graphical progress for installation/fetch/operations. - - This widget provides a progress bar, a terminal and a status bar for - showing the progress of package manipulation tasks. - """ - - def __init__(self): - gtk.VBox.__init__(self) - self.set_spacing(6) - # Setup some child widgets - self._expander = gtk.Expander(_("Details")) - self._terminal = vte.Terminal() - #self._terminal.set_font_from_string("monospace 10") - self._expander.add(self._terminal) - self._progressbar = gtk.ProgressBar() - # Setup the always italic status label - self._label = gtk.Label() - attr_list = pango.AttrList() - attr_list.insert(pango.AttrStyle(pango.STYLE_ITALIC, 0, -1)) - self._label.set_attributes(attr_list) - self._label.set_ellipsize(pango.ELLIPSIZE_END) - self._label.set_alignment(0, 0) - # add child widgets - self.pack_start(self._progressbar, False) - self.pack_start(self._label, False) - self.pack_start(self._expander, False) - # Setup the internal progress handlers - self._progress_open = GOpProgress() - self._progress_open.connect("status-changed", self._on_status_changed) - self._progress_open.connect("status-started", self._on_status_started) - self._progress_open.connect("status-finished", - self._on_status_finished) - self._progress_fetch = GFetchProgress() - self._progress_fetch.connect("status-changed", self._on_status_changed) - self._progress_fetch.connect("status-started", self._on_status_started) - self._progress_fetch.connect("status-finished", - self._on_status_finished) - self._progress_install = GInstallProgress(self._terminal) - self._progress_install.connect("status-changed", - self._on_status_changed) - self._progress_install.connect("status-started", - self._on_status_started) - self._progress_install.connect("status-finished", - self._on_status_finished) - self._progress_install.connect("status-timeout", - self._on_status_timeout) - self._progress_install.connect("status-error", - self._on_status_timeout) - self._progress_install.connect("status-conffile", - self._on_status_timeout) - self._progress_dpkg_install = GDpkgInstallProgress(self._terminal) - self._progress_dpkg_install.connect("status-changed", - self._on_status_changed) - self._progress_dpkg_install.connect("status-started", - self._on_status_started) - self._progress_dpkg_install.connect("status-finished", - self._on_status_finished) - self._progress_dpkg_install.connect("status-timeout", - self._on_status_timeout) - self._progress_dpkg_install.connect("status-error", - self._on_status_timeout) - self._progress_dpkg_install.connect("status-conffile", - self._on_status_timeout) - - def clear(self): - """Reset all status information.""" - self._label.set_label("") - self._progressbar.set_fraction(0) - self._expander.set_expanded(False) - - @property - def open(self): - """Return the cache opening progress handler.""" - return self._progress_open - - @property - def install(self): - """Return the install progress handler.""" - return self._progress_install - - @property - def dpkg_install(self): - """Return the install progress handler for dpkg.""" - return self._dpkg_progress_install - - @property - def fetch(self): - """Return the fetch progress handler.""" - return self._progress_fetch - - def _on_status_started(self, progress): - """Called when something starts.""" - self._on_status_changed(progress, _("Starting..."), 0) - while gtk.events_pending(): - gtk.main_iteration() - - def _on_status_finished(self, progress): - """Called when something finished.""" - self._on_status_changed(progress, _("Complete"), 100) - while gtk.events_pending(): - gtk.main_iteration() - - def _on_status_changed(self, progress, status, percent): - """Called when the status changed.""" - self._label.set_text(status) - if percent is None: - self._progressbar.pulse() - else: - self._progressbar.set_fraction(percent/100.0) - while gtk.events_pending(): - gtk.main_iteration() - - def _on_status_timeout(self, progress): - """Called when timeout happens.""" - self._expander.set_expanded(True) - while gtk.events_pending(): - gtk.main_iteration() - - def cancel_download(self): - """Cancel a currently running download.""" - self._progress_fetch.cancel() - - def show_terminal(self, expanded=False): - """Show the expander for the terminal. - - Show an expander with a terminal widget which provides a way - to interact with dpkg - """ - self._expander.show() - self._terminal.show() - self._expander.set_expanded(expanded) - while gtk.events_pending(): - gtk.main_iteration() - - def hide_terminal(self): - """Hide the expander with the terminal widget.""" - self._expander.hide() - while gtk.events_pending(): - gtk.main_iteration() - - def show(self): - """Show the Box""" - gtk.HBox.show(self) - self._label.show() - self._progressbar.show() - while gtk.events_pending(): - gtk.main_iteration() - - -def _test(): - """Test function""" - import sys - - from apt.debfile import DebPackage - - win = gtk.Window() - apt_progress = GtkAptProgress() - win.set_title("GtkAptProgress Demo") - win.add(apt_progress) - apt_progress.show() - win.show() - cache = apt.cache.Cache(apt_progress.open) - pkg = cache["xterm"] - if pkg.isInstalled: - pkg.markDelete() - else: - pkg.markInstall() - apt_progress.show_terminal(True) - try: - cache.commit(apt_progress.fetch, apt_progress.install) - except Exception, exc: - print >> sys.stderr, "Exception happened:", exc - if len(sys.argv) > 1: - deb = DebPackage(sys.argv[1], cache) - deb.install(apt_progress.dpkg_install) - gtk.main() - - -if __name__ == "__main__": - _test() - -# vim: ts=4 et sts=4 diff --git a/apt/progress.py b/apt/progress.py deleted file mode 100644 index 51eb2426..00000000 --- a/apt/progress.py +++ /dev/null @@ -1,356 +0,0 @@ -# Progress.py - progress reporting classes -# -# Copyright (c) 2005 Canonical -# -# Author: Michael Vogt -# -# 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. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -# USA -"""progress reporting classes. - -This module provides classes for progress reporting. They can be used with -e.g., for reporting progress on the cache opening process, the cache update -progress, or the package install progress. -""" - -import errno -import fcntl -import os -import re -import select -import sys - -import apt_pkg - - -__all__ = ('CdromProgress', 'DpkgInstallProgress', 'DumbInstallProgress', - 'FetchProgress', 'InstallProgress', 'OpProgress', 'OpTextProgress', - 'TextFetchProgress') - - -class OpProgress(object): - """Abstract class to implement reporting on cache opening. - - Subclass this class to implement simple Operation progress reporting. - """ - - def __init__(self): - self.op = None - self.subOp = None - - def update(self, percent): - """Called periodically to update the user interface.""" - - def done(self): - """Called once an operation has been completed.""" - - -class OpTextProgress(OpProgress): - """A simple text based cache open reporting class.""" - - def __init__(self): - OpProgress.__init__(self) - - def update(self, percent): - """Called periodically to update the user interface.""" - sys.stdout.write("\r%s: %.2i " % (self.subOp, percent)) - sys.stdout.flush() - - def done(self): - """Called once an operation has been completed.""" - sys.stdout.write("\r%s: Done\n" % self.op) - - -class FetchProgress(object): - """Report the download/fetching progress. - - Subclass this class to implement fetch progress reporting - """ - - # download status constants - dlDone = 0 - dlQueued = 1 - dlFailed = 2 - dlHit = 3 - dlIgnored = 4 - dlStatusStr = {dlDone: "Done", - dlQueued: "Queued", - dlFailed: "Failed", - dlHit: "Hit", - dlIgnored: "Ignored"} - - def __init__(self): - self.eta = 0.0 - self.percent = 0.0 - # Make checking easier - self.currentBytes = 0 - self.currentItems = 0 - self.totalBytes = 0 - self.totalItems = 0 - self.currentCPS = 0 - - def start(self): - """Called when the fetching starts.""" - - def stop(self): - """Called when all files have been fetched.""" - - def updateStatus(self, uri, descr, shortDescr, status): - """Called when the status of an item changes. - - This happens eg. when the downloads fails or is completed. - """ - - def pulse(self): - """Called periodically to update the user interface. - - Return True to continue or False to cancel. - """ - self.percent = (((self.currentBytes + self.currentItems) * 100.0) / - float(self.totalBytes + self.totalItems)) - if self.currentCPS > 0: - self.eta = ((self.totalBytes - self.currentBytes) / - float(self.currentCPS)) - return True - - def mediaChange(self, medium, drive): - """react to media change events.""" - - -class TextFetchProgress(FetchProgress): - """ Ready to use progress object for terminal windows """ - - def __init__(self): - FetchProgress.__init__(self) - self.items = {} - - def updateStatus(self, uri, descr, shortDescr, status): - """Called when the status of an item changes. - - This happens eg. when the downloads fails or is completed. - """ - if status != self.dlQueued: - print "\r%s %s" % (self.dlStatusStr[status], descr) - self.items[uri] = status - - def pulse(self): - """Called periodically to update the user interface. - - Return True to continue or False to cancel. - """ - FetchProgress.pulse(self) - if self.currentCPS > 0: - s = "[%2.f%%] %sB/s %s" % (self.percent, - apt_pkg.SizeToStr(int(self.currentCPS)), - apt_pkg.TimeToStr(int(self.eta))) - else: - s = "%2.f%% [Working]" % (self.percent) - print "\r%s" % (s), - sys.stdout.flush() - return True - - def stop(self): - """Called when all files have been fetched.""" - print "\rDone downloading " - - def mediaChange(self, medium, drive): - """react to media change events.""" - print ("Media change: please insert the disc labeled " - "'%s' in the drive '%s' and press enter") % (medium, drive) - - return raw_input() not in ('c', 'C') - - -class DumbInstallProgress(object): - """Report the install progress. - - Subclass this class to implement install progress reporting. - """ - - def startUpdate(self): - """Start update.""" - - def run(self, pm): - """Start installation.""" - return pm.DoInstall() - - def finishUpdate(self): - """Called when update has finished.""" - - def updateInterface(self): - """Called periodically to update the user interface""" - - -class InstallProgress(DumbInstallProgress): - """An InstallProgress that is pretty useful. - - It supports the attributes 'percent' 'status' and callbacks for the dpkg - errors and conffiles and status changes. - """ - - def __init__(self): - DumbInstallProgress.__init__(self) - self.selectTimeout = 0.1 - (read, write) = os.pipe() - self.writefd = write - self.statusfd = os.fdopen(read, "r") - fcntl.fcntl(self.statusfd.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) - self.read = "" - self.percent = 0.0 - self.status = "" - - def error(self, pkg, errormsg): - """Called when a error is detected during the install.""" - - def conffile(self, current, new): - """Called when a conffile question from dpkg is detected.""" - - def statusChange(self, pkg, percent, status): - """Called when the status changed.""" - - def updateInterface(self): - """Called periodically to update the interface.""" - if self.statusfd is None: - return - try: - while not self.read.endswith("\n"): - self.read += os.read(self.statusfd.fileno(), 1) - except OSError, (errno_, errstr): - # resource temporarly unavailable is ignored - if errno_ != errno.EAGAIN and errno_ != errno.EWOULDBLOCK: - print errstr - if not self.read.endswith("\n"): - return - - s = self.read - #print s - try: - (status, pkg, percent, status_str) = s.split(":", 3) - except ValueError: - # silently ignore lines that can't be parsed - self.read = "" - return - #print "percent: %s %s" % (pkg, float(percent)/100.0) - if status == "pmerror": - self.error(pkg, status_str) - elif status == "pmconffile": - # we get a string like this: - # 'current-conffile' 'new-conffile' useredited distedited - match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status_str) - if match: - self.conffile(match.group(1), match.group(2)) - elif status == "pmstatus": - if float(percent) != self.percent or status_str != self.status: - self.statusChange(pkg, float(percent), - status_str.strip()) - self.percent = float(percent) - self.status = status_str.strip() - self.read = "" - - def fork(self): - """Fork.""" - return os.fork() - - def waitChild(self): - """Wait for child progress to exit.""" - while True: - select.select([self.statusfd], [], [], self.selectTimeout) - self.updateInterface() - (pid, res) = os.waitpid(self.child_pid, os.WNOHANG) - if pid == self.child_pid: - break - return res - - def run(self, pm): - """Start installing.""" - pid = self.fork() - if pid == 0: - # child - res = pm.DoInstall(self.writefd) - os._exit(res) - self.child_pid = pid - res = self.waitChild() - return os.WEXITSTATUS(res) - - -class CdromProgress(object): - """Report the cdrom add progress. - - Subclass this class to implement cdrom add progress reporting. - """ - - def __init__(self): - pass - - def update(self, text, step): - """Called periodically to update the user interface.""" - - def askCdromName(self): - """Called to ask for the name of the cdrom.""" - - def changeCdrom(self): - """Called to ask for the cdrom to be changed.""" - - -class DpkgInstallProgress(InstallProgress): - """Progress handler for a local Debian package installation.""" - - def run(self, debfile): - """Start installing the given Debian package.""" - self.debfile = debfile - self.debname = os.path.basename(debfile).split("_")[0] - pid = self.fork() - if pid == 0: - # child - res = os.system("/usr/bin/dpkg --status-fd %s -i %s" % \ - (self.writefd, self.debfile)) - os._exit(os.WEXITSTATUS(res)) - self.child_pid = pid - res = self.waitChild() - return res - - def updateInterface(self): - """Process status messages from dpkg.""" - if self.statusfd is None: - return - while True: - try: - self.read += os.read(self.statusfd.fileno(), 1) - except OSError, (errno_, errstr): - # resource temporarly unavailable is ignored - if errno_ != 11: - print errstr - break - if not self.read.endswith("\n"): - continue - - statusl = self.read.split(":") - if len(statusl) < 3: - print "got garbage from dpkg: '%s'" % self.read - self.read = "" - break - status = statusl[2].strip() - #print status - if status == "error": - self.error(self.debname, status) - elif status == "conffile-prompt": - # we get a string like this: - # 'current-conffile' 'new-conffile' useredited distedited - match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", statusl[3]) - if match: - self.conffile(match.group(1), match.group(2)) - else: - self.status = status - self.read = "" diff --git a/apt/progress/__init__.py b/apt/progress/__init__.py new file mode 100644 index 00000000..51eb2426 --- /dev/null +++ b/apt/progress/__init__.py @@ -0,0 +1,356 @@ +# Progress.py - progress reporting classes +# +# Copyright (c) 2005 Canonical +# +# Author: Michael Vogt +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +"""progress reporting classes. + +This module provides classes for progress reporting. They can be used with +e.g., for reporting progress on the cache opening process, the cache update +progress, or the package install progress. +""" + +import errno +import fcntl +import os +import re +import select +import sys + +import apt_pkg + + +__all__ = ('CdromProgress', 'DpkgInstallProgress', 'DumbInstallProgress', + 'FetchProgress', 'InstallProgress', 'OpProgress', 'OpTextProgress', + 'TextFetchProgress') + + +class OpProgress(object): + """Abstract class to implement reporting on cache opening. + + Subclass this class to implement simple Operation progress reporting. + """ + + def __init__(self): + self.op = None + self.subOp = None + + def update(self, percent): + """Called periodically to update the user interface.""" + + def done(self): + """Called once an operation has been completed.""" + + +class OpTextProgress(OpProgress): + """A simple text based cache open reporting class.""" + + def __init__(self): + OpProgress.__init__(self) + + def update(self, percent): + """Called periodically to update the user interface.""" + sys.stdout.write("\r%s: %.2i " % (self.subOp, percent)) + sys.stdout.flush() + + def done(self): + """Called once an operation has been completed.""" + sys.stdout.write("\r%s: Done\n" % self.op) + + +class FetchProgress(object): + """Report the download/fetching progress. + + Subclass this class to implement fetch progress reporting + """ + + # download status constants + dlDone = 0 + dlQueued = 1 + dlFailed = 2 + dlHit = 3 + dlIgnored = 4 + dlStatusStr = {dlDone: "Done", + dlQueued: "Queued", + dlFailed: "Failed", + dlHit: "Hit", + dlIgnored: "Ignored"} + + def __init__(self): + self.eta = 0.0 + self.percent = 0.0 + # Make checking easier + self.currentBytes = 0 + self.currentItems = 0 + self.totalBytes = 0 + self.totalItems = 0 + self.currentCPS = 0 + + def start(self): + """Called when the fetching starts.""" + + def stop(self): + """Called when all files have been fetched.""" + + def updateStatus(self, uri, descr, shortDescr, status): + """Called when the status of an item changes. + + This happens eg. when the downloads fails or is completed. + """ + + def pulse(self): + """Called periodically to update the user interface. + + Return True to continue or False to cancel. + """ + self.percent = (((self.currentBytes + self.currentItems) * 100.0) / + float(self.totalBytes + self.totalItems)) + if self.currentCPS > 0: + self.eta = ((self.totalBytes - self.currentBytes) / + float(self.currentCPS)) + return True + + def mediaChange(self, medium, drive): + """react to media change events.""" + + +class TextFetchProgress(FetchProgress): + """ Ready to use progress object for terminal windows """ + + def __init__(self): + FetchProgress.__init__(self) + self.items = {} + + def updateStatus(self, uri, descr, shortDescr, status): + """Called when the status of an item changes. + + This happens eg. when the downloads fails or is completed. + """ + if status != self.dlQueued: + print "\r%s %s" % (self.dlStatusStr[status], descr) + self.items[uri] = status + + def pulse(self): + """Called periodically to update the user interface. + + Return True to continue or False to cancel. + """ + FetchProgress.pulse(self) + if self.currentCPS > 0: + s = "[%2.f%%] %sB/s %s" % (self.percent, + apt_pkg.SizeToStr(int(self.currentCPS)), + apt_pkg.TimeToStr(int(self.eta))) + else: + s = "%2.f%% [Working]" % (self.percent) + print "\r%s" % (s), + sys.stdout.flush() + return True + + def stop(self): + """Called when all files have been fetched.""" + print "\rDone downloading " + + def mediaChange(self, medium, drive): + """react to media change events.""" + print ("Media change: please insert the disc labeled " + "'%s' in the drive '%s' and press enter") % (medium, drive) + + return raw_input() not in ('c', 'C') + + +class DumbInstallProgress(object): + """Report the install progress. + + Subclass this class to implement install progress reporting. + """ + + def startUpdate(self): + """Start update.""" + + def run(self, pm): + """Start installation.""" + return pm.DoInstall() + + def finishUpdate(self): + """Called when update has finished.""" + + def updateInterface(self): + """Called periodically to update the user interface""" + + +class InstallProgress(DumbInstallProgress): + """An InstallProgress that is pretty useful. + + It supports the attributes 'percent' 'status' and callbacks for the dpkg + errors and conffiles and status changes. + """ + + def __init__(self): + DumbInstallProgress.__init__(self) + self.selectTimeout = 0.1 + (read, write) = os.pipe() + self.writefd = write + self.statusfd = os.fdopen(read, "r") + fcntl.fcntl(self.statusfd.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) + self.read = "" + self.percent = 0.0 + self.status = "" + + def error(self, pkg, errormsg): + """Called when a error is detected during the install.""" + + def conffile(self, current, new): + """Called when a conffile question from dpkg is detected.""" + + def statusChange(self, pkg, percent, status): + """Called when the status changed.""" + + def updateInterface(self): + """Called periodically to update the interface.""" + if self.statusfd is None: + return + try: + while not self.read.endswith("\n"): + self.read += os.read(self.statusfd.fileno(), 1) + except OSError, (errno_, errstr): + # resource temporarly unavailable is ignored + if errno_ != errno.EAGAIN and errno_ != errno.EWOULDBLOCK: + print errstr + if not self.read.endswith("\n"): + return + + s = self.read + #print s + try: + (status, pkg, percent, status_str) = s.split(":", 3) + except ValueError: + # silently ignore lines that can't be parsed + self.read = "" + return + #print "percent: %s %s" % (pkg, float(percent)/100.0) + if status == "pmerror": + self.error(pkg, status_str) + elif status == "pmconffile": + # we get a string like this: + # 'current-conffile' 'new-conffile' useredited distedited + match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status_str) + if match: + self.conffile(match.group(1), match.group(2)) + elif status == "pmstatus": + if float(percent) != self.percent or status_str != self.status: + self.statusChange(pkg, float(percent), + status_str.strip()) + self.percent = float(percent) + self.status = status_str.strip() + self.read = "" + + def fork(self): + """Fork.""" + return os.fork() + + def waitChild(self): + """Wait for child progress to exit.""" + while True: + select.select([self.statusfd], [], [], self.selectTimeout) + self.updateInterface() + (pid, res) = os.waitpid(self.child_pid, os.WNOHANG) + if pid == self.child_pid: + break + return res + + def run(self, pm): + """Start installing.""" + pid = self.fork() + if pid == 0: + # child + res = pm.DoInstall(self.writefd) + os._exit(res) + self.child_pid = pid + res = self.waitChild() + return os.WEXITSTATUS(res) + + +class CdromProgress(object): + """Report the cdrom add progress. + + Subclass this class to implement cdrom add progress reporting. + """ + + def __init__(self): + pass + + def update(self, text, step): + """Called periodically to update the user interface.""" + + def askCdromName(self): + """Called to ask for the name of the cdrom.""" + + def changeCdrom(self): + """Called to ask for the cdrom to be changed.""" + + +class DpkgInstallProgress(InstallProgress): + """Progress handler for a local Debian package installation.""" + + def run(self, debfile): + """Start installing the given Debian package.""" + self.debfile = debfile + self.debname = os.path.basename(debfile).split("_")[0] + pid = self.fork() + if pid == 0: + # child + res = os.system("/usr/bin/dpkg --status-fd %s -i %s" % \ + (self.writefd, self.debfile)) + os._exit(os.WEXITSTATUS(res)) + self.child_pid = pid + res = self.waitChild() + return res + + def updateInterface(self): + """Process status messages from dpkg.""" + if self.statusfd is None: + return + while True: + try: + self.read += os.read(self.statusfd.fileno(), 1) + except OSError, (errno_, errstr): + # resource temporarly unavailable is ignored + if errno_ != 11: + print errstr + break + if not self.read.endswith("\n"): + continue + + statusl = self.read.split(":") + if len(statusl) < 3: + print "got garbage from dpkg: '%s'" % self.read + self.read = "" + break + status = statusl[2].strip() + #print status + if status == "error": + self.error(self.debname, status) + elif status == "conffile-prompt": + # we get a string like this: + # 'current-conffile' 'new-conffile' useredited distedited + match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", statusl[3]) + if match: + self.conffile(match.group(1), match.group(2)) + else: + self.status = status + self.read = "" diff --git a/apt/progress/gtk2.py b/apt/progress/gtk2.py new file mode 100644 index 00000000..435265d4 --- /dev/null +++ b/apt/progress/gtk2.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python +# +# Copyright (c) 2004-2005 Canonical +# +# Authors: Michael Vogt +# Sebastian Heinlein +# Julian Andres Klode +# +# 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. +# +# his program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +"""GObject-powered progress classes and a GTK+ status widget.""" + +from gettext import gettext as _ +import os +import time + +import pygtk +pygtk.require('2.0') +import gtk +try: + import glib +except ImportError: + import gobject as glib +import gobject +import pango +import vte + +import apt +import apt_pkg + + +def mksig(params=(), run=gobject.SIGNAL_RUN_FIRST, rettype=gobject.TYPE_NONE): + """Simplified Create a gobject signal. + + This allows us to write signals easier, because we just need to define the + type of the parameters (in most cases). + + ``params`` is a tuple which defines the types of the arguments. + """ + return (run, rettype, params) + + +class GOpProgress(gobject.GObject, apt.progress.OpProgress): + """Operation progress with GObject signals. + + Signals: + + * status-changed(str: operation, int: percent) + * status-started() - Not Implemented yet + * status-finished() + + """ + + __gsignals__ = {"status-changed": mksig((str, int)), + "status-started": mksig(), + "status-finished": mksig()} + + def __init__(self): + apt.progress.OpProgress.__init__(self) + gobject.GObject.__init__(self) + self._context = glib.main_context_default() + + def update(self, percent): + """Called to update the percentage done""" + self.emit("status-changed", self.op, percent) + while self._context.pending(): + self._context.iteration() + + def done(self): + """Called when all operation have finished.""" + self.emit("status-finished") + + +class GInstallProgress(gobject.GObject, apt.progress.InstallProgress): + """Installation progress with GObject signals. + + Signals: + + * status-changed(str: status, int: percent) + * status-started() + * status-finished() + * status-timeout() + * status-error() + * status-conffile() + + """ + # Seconds until a maintainer script will be regarded as hanging + INSTALL_TIMEOUT = 5 * 60 + + __gsignals__ = {"status-changed": mksig((str, int)), + "status-started": mksig(), + "status-timeout": mksig(), + "status-error": mksig(), + "status-conffile": mksig(), + "status-finished": mksig()} + + def __init__(self, term): + apt.progress.InstallProgress.__init__(self) + gobject.GObject.__init__(self) + self.finished = False + self.time_last_update = time.time() + self.term = term + reaper = vte.reaper_get() + reaper.connect("child-exited", self.childExited) + self.env = ["VTE_PTY_KEEP_FD=%s" % self.writefd, + "DEBIAN_FRONTEND=gnome", + "APT_LISTCHANGES_FRONTEND=gtk"] + self._context = glib.main_context_default() + + def childExited(self, term, pid, status): + """Called when a child process exits""" + self.apt_status = os.WEXITSTATUS(status) + self.finished = True + + def error(self, pkg, errormsg): + """Called when an error happens. + + Emits: status-error() + """ + self.emit("status-error") + + def conffile(self, current, new): + """Called during conffile. + + Emits: status-conffile() + """ + self.emit("status-conffile") + + def startUpdate(self): + """Called when the update starts. + + Emits: status-started() + """ + self.emit("status-started") + + def finishUpdate(self): + """Called when the update finished. + + Emits: status-finished() + """ + self.emit("status-finished") + + def statusChange(self, pkg, percent, status): + """Called when the status changed. + + Emits: status-changed(status, percent) + """ + self.time_last_update = time.time() + self.emit("status-changed", status, percent) + + def updateInterface(self): + """Called periodically to update the interface. + + Emits: status-timeout() [When a timeout happens] + """ + apt.progress.InstallProgress.updateInterface(self) + while self._context.pending(): + self._context.iteration() + if self.time_last_update + self.INSTALL_TIMEOUT < time.time(): + self.emit("status-timeout") + + def fork(self): + """Fork the process.""" + return self.term.forkpty(envv=self.env) + + def waitChild(self): + """Wait for the child process to exit.""" + while not self.finished: + self.updateInterface() + return self.apt_status + + +class GDpkgInstallProgress(apt.progress.DpkgInstallProgress, GInstallProgress): + """An InstallProgress for local installations. + + Signals: + + * status-changed(str: status, int: percent) + * status-started() - Not Implemented yet + * status-finished() + * status-timeout() - When the maintainer script hangs + * status-error() - When an error happens + * status-conffile() - On Conffile + """ + + def run(self, debfile): + """Install the given package.""" + apt.progress.DpkgInstallProgress.run(self, debfile) + + def updateInterface(self): + """Called periodically to update the interface. + + Emits: status-timeout() [When a timeout happens]""" + apt.progress.DpkgInstallProgress.updateInterface(self) + if self.time_last_update + self.INSTALL_TIMEOUT < time.time(): + self.emit("status-timeout") + + +class GFetchProgress(gobject.GObject, apt.progress.FetchProgress): + """A Fetch Progress with GObject signals. + + Signals: + + * status-changed(str: description, int: percent) + * status-started() + * status-finished() + """ + + __gsignals__ = {"status-changed": mksig((str, int)), + "status-started": mksig(), + "status-finished": mksig()} + + def __init__(self): + apt.progress.FetchProgress.__init__(self) + gobject.GObject.__init__(self) + self._continue = True + self._context = glib.main_context_default() + + def start(self): + self.emit("status-started") + + def stop(self): + self.emit("status-finished") + + def cancel(self): + self._continue = False + + def pulse(self): + apt.progress.FetchProgress.pulse(self) + currentItem = self.currentItems + 1 + if currentItem > self.totalItems: + currentItem = self.totalItems + if self.currentCPS > 0: + text = (_("Downloading file %(current)li of %(total)li with " + "%(speed)s/s") % \ + {"current": currentItem, + "total": self.totalItems, + "speed": apt_pkg.SizeToStr(self.currentCPS)}) + else: + text = (_("Downloading file %(current)li of %(total)li") % \ + {"current": currentItem, + "total": self.totalItems}) + self.emit("status-changed", text, self.percent) + while self._context.pending(): + self._context.iteration() + return self._continue + + +class GtkAptProgress(gtk.VBox): + """Graphical progress for installation/fetch/operations. + + This widget provides a progress bar, a terminal and a status bar for + showing the progress of package manipulation tasks. + """ + + def __init__(self): + gtk.VBox.__init__(self) + self.set_spacing(6) + # Setup some child widgets + self._expander = gtk.Expander(_("Details")) + self._terminal = vte.Terminal() + #self._terminal.set_font_from_string("monospace 10") + self._expander.add(self._terminal) + self._progressbar = gtk.ProgressBar() + # Setup the always italic status label + self._label = gtk.Label() + attr_list = pango.AttrList() + attr_list.insert(pango.AttrStyle(pango.STYLE_ITALIC, 0, -1)) + self._label.set_attributes(attr_list) + self._label.set_ellipsize(pango.ELLIPSIZE_END) + self._label.set_alignment(0, 0) + # add child widgets + self.pack_start(self._progressbar, False) + self.pack_start(self._label, False) + self.pack_start(self._expander, False) + # Setup the internal progress handlers + self._progress_open = GOpProgress() + self._progress_open.connect("status-changed", self._on_status_changed) + self._progress_open.connect("status-started", self._on_status_started) + self._progress_open.connect("status-finished", + self._on_status_finished) + self._progress_fetch = GFetchProgress() + self._progress_fetch.connect("status-changed", self._on_status_changed) + self._progress_fetch.connect("status-started", self._on_status_started) + self._progress_fetch.connect("status-finished", + self._on_status_finished) + self._progress_install = GInstallProgress(self._terminal) + self._progress_install.connect("status-changed", + self._on_status_changed) + self._progress_install.connect("status-started", + self._on_status_started) + self._progress_install.connect("status-finished", + self._on_status_finished) + self._progress_install.connect("status-timeout", + self._on_status_timeout) + self._progress_install.connect("status-error", + self._on_status_timeout) + self._progress_install.connect("status-conffile", + self._on_status_timeout) + self._progress_dpkg_install = GDpkgInstallProgress(self._terminal) + self._progress_dpkg_install.connect("status-changed", + self._on_status_changed) + self._progress_dpkg_install.connect("status-started", + self._on_status_started) + self._progress_dpkg_install.connect("status-finished", + self._on_status_finished) + self._progress_dpkg_install.connect("status-timeout", + self._on_status_timeout) + self._progress_dpkg_install.connect("status-error", + self._on_status_timeout) + self._progress_dpkg_install.connect("status-conffile", + self._on_status_timeout) + + def clear(self): + """Reset all status information.""" + self._label.set_label("") + self._progressbar.set_fraction(0) + self._expander.set_expanded(False) + + @property + def open(self): + """Return the cache opening progress handler.""" + return self._progress_open + + @property + def install(self): + """Return the install progress handler.""" + return self._progress_install + + @property + def dpkg_install(self): + """Return the install progress handler for dpkg.""" + return self._dpkg_progress_install + + @property + def fetch(self): + """Return the fetch progress handler.""" + return self._progress_fetch + + def _on_status_started(self, progress): + """Called when something starts.""" + self._on_status_changed(progress, _("Starting..."), 0) + while gtk.events_pending(): + gtk.main_iteration() + + def _on_status_finished(self, progress): + """Called when something finished.""" + self._on_status_changed(progress, _("Complete"), 100) + while gtk.events_pending(): + gtk.main_iteration() + + def _on_status_changed(self, progress, status, percent): + """Called when the status changed.""" + self._label.set_text(status) + if percent is None: + self._progressbar.pulse() + else: + self._progressbar.set_fraction(percent/100.0) + while gtk.events_pending(): + gtk.main_iteration() + + def _on_status_timeout(self, progress): + """Called when timeout happens.""" + self._expander.set_expanded(True) + while gtk.events_pending(): + gtk.main_iteration() + + def cancel_download(self): + """Cancel a currently running download.""" + self._progress_fetch.cancel() + + def show_terminal(self, expanded=False): + """Show the expander for the terminal. + + Show an expander with a terminal widget which provides a way + to interact with dpkg + """ + self._expander.show() + self._terminal.show() + self._expander.set_expanded(expanded) + while gtk.events_pending(): + gtk.main_iteration() + + def hide_terminal(self): + """Hide the expander with the terminal widget.""" + self._expander.hide() + while gtk.events_pending(): + gtk.main_iteration() + + def show(self): + """Show the Box""" + gtk.HBox.show(self) + self._label.show() + self._progressbar.show() + while gtk.events_pending(): + gtk.main_iteration() + + +def _test(): + """Test function""" + import sys + + from apt.debfile import DebPackage + + win = gtk.Window() + apt_progress = GtkAptProgress() + win.set_title("GtkAptProgress Demo") + win.add(apt_progress) + apt_progress.show() + win.show() + cache = apt.cache.Cache(apt_progress.open) + pkg = cache["xterm"] + if pkg.isInstalled: + pkg.markDelete() + else: + pkg.markInstall() + apt_progress.show_terminal(True) + try: + cache.commit(apt_progress.fetch, apt_progress.install) + except Exception, exc: + print >> sys.stderr, "Exception happened:", exc + if len(sys.argv) > 1: + deb = DebPackage(sys.argv[1], cache) + deb.install(apt_progress.dpkg_install) + gtk.main() + + +if __name__ == "__main__": + _test() + +# vim: ts=4 et sts=4 diff --git a/debian/changelog b/debian/changelog index 1ed96cc3..ab188657 100644 --- a/debian/changelog +++ b/debian/changelog @@ -7,6 +7,9 @@ python-apt (0.7.9~exp3) experimental; urgency=low - Deprecate Package.candidate*() and Package.installed*(), except for installedFiles. - Provide Version.get_source() (LP: #118788) + * apt/progress/: New package, replaces apt.progress and apt.gtk + - apt/progress/gtk2.py: Moved here from apt/gtk/widgets.py + - apt/progress/__init__.py: Move here from apt/progress.py [ Michael Vogt ] * aptsources/distro.py: diff --git a/doc/examples/gui-inst.py b/doc/examples/gui-inst.py index 8138d922..68f06fc0 100755 --- a/doc/examples/gui-inst.py +++ b/doc/examples/gui-inst.py @@ -5,13 +5,13 @@ import pygtk pygtk.require('2.0') import gtk -import apt.gtk.widgets +import apt.progress.gtk2 if __name__ == "__main__": win = gtk.Window() - progress = apt.gtk.widgets.GtkAptProgress() + progress = apt.progress.gtk2.GtkAptProgress() win.set_title("GtkAptProgress Demo") win.add(progress) progress.show() diff --git a/doc/source/apt/gtk.widgets.rst b/doc/source/apt/gtk.widgets.rst deleted file mode 100644 index 9fa84ead..00000000 --- a/doc/source/apt/gtk.widgets.rst +++ /dev/null @@ -1,29 +0,0 @@ -:mod:`apt.gtk.widgets` --- GTK widgets -====================================== -.. automodule:: apt.gtk.widgets - - -GObject progress classes -------------------------- - -.. autoclass:: GDpkgInstallProgress - :members: - -.. autoclass:: GFetchProgress - :members: - -.. autoclass:: GInstallProgress - :members: - -.. autoclass:: GOpProgress - :members: - -GTK+ Class ----------- -.. autoclass:: GtkAptProgress - :members: - - -Example -------- -.. literalinclude:: ../examples/apt-gtk.py diff --git a/doc/source/apt/progress.gtk2.rst b/doc/source/apt/progress.gtk2.rst new file mode 100644 index 00000000..a83ab111 --- /dev/null +++ b/doc/source/apt/progress.gtk2.rst @@ -0,0 +1,29 @@ +:mod:`apt.progress.gtk2` --- GTK widgets +======================================== +.. automodule:: apt.progress.gtk2 + + +GObject progress classes +------------------------- + +.. autoclass:: GDpkgInstallProgress + :members: + +.. autoclass:: GFetchProgress + :members: + +.. autoclass:: GInstallProgress + :members: + +.. autoclass:: GOpProgress + :members: + +GTK+ Class +---------- +.. autoclass:: GtkAptProgress + :members: + + +Example +------- +.. literalinclude:: ../examples/apt-gtk.py diff --git a/doc/source/examples/apt-gtk.py b/doc/source/examples/apt-gtk.py index c3bb09d5..835ea4ee 100644 --- a/doc/source/examples/apt-gtk.py +++ b/doc/source/examples/apt-gtk.py @@ -4,14 +4,14 @@ import pygtk pygtk.require("2.0") import gtk -import apt.gtk.widgets +import apt.progress.gtk2 def main(): """Main function.""" win = gtk.Window() win.connect("destroy", gtk.main_quit) - progress = apt.gtk.widgets.GtkAptProgress() + progress = apt.progress.gtk2.GtkAptProgress() win.set_title("GtkAptProgress Demo") win.add(progress) progress.show() -- cgit v1.2.3