From d632109cd5964f7d4baa408d517e44f801e1be8d Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 15 Nov 2005 14:18:07 +0100 Subject: * initial revision (after accidently killing it) --- src/Makefile.am | 10 + src/aptsources.py.in | 442 ++++++++++++++++++++ src/dialog_add.py.in | 107 +++++ src/dialog_apt_key.py.in | 163 ++++++++ src/dialog_edit.py.in | 102 +++++ src/dialog_settings.py.in | 144 +++++++ src/gnome-software-properties.in | 332 +++++++++++++++ src/update-manager.in | 874 +++++++++++++++++++++++++++++++++++++++ src/utils.py | 12 + 9 files changed, 2186 insertions(+) create mode 100644 src/Makefile.am create mode 100644 src/aptsources.py.in create mode 100644 src/dialog_add.py.in create mode 100644 src/dialog_apt_key.py.in create mode 100644 src/dialog_edit.py.in create mode 100644 src/dialog_settings.py.in create mode 100755 src/gnome-software-properties.in create mode 100644 src/update-manager.in create mode 100644 src/utils.py (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 00000000..62ea2e68 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,10 @@ +exedir = $(prefix)/bin + +exe_SCRIPTS = update-manager + +properties_modules_DATA = utils.py +properties_modulesdir = $(datadir)/update-manager/python + + +EXTRA_DIST = update-manager.in \ + utils.py diff --git a/src/aptsources.py.in b/src/aptsources.py.in new file mode 100644 index 00000000..7badd141 --- /dev/null +++ b/src/aptsources.py.in @@ -0,0 +1,442 @@ +# aptsource.py.in - parse sources.list +# +# Copyright (c) 2004 Canonical +# 2004 Michiel Sikkes +# +# Author: Michiel Sikkes +# 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 + +import string +import gettext +import re + +# actual source.list entries +class SourceEntry: + + # works mostely like split but takes [] into account + def mysplit(self, line): + line = string.strip(line) + pieces = [] + tmp = "" + # we are inside a [..] block + p_found = False + space_found = False + for i in range(len(line)): + if line[i] == "[": + p_found=True + tmp += line[i] + elif line[i] == "]": + p_found=False + tmp += line[i] + elif space_found and not line[i].isspace(): # we skip one or more space + space_found = False + pieces.append(tmp) + tmp = line[i] + elif line[i].isspace() and not p_found: # found a whitespace + space_found = True + else: + tmp += line[i] + # append last piece + if len(tmp) > 0: + pieces.append(tmp) + return pieces + + + # parse a given source line and split it into the fields we need + def parse(self,line): + line = string.strip(self.line) + #print line + # check if the source is enabled/disabled + if line == "" or line == "#": + self.invalid = True + return + if line[0] == "#": + self.disabled = True + pieces = string.split(line[1:]) + # if it looks not like a disabled deb line return + if not (pieces[0] == "deb" or pieces[0] == "deb-src"): + self.invalid = True + return + else: + line = line[1:] + # check for another "#" in the line (this is treated as a comment) + i = line.find("#") + if i > 0: + self.comment = line[i+1:] + line = line[:i] + # source is ok, split it and see what we have + pieces = self.mysplit(line) + # Type, deb or deb-src + self.type = string.strip(pieces[0]) + # URI + self.uri = string.strip(pieces[1]) + # distro and components (optional) + # Directory or distro + self.dist = string.strip(pieces[2]) + if len(pieces) > 3: + # List of components + self.comps = pieces[3:] + else: + self.comps = [] + + #print self.__dict__ + + + # set enabled/disabled + def set_enabled(self, new_value): + self.disabled = not new_value + # enable, remove all "#" from the start of the line + if new_value == True: + i=0 + self.line = string.lstrip(self.line) + while self.line[i] == "#": + i += 1 + self.line = self.line[i:] + else: + # disabled, add a "#" + if string.strip(self.line)[0] != "#": + self.line = "#" + self.line + + + def __init__(self, line): + self.invalid = False + self.disabled = False + self.type = "" + self.uri = "" + self.dist = "" + self.comps = [] + self.comment = "" + self.line = line + self.parse(line) + + + def str(self): + return self.line + + +def uniq(s): + """ simple (and not efficient) way to return uniq list """ + u = [] + for x in s: + if x not in u: + u.append(x) + return u + + +# the SourceList file as a class +class SourcesList: + def __init__(self, file): + self.list = [] # of Type SourceEntries + self.load(file) + + def is_mirror(self, add_uri, orig_uri): + """check if the given add_url is idential or a mirror of orig_uri + e.g. add_uri = archive.ubuntu.com + orig_uri = de.archive.ubuntu.com + -> True + """ + # remove traling spaces and "/" + add_uri = add_uri.rstrip("/ ") + orig_uri = orig_uri.rstrip("/ ") + # uri is identical + if add_uri == orig_uri: + #print "Identical" + return True + # add uri is a master site and orig_uri has the from "XX.mastersite" + # (e.g. de.archive.ubuntu.com) + try: + add_srv = add_uri.split("//")[1] + orig_srv = orig_uri.split("//")[1] + #print "%s == %s " % (add_srv, orig_srv) + except IndexError: # ok, somethings wrong here + #print "IndexError" + return False + if add_srv == orig_srv[3:]: + #print "Mirror" + return True + return False + + def add(self, type, uri, dist, comps, comment="", pos=-1): + # if there is a repo with the same (type, uri, dist) just add the + # components + for i in self.list: + if i.type == type and self.is_mirror(uri,i.uri) and i.dist == dist: + comps = uniq(i.comps + comps) + # preserver mirror + uri = i.uri + # set to the old position and preserve comment + comment = i.comment + pos = self.list.index(i) + self.list.remove(i) + line = "%s %s %s" % (type,uri,dist) + for c in comps: + line = line + " " + c; + if comment != "": + line = "%s #%s\n" %(line,comment) + line = line + "\n" + self.list.insert(pos, SourceEntry(line)) + + def remove(self, source_entry): + self.list.remove(source_entry) + + def load(self,file): + f = open(file, "r") + lines = f.readlines() + for line in lines: + source = SourceEntry(line) + self.list.append(source) + f.close() + + def save(self,file): + f=open(file,"w") + for source in self.list: + f.write(source.str()) + f.close() + + +# templates for the add dialog +class SourceEntryTemplate(SourceEntry): + def __init__(self,a_type,uri,dist,description,comps): + self.comps = [] + self.comps_descriptions = [] + self.type = a_type + self.uri = uri + self.dist = dist + self.description = description + self.comps = comps + +class SourceCompTemplate: + def __init__(self, name, description, on_by_default): + self.name = name + self.description = description + self.on_by_default = on_by_default + +class SourceEntryTemplates: + def __init__(self): + _ = gettext.gettext + self.templates = [] + + # ubuntu components templates + ubuntu_comps = [] + ubuntu_comps.append(SourceCompTemplate("main",_("Officially supported"),True)) + ubuntu_comps.append(SourceCompTemplate("restricted",_("Restricted copyright"),True)) + ubuntu_comps.append(SourceCompTemplate("universe",_("Community maintained (Universe)"),False)) + ubuntu_comps.append(SourceCompTemplate("multiverse",_("Non-free (Multiverse)"),False)) + + # ubuntu distro + self.templates.append(SourceEntryTemplate("deb", + "http://archive.ubuntu.com/ubuntu/", + "breezy", + "Ubuntu 5.10 \"Breezy Badger\"", + ubuntu_comps )) + self.templates.append(SourceEntryTemplate("deb", + "http://security.ubuntu.com/ubuntu/", + "breezy-security", + _("Ubuntu 5.10 Security Updates"), + ubuntu_comps)) + self.templates.append(SourceEntryTemplate("deb", + "http://archive.ubuntu.com/ubuntu/", + "breezy-updates", + _("Ubuntu 5.10 Updates"), + ubuntu_comps)) + + +# matcher class to make a source entry look nice +# lots of predefined matchers to make it i18n/gettext friendly +class SourceEntryMatcher: + class MatchType: + def __init__(self, a_type,a_descr): + self.type = a_type + self.description = a_descr + + class MatchDist: + def __init__(self,a_uri,a_dist, a_descr,l_comps, l_comps_descr): + self.uri = a_uri + self.dist = a_dist + self.description = a_descr + self.comps = l_comps + self.comps_descriptions = l_comps_descr + + def __init__(self): + _ = gettext.gettext + self.type_list = [] + self.type_list.append(self.MatchType("^deb$",_("Binary"))) + self.type_list.append(self.MatchType("^deb-src$",_("Source"))) + + self.dist_list = [] + + ubuntu_comps = ["^main$","^restricted$","^universe$","^multiverse$"] + ubuntu_comps_descr = [_("Officially supported"), + _("Restricted copyright"), + _("Community maintained (Universe)"), + _("Non-free (Multiverse)")] + # CDs + self.dist_list.append(self.MatchDist("cdrom:\[Ubuntu.*4.10", + ".*", + _("CD") + + " Ubuntu 4.10 \"Warty Warthog\"", + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist("cdrom:\[Ubuntu.*5.04", + ".*", + _("CD") + + " Ubuntu 5.04 \"Hoary Hedgehog\"", + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist("cdrom:\[Ubuntu.*5.10", + ".*", + _("CD") + + " Ubuntu 5.10 \"Breezy Badger\"", + ubuntu_comps, ubuntu_comps_descr)) + + # URIs + # normal archive + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^warty$", + "Ubuntu 4.10 \"Warty Warthog\"", + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^hoary$", + "Ubuntu 5.04 \"Hoary Hedgehog\"", + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^breezy$", + "Ubuntu 5.10 \"Breezy Badger\"", + ubuntu_comps, ubuntu_comps_descr)) + # updates + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^hoary-updates$", + _("Ubuntu 5.04 Updates"), + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^breezy-updates$", + _("Ubuntu 5.10 Updates"), + ubuntu_comps, ubuntu_comps_descr)) + + # security + self.dist_list.append(self.MatchDist(".*security.ubuntu.com/ubuntu", + "^warty-security$", + _("Ubuntu 4.10 Security Updates"), + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*security.ubuntu.com/ubuntu", + "^hoary-security$", + _("Ubuntu 5.04 Security Updates"), + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*security.ubuntu.com/ubuntu", + "^breezy-security$", + _("Ubuntu 5.10 Security Updates"), + ubuntu_comps, ubuntu_comps_descr)) + # security (normal archive uri) + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^warty-security$", + _("Ubuntu 4.10 Security Updates"), + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^hoary-security$", + _("Ubuntu 5.04 Security Updates"), + ubuntu_comps, ubuntu_comps_descr)) + self.dist_list.append(self.MatchDist(".*archive.ubuntu.com/ubuntu", + "^breezy-security$", + _("Ubuntu 5.10 Security Updates"), + ubuntu_comps, ubuntu_comps_descr)) + + + # DEBIAN + debian_comps = ["^main$","^contrib$","^non-free$","^non-US$"] + debian_comps_descr = [_("Officially supported"), + _("Contributed software"), + _("Non-free software"), + _("US export restricted software") + ] + + # dists by name + self.dist_list.append(self.MatchDist(".*debian.org/debian", + "^sarge$", + "Debian 3.1 \"Sarge\"", + debian_comps, debian_comps_descr)) + self.dist_list.append(self.MatchDist(".*debian.org/debian", + "^woody$", + "Debian 3.0 \"Woody\"", + debian_comps, debian_comps_descr)) + # securtiy + self.dist_list.append(self.MatchDist(".*security.debian.org", + "^stable.*$", + _("Debian Stable Security Updates"), + debian_comps, debian_comps_descr)) + # dists by status + self.dist_list.append(self.MatchDist(".*debian.org/debian", + "^stable$", + "Debian Stable", + debian_comps, debian_comps_descr)) + self.dist_list.append(self.MatchDist(".*debian.org/debian", + "^testing$", + "Debian Testing", + debian_comps, debian_comps_descr)) + self.dist_list.append(self.MatchDist(".*debian.org/debian", + "^unstable$", + "Debian Unstable \"Sid\"", + debian_comps, debian_comps_descr)) + + # non-us + self.dist_list.append(self.MatchDist(".*debian.org/debian-non-US", + "^stable.*$", + "Debian Non-US (Stable)", + debian_comps, debian_comps_descr)) + self.dist_list.append(self.MatchDist(".*debian.org/debian-non-US", + "^testing.*$", + "Debian Non-US (Testing)", + debian_comps, debian_comps_descr)) + self.dist_list.append(self.MatchDist(".*debian.org/debian-non-US", + "^unstable.*$", + "Debian Non-US (Unstable)", + debian_comps, debian_comps_descr)) + + + + + def match(self,source): + _ = gettext.gettext + # some sane defaults first + type_description = source.type + dist_description = source.uri + " " + source.dist + comp_description = "" + for c in source.comps: + comp_description = comp_description + " " + c + + for t in self.type_list: + if re.match(t.type, source.type): + type_description = _(t.description) + break + + for d in self.dist_list: + #print "'%s'" %source.uri + if re.match(d.uri, source.uri) and re.match(d.dist,source.dist): + dist_description = d.description + comp_description = "" + for c in source.comps: + found = False + for i in range(len(d.comps)): + if re.match(d.comps[i], c): + comp_description = comp_description+"\n"+d.comps_descriptions[i] + found = True + if found == False: + comp_description = comp_description+" "+c + break + + + return (type_description,dist_description,comp_description) + + diff --git a/src/dialog_add.py.in b/src/dialog_add.py.in new file mode 100644 index 00000000..141fc009 --- /dev/null +++ b/src/dialog_add.py.in @@ -0,0 +1,107 @@ +# dialog_add.py.in - dialog to add a new repository +# +# Copyright (c) 2004 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 + +import os +import gobject +import gtk +import gtk.glade + +import aptsources + +class dialog_add: + def __init__(self,parent,sourceslist): + self.sourceslist = sourceslist + + # templates + self.templatelist = aptsources.SourceEntryTemplates() + + # gtk stuff + if os.path.exists("../data/gnome-software-properties.glade"): + self.gladexml = gtk.glade.XML("../data/gnome-software-properties.glade") + else: + self.gladexml = gtk.glade.XML("@prefix@/share/update-manager/gnome-software-properties.glade") + + self.main = widget = self.gladexml.get_widget("dialog_add") + self.main.set_transient_for(parent) + + combo = self.gladexml.get_widget("combobox_what") + self.gladexml.signal_connect("on_combobox_what_changed", self.on_combobox_what_changed, None) + # combox box needs + cell = gtk.CellRendererText() + combo.pack_start(cell, True) + combo.add_attribute(cell, 'text', 0) + self.fill_combo(combo) + self.gladexml.signal_connect("on_button_custom_clicked", + self.on_button_custom_clicked, None) + + + def fill_combo(self,combo): + liststore = gtk.ListStore(gobject.TYPE_STRING,gobject.TYPE_PYOBJECT) + for item in self.templatelist.templates: + liststore.append((item.description, item)) + combo.set_model(liststore) + combo.set_active(0) + + def on_combobox_what_changed(self, combobox, user): + #print "on_combobox_what_changed" + vbox = self.gladexml.get_widget("vbox_comps") + vbox.foreach(lambda widget,vbox: vbox.remove(widget), vbox) + liststore = combobox.get_model() + a_iter = liststore.iter_nth_child(None, combobox.get_active()) + (name, template) = liststore.get(a_iter, 0,1) + self.selected = template + comps = template.comps + for c in comps: + checkbox = gtk.CheckButton(c.description) + checkbox.set_active(c.on_by_default) + checkbox.set_data("name",c.name) + vbox.pack_start(checkbox) + checkbox.show() + + def on_button_custom_clicked(self, widget, data): + #print "on_button_custom_clicked()" + # this hide here is ugly :/ + self.main.hide() + dialog = self.gladexml.get_widget("dialog_add_custom") + res = dialog.run() + dialog.hide() + entry = self.gladexml.get_widget("entry_source_line") + line = entry.get_text() + "\n" + self.sourceslist.list.append(aptsources.SourceEntry(line)) + self.main.response(res) + + def get_enabled_comps(self, checkbutton): + if checkbutton.get_active(): + self.selected_comps.append(checkbutton.get_data("name")) + + def run(self): + res = self.main.run() + if res == gtk.RESPONSE_OK: + # add repository + self.selected_comps = [] + vbox = self.gladexml.get_widget("vbox_comps") + vbox.foreach(self.get_enabled_comps) + self.sourceslist.add(self.selected.type, + self.selected.uri, + self.selected.dist, + self.selected_comps) + self.main.hide() + return res diff --git a/src/dialog_apt_key.py.in b/src/dialog_apt_key.py.in new file mode 100644 index 00000000..d11dfd0a --- /dev/null +++ b/src/dialog_apt_key.py.in @@ -0,0 +1,163 @@ +# dialog_apt_key.py.in - edit the apt keys +# +# Copyright (c) 2004 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 + +import os +import gobject +import gtk +import gtk.glade +import subprocess +import gettext +from utils import error +from subprocess import PIPE + +# gettext convenient +_ = gettext.gettext +def dummy(e): return e +N_ = dummy + +# some known keys +N_("Ubuntu Archive Automatic Signing Key ") +N_("Ubuntu CD Image Automatic Signing Key ") + +class apt_key: + def __init__(self): + self.gpg = ["/usr/bin/gpg"] + self.base_opt = self.gpg + ["--no-options", "--no-default-keyring", + "--secret-keyring", "/etc/apt/secring.gpg", + "--trustdb-name", "/etc/apt/trustdb.gpg", + "--keyring", "/etc/apt/trusted.gpg"] + self.list_opt = self.base_opt + ["--with-colons", "--batch", + "--list-keys"] + self.rm_opt = self.base_opt + ["--quiet", "--batch", + "--delete-key", "--yes"] + self.add_opt = self.base_opt + ["--quiet", "--batch", + "--import"] + + + def list(self): + res = [] + #print self.list_opt + p = subprocess.Popen(self.list_opt,stdout=PIPE).stdout + for line in p.readlines(): + fields = line.split(":") + if fields[0] == "pub": + name = fields[9] + res.append("%s %s\n%s" %((fields[4])[-8:],fields[5], _(name))) + return res + + def add(self, filename): + #print "request to add " + filename + cmd = self.add_opt[:] + cmd.append(filename) + p = subprocess.Popen(cmd) + return (p.wait() == 0) + + def update(self): + cmd = ["/usr/bin/apt-key", "update"] + p = subprocess.Popen(cmd) + return (p.wait() == 0) + + def rm(self, key): + #print "request to remove " + key + cmd = self.rm_opt[:] + cmd.append(key) + p = subprocess.Popen(cmd) + return (p.wait() == 0) + +class dialog_apt_key: + def __init__(self, parent): + # gtk stuff + if os.path.exists("../data/gnome-software-properties.glade"): + self.gladexml = gtk.glade.XML("../data/gnome-software-properties.glade") + else: + self.gladexml = gtk.glade.XML("@prefix@/share/update-manager/gnome-software-properties.glade") + self.main = self.gladexml.get_widget("dialog_apt_key") + self.main.set_transient_for(parent) + + self.gladexml.signal_connect("on_button_key_add_clicked", + self.on_button_key_add_clicked) + self.gladexml.signal_connect("on_button_key_remove_clicked", + self.on_button_key_remove_clicked) + self.gladexml.signal_connect("on_button_apt_key_update_clicked", + self.on_button_apt_key_update_clicked) + + # create apt-key object (abstraction for the apt-key command) + self.apt_key = apt_key() + + # get some widgets + self.treeview_apt_key = self.gladexml.get_widget("treeview_apt_key") + self.liststore_apt_key = gtk.ListStore(str) + self.treeview_apt_key.set_model(self.liststore_apt_key) + # Create columns and append them. + tr = gtk.CellRendererText() + tr.set_property("xpad", 10) + tr.set_property("ypad", 10) + c0 = gtk.TreeViewColumn("Key", tr, text=0) + self.treeview_apt_key.append_column(c0) + self.update_key_list() + + def on_button_apt_key_update_clicked(self, widget): + self.apt_key.update() + self.update_key_list() + + def on_button_key_add_clicked(self, widget): + chooser = gtk.FileChooserDialog(title=_("Choose a key-file"), + parent=self.main, + buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_REJECT, + gtk.STOCK_OK,gtk.RESPONSE_ACCEPT)) + res = chooser.run() + chooser.hide() + if res == gtk.RESPONSE_ACCEPT: + #print chooser.get_filename() + if not self.apt_key.add(chooser.get_filename()): + error(self.main, + _("Error importing selected file"), + _("The selected file may not be a GPG key file " + "or it might be corrupt.")) + self.update_key_list() + + def on_button_key_remove_clicked(self, widget): + selection = self.treeview_apt_key.get_selection() + (model,a_iter) = selection.get_selected() + if a_iter == None: + return + key = model.get_value(a_iter,0) + if not self.apt_key.rm(key[:8]): + error(self.main, + _("Error removing the key"), + _("The key you selected could not be removed. " + "Please report this as a bug.")) + self.update_key_list() + + def update_key_list(self): + self.liststore_apt_key.clear() + for key in self.apt_key.list(): + self.liststore_apt_key.append([key]) + + def run(self): + res = self.main.run() + self.main.hide() + + +if __name__ == "__main__": + ui = dialog_apt_key(None) + ui.run() + diff --git a/src/dialog_edit.py.in b/src/dialog_edit.py.in new file mode 100644 index 00000000..02eac6c0 --- /dev/null +++ b/src/dialog_edit.py.in @@ -0,0 +1,102 @@ +# dialog_edit.py.in - edit a existing repository +# +# Copyright (c) 2004 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 + +import os +import gobject +import gtk +import gtk.glade + +import aptsources + +class dialog_edit: + def __init__(self,parent,sourceslist,source_entry): + self.sourceslist = sourceslist + self.source_entry = source_entry + + # gtk stuff + if os.path.exists("../data/gnome-software-properties.glade"): + self.gladexml = gtk.glade.XML("../data/gnome-software-properties.glade") + else: + self.gladexml = gtk.glade.XML("@prefix@/share/update-manager/gnome-software-properties.glade") + self.main = self.gladexml.get_widget("dialog_edit") + self.main.set_transient_for(parent) + + # type + combo_type = self.gladexml.get_widget("combobox_type") + if source_entry.type == "deb": + combo_type.set_active(0) + elif source_entry.type == "deb-src": + combo_type.set_active(1) + else: + print "Error, unknown source type: '%s'" % source_enrty.type + + # uri + entry = self.gladexml.get_widget("entry_uri") + entry.set_text(source_entry.uri) + + entry = self.gladexml.get_widget("entry_dist") + entry.set_text(source_entry.dist) + + entry = self.gladexml.get_widget("entry_comps") + comps = "" + for c in source_entry.comps: + if len(comps) > 0: + comps = comps + " " + c + else: + comps = c + entry.set_text(comps) + + entry = self.gladexml.get_widget("entry_comment") + entry.set_text(source_entry.comment) + + def run(self): + res = self.main.run() + if res == gtk.RESPONSE_OK: + # get values + combo_type = self.gladexml.get_widget("combobox_type") + if combo_type.get_active() == 0: + line = "deb" + else: + line = "deb-src" + entry = self.gladexml.get_widget("entry_uri") + line = line + " " + entry.get_text() + + entry = self.gladexml.get_widget("entry_dist") + line = line + " " + entry.get_text() + + entry = self.gladexml.get_widget("entry_comps") + line = line + " " + entry.get_text() + + entry = self.gladexml.get_widget("entry_comment") + if entry.get_text() != "": + line = line + " #" + entry.get_text() + "\n" + else: + line = line + "\n" + + # change repository + index = self.sourceslist.list.index(self.source_entry) + self.sourceslist.list[index] = aptsources.SourceEntry(line) + #self.sourceslist.add(self.selected.type, + # self.selected.uri, + # self.selected.dist, + # self.selected_comps) + self.main.hide() + return res diff --git a/src/dialog_settings.py.in b/src/dialog_settings.py.in new file mode 100644 index 00000000..cfa13e38 --- /dev/null +++ b/src/dialog_settings.py.in @@ -0,0 +1,144 @@ +# dialog_settings.py.in - edit some settings +# +# 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 + +import gconf +import apt_pkg +import gtk +import os + +periodicAptConfFile = "/etc/apt/apt.conf.d/10periodic" +archiveAptConfFile = "/etc/apt/apt.conf.d/20archive" + +class dialog_settings: + def save_periodic_config(self): + #print "saving ..." + + # get the new values + for key in self.conf_map: + cb = self.gladexml.get_widget("checkbutton_%s"% key) + sb = self.gladexml.get_widget("spinbutton_%s"% key) + if cb and not cb.get_active(): + #print "%s=%s" % (self.conf_map[key], "0") + apt_pkg.Config.Set(self.conf_map[key], "0") + elif sb: + value = sb.get_value() + apt_pkg.Config.Set(self.conf_map[key], str(value)) + #print "%s=%s" % (self.conf_map[key], value) + + # special case for autodownload, it has the same interval as + # Update-Package-Lists + cb = self.gladexml.get_widget("checkbutton_autodownload") + key = "autodownload" + if cb.get_active(): + autoupdate = str(apt_pkg.Config.FindI("APT::Periodic::Update-Package-Lists")) + apt_pkg.Config.Set(self.conf_map[key], autoupdate) + else: + apt_pkg.Config.Set(self.conf_map[key], "0") + + + # write both config-prefixes to different files + for (file, prefix) in ((periodicAptConfFile, "APT::Periodic"), + (archiveAptConfFile, "APT::Archives")): + + content = [] + if os.path.isfile(file): + content=open(file,"r").readlines() + + cnf = apt_pkg.Config.SubTree(prefix) + + f = open(file,"w+") + for line in content: + # don't write the udpated keys + found = False + for key in cnf.List(): + #print "%s=%s" % (key, cnf[key]) + if line.find("%s::%s" % (prefix,key)) >= 0: + found = True + break + if not found: + f.write(line) + # write new keys + for i in cnf.List(): + f.write("%s::%s \"%s\";\n" % (prefix,i,cnf.FindI((i)))) + f.close() + + def toggle_show_disabled(self, widget, data): + self.show_disabled = widget.get_active() + self.gconfclient.set_bool("/apps/gnome-software-properties/show_disabled",\ + self.show_disabled) + + def toggle_settings_cb(self, widget, data): + mode = widget.get_active() + self.gladexml.get_widget(data).set_sensitive(mode) + + def run(self): + res = self.main_window.run() + self.save_periodic_config() + self.main_window.hide() + return res + + def __init__(self, parent, glade): + + self.gladexml = glade + self.main_window = self.gladexml.get_widget("dialog_settings") + self.main_window.set_transient_for(parent) + self.parent = parent + self.gconfclient = gconf.client_get_default() + + # preferences entries + self.show_disabled = self.gconfclient.get_bool("/apps/gnome-software-properties/show_disabled") + + checkbutton_show_disabled = self.gladexml.get_widget("checkbutton_show_disabled") + checkbutton_show_disabled.set_active(self.show_disabled) + checkbutton_show_disabled.connect("toggled", self.toggle_show_disabled, None) + + + # apt-config + + # set the update stuff + self.conf_map = { + "autoupdate" : "APT::Periodic::Update-Package-Lists", + "autodownload" : "APT::Periodic::Download-Upgradeable-Packages", + "autoclean" : "APT::Periodic::AutocleanInterval", + "max_size" : "APT::Archives::MaxSize", + "max_age" : "APT::Archives::MaxAge" + } + + for key in self.conf_map: + value = apt_pkg.Config.FindI(self.conf_map[key]) + #print "%s=%s" % (key, value) + cb = self.gladexml.get_widget("checkbutton_%s"% key) + #if cb == None: + # print "checkbutton_%s not found" % key + sb = self.gladexml.get_widget("spinbutton_%s"% key) + if sb != None: + #print "setting %s to %s" % (key, value) + sb.set_value(value) + #else: + # print "spinbutton_%s not found" % key + box = self.gladexml.get_widget("vbox_%s"% key) + #if box == None: + # print "vbox_%s not found" % key + if box and cb: + cb.connect("toggled", self.toggle_settings_cb, ("vbox_%s" % key)) + if cb: + cb.set_active(value) + diff --git a/src/gnome-software-properties.in b/src/gnome-software-properties.in new file mode 100755 index 00000000..6c9b8d3b --- /dev/null +++ b/src/gnome-software-properties.in @@ -0,0 +1,332 @@ +#!/usr/bin/python2.4 +# gnome-software-properties.in - edit /etc/apt/sources.list +# +# Copyright (c) 2004 Canonical +# 2004 Michiel Sikkes +# +# Author: Michiel Sikkes +# 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 + +import pygtk +import sys +pygtk.require('2.0') +import gtk +import gtk.glade +import gconf +import gobject +import os +import gettext +import re +import string +import time +import tempfile +import subprocess +from optparse import OptionParser +import apt_pkg + +sys.path.append("@prefix@/share/update-manager/python") + +import aptsources +import dialog_add +import dialog_edit +import dialog_apt_key +from dialog_settings import dialog_settings +import shutil + + +(LIST_MARKUP, LIST_ENABLED, LIST_ENTRY_OBJ) = range(3) + +class SoftwareConfigurator: + + def on_button_ok_clicked(self, widget, data): + #self.save_periodic_config(periodicAptConfFile) + + location = "/etc/apt/sources.list" + #backup first + shutil.copy(location,location+".save") + # write + self.sourceslist.save(location) + + # write the source.list first, even if dirty=False, because + # e.g. CD-ROM add does not set dirty variable even if it adds + # something to the sources.list (but no reload needed) + if self.dirty == False: + gtk.main_quit() + sys.exit(0) + + primary = "" + _("Repositories " + "changed") + "" + secondary = _("The repository information has changes. A backup copy of " + "your sources.list is stored in %s.save. " + "\n\n" + "You need to reload the package list from the servers " + "for your changes to take effect. Do you want to do this " + "now?") % location + dialog = gtk.MessageDialog(self.main_window,gtk.DIALOG_MODAL, + gtk.MESSAGE_INFO,gtk.BUTTONS_YES_NO,"") + dialog.set_markup(primary); + dialog.format_secondary_text(secondary); + #textview = gtk.TextView() + #textview.set_editable(gtk.FALSE) + #textbuffer = textview.get_buffer() + #f = os.popen("/usr/bin/diff -u %s.save %s" % (location,location)) + #diff = f.read() + #textbuffer.set_text(diff) + #if f.close() != None: + # win = gtk.ScrolledWindow() + # win.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) + # win.add(textview) + # win.set_size_request(400,300) + # win.show() + # dialog.vbox.pack_start(child = win, padding = 6) + # textview.show() + res = dialog.run() + dialog.destroy() + if res == gtk.RESPONSE_YES: + self.main_window.hide() + # we are in "no-update" mode, this is used when the calling application + # wants to deal with this itself + if options.no_update: + gtk.main_quit() + sys.exit(1) + child = subprocess.Popen(["/usr/sbin/synaptic", "--update-at-startup", + "--hide-main-window","--non-interactive"], + close_fds=True) + # wait for the child to finish + while child.poll() == None: + time.sleep(0.05) + while gtk.events_pending(): + gtk.main_iteration() + gtk.main_quit() + sys.exit(1) + gtk.main_quit() + sys.exit(0) + + def on_button_edit_clicked(self, widget, data): + selection = self.sourceslist_view.get_selection() + (model, iter) = selection.get_selected() + source_entry = model.get_value(iter, LIST_ENTRY_OBJ) + edit = dialog_edit.dialog_edit(self.main_window,self.sourceslist, + source_entry) + if edit.run() == gtk.RESPONSE_OK: + self.reloadsources(self.sourceslist,self.matcher) + self.dirty = True + + def on_sourceslist_selection_changed(self, selection, data): + (model, iter) = selection.get_selected() + if iter != None: + self.button_del.set_sensitive(True) + self.button_edit.set_sensitive(True) + else: + self.button_del.set_sensitive(False) + self.button_edit.set_sensitive(False) + + #def on_sourceslist_row_activated(self, treeview, path, column, data): + # #print "on_row_activated()" + # model = treeview.get_model() + # source_entry = model.get_value(model.get_iter(path), LIST_ENTRY_OBJ) + # edit = dialog_edit.dialog_edit(self.main_window,self.sourceslist, + # source_entry) + # edit.run() + # self.reloadsources(self.sourceslist,self.matcher) + + def on_button_remove_clicked(self, widget, data): + selection = self.sourceslist_view.get_selection() + (model,a_iter) = selection.get_selected() + if a_iter == None: + return + source = model.get_value(a_iter, LIST_ENTRY_OBJ) + self.sourceslist.remove(source) + self.reloadsources(self.sourceslist,self.matcher) + self.dirty=True + + def on_button_add_clicked(self, widget, data): + add = dialog_add.dialog_add(self.main_window,self.sourceslist) + if add.run() == gtk.RESPONSE_OK: + self.reloadsources(self.sourceslist,self.matcher) + self.dirty=True + + def on_button_settings_clicked(self, widget, data): + settings = dialog_settings(self.main_window, self.gladexml) + settings.run() + self.show_disabled = self.gconfclient.get_bool("/apps/gnome-software-properties/show_disabled") + self.c_enabled.set_property("visible", self.show_disabled) + self.reloadsources(self.sourceslist, self.matcher) + + def on_button_add_cdrom_clicked(self, widget, data): + tmp = tempfile.NamedTemporaryFile() + cmd = ["/usr/sbin/synaptic", "--hide-main-window", "--non-interactive", + "-o","Dir::Etc::sourcelist=%s" % tmp.name,"--ask-cdrom" ] + self.main_window.set_sensitive(False) + proc = subprocess.Popen(cmd) + # wait for process to finish + while proc.poll() == None: + while gtk.events_pending(): + gtk.main_iteration() + time.sleep(0.05) + self.main_window.set_sensitive(True) + # read tmp file with source name + line = "" + for x in open(tmp.name): + line = x + if line != "": + self.sourceslist.list.append(aptsources.SourceEntry(line)) + self.reloadsources(self.sourceslist,self.matcher) + #self.dirty=True # no need here + + def on_button_authentication_clicked(self, widget, data): + auth = dialog_apt_key.dialog_apt_key(self.main_window) + auth.run() + + def reloadsources(self, sourceslist, matcher): + #self.meta = meta_data() + self.sourcesstore.clear() + for source in sourceslist.list: + if source.invalid or (source.disabled and not self.show_disabled): + continue + (a_type,dist,comps) = matcher.match(source) + contents = "" + if source.comment != "": + contents += "%s\n\n" % (source.comment) + contents +="%s (%s) \n%s" % (dist,a_type, comps) + iter = self.sourcesstore.append([contents, not source.disabled, source]) + + def show_help(self, widget, data): + print "self.show_help() called" + + # toggled on/off a source in the listview + def toggled_enabled(self, renderer, path_string): + iter = self.sourcesstore.get_iter_from_string(path_string) + source = self.sourcesstore.get_value(iter, LIST_ENTRY_OBJ) + self.dirty=True + if self.sourcesstore.get_value(iter, LIST_ENABLED): + self.sourcesstore.set_value(iter, LIST_ENABLED, False) + source.set_enabled(False) + else: + self.sourcesstore.set_value(iter, LIST_ENABLED, True) + source.set_enabled(True) + + def __init__(self): + self.gconfclient = gconf.client_get_default() + + if os.path.exists("../data/gnome-software-properties.glade"): + self.gladexml = gtk.glade.XML("../data/gnome-software-properties.glade") + else: + self.gladexml = gtk.glade.XML("@prefix@/share/update-manager/gnome-software-properties.glade") + + # do we show disabled sources? + self.show_disabled = self.gconfclient.get_bool("/apps/gnome-software-properties/show_disabled") + self.main_window = self.gladexml.get_widget("SoftwareConfigurator") + # button on the right + self.gladexml.signal_connect("on_button_edit_clicked", + self.on_button_edit_clicked, None) + self.button_edit = self.gladexml.get_widget("button_edit") + self.button_del = self.gladexml.get_widget("button_remove") + + # Gets the treeview and creates a store for it. + self.sourceslist_view = self.gladexml.get_widget("sourceslist") + self.sourcesstore = gtk.ListStore(str, bool,gobject.TYPE_PYOBJECT) + self.sourceslist_view.set_model(self.sourcesstore) + #self.gladexml.signal_connect("on_sourceslist_row_activated", + # self.on_sourceslist_row_activated, None) + self.sourceslist_view.get_selection().connect("changed", self.on_sourceslist_selection_changed, None) + + # was something modified + self.dirty=False + + # Create columns and append them. + cr = gtk.CellRendererToggle() + cr.set_property("activatable", True) + cr.set_property("xpad", 10) + cr.set_property("ypad", 10) + cr.connect("toggled", self.toggled_enabled) + self.c_enabled = gtk.TreeViewColumn("Enabled", cr, active=LIST_ENABLED) + self.sourceslist_view.append_column(self.c_enabled) + self.c_enabled.set_property("visible", self.show_disabled) + + tr = gtk.CellRendererText() + tr.set_property("xpad", 10) + tr.set_property("ypad", 10) + c0 = gtk.TreeViewColumn("Entry", tr, markup=LIST_MARKUP) + self.sourceslist_view.append_column(c0) + + self.sourceslist = aptsources.SourcesList("@sysconfdir@/apt/sources.list") + self.matcher = aptsources.SourceEntryMatcher() + # Empty and fill the sources store. + self.reloadsources(self.sourceslist,self.matcher) + + self.main_window = self.gladexml.get_widget("SoftwareConfigurator") + self.main_window.connect("delete_event", lambda widget,ev: gtk.main_quit()) + + okbutton = self.gladexml.get_widget("button_ok") + okbutton.connect("clicked", self.on_button_ok_clicked, None) + + cancelbutton = self.gladexml.get_widget("button_cancel") + cancelbutton.connect("clicked", lambda w,v: gtk.main_quit(), None) + + self.gladexml.signal_connect("on_button_add_clicked", self.on_button_add_clicked, None) + self.gladexml.signal_connect("on_button_add_cdrom_clicked", self.on_button_add_cdrom_clicked, None) + self.gladexml.signal_connect("on_button_remove_clicked", self.on_button_remove_clicked, None) + self.gladexml.signal_connect("on_button_authentication_clicked", self.on_button_authentication_clicked, None) + + # settings + self.gladexml.signal_connect("on_button_settings_clicked", self.on_button_settings_clicked, None) + + self.main_window.show() + if options.toplevel != None: + # don't show the add-cdrom button for now + # FIXME: on the long run interface with apt-pkg/cdrom.h + b = self.gladexml.get_widget("button_add_cdrom") + b.hide() + toplevel = gtk.gdk.window_foreign_new(int(options.toplevel)) + self.main_window.window.set_transient_for(toplevel) + + + # init the config + apt_pkg.InitConfig() + + def main(self): + gtk.main() + + +if __name__ == "__main__": + APP="update-manager" + DIR="@prefix@/share/locale" + gettext.bindtextdomain(APP, DIR) + gettext.textdomain(APP) + gtk.glade.bindtextdomain(APP, DIR) + gtk.glade.textdomain(APP) + _ = gettext.gettext + #print "Software Configurator started..." + + # add option parser + parser = OptionParser() + parser.add_option("-n", "--no-update", action="store_true", + dest="no_update", default=False, + help="No update on repository change (usefull if called "\ + "from a external program).") + parser.add_option("-t", "--toplevel", + action="store", type="string", dest="toplevel", + help="Set x-window-id of the toplevel parent for the "\ + "dialog (usefull for embedding)") + + + (options, args) = parser.parse_args() + ui = SoftwareConfigurator() + ui.main() + #print "Software Configurator ended..." diff --git a/src/update-manager.in b/src/update-manager.in new file mode 100644 index 00000000..90928b8e --- /dev/null +++ b/src/update-manager.in @@ -0,0 +1,874 @@ +#!/usr/bin/python2.4 +# update-manager.in - easy updating application +# +# Copyright (c) 2004 Canonical +# 2004 Michiel Sikkes +# +# Author: Michiel Sikkes +# 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 + +import pygtk +pygtk.require('2.0') +import gtk +import gtk.gdk +import gtk.glade +import gobject +import gnome +import apt_pkg +import gettext +import copy +import string +import sys +import os +import os.path +import urllib2 +import re +import thread +import tempfile +import time +import rfc822 +import gconf +import pango +import subprocess +import pwd +import xml.sax.saxutils + + +# FIXME: +# - cary a reference to the update-class around in the ListStore +# - kill "all_changes" and move the changes into the "Update" class + +# list constants +(LIST_INSTALL, LIST_CONTENTS, LIST_NAME, LIST_SHORTDESC, + LIST_VERSION, LIST_LONG_DESCR, LIST_PKG) = range(7) + +# actions for "invoke_manager" +(INSTALL, UPDATE) = range(2) + +SYNAPTIC_PINFILE = "/var/lib/synaptic/preferences" + +METARELEASE_URI = "http://changelogs.ubuntu.com/meta-release" +#METARELEASE_URI = "http://people.ubuntu.com/~mvo/meta-release-test" +METARELEASE_FILE = "/var/lib/update-manager/meta-release" + +CHANGELOGS_URI="http://changelogs.ubuntu.com/changelogs/pool/%s/%s/%s/%s_%s/changelog" + +# fixme: use a utils package for this sort of stuff +def str_to_bool(str): + if str == "0" or str.upper() == "FALSE": + return False + return True + +def utf8(str): + return unicode(str, 'latin1').encode('utf-8') + +class Update: + + def __init__(self, package, cache, records, depcache): + #package = cache[name] + name = package.Name + version = depcache.GetCandidateVer(package) + file, index = version.FileList.pop(0) + records.Lookup((file, index)) + + self.name = name + self.version = version.VerStr + self.shortdesc = records.ShortDesc + self.longdesc = "" + self.size = version.Size + + longdesc = records.LongDesc + lines = longdesc.split("\n") + lines.pop(0) + for line in lines: + line = line[1:] + first_char = string.strip(line)[0] + if line == ".": + self.longdesc = self.longdesc + "\n" + else: + self.longdesc = self.longdesc + line + "\n" + +class UpdateList: + def __init__(self, parent_window): + self.pkgs = [] + self.num_updates = 0 + self.parent_window = parent_window + + def saveDistUpgrade(self, cache, depcache): + """ this functions mimics a upgrade but will never remove anything """ + depcache.Upgrade(True) + if depcache.DelCount > 0: + # nice try, falling back + for pkg in cache.Packages: + depcache.MarkKeep(pkg) + assert depcache.BrokenCount == 0 and depcache.DelCount == 0 + depcache.Upgrade() + + def update(self, cache, records, depcache): + held_back = [] + broken = [] + self.saveDistUpgrade(cache, depcache) + for pkg in cache.Packages: + if depcache.MarkedUpgrade(pkg) or depcache.MarkedInstall(pkg): + self.pkgs.append(Update(pkg, cache, records, depcache)) + self.num_updates = self.num_updates + 1 + elif depcache.IsInstBroken(pkg) or depcache.IsNowBroken(pkg): + broken.append(pkg.Name) + elif pkg.CurrentVer != None and depcache.IsUpgradable(pkg): + #print "MarkedKeep: %s " % pkg.Name + held_back.append(pkg.Name) + self.pkgs.sort(lambda x,y: cmp(x.name,y.name)) + if depcache.BrokenCount > 0: + # FIXME: show what packages are broken + msg=("%s\n\n%s"%(_("Your system has broken packages!"), + _("This means that some dependencies " + "of the installed packages are not " + "satisfied. Please use \"Synaptic\" " + "or \"apt-get\" to fix the " + "situation." + ))) + dialog = gtk.MessageDialog(self.parent_window, 0, gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK,"") + dialog.set_markup(msg) + dialog.vbox.set_spacing(6) + dialog.run() + dialog.destroy() + sys.exit(1) + if depcache.KeepCount > 0: + #print "WARNING, keeping packages" + msg=("%s\n\n%s"%(_("It is not possible to upgrade " + "all packages."), + _("This means that " + "besides the actual upgrade of the " + "packages some further action " + "(such as installing or removing " + "packages) " + "is required. Please use Synaptic " + "\"Smart Upgrade\" or " + "\"apt-get dist-upgrade\" to fix " + "the situation." + ))) + dialog = gtk.MessageDialog(self.parent_window, 0, gtk.MESSAGE_INFO, + gtk.BUTTONS_OK,"") + dialog.set_default_response(gtk.RESPONSE_OK) + dialog.set_markup(msg) + dialog.vbox.set_spacing(6) + label = gtk.Label(_("The following packages are not upgraded: ")) + label.set_alignment(0.0,0.5) + dialog.set_border_width(6) + label.show() + dialog.vbox.pack_start(label) + scroll = gtk.ScrolledWindow() + scroll.set_size_request(-1,200) + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + text = gtk.TextView() + text.set_editable(False) + text.set_cursor_visible(False) + buf = text.get_buffer() + held_back.sort() + buf.set_text("\n".join(held_back)) + scroll.add(text) + dialog.vbox.pack_start(scroll) + scroll.show_all() + dialog.run() + dialog.destroy() + + +class UpdateManager: + + # FIXME: wrong location for this func + # don't touch the gui in this function, it needs to be thread-safe + def get_changelog(self, name, lock): + pkg = self.cache[name] + + # FIXME: not correct, need to get canidateVer + version = self.depcache.GetCandidateVer(pkg) + file, index = version.FileList.pop(0) + self.records.Lookup((file, index)) + if self.records.SourcePkg != "": + srcpkg = self.records.SourcePkg + else: + srcpkg = name + + src_section = "main" + l = string.split(pkg.Section,"/") + if len(l) > 1: + sec_section = l[0] + + prefix = srcpkg[0] + if srcpkg.startswith("lib"): + prefix = "lib" + srcpkg[3] + + verstr = version.VerStr + l = string.split(verstr,":") + if len(l) > 1: + verstr = l[1] + + try: + uri = CHANGELOGS_URI % (src_section,prefix,srcpkg,srcpkg, verstr) + changelog = urllib2.urlopen(uri) + #print changelog.read() + # do only get the lines that are new + alllines = "" + regexp = "^%s \((.*)\)(.*)$" % (srcpkg) + + i=0 + while True: + line = changelog.readline() + #print line + if line == "": + break + match = re.match(regexp,line) + if match: + if apt_pkg.VersionCompare(match.group(1),pkg.CurrentVer.VerStr) <= 0: + break + # EOF (shouldn't really happen) + alllines = alllines + line + + # only write if we where not canceld + if lock.locked(): + self.all_changes[name] = [alllines, srcpkg] + except urllib2.HTTPError: + if lock.locked(): + self.all_changes[name] = [_("Changes not found, the server may not be updated yet."), srcpkg] + except IOError: + if lock.locked(): + self.all_changes[name] = [_("Failed to download changes. Please check if there is an active internet connection."), srcpkg] + if lock.locked(): + lock.release() + + def set_changes_buffer(self, changes_buffer, text, name, srcpkg): + changes_buffer.set_text("") + lines = text.split("\n") + if len(lines) == 1: + changes_buffer.set_text(text) + return + + for line in lines: + + end_iter = changes_buffer.get_end_iter() + + version_match = re.match("^%s \((.*)\)(.*)$" % (srcpkg), line) + #bullet_match = re.match("^.*[\*-]", line) + author_match = re.match("^.*--.*<.*@.*>.*$", line) + if version_match: + version = version_match.group(1) + version_text = _("Version %s: \n") % version + changes_buffer.insert_with_tags_by_name(end_iter, version_text, "versiontag") + # mvo: disabled for now as it does not catch multi line entries + # (see ubuntu #7034 for rational) + #elif bullet_match and not author_match: + # bullet_text = " " + line + "\n" + # changes_buffer.insert(end_iter, bullet_text) + elif (author_match): + pass + #chanages_buffer.insert(end_iter, "\n") + else: + changes_buffer.insert(end_iter, line+"\n") + + + def cursor_changed(self, widget): + tuple = widget.get_cursor() + path = tuple[0] + # check if we have a path at all + if path == None: + return + model = widget.get_model() + iter = model.get_iter(path) + + # set descr + long_desc = model.get_value(iter, 5) + if long_desc == None: + return + desc_buffer = self.DescView.get_buffer() + desc_buffer.set_text(utf8(long_desc)) + + # now do the changelog + name = model.get_value(iter, 2) + if name == None: + return + + changes_buffer = self.ChangesView.get_buffer() + + # check if we have the changes already + if self.all_changes.has_key(name): + changes = self.all_changes[name] + self.set_changes_buffer(changes_buffer, changes[0], name, changes[1]) + else: + if self.expander.get_expanded(): + self.treeview.set_sensitive(False) + self.Glade.get_widget("hbox_footer").set_sensitive(False) + lock = thread.allocate_lock() + lock.acquire() + t=thread.start_new_thread(self.get_changelog,(name,lock)) + changes_buffer.set_text(_("Downloading changes...")) + button = self.Glade.get_widget("button_cancel_dl_changelog") + button.show() + id = button.connect("clicked", + lambda w,lock: lock.release(), lock) + # wait for the dl-thread + while lock.locked(): + time.sleep(0.05) + while gtk.events_pending(): + gtk.main_iteration() + # download finished (or canceld, or time-out) + button.hide() + button.disconnect(id); + self.treeview.set_sensitive(True) + self.Glade.get_widget("hbox_footer").set_sensitive(True) + + if self.all_changes.has_key(name): + changes = self.all_changes[name] + self.set_changes_buffer(changes_buffer, changes[0], name, changes[1]) + + def remove_update(self, pkg): + name = pkg.name + if name in self.packages: + self.packages.remove(name) + self.dl_size -= pkg.size + if len(self.packages) == 0: + self.installbutton.set_sensitive(False) + self.update_count() + + def add_update(self, pkg): + name = pkg.name + if name not in self.packages: + self.packages.append(name) + self.dl_size += pkg.size + if len(self.packages) > 0: + self.installbutton.set_sensitive(True) + self.update_count() + + def update_count(self): + text = "%i (%s)" % (len(self.packages), + apt_pkg.SizeToStr(self.dl_size)) + self.NumUpdates.set_text(text) + + def activate_details(self, expander, data): + expanded = self.expander.get_expanded() + self.gconfclient.set_bool("/apps/update-manager/show_details",expanded) + if expanded: + self.cursor_changed(self.treeview) + + def run_synaptic(self, id, action, lock): + apt_pkg.PkgSystemUnLock() + cmd = ["/usr/sbin/synaptic", "--hide-main-window", "--non-interactive", + "--plug-progress-into", "%s" % (id) ] + if action == INSTALL: + cmd.append("--set-selections") + cmd.append("--progress-str") + cmd.append("%s" % _("The updates are being applied.")) + cmd.append("--finish-str") + cmd.append("%s" % _("Upgrade finished")) + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) + f = proc.stdin + for s in self.packages: + f.write("%s\tinstall\n" % s) + f.close() + proc.wait() + elif action == UPDATE: + cmd.append("--update-at-startup") + subprocess.call(cmd) + else: + print "run_synaptic() called with unknown action" + sys.exit(1) + + # use this once gksudo does propper reporting + #if os.geteuid() != 0: + # if os.system("gksudo /bin/true") != 0: + # return + # cmd = "sudo " + cmd; + lock.release() + + def plug_removed(self, w, (win,socket)): + #print "plug_removed" + # plug was removed, but we don't want to get it removed, only hiden + # unti we get more + win.hide() + return True + + def plug_added(self, sock, win): + win.show() + while gtk.events_pending(): + gtk.main_iteration() + + def on_button_reload_clicked(self, widget): + #print "on_button_reload_clicked" + self.invoke_manager(UPDATE) + + def on_button_help_clicked(self, widget): + gnome.help_display_desktop(self.gnome_program, "update-manager", "update-manager", "") + + def on_button_install_clicked(self, widget): + #print "on_button_install_clicked" + self.invoke_manager(INSTALL) + + def invoke_manager(self, action): + # check first if no other package manager is runing + import struct, fcntl + lock = os.path.dirname(apt_pkg.Config.Find("Dir::State::status"))+"/lock" + lock_file= open(lock) + flk=struct.pack('hhllhl',fcntl.F_WRLCK,0,0,0,0,0) + try: + rv = fcntl.fcntl(lock_file, fcntl.F_GETLK, flk) + except IOError: + print "Error getting lockstatus" + raise + locked = struct.unpack('hhllhl', rv)[0] + if locked != fcntl.F_UNLCK: + msg=("%s\n\n%s"%(_("Another package manager is " + "running"), + _("You can run only one " + "package management application " + "at the same time. Please close " + "this other application first."))); + dialog = gtk.MessageDialog(self.main_window, 0, gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK,"") + dialog.set_markup(msg) + dialog.run() + dialog.destroy() + return + + # don't display apt-listchanges, we already showed the changelog + os.environ["APT_LISTCHANGES_FRONTEND"]="none" + + # set window to insensitive + self.main_window.set_sensitive(False) + # create a progress window that will swallow the synaptic progress bars + win = gtk.Window() + if action==UPDATE: + win.set_title(_("Updating package list...")) + else: + win.set_title(_("Installing updates...")) + win.set_border_width(6) + win.set_transient_for(self.main_window) + win.set_position(gtk.WIN_POS_CENTER_ON_PARENT) + win.resize(400,200) + win.set_resizable(False) + # prevent the window from closing with the delete button (there is + # a cancel button in the window) + win.connect("delete_event", lambda e,w: True); + + # create the socket + socket = gtk.Socket() + socket.show() + win.add(socket) + + socket.connect("plug-added", self.plug_added, win) + socket.connect("plug-removed", self.plug_removed, (win,socket)) + lock = thread.allocate_lock() + lock.acquire() + t = thread.start_new_thread(self.run_synaptic,(socket.get_id(),action,lock)) + while lock.locked(): + while gtk.events_pending(): + gtk.main_iteration() + time.sleep(0.05) + win.destroy() + while gtk.events_pending(): + gtk.main_iteration() + self.fillstore() + self.main_window.set_sensitive(True) + + def toggled(self, renderer, path_string): + """ a toggle button in the listview was toggled """ + iter = self.store.get_iter_from_string(path_string) + if self.store.get_value(iter, LIST_INSTALL): + self.store.set_value(iter, LIST_INSTALL, False) + self.remove_update(self.store.get_value(iter, LIST_PKG)) + else: + self.store.set_value(iter, LIST_INSTALL, True) + self.add_update(self.store.get_value(iter, LIST_PKG)) + + + def exit(self): + """ exit the application, save the state """ + self.save_state() + gtk.main_quit() + sys.exit(0) + + def save_state(self): + """ save the state (window-size for now) """ + (x,y) = self.main_window.get_size() + self.gconfclient.set_pair("/apps/update-manager/window_size", + gconf.VALUE_INT, gconf.VALUE_INT, x, y) + + def restore_state(self): + """ restore the state (window-size for now) """ + expanded = self.gconfclient.get_bool("/apps/update-manager/show_details") + self.expander.set_expanded(expanded) + (x,y) = self.gconfclient.get_pair("/apps/update-manager/window_size", + gconf.VALUE_INT, gconf.VALUE_INT) + if x > 0 and y > 0: + self.main_window.resize(x,y) + + def on_button_preferences_clicked(self, widget): + """ start gnome-software preferences """ + # args: "-n" means we take care of the reloading of the + # package list ourself + apt_pkg.PkgSystemUnLock() + args = ['/usr/bin/gnome-software-properties', '-n'] + child = subprocess.Popen(args) + self.main_window.set_sensitive(False) + res = None + while res == None: + res = child.poll() + time.sleep(0.05) + while gtk.events_pending(): + gtk.main_iteration() + # repository information changed, call "reload" + try: + apt_pkg.PkgSystemLock() + except SystemError: + print "Error geting the cache" + apt_pkg.PkgSystemLock() + if res > 0: + self.on_button_reload_clicked(None) + self.main_window.set_sensitive(True) + + def __init__(self, download_changes_at_startup=False): + + self.gnome_program = gnome.init("update-manager", "0.39") + + self.download_changes_at_startup = download_changes_at_startup + self.packages = [] + self.dl_size = 0 + self.all_changes = {} + self.dist = self.get_dist() + if os.path.exists("../data/update-manager.glade"): + self.Glade = gtk.glade.XML("../data/update-manager.glade") + else: + self.Glade = gtk.glade.XML("@prefix@/share/update-manager/update-manager.glade") + + self.NumUpdates = self.Glade.get_widget("num_updates") + self.main_window = self.Glade.get_widget("MainWindow") + self.main_window.connect("delete_event", lambda w, ev: self.exit()) + self.DescView = self.Glade.get_widget("descview") + self.ChangesView = self.Glade.get_widget("textview_changes") + changes_buffer = self.ChangesView.get_buffer() + changes_buffer.create_tag("versiontag", weight=pango.WEIGHT_BOLD) + self.expander = self.Glade.get_widget("expander_details") + self.expander.connect("notify::expanded", self.activate_details) + + self.installbutton = self.Glade.get_widget("button_install") + self.Glade.signal_connect("on_button_install_clicked", + self.on_button_install_clicked) + self.Glade.signal_connect("on_button_close_clicked", + lambda w: self.exit()) + self.Glade.signal_connect("on_button_reload_clicked", + self.on_button_reload_clicked) + self.Glade.signal_connect("on_button_preferences_clicked", + self.on_button_preferences_clicked) + self.Glade.signal_connect("on_button_help_clicked", + self.on_button_help_clicked) + + self.treeview = self.Glade.get_widget("updatelist") + + self.store = gtk.ListStore(gobject.TYPE_BOOLEAN, str, str, str, str, str, + gobject.TYPE_PYOBJECT) + self.treeview.set_model(self.store) + self.treeview.set_headers_clickable(True); + + self.treeview.connect('cursor-changed', self.cursor_changed) + + tr = gtk.CellRendererText() + tr.set_property("xpad", 10) + tr.set_property("ypad", 10) + cr = gtk.CellRendererToggle() + cr.set_property("activatable", True) + cr.set_property("xpad", 10) + cr.connect("toggled", self.toggled) + self.cb = gtk.TreeViewColumn("Install", cr, active=LIST_INSTALL) + c0 = gtk.TreeViewColumn("Name", tr, markup=LIST_CONTENTS) + c0.set_resizable(True) + major,minor,patch = gtk.pygtk_version + if (major >= 2) and (minor >= 5): + self.cb.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self.cb.set_fixed_width(30) + c0.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + c0.set_fixed_width(100) + #self.treeview.set_fixed_height_mode(True) + + self.treeview.append_column(self.cb) + self.cb.set_visible(False); + self.treeview.append_column(c0) + self.treeview.set_search_column(LIST_NAME) + #self.treeview.append_column(c1) + #self.treeview.append_column(c2) + #self.treeview.set_headers_visible(False) + # set expander to last position + + # proxy stuff + SYNAPTIC_CONF_FILE = "%s/.synaptic/synaptic.conf" % pwd.getpwuid(0)[5] + if os.path.exists(SYNAPTIC_CONF_FILE): + cnf = apt_pkg.newConfiguration() + apt_pkg.ReadConfigFile(cnf, SYNAPTIC_CONF_FILE) + use_proxy = cnf.FindB("Synaptic::useProxy", False) + if use_proxy: + proxy_host = cnf.Find("Synaptic::httpProxy") + proxy_port = str(cnf.FindI("Synaptic::httpProxyPort")) + if proxy_host and proxy_port: + proxy_support = urllib2.ProxyHandler({"http":"http://%s:%s" % (proxy_host, proxy_port)}) + opener = urllib2.build_opener(proxy_support) + urllib2.install_opener(opener) + + self.gconfclient = gconf.client_get_default() + # restore state + self.restore_state() + + def fillstore(self): + if self.download_changes_at_startup: + dialog = self.Glade.get_widget("dialog_fetching") + dialog.set_transient_for(self.main_window) + dialog.set_modal(True) + progress = self.Glade.get_widget("progressbar_fetching") + dialog.show() + while gtk.events_pending(): + gtk.main_iteration() + + # clean most objects + self.packages = [] + self.dl_size = 0 + self.all_changes = {} + self.store.clear() + self.initCache() + self.list = UpdateList(self.main_window) + + # fill them again + self.list.update(self.cache, self.records, self.depcache) + if self.list.num_updates < 1: + # set the label and treeview and hide the checkbox column + self.cb.set_visible(False) + self.expander.hide() + label = self.Glade.get_widget("label_header") + text = "%s\n\n%s" % (_("Your system is up-to-date!"), + _("There are no updates available.")) + label.set_markup(text) + self.store.append([False, _("Your system is up-to-date!"), None, None, None, None, None]) + # make sure no install is possible + self.installbutton.set_sensitive(False) + else: + self.cb.set_visible(True) + self.expander.show() + self.treeview.set_headers_visible(False) + label = self.Glade.get_widget("label_header") + text = _("Available Updates\n" + "\n" + "The following packages are found to be upgradable. You can upgrade them by " + "using the Install button.") + label.set_markup(text) + i=0 + for pkg in self.list.pkgs: + if self.download_changes_at_startup: + progress.set_fraction(float(i)/len(self.list.pkgs)) + while gtk.events_pending(): + gtk.main_iteration() + lock = thread.allocate_lock() + self.all_changes[pkg.name] = self.get_changelog(pkg.name,lock) + + name = xml.sax.saxutils.escape(pkg.name) + summary = xml.sax.saxutils.escape(pkg.shortdesc) + contents = "%s\n%s\n\n" % (name, summary) + contents = contents + _("New version: %s") % (pkg.version) + "" + + iter = self.store.append([True, contents, pkg.name, pkg.shortdesc, pkg.version, pkg.longdesc, pkg]) + self.add_update(pkg) + i = i + 1 + + if self.download_changes_at_startup: + dialog.hide() + + self.update_count() + return False + + # FIXME: use lsb-release binary and cache the result + def get_dist(self): + f = open("/etc/lsb-release", "r") + lines = f.readlines() + for line in lines: + key, value = line.split("=") + if (key == "DISTRIB_CODENAME"): + return value[:-1] + f.close() + + def current_dist_not_supported(self, name): + #print name + msg = "%s\n\n%s" % (_("Your distribution is no longer supported"), _("Please upgrade to a newer version of Ubuntu Linux. The version you are running will no longer get security fixes or other critical updates. Please see http://www.ubuntulinux.org for upgrade information.")) + dialog = gtk.MessageDialog(self.main_window, 0, gtk.MESSAGE_WARNING, + gtk.BUTTONS_OK,"") + dialog.set_markup(msg) + dialog.run() + dialog.destroy() + + + def new_dist_available(self, name): + #print name + # check if the user already knowns about this dist + seen = self.gconfclient.get_string("/apps/update-manager/seen_dist") + if name == seen: + return + + msg = "%s\n\n%s" % (_("There is a new release of Ubuntu available!"), _("A new release with the codename '%s' is available. Please see http://www.ubuntulinux.org/ for upgrade instructions.") % name) + dialog = gtk.MessageDialog(self.main_window, 0, gtk.MESSAGE_INFO, + gtk.BUTTONS_CLOSE, "") + dialog.set_markup(msg) + check = gtk.CheckButton(_("Never show this message again")) + check.show() + dialog.vbox.pack_start(check) + dialog.run() + if check.get_active(): + self.gconfclient.set_string("/apps/update-manager/seen_dist",name) + dialog.destroy() + + # code that does the meta release file checking + def check_meta_release(self): + #print "check_meta_release" + current_dist = self.dist + dists = {} + if self.metarelease_information != None: + #print "meta_release found (current_dist: %s)" % (current_dist) + # we have a meta-release file + current_dist_date = 0 + current_dist_supported = False + new_dist_available = False + # parse it + index_tag = apt_pkg.ParseTagFile(self.metarelease_information) + step_result = index_tag.Step() + while step_result: + if index_tag.Section.has_key("Dist"): + dist = index_tag.Section["Dist"] + date = time.mktime(rfc822.parsedate(index_tag.Section["Date"])) + dists[dist] = date + if dist == current_dist: + current_dist_supported = str_to_bool(index_tag.Section["Supported"]) + current_dist_date = time.mktime(rfc822.parsedate(index_tag.Section["Date"])) + step_result = index_tag.Step() + # check for newer dists + new_dist = "" + found = False + for dist in dists: + if dist == current_dist: + found = True + if dists[dist] > current_dist_date and not dist == current_dist: + new_dist = dist + current_dist_date = dists[dist] + + # we know nothing about the installed distro, so we just return + # silently + if not found: + return False + + # only warn if unsupported and a new dist is available (because + # the development version is also unsupported) + if new_dist != "" and not current_dist_supported: + self.current_dist_not_supported(new_dist) + elif new_dist != "": + self.new_dist_available(new_dist) + # don't run this event again + return False + # we have no information about the meta-release, so run it again + return True + + # the network thread that tries to fetch the meta-index file + def get_meta_release(self): + lastmodified = 0 + req = urllib2.Request(METARELEASE_URI) + if os.access(METARELEASE_FILE, os.W_OK): + lastmodified = os.stat(METARELEASE_FILE).st_mtime + if lastmodified > 0: + req.add_header("If-Modified-Since", lastmodified) + try: + uri=urllib2.urlopen(req) + f=open(METARELEASE_FILE,"w+") + for line in uri.readlines(): + f.write(line) + f.flush() + f.seek(0,0) + self.metarelease_information=f + uri.close() + except urllib2.URLError: + pass + + # fixme: we should probably abstract away all the stuff from libapt + def initCache(self): + # get the lock + try: + apt_pkg.PkgSystemLock() + except SystemError: + d = gtk.MessageDialog(parent=self.main_window, + flags=gtk.DIALOG_MODAL, + type=gtk.MESSAGE_ERROR, + buttons=gtk.BUTTONS_OK) + d.set_markup("%s\n\n%s" % ( + _("Unable to get exclusive lock"), + _("This usually means that another package management " + "application (like apt-get or aptitude) already running. " + "Please close that application first"))) + res = d.run() + d.destroy() + sys.exit() + + self.cache = apt_pkg.GetCache() + #apt_pkg.Config.Set("Debug::pkgPolicy","1") + self.depcache = apt_pkg.GetDepCache(self.cache) + self.depcache.ReadPinFile() + if os.path.exists(SYNAPTIC_PINFILE): + self.depcache.ReadPinFile(SYNAPTIC_PINFILE) + self.depcache.Init() + self.records = apt_pkg.GetPkgRecords(self.cache) + + + def main(self): + # FIXME: stat a check update thread + self.metarelease_information = None + t=thread.start_new_thread(self.get_meta_release, ()) + gobject.timeout_add(1000, self.check_meta_release) + #self.get_meta_release() + + self.store.append([True, _("Initializing and getting list of updates..."), + None, None, None, None, None]) + + while gtk.events_pending(): + gtk.main_iteration() + + # global init of apt, FIXME: move all the apt details in it's own class + apt_pkg.init() + self.fillstore() + gtk.main() + + +if __name__ == "__main__": + + APP="update-manager" + DIR="@prefix@/share/locale" + gettext.bindtextdomain(APP, DIR) + gettext.textdomain(APP) + gtk.glade.bindtextdomain(APP, DIR) + gtk.glade.textdomain(APP) + _ = gettext.gettext + if os.geteuid() != 0: + dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, + _("You need to be root to run this program")) + dialog.run() + dialog.destroy() + sys.exit(1) + + if (len(sys.argv) > 1) and (sys.argv[1].strip() == "--download-changes-at-startup"): + updatemanager = UpdateManager(True) + else: + updatemanager = UpdateManager() + updatemanager.main() diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 00000000..3231ba98 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,12 @@ +import gobject +import gtk +import gtk.glade + +def error(parent, primary, secondary): + p = "%s" % primary + dialog = gtk.MessageDialog(parent,gtk.DIALOG_MODAL, + gtk.MESSAGE_ERROR,gtk.BUTTONS_OK,"") + dialog.set_markup(p); + dialog.format_secondary_text(secondary); + dialog.run() + dialog.hide() -- cgit v1.2.3