#!/usr/bin/python -tt # # Copyright 2004-2007 Red Hat, Inc. # # Jeremy Katz # Paul Nasrat # Luke Macken # # 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; version 2 only # # 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 Library 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. import os # # Python gettext # import gettext import string import subprocess import webbrowser from optparse import OptionParser import gtk import gtk.glade import gtk.gdk as gdk import gobject import pango import yum.Errors from yum.constants import * import rpmUtils.miscutils from yum.update_md import UpdateMetadata, UpdateNoticeException from rhpl.exception import installExceptionHandler from rhpl.translate import _, N_, textdomain, utf8 from pirut import * from pirut.constants import * from pirut.Errors import * # # Python gettext # t = gettext.translation(I18N_DOMAIN, "/usr/share/locale", fallback = True) # _ = t.lgettext class PackageUpdater(GraphicalYumBase): def __init__(self, interactive = True, config = None): self.interactive = interactive if os.path.exists("data/pup.glade"): fn = "data/pup.glade" else: fn = "/usr/share/pirut/ui/pup.glade" self.pupxml = gtk.glade.XML(fn, domain="pirut") self.mainwin = self.pupxml.get_widget("pupWindow") self.mainwin.set_icon_from_file(PIRUTPIX + "pup.png") # self.mainwin.set_icon_name("system-software-update") self.vpaned = self.pupxml.get_widget("vpaned1") self.details = self.pupxml.get_widget("updateDetails") self.expander = self.pupxml.get_widget("detailsExpander") self.scratchBuffer = gtk.TextBuffer() self.updateMetadata = UpdateMetadata() self._connectSignals() self._createUpdateStore() self.mainwin.connect("delete_event", self.quit) self.pupMenu = self.pupxml.get_widget("pupMenu") self.registered = False # note that nothing which takes "time" should be called here! GraphicalYumBase.__init__(self, False, config) def _connectSignals(self): sigs = {"on_quitButton_clicked": self.quit, "on_pupWindow_delete": self.quit, "on_applyButton_clicked": self._apply, "on_updateList_button_press": self._updateButtonPress, "on_updateList_popup_menu": self._updatePopup, "on_updateNotebook_scroll_event": self._notebookScroll, "on_refreshButton_clicked": self.doRefresh, "on_pupMenu_select": self._selectPackages, "on_pupMenu_unselect": self._unselectPackages } self.pupxml.signal_autoconnect(sigs) self.details.set_buffer(gtk.TextBuffer()) self.details.connect("event-after", UpdateDetails.event_after) # FIXME: figure out why this event only gets called when your cursor # enters and leaves the TextView (making it impossible to change the # cursor when hovering over a link) #self.details.connect("motion-notify-event", # UpdateDetails.motion_notify_event) def _createUpdateStore(self): # checkbox, display string, list of # (updateFunc, printFunc, new, old, notice) tuples self.store = gtk.TreeStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT) tree = self.pupxml.get_widget("updateList") tree.set_model(self.store) column = gtk.TreeViewColumn(None, None) column.set_clickable(True) column.set_spacing(6) pixr = gtk.CellRendererPixbuf() pixr.set_property('stock-size', 1) column.pack_start(pixr, False) column.add_attribute(pixr, 'stock-id', 3) cbr = gtk.CellRendererToggle() cbr.connect ("toggled", self._toggledUpdate) column.pack_start(cbr, False) column.add_attribute(cbr, 'active', 0) tree.append_column(column) renderer = gtk.CellRendererText() column = gtk.TreeViewColumn('Text', renderer, text=1) tree.append_column(column) tree.columns_autosize() tree.connect ("row-activated", self._rowToggled) self.store.set_sort_column_id(1, gtk.SORT_ASCENDING) self.details.set_buffer(self.scratchBuffer) selection = tree.get_selection() selection.connect("changed", self._updateSelected) selection.set_mode(gtk.SELECTION_MULTIPLE) tree.set_search_equal_func(self.__search_pkgs) def __search_pkgs(self, model, col, key, i): lst = model.get_value(i, 2) if len(lst) < 1: return True (updf, strf, new, old) = lst[0] val = new.returnSimple('sourcerpm') if val.lower().startswith(key.lower()): return False return True def _rowToggled(self, tree, path, col): self._toggledUpdate(None, path[0]) def _toggledUpdate(self, data, row): i = self.store.get_iter((int(row),)) val = self.store.get_value(i, 0) self.store.set_value(i, 0, not val) def _updateSelected(self, selection): if selection.count_selected_rows() != 1: self.details.get_buffer().set_text("") return (model, paths) = selection.get_selected_rows() i = model.get_iter(paths[0]) lst = model.get_value(i, 2) notice = model.get_value(i, 4) if notice: self.details.set_buffer(notice) return # FIXME: if we have other indicators, this won't work anymore :-P needsReboot = False if model.get_value(i, 3) is not None: needsReboot = True strs = [] for (updfunc, strfunc, new, old) in lst: md = self.updateMetadata.get_notice((new.name,new.ver,new.rel)) if md: # use the update metadata details = UpdateDetails(md.get_metadata(), needsReboot) self.details.set_buffer(details) model.set_value(i, 4, details) return else: # use the predefined strfunc strs.append(strfunc(new, old)) self.scratchBuffer.set_text(string.join(strs, '\n')) if needsReboot: tag = self.scratchBuffer.create_tag(weight=pango.WEIGHT_BOLD) theiter = self.scratchBuffer.get_end_iter() self.scratchBuffer.insert(theiter, "\n\n") self.scratchBuffer.insert_with_tags(theiter, _("This update will require a reboot."), tag) self.details.set_buffer(self.scratchBuffer) def _changeSelected(self, state): tree = self.pupxml.get_widget("updateList") sel = tree.get_selection() if sel.count_selected_rows() < 0: return (model, paths) = sel.get_selected_rows() for p in paths: i = model.get_iter(p) model.set_value(i, 0, state) def _selectPackages(self, *args): self._changeSelected(True) def _unselectPackages(self, *args): self._changeSelected(False) def __doUpdatePopup(self, button, time): menu = self.pupMenu menu.popup(None, None, None, button, time) menu.show_all() def _updateButtonPress(self, widget, event): if event.button == 3: x = int(event.x) y = int(event.y) pthinfo = widget.get_path_at_pos(x, y) if pthinfo is not None: sel = widget.get_selection() if sel.count_selected_rows() == 1: path, col, cellx, celly = pthinfo widget.grab_focus() widget.set_cursor(path, col, 0) self.__doUpdatePopup(event.button, event.time) return 1 def _updatePopup(self, widget): sel = widget.get_selection() if sel.count_selected_rows() > 0: self.__doUpdatePopup(0, 0) # block mouse scroll scrolling tabs def _notebookScroll(self, *args): pass def _runGtkmain(self, *args): while gtk.events_pending(): gtk.main_iteration() def _busyCursor(self): self.mainwin.window.set_cursor(gdk.Cursor(gdk.WATCH)) self.mainwin.set_sensitive(False) self._runGtkmain() def _normalCursor(self): self.mainwin.window.set_cursor(None) self.mainwin.set_sensitive(True) self._runGtkmain() def doRefresh(self, *args): self.mainwin.show() pbar = self.doRefreshRepos(destroy=False) # FIXME: this should call real repo config code that let's you # generically add repos and would pluggably call rhn_register. # but, for now, we just need something that works if not self.registered and len(self.repos.listEnabled()) == 0: self.registered = True if os.path.exists("/etc/sysconfig/rhn") and os.path.exists("/usr/sbin/rhn_register"): def checkRegister(p): # when rhn_register exits, refresh us if p.poll() is not None: gobject.idle_add(self.doRefresh) return False return True pbar.destroy() self._normalCursor() self.mainwin.hide() self._runGtkmain() p = subprocess.Popen(["/usr/sbin/rhn_register"], close_fds = True) gobject.timeout_add(2 * 1000, checkRegister, p) self.doUpdateSetup() pbar.next_task() self.populateUpdates() pbar.next_task() self._normalCursor() pbar.destroy() def _doUpdate(self, new, old): self.tsInfo.addUpdate(new, old) def _doObsolete(self, new, old): self.tsInfo.addObsoleting(new, old) self.tsInfo.addObsoleted(old, new) def _printUpdate(self, new, old): return _("%s updates %s") %(new, old) def _printObsolete(self, new, old): return _("%s obsoletes %s") %(new, old) def populateUpdates(self): self.store.clear() upds = {} reboots = {} repos = [] # handle obsoletes opt = self.conf.obsoletes if opt: obsoletes = self.up.getObsoletesTuples(newest=1) else: obsoletes = [] for (obs, inst) in obsoletes: obsoleting = self.getPackageObject(obs) installed = self.rpmdb.searchPkgTuple(inst)[0] srpm = obsoleting.returnSimple("sourcerpm") if upds.has_key(srpm): upds[srpm].append( (self._doObsolete, self._printObsolete, obsoleting, installed) ) else: upds[srpm] = [ (self._doObsolete, self._printObsolete, obsoleting, installed) ] reboots[srpm] = False if obsoleting.returnSimple("name") in rebootpkgs: reboots[srpm] = True # and updates updates = self.up.getUpdatesTuples() for (new, old) in updates: updating = self.getPackageObject(new) updated = self.rpmdb.searchPkgTuple(old)[0] # populate update metadata if not updating.repoid in repos: repo = self.repos.getRepo(updating.repoid) try: # attempt to grab the updateinfo.xml.gz from the repodata self.updateMetadata.add(repo) except yum.Errors.RepoMDError: pass # No metadata found for this repo except UpdateNoticeException: pass # Metadata parsing error repos.append(updating.repoid) srpm = updating.returnSimple("sourcerpm") if upds.has_key(srpm): upds[srpm].append( (self._doUpdate, self._printUpdate, updating, updated) ) else: upds[srpm] = [ (self._doUpdate, self._printUpdate, updating, updated) ] reboots[srpm] = False if updating.returnSimple("name") in rebootpkgs: reboots[srpm] = True for (srpm, lst) in upds.items(): if reboots[srpm]: pix = 'gtk-refresh' else: pix = None self.store.append(None, [True, _("Updated %s packages available") % (rpmUtils.miscutils.splitFilename(srpm)[0],), lst, pix, None]) if len(upds) == 0: self.pupxml.get_widget("updateNotebook").set_current_page(1) self.pupxml.get_widget("applyButton").set_sensitive(False) def _apply(self, *args): needReboot = False map(lambda x: self.tsInfo.remove(x.pkgtup), self.tsInfo) self.tsInfo.makelists() del self.ts self.initActionTs() # select packages that are chosen for row in self.store: (on, pkgstr, lst, pix, bar) = row if on: for (updfunc, strfunc, new, old) in lst: if new.name in rebootpkgs: needReboot = True updfunc(new, old) if len(self.tsInfo) <= 0: d = gtk.MessageDialog(self.mainwin, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("No packages were selected for upgrade.")) d.show_all() d.run() d.destroy() return try: output = self.applyChanges(self.mainwin) except PirutError: return # this is a little tricky and bears some explanation # 1) if there's no warning output and we don't need to reboot, show # a simple "success!" dialog. # 2) if there's no warning output and we need to reboot, only show # the "reboot recommended" dialog # 3) if there's warning output _and_ we need to reboot, we need # to show as two dialogs to avoid confusion d = PirutDetailsDialog(self.mainwin, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE, _("Software update successfully completed.")) if output: d.format_secondary_text("Some warnings were seen during update.") d.set_details(buffer = outputDictAsTextBuffer(output)) if not needReboot or output: d.set_buttons(gtk.BUTTONS_OK) rc = d.run() d.destroy() if needReboot: d = PirutDetailsDialog(self.mainwin, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE, _("Reboot recommended")) d.format_secondary_text(_("Due to the updates installed, it is recommended that you reboot your system. You can either reboot now or choose to do so at a later time.")) d.set_buttons([(_("Reboot _later"), gtk.RESPONSE_CANCEL), (_("_Reboot now"), gtk.RESPONSE_DELETE_EVENT)]) d.set_default_response(gtk.RESPONSE_CANCEL) rc = d.run() if rc == gtk.RESPONSE_DELETE_EVENT: d.hide() subprocess.call(["/sbin/shutdown", "-r", "now"]) self.quit() d.destroy() # and we're done self.quit() def run(self): self.mainwin.show() self._runGtkmain() self.doRefresh() if not self.interactive: gobject.idle_add(self._apply) gtk.main() hovering_over_link = False hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM) class UpdateDetails(gtk.TextBuffer): def __init__(self, metadata, needsReboot = False): gtk.TextBuffer.__init__(self) self.md = metadata self.needsReboot = needsReboot self.iter = self.get_start_iter() self._build_tags() self._parse_references() self._populate_details() def _build_tags(self): self.bold_tag = self.create_tag(weight=pango.WEIGHT_BOLD) self.title_tag = self.create_tag(font='DejaVu LGC Sans Mono Bold') self.title_tag.set_property('foreground', 'white') self.title_tag.set_property('background-gdk', gtk.gdk.color_parse('#CCCCCC')) def _parse_references(self): self.cves = [] self.bzs = [] for ref in self.md['references']: type = ref['type'] if type == 'cve': self.cves.append((ref['id'], ref['href'])) elif type == 'bugzilla': self.bzs.append((ref['id'], ref['href'])) def _populate_details(self): titlecol_width = 12 margin = ''.zfill(titlecol_width + 1).replace('0', ' ') def _add_item(title, field=None): title = title.zfill(titlecol_width).replace('0', ' ') self.insert_with_tags(self.iter, '%s ' % title, self.title_tag) if field: self.insert_with_tags(self.iter, ' %s\n' % self.md[field]) _add_item('ID', 'update_id') _add_item('Type', 'type') _add_item('Status', 'status') _add_item('Issued', 'issued') # Append the references for title, list, lmt in (('Bugs', self.bzs, 6), ('CVEs', self.cves, 4)): if len(list) == 0: continue title = title.zfill(titlecol_width).replace('0', ' ') self.insert_with_tags(self.iter, '%s ' % title, self.title_tag) i = 0 for id, url in list: self.insert(self.iter, ' %s' % id) # Disable linking bugs and CVEs until we figure out a way to # NOT launch firefox as root (Bug #216552) #self._insert_link(id, url) #self.insert(self.iter, ' ') i += 1 if i % lmt == 0: # allow lmt references per line self.insert(self.iter, '\n') self.insert_with_tags(self.iter, margin, self.title_tag) self.insert(self.iter, '\n') if self.md['description']: desc = 'Description'.zfill(titlecol_width).replace('0', ' ') self.insert_with_tags(self.iter, desc + ' ', self.title_tag) lines = self.md['description'].split('\n') self.insert(self.iter, ' %s' % lines[0]) for line in lines[1:]: self.insert(self.iter, '\n') self.insert_with_tags(self.iter, margin, self.title_tag) self.insert(self.iter, ' ' + line) if self.needsReboot: self.insert(self.iter, "\n") self.insert_with_tags(self.iter, _("This update will require a reboot."), self.bold_tag) def _insert_link(self, text, url): tag = self.create_tag(underline=pango.UNDERLINE_SINGLE, foreground='blue') tag.set_data('page', url) self.insert(self.iter, ' ') self.insert_with_tags(self.iter, text, tag) @staticmethod def event_after(view, event): """ Callback to monitor mouse clicks and handle links. """ if event.type != gtk.gdk.BUTTON_RELEASE or event.button != 1: return False # don't follow a link if the user has selected something bounds = view.get_buffer().get_selection_bounds() if len(bounds) != 0: return False x, y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y)) iter = view.get_iter_at_location(x, y) for tag in iter.get_tags(): page = tag.get_data('page') if page: webbrowser.open(page, new=True) ## FIXME: figure out why this method is only getting called when the ## cursor enters or leaves the TextView. @staticmethod def motion_notify_event(view, event): """ Callback to monitor mouse motion and change the cursor if it is hovering over a link. """ print "motion_notify_event" global hovering_over_link hovering = False x, y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, int(event.x), int(event.y)) iter = view.get_iter_at_location(x, y) for tag in iter.get_tags(): page = tag.get_data('page') if page: print "Hovering == True" hovering = True break if hovering != hovering_over_link: print "Changing cursor" hovering_over_link = hovering win = view.get_window(gtk.TEXT_WINDOW_WIDGET) win.set_cursor(hovering_over_link and hand_cursor or regular_cursor) def main(): textdomain("pirut") parser = OptionParser() parser.add_option("-a", "--apply", action="store_true", dest="autoapply", help="Automatically apply updates") parser.add_option("-c", "--config", type="string", dest="config", default="/etc/yum.conf", help="Config file to use (default: /etc/yum.conf)") (options,args) = parser.parse_args() gtk.glade.bindtextdomain("pirut", "/usr/share/locale") try: # right now, we have to run privileged... if os.getuid() != 0: raise PirutError(_("Must be run as root.")) pup = PackageUpdater(not options.autoapply, options.config) except PirutError, e: print e startupError(e) pup.run() if __name__ == "__main__": installExceptionHandler("pirut", "") main()