summaryrefslogtreecommitdiff
path: root/portato/gui/queue.py
diff options
context:
space:
mode:
Diffstat (limited to 'portato/gui/queue.py')
-rw-r--r--portato/gui/queue.py580
1 files changed, 580 insertions, 0 deletions
diff --git a/portato/gui/queue.py b/portato/gui/queue.py
new file mode 100644
index 0000000..cfd0581
--- /dev/null
+++ b/portato/gui/queue.py
@@ -0,0 +1,580 @@
+# -*- coding: utf-8 -*-
+#
+# File: portato/gui/queue.py
+# This file is part of the Portato-Project, a graphical portage-frontend.
+#
+# Copyright (C) 2008 René 'Necoro' Neumann
+# This is free software. You may redistribute copies of it under the terms of
+# the GNU General Public License version 2.
+# There is NO WARRANTY, to the extent permitted by law.
+#
+# Written by René 'Necoro' Neumann <necoro@necoro.net>
+
+from __future__ import absolute_import
+
+# some stuff needed
+import os, pty
+import signal, threading, time
+from subprocess import Popen
+
+# some backend things
+from .. import backend, plugin
+from ..backend import flags, system
+from ..helper import _, debug, info, send_signal_to_group, unique_array
+from ..waiting_queue import WaitingQueue
+from .updater import Updater
+
+# the wrapper
+from .gtk.wrapper import GtkConsole, GtkTree
+
+class EmergeQueue:
+ """This class manages the emerge queue."""
+
+ def __init__ (self, tree = None, console = None, db = None, title_update = None, threadClass = threading.Thread):
+ """Constructor.
+
+ @param tree: Tree to append all the items to.
+ @type tree: GtkTree
+ @param console: Output is shown here.
+ @type console: GtkConsole
+ @param db: A database instance.
+ @type db: Database
+ @param title_update: A function, which will be called whenever there is a title update.
+ @type title_update: function(string)"""
+
+ # the different queues
+ self.mergequeue = [] # for emerge
+ self.unmergequeue = [] # for emerge -C
+ self.oneshotmerge = [] # for emerge --oneshot
+
+ # the emerge process
+ self.process = None
+ self.threadQueue = WaitingQueue(threadClass = threadClass)
+ self.pty = None
+
+ # dictionaries with data about the packages in the queue
+ self.iters = {"install" : {}, "uninstall" : {}, "update" : {}} # iterator in the tree
+ self.deps = {"install" : {}, "update" : {}} # all the deps of the package
+
+ # member vars
+ self.tree = tree
+ if self.tree and not isinstance(self.tree, GtkTree): raise TypeError, "tree passed is not a GtkTree-object"
+
+ self.console = console
+ if self.console and not isinstance(self.console, GtkConsole): raise TypeError, "console passed is not a GtkConsole-object"
+
+ self.db = db
+ self.title_update = title_update
+ self.threadClass = threadClass
+
+ if self.console:
+ self.pty = pty.openpty()
+ self.console.set_pty(self.pty[0])
+
+ def _get_pkg_from_cpv (self, cpv, unmask = False):
+ """Gets a L{backend.Package}-object from a cpv.
+
+ @param cpv: the cpv to get the package for
+ @type cpv: string (cpv)
+ @param unmask: if True we will look for masked packages if we cannot find unmasked ones
+ @type unmask: boolean
+ @return: created package
+ @rtype: backend.Package
+
+ @raises backend.PackageNotFoundException: If no package could be found - normally it is existing but masked."""
+
+ # for the beginning: let us create a package object - but it is not guaranteed, that it actually exists in portage
+ pkg = system.new_package(cpv)
+ masked = not (pkg.is_masked() or pkg.is_testing(use_keywords=True)) # we are setting this to True in case we have unmasked it already, but portage does not know this
+
+ # and now try to find it in portage
+ pkg = system.find_packages("="+cpv, masked = masked)
+
+ if pkg: # gotcha
+ pkg = pkg[0]
+
+ elif unmask: # no pkg returned, but we are allowed to unmask it
+ pkg = system.find_packages("="+cpv, masked = True)
+
+ if not pkg:
+ raise backend.PackageNotFoundException(cpv) # also not found
+ else:
+ pkg = pkg[0]
+
+ if pkg.is_testing(use_keywords = True):
+ pkg.set_testing(True)
+ if pkg.is_masked():
+ pkg.set_masked()
+
+ else: # no pkg returned - and we are not allowed to unmask
+ raise backend.PackageNotFoundException(cpv)
+
+ return pkg
+
+ def update_tree (self, it, cpv, unmask = False, oneshot = False, type = "install"):
+ """This updates the tree recursivly, or? Isn't it? Bjorn!
+
+ @param it: iterator where to append
+ @type it: Iterator
+ @param cpv: The package to append.
+ @type cpv: string (cat/pkg-ver)
+ @param unmask: True if we are allowed to look for masked packages
+ @type unmask: boolean
+ @param oneshot: True if we want to emerge is oneshot
+ @type oneshot: boolean
+ @param type: the type of the updating
+ @type type: string
+
+ @raises backend.BlockedException: When occured during dependency-calculation.
+ @raises backend.PackageNotFoundException: If no package could be found - normally it is existing but masked."""
+
+ if cpv in self.deps[type]:
+ return # in list already and therefore it's already in the tree too
+
+ # try to find an already installed instance
+ update = False
+ downgrade = False
+ uVersion = None
+ changedUse = []
+ try:
+ pkg = self._get_pkg_from_cpv(cpv, unmask)
+ if not pkg.is_installed():
+ old = system.find_installed_packages(pkg.get_slot_cp())
+ if old:
+ old = old[0] # assume we have only one there
+ cmp = pkg.compare_version(old)
+ if cmp > 0:
+ update = True
+ elif cmp < 0:
+ downgrade = True
+
+ uVersion = old.get_version()
+
+ old_iuse = set(old.get_iuse_flags())
+ new_iuse = set(pkg.get_iuse_flags())
+
+ for i in old_iuse.difference(new_iuse):
+ changedUse.append("-"+i)
+
+ for i in new_iuse.difference(old_iuse):
+ changedUse.append("+"+i)
+ else:
+ old_iuse = set(pkg.get_iuse_flags(installed = True))
+ new_iuse = set(pkg.get_iuse_flags(installed = False))
+
+ for i in old_iuse.difference(new_iuse):
+ changedUse.append("-"+i)
+
+ for i in new_iuse.difference(old_iuse):
+ changedUse.append("+"+i)
+
+ except backend.PackageNotFoundException, e: # package not found / package is masked -> delete current tree and re-raise the exception
+ if self.tree.iter_has_parent(it):
+ while self.tree.iter_has_parent(it):
+ it = self.tree.parent_iter(it)
+ self.remove_with_children(it, removeNewFlags = False)
+ raise
+
+ # add iter
+ subIt = self.tree.append(it, self.tree.build_append_value(cpv, oneshot = oneshot, update = update, downgrade = downgrade, version = uVersion, useChange = changedUse))
+ self.iters[type].update({cpv: subIt})
+
+ # get dependencies
+ deps = pkg.get_dep_packages() # this might raise a BlockedException
+ self.deps[type].update({cpv : deps})
+
+ # recursive call
+ for d in deps:
+ try:
+ self.update_tree(subIt, d, unmask, type = type)
+ except backend.BlockedException, e: # BlockedException occured -> delete current tree and re-raise exception
+ debug("Something blocked: %s", e[0])
+ self.remove_with_children(subIt)
+ raise
+
+ def append (self, cpv, type = "install", update = False, forceUpdate = False, unmask = False, oneshot = False):
+ """Appends a cpv either to the merge queue or to the unmerge-queue.
+ Also updates the tree-view.
+
+ @param cpv: Package to add
+ @type cpv: string (cat/pkg-ver)
+ @param type: The type of this append process. Possible values are "install", "uninstall", "update".
+ @type type: string
+ @param update: Set to True if a package is going to be updated (e.g. if the use-flags changed).
+ @type update: boolean
+ @param forceUpdate: Set to True if the update should be forced.
+ @type forceUpdate: boolean
+ @param unmask: True if we are allowed to look for masked packages
+ @type unmask: boolean
+ @param oneshot: True if this package should not be added to the world-file.
+ @type oneshot: boolean
+
+ @raises portato.backend.PackageNotFoundException: if trying to add a package which does not exist"""
+
+ if type in ("install", "update"): # emerge
+ # insert dependencies
+ pkg = self._get_pkg_from_cpv(cpv, unmask)
+ deps = pkg.get_dep_packages()
+
+ if update:
+ if not forceUpdate and cpv in self.deps[type] and deps == self.deps[type][cpv]:
+ return # nothing changed - return
+ else:
+ hasBeenInQueue = (cpv in self.mergequeue or cpv in self.oneshotmerge)
+ parentIt = self.tree.parent_iter(self.iters[type][cpv])
+
+ # delete it out of the tree - but NOT the changed flags
+ self.remove_with_children(self.iters[type][cpv], removeNewFlags = False)
+
+ if hasBeenInQueue: # package has been in queue before
+ self._queue_append(cpv, oneshot)
+
+ self.update_tree(parentIt, cpv, unmask, oneshot = oneshot, type = type)
+ else: # not update
+ if type == "install":
+ self._queue_append(cpv, oneshot)
+ if self.tree:
+ self.update_tree(self.tree.get_emerge_it(), cpv, unmask, type = type, oneshot = oneshot)
+ elif type == "update" and self.tree:
+ self.update_tree(self.tree.get_update_it(), cpv, unmask, type = type, oneshot = oneshot)
+
+ else: # unmerge
+ self.unmergequeue.append(cpv)
+ if self.tree: # update tree
+ self.iters["uninstall"].update({cpv: self.tree.append(self.tree.get_unmerge_it(), self.tree.build_append_value(cpv))})
+
+ def _queue_append (self, cpv, oneshot = False):
+ """Convenience function appending a cpv either to self.mergequeue or to self.oneshotmerge.
+
+ @param cpv: cpv to add
+ @type cpv: string (cpv)
+ @param oneshot: True if this package should not be added to the world-file.
+ @type oneshot: boolean"""
+
+ if not oneshot:
+ if cpv not in self.mergequeue:
+ self.mergequeue.append(cpv)
+ else:
+ if cpv not in self.oneshotmerge:
+ self.oneshotmerge.append(cpv)
+
+ def doEmerge (self, options, packages, it, *args, **kwargs):
+ top = None
+ if self.tree and it:
+ for v in it.itervalues():
+ self.tree.set_in_progress(v)
+ top = self.tree.first_iter(v)
+ break
+
+ self.threadQueue.put(self.__emerge, options, packages, it, top, *args, **kwargs)
+
+ def __emerge (self, options, packages, it, top, command = None):
+ """Calls emerge and updates the terminal.
+
+ @param options: options to send to emerge
+ @type options: string[]
+ @param packages: packages to emerge
+ @type packages: string[]
+ @param it: Iterators which point to these entries whose children will be removed after completion.
+ @type it: dict(string -> Iterator)
+ @param top: The top iterator
+ @type top: Iterator
+ @param command: the command to execute - default is "/usr/bin/python /usr/bin/emerge"
+ @type command: string[]"""
+
+ @plugin.hook("emerge", packages = packages, command = command, console = self.console, title_update = self.title_update)
+ def sub_emerge(command):
+ if command is None:
+ command = system.get_merge_command()
+
+ # open tty
+ if self.console:
+ self.console.reset()
+
+ def pre ():
+ os.setsid() # new session
+ if self.console:
+ import fcntl, termios
+ fcntl.ioctl(self.pty[1], termios.TIOCSCTTY, 0) # set pty-slave as session tty
+ os.dup2(self.pty[1], 0)
+ os.dup2(self.pty[1], 1)
+ os.dup2(self.pty[1], 2)
+
+ # start emerge
+ self.process = Popen(command+options+packages, shell = False, env = system.get_environment(), preexec_fn = pre)
+
+ # remove packages from queue
+ if self.tree and it and not self.tree.is_in_unmerge(top):
+ self.up = Updater(self, it, self.threadClass)
+ else:
+ self.up = None
+
+ # update title
+ if self.console:
+ old_title = self.console.get_window_title()
+ while self.process and self.process.poll() is None:
+ if self.title_update :
+ title = self.console.get_window_title()
+ if title != old_title:
+ self.title_update(title)
+ old_title = title
+ time.sleep(0.5)
+
+ if self.up:
+ self.up.stop()
+ if it:
+ self.tree.set_in_progress(top, False)
+ else:
+ self.remove(top)
+ elif self.tree and it:
+ self.remove_with_children(top)
+
+ if self.title_update: self.title_update(None)
+
+ if self.process is None: # someone resetted this
+ self.threadQueue.next()
+ return
+ else:
+ ret = self.process.returncode
+ self.process = None
+ self.threadQueue.next()
+
+ @plugin.hook("after_emerge", packages = packages, retcode = ret)
+ def update_packages():
+ if self.db:
+ for cat in unique_array([system.split_cpv(p)[0] for p in packages if p not in ["world", "system"]]):
+ self.db.reload(cat)
+ debug("Category %s refreshed", cat)
+
+ update_packages()
+
+ sub_emerge(command)
+
+ def emerge (self, force = False, options = None):
+ """Emerges everything in the merge-queue.
+
+ @param force: If False, '-pv' is send to emerge. Default: False.
+ @type force: boolean
+ @param options: Additional options to send to the emerge command
+ @type options: string[]"""
+
+ def prepare(queue):
+ """Prepares the list of iterators and the list of packages."""
+ list = []
+ its = {}
+ for k in queue:
+ list += ["="+k]
+ if self.tree:
+ its.update({k : self.iters["install"][k]})
+
+ return list, its
+
+ if self.tree:
+ ownit = self.iters["install"]
+ else:
+ ownit = {}
+
+ # oneshot-queue
+ if self.oneshotmerge:
+ # prepare package-list for oneshot
+ list, its = prepare(self.oneshotmerge)
+ if not self.mergequeue :# the other one does not exist - remove completely
+ its = ownit
+
+ s = system.get_oneshot_option()
+ if not force: s += system.get_pretend_option()
+ if options is not None: s += options
+
+ self.doEmerge(s, list, its, caller = self.emerge)
+
+ # normal queue
+ if self.mergequeue:
+ # prepare package-list
+ list, its = prepare(self.mergequeue)
+ if not self.oneshotmerge: # the other one does not exist - remove completely
+ its = ownit
+
+ s = []
+ if not force: s = system.get_pretend_option()
+ if options is not None: s += options
+
+ self.doEmerge(s, list, its, caller = self.emerge)
+
+ def unmerge (self, force = False, options = None):
+ """Unmerges everything in the umerge-queue.
+
+ @param force: If False, '-pv' is send to emerge. Default: False.
+ @type force: boolean
+ @param options: Additional options to send to the emerge command
+ @type options: string[]"""
+
+ if len(self.unmergequeue) == 0: return # nothing in queue
+
+ list = self.unmergequeue[:] # copy the unmerge-queue
+
+ # set options
+ s = system.get_unmerge_option()
+ if not force: s += system.get_pretend_option()
+ if options is not None: s += options
+
+ if self.tree:
+ it = self.iters["uninstall"]
+ else:
+ it = {}
+
+ self.doEmerge(s,list, it, caller = self.unmerge)
+
+ def update_world(self, force = False, newuse = False, deep = False, options = None):
+ """Does an update world. newuse and deep are the arguments handed to emerge.
+
+ @param force: If False, '-pv' is send to emerge. Default: False.
+ @type force: boolean
+ @param newuse: If True, append newuse options
+ @type newuse: boolean
+ @param deep: If True, append deep options
+ @type deep: boolean
+ @param options: Additional options to send to the emerge command
+ @type options: string[]"""
+
+ opts = system.get_update_option()
+
+ if newuse: opts += system.get_newuse_option()
+ if deep: opts += system.get_deep_option()
+ if not force: opts += system.get_pretend_option()
+ if options is not None: opts += options
+
+ if self.tree:
+ it = self.iters["update"]
+ else:
+ it = {}
+
+ self.doEmerge(opts, ["world"], it, caller = self.update_world)
+
+ def sync (self, command = None):
+ """Calls "emerge --sync".
+
+ @param command: command to execute to sync. If None "emerge --sync" is taken.
+ @type command: string[]"""
+
+ if command is None:
+ command = system.get_sync_command()
+
+ try:
+ while True:
+ idx = command.index("&&")
+ self.doEmerge([],[],[], command[:idx], caller = self.sync)
+ command = command[idx+1:]
+ except ValueError: # no && in command
+ self.doEmerge([],[],[], command, caller = self.sync)
+
+ def kill_emerge (self):
+ """Kills the emerge process."""
+ if self.process is not None:
+ self.threadQueue.clear() # remove all pending emerge threads
+ try:
+ pgid = os.getpgid(self.process.pid)
+ os.killpg(pgid, signal.SIGTERM)
+ debug("Process should be terminated")
+ if self.process.poll() is None:
+ os.killpg(pgid, signal.SIGKILL)
+ debug("Process should be killed")
+ except AttributeError:
+ debug("AttributeError occured ==> process not exisiting - ignore")
+ except OSError:
+ debug("OSError occured ==> process already stopped - ignore")
+
+ self.process = None
+
+ def stop_emerge (self):
+ if self.process is not None:
+ os.killpg(os.getpgid(self.process.pid), signal.SIGSTOP)
+ debug("Process should be stopped")
+
+ def continue_emerge (self):
+ if self.process is not None:
+ os.killpg(os.getpgid(self.process.pid), signal.SIGCONT)
+ debug("Process should continue")
+
+ def remove_with_children (self, it, removeNewFlags = True):
+ """Convenience function which removes all children of an iterator and than the iterator itself.
+
+ @param it: The iter which to remove.
+ @type it: Iterator
+ @param removeNewFlags: True if new flags should be removed; False otherwise. Default: True.
+ @type removeNewFlags: boolean"""
+
+ self.remove_children(it, removeNewFlags)
+ self.remove(it, removeNewFlags)
+
+ def remove_children (self, parentIt, removeNewFlags = True):
+ """Removes all children of a given parent TreeIter recursivly.
+
+ @param parentIt: The iter from which to remove all children.
+ @type parentIt: Iterator
+ @param removeNewFlags: True if new flags should be removed; False otherwise. Default: True.
+ @type removeNewFlags: boolean"""
+
+ childIt = self.tree.first_child_iter(parentIt)
+
+ while childIt:
+ if (self.tree.iter_has_children(childIt)): # recursive call
+ self.remove_children(childIt, removeNewFlags)
+ temp = childIt
+ childIt = self.tree.next_iter(childIt)
+ self.remove(temp, removeNewFlags)
+
+ def remove (self, it, removeNewFlags = True):
+ """Removes a specific item in the tree. This does not remove the top-entries.
+
+ @param it: Iterator which points to the entry we are going to remove.
+ @type it: Iterator
+ @param removeNewFlags: True if new flags should be removed; False otherwise. Default: True.
+ @type removeNewFlags: boolean"""
+
+ if self.tree.iter_has_parent(it):
+ cpv = self.tree.get_value(it, self.tree.get_cpv_column())
+ if self.tree.is_in_emerge(it): # Emerge
+ del self.iters["install"][cpv]
+ try:
+ del self.deps["install"][cpv]
+ except KeyError: # this seems to be removed due to a BlockedException - so no deps here atm ;)
+ debug("Catched KeyError => %s seems not to be in self.deps. Should be no harm in normal cases.", cpv)
+ try:
+ self.mergequeue.remove(cpv)
+ except ValueError: # this is a dependency - ignore
+ try:
+ self.oneshotmerge.remove(cpv)
+ except ValueError:
+ debug("Catched ValueError => %s seems not to be in merge-queue. Should be no harm.", cpv)
+
+ if removeNewFlags: # remove the changed flags
+ flags.remove_new_use_flags(cpv)
+ flags.remove_new_masked(cpv)
+ flags.remove_new_testing(cpv)
+
+ elif self.tree.is_in_unmerge(it): # in Unmerge
+ del self.iters["uninstall"][cpv]
+ self.unmergequeue.remove(cpv)
+
+ elif self.tree.is_in_update(it):
+ del self.iters["update"][cpv]
+ try:
+ del self.deps["update"][cpv]
+ except KeyError: # this seems to be removed due to a BlockedException - so no deps here atm ;)
+ debug("Catched KeyError => %s seems not to be in self.deps. Should be no harm in normal cases.", cpv)
+
+ if removeNewFlags: # remove the changed flags
+ flags.remove_new_use_flags(cpv)
+ flags.remove_new_masked(cpv)
+ flags.remove_new_testing(cpv)
+
+ self.tree.remove(it)
+
+ def is_empty (self):
+ """Checks whether the current queue is empty and not working. Therefore it looks, whether the queues are empty,
+ and the process is not running.
+
+ @returns: True if everything is empty and the process is not running.
+ @rtype: bool"""
+
+ return not (self.process or any(map(len, self.iters.itervalues())))