4735: Check for updates in a separate thread

svn: r22924
This commit is contained in:
Nick Hall 2013-08-28 23:32:30 +00:00
parent 314cf24850
commit b0a4f75106
6 changed files with 360 additions and 261 deletions

View File

@ -358,6 +358,7 @@ class DisplayState(gen.utils.Callback):
'filters-changed' : (str, ),
'filter-name-changed' : (str, unicode, unicode),
'nameformat-changed' : None,
'update-available' : (list, ),
}
#nav_type to message

View File

@ -32,6 +32,15 @@ General utility functions useful for the generic plugin system
import locale
import sys
import os
import datetime
#-------------------------------------------------------------------------
#
# set up logging
#
#-------------------------------------------------------------------------
import logging
LOG = logging.getLogger(".gen.plug")
#-------------------------------------------------------------------------
#
@ -41,6 +50,9 @@ import os
from gen.plug._pluginreg import make_environment
import const
import Utils
from gen.plug import BasePluginManager
from gen.utils.configmanager import safe_eval
import config
from gen.ggettext import gettext as _
#-------------------------------------------------------------------------
@ -188,6 +200,96 @@ class Zipfile(object):
"""
return os.path.split(name)[1]
def available_updates():
whattypes = config.get('behavior.check-for-update-types')
if sys.version_info[0] < 3:
from urllib2 import urlopen
else:
from urllib.request import urlopen
LOG.debug("Checking for updated addons...")
langs = []
lang = locale.getlocale()[0] # not None
if lang:
langs.append(lang)
if "_" in lang:
lang, variation = lang.split("_", 1)
langs.append(lang)
langs.append("en")
# now we have a list of languages to try:
fp = None
for lang in langs:
URL = ("%s/listings/addons-%s.txt" %
(config.get("behavior.addons-url"), lang))
LOG.debug(" trying: %s" % URL)
try:
fp = urlopen(URL, timeout=10) # abort after 10 seconds
except:
try:
URL = ("%s/listings/addons-%s.txt" %
(config.get("behavior.addons-url"), lang[:2]))
fp = urlopen(URL, timeout=10)
except Exception as err: # some error
LOG.warn("Failed to open %s: %s" % (lang, err))
fp = None
if fp and fp.getcode() == 200: # ok
break
pmgr = BasePluginManager.get_instance()
addon_update_list = []
if fp and fp.getcode() == 200:
lines = list(fp.readlines())
count = 0
for line in lines:
line = line.decode('utf-8')
try:
plugin_dict = safe_eval(line)
if type(plugin_dict) != type({}):
raise TypeError("Line with addon metadata is not "
"a dictionary")
except:
LOG.warning("Skipped a line in the addon listing: " +
str(line))
continue
id = plugin_dict["i"]
plugin = pmgr.get_plugin(id)
if plugin:
LOG.debug("Comparing %s > %s" %
(version_str_to_tup(plugin_dict["v"], 3),
version_str_to_tup(plugin.version, 3)))
if (version_str_to_tup(plugin_dict["v"], 3) >
version_str_to_tup(plugin.version, 3)):
LOG.debug(" Downloading '%s'..." % plugin_dict["z"])
if "update" in whattypes:
if (not config.get('behavior.do-not-show-previously-seen-updates') or
plugin_dict["i"] not in config.get('behavior.previously-seen-updates')):
addon_update_list.append((_("Updated"),
"%s/download/%s" %
(config.get("behavior.addons-url"),
plugin_dict["z"]),
plugin_dict))
else:
LOG.debug(" '%s' is ok" % plugin_dict["n"])
else:
LOG.debug(" '%s' is not installed" % plugin_dict["n"])
if "new" in whattypes:
if (not config.get('behavior.do-not-show-previously-seen-updates') or
plugin_dict["i"] not in config.get('behavior.previously-seen-updates')):
addon_update_list.append((_("New"),
"%s/download/%s" %
(config.get("behavior.addons-url"),
plugin_dict["z"]),
plugin_dict))
config.set("behavior.last-check-for-updates",
datetime.date.today().strftime("%Y/%m/%d"))
count += 1
if fp:
fp.close()
else:
LOG.debug("Checking Addons Failed")
LOG.debug("Done checking!")
return addon_update_list
def load_addon_file(path, callback=None):
"""
Load an addon from a particular path (from URL or file system).

View File

@ -58,6 +58,9 @@ import ManagedWindow
from gui.widgets import MarkupLabel, BasicLabel
from QuestionDialog import ErrorDialog, QuestionDialog2, OkDialog
from glade import Glade
from gen.plug.utils import available_updates
from gui.plug import PluginWindows
from Errors import WindowActiveError
#-------------------------------------------------------------------------
#
@ -1142,12 +1145,39 @@ class GrampsPreferences(ConfigureDialog):
table.attach(checkbutton, 1, 2, 9, 10, yoptions=0)
button = gtk.Button(_("Check now"))
button.connect("clicked", lambda obj: \
self.uistate.viewmanager.check_for_updates(force=True))
button.connect("clicked", self.check_for_updates)
table.attach(button, 3, 4, 9, 10, yoptions=0)
return _('General'), table
def check_for_updates(self, button):
try:
addon_update_list = available_updates()
except:
OkDialog(_("Checking Addons Failed"),
_("The addon repository appears to be unavailable. "
"Please try again later."),
self.window)
return
if len(addon_update_list) > 0:
try:
PluginWindows.UpdateAddons(self.uistate, [], addon_update_list)
except WindowActiveError:
pass
else:
check_types = config.get('behavior.check-for-update-types')
OkDialog(_("There are no available addons of this type"),
_("Checked for '%s'") %
_("' and '").join([_(t) for t in check_types]),
self.window)
# List of translated strings used here
# Dead code for l10n
_('new'), _('update')
self.uistate.viewmanager.do_reg_plugins(self.dbstate, self.uistate)
def add_famtree_panel(self, configdialog):
table = gtk.Table(2, 2)
table.set_border_width(12)

View File

@ -32,6 +32,14 @@ import traceback
import os
import sys
#-------------------------------------------------------------------------
#
# set up logging
#
#-------------------------------------------------------------------------
import logging
LOG = logging.getLogger(".gui.plug")
#-------------------------------------------------------------------------
#
# GTK modules
@ -50,14 +58,19 @@ import ManagedWindow
import Errors
from gen.plug import PluginRegister, PTYPE_STR, load_addon_file
from gen.ggettext import gettext as _
from gen.ggettext import ngettext
from gui.utils import open_file_with_default_application
from gui.pluginmanager import GuiPluginManager
from gui.plug import tool, add_gui_options
from QuestionDialog import InfoDialog
from QuestionDialog import InfoDialog, OkDialog
from gui.editors import EditPerson
from glade import Glade
from ListModel import ListModel, NOSORT, TOGGLE
import Utils
import const
import config
from gui.widgets.progressdialog import (LongOpStatus, ProgressMonitor,
GtkProgressDialog)
def display_message(message):
"""
@ -1041,3 +1054,176 @@ class ToolManagedWindow(tool.Tool, ToolManagedWindowBase):
tool.Tool.__init__(self, dbstate, options_class, name)
ToolManagedWindowBase.__init__(self, dbstate, uistate, options_class,
name, callback)
#-------------------------------------------------------------------------
#
# UpdateAddons
#
#-------------------------------------------------------------------------
class UpdateAddons(ManagedWindow.ManagedWindow):
def __init__(self, uistate, track, addon_update_list):
self.title = _('Available Gramps Updates for Addons')
ManagedWindow.ManagedWindow.__init__(self, uistate, track,
self.__class__)
glade = Glade("updateaddons.glade")
self.update_dialog = glade.toplevel
self.set_window(self.update_dialog, glade.get_object('title'),
self.title)
self.window.set_size_request(750, 400)
apply_button = glade.get_object('apply')
cancel_button = glade.get_object('cancel')
select_all = glade.get_object('select_all')
select_all.connect("clicked", self.select_all_clicked)
select_none = glade.get_object('select_none')
select_none.connect("clicked", self.select_none_clicked)
apply_button.connect("clicked", self.install_addons)
cancel_button.connect("clicked", self.close)
self.list = ListModel(glade.get_object("list"), [
# name, click?, width, toggle
{"name": _('Select'),
"width": 60,
"type": TOGGLE,
"visible_col": 6,
"editable": True}, # 0 selected?
(_('Type'), 1, 180), # 1 new gramplet
(_('Name'), 2, 200), # 2 name (version)
(_('Description'), 3, 200), # 3 description
('', NOSORT, 0), # 4 url
('', NOSORT, 0), # 5 id
{"name": '', "type": TOGGLE}, # 6 visible? bool
], list_mode="tree")
pos = None
addon_update_list.sort(key=lambda x: "%s %s" % (x[0], x[2]["t"]))
last_category = None
for (status,plugin_url,plugin_dict) in addon_update_list:
count = get_count(addon_update_list, plugin_dict["t"])
category = _("%(adjective)s: %(addon)s") % {
"adjective": status,
"addon": _(plugin_dict["t"])}
if last_category != category:
last_category = category
node = self.list.add([False, # initially selected?
category,
"",
"",
"",
"",
False]) # checkbox visible?
iter = self.list.add([False, # initially selected?
"%s %s" % (status, _(plugin_dict["t"])),
"%s (%s)" % (plugin_dict["n"],
plugin_dict["v"]),
plugin_dict["d"],
plugin_url,
plugin_dict["i"],
True], node=node)
if pos is None:
pos = iter
if pos:
self.list.selection.select_iter(pos)
self.update_dialog.run()
def build_menu_names(self, obj):
return (self.title, "")
def select_all_clicked(self, widget):
"""
Select all of the addons for download.
"""
self.list.model.foreach(update_rows, True)
self.list.tree.expand_all()
def select_none_clicked(self, widget):
"""
Select none of the addons for download.
"""
self.list.model.foreach(update_rows, False)
self.list.tree.expand_all()
def install_addons(self, obj):
"""
Process all of the selected addons.
"""
self.update_dialog.hide()
model = self.list.model
iter = model.get_iter_first()
length = 0
while iter:
iter = model.iter_next(iter)
if iter:
length += model.iter_n_children(iter)
longop = LongOpStatus(
_("Downloading and installing selected addons..."),
length, 1, # total, increment-by
can_cancel=True)
pm = ProgressMonitor(GtkProgressDialog,
("Title", self.window, gtk.DIALOG_MODAL))
pm.add_op(longop)
count = 0
if not config.get('behavior.do-not-show-previously-seen-updates'):
# reset list
config.get('behavior.previously-seen-updates')[:] = []
iter = model.get_iter_first()
while iter:
for rowcnt in range(model.iter_n_children(iter)):
child = model.iter_nth_child(iter, rowcnt)
row = [model.get_value(child, n) for n in range(6)]
if longop.should_cancel():
break
elif row[0]: # toggle on
load_addon_file(row[4], callback=LOG.debug)
count += 1
else: # add to list of previously seen, but not installed
if row[5] not in config.get('behavior.previously-seen-updates'):
config.get('behavior.previously-seen-updates').append(row[5])
longop.heartbeat()
pm._get_dlg()._process_events()
iter = model.iter_next(iter)
if not longop.was_cancelled():
longop.end()
if count:
OkDialog(_("Done downloading and installing addons"),
"%s %s" % (ngettext("%d addon was installed.",
"%d addons were installed.",
count) % count,
_("You need to restart Gramps to see new views.")),
self.window)
else:
OkDialog(_("Done downloading and installing addons"),
_("No addons were installed."),
self.window)
self.close()
#-------------------------------------------------------------------------
#
# Local Functions
#
#-------------------------------------------------------------------------
def update_rows(model, path, iter, user_data):
"""
Update the rows of a model.
"""
#path: (8,) iter: <GtkTreeIter at 0xbfa89fa0>
#path: (8, 0) iter: <GtkTreeIter at 0xbfa89f60>
if len(path) == 2:
row = model[path]
row[0] = user_data
model.row_changed(path, iter)
def get_count(addon_update_list, category):
"""
Get the count of matching category items.
"""
count = 0
for (status,plugin_url,plugin_dict) in addon_update_list:
if plugin_dict["t"] == category and plugin_url:
count += 1
return count

View File

@ -32,6 +32,7 @@ Utility functions that depend on GUI components or for GUI components
import os
import sys
import subprocess
import threading
from gen.ggettext import gettext as _
import constfunc
# gtk is not included here, because this file is currently imported
@ -43,6 +44,7 @@ import constfunc
# GNOME/GTK
#
#-------------------------------------------------------------------------
import gobject
#-------------------------------------------------------------------------
#
@ -52,6 +54,7 @@ import constfunc
import gen.lib
import Errors
import constfunc
from gen.plug.utils import available_updates
#-------------------------------------------------------------------------
#
@ -391,3 +394,22 @@ def is_right_click(event):
if event.button == 3:
return True
#-------------------------------------------------------------------------
#
# AvailableUpdates
#
#-------------------------------------------------------------------------
class AvailableUpdates(threading.Thread):
def __init__(self, uistate):
threading.Thread.__init__(self)
self.uistate = uistate
self.addon_update_list = []
def emit_update_available(self):
self.uistate.emit('update-available', (self.addon_update_list, ))
def run(self):
self.addon_update_list = available_updates()
if len(self.addon_update_list) > 0:
gobject.idle_add(self.emit_update_available)

View File

@ -40,7 +40,6 @@ import os
import time
import datetime
from gen.ggettext import sgettext as _
from gen.ggettext import ngettext
from cStringIO import StringIO
from collections import defaultdict
import sys
@ -73,7 +72,7 @@ from gen.plug import REPORT
from gen.plug.report._constants import standalone_categories
from gui.plug import (PluginWindows, ReportPluginDialog, ToolPluginDialog)
from gui.plug.report import report
from gen.plug.utils import version_str_to_tup, load_addon_file
from gui.utils import AvailableUpdates
from gui.pluginmanager import GuiPluginManager
import Relationship
import DisplayState
@ -95,7 +94,6 @@ from gen.db.exceptions import DbException
from gui.aboutdialog import GrampsAboutDialog
from gui.navigator import Navigator
from gui.views.tags import Tags
from gen.utils.configmanager import safe_eval
#-------------------------------------------------------------------------
#
@ -228,32 +226,6 @@ WIKI_HELP_PAGE_FAQ = '%s_-_FAQ' % const.URL_MANUAL_PAGE
WIKI_HELP_PAGE_KEY = '%s_-_Keybindings' % const.URL_MANUAL_PAGE
WIKI_HELP_PAGE_MAN = '%s' % const.URL_MANUAL_PAGE
#-------------------------------------------------------------------------
#
# Local Functions
#
#-------------------------------------------------------------------------
def update_rows(model, path, iter, user_data):
"""
Update the rows of a model.
"""
#path: (8,) iter: <GtkTreeIter at 0xbfa89fa0>
#path: (8, 0) iter: <GtkTreeIter at 0xbfa89f60>
if len(path) == 2:
row = model[path]
row[0] = user_data
model.row_changed(path, iter)
def get_count(addon_update_list, category):
"""
Get the count of matching category items.
"""
count = 0
for (status,plugin_url,plugin_dict) in addon_update_list:
if plugin_dict["t"] == category and plugin_url:
count += 1
return count
#-------------------------------------------------------------------------
#
# ViewManager
@ -333,19 +305,18 @@ class ViewManager(CLIManager):
self.rel_class = Relationship.get_relationship_calculator()
self.uistate.set_relationship_class()
# Need to call after plugins have been registered
self.uistate.connect('update-available', self.process_updates)
self.check_for_updates()
def check_for_updates(self, force=False):
def check_for_updates(self):
"""
Check for add-on updates.
"""
howoften = config.get("behavior.check-for-updates")
whattypes = config.get('behavior.check-for-update-types')
update = False
if force:
update = True
elif howoften != 0: # update never if zero
y,m,d = map(int,
config.get("behavior.last-check-for-updates").split("/"))
if howoften != 0: # update never if zero
y,m,d = list(map(int,
config.get("behavior.last-check-for-updates").split("/")))
days = (datetime.date.today() - datetime.date(y, m, d)).days
if howoften == 1 and days >= 30: # once a month
update = True
@ -355,233 +326,20 @@ class ViewManager(CLIManager):
update = True
elif howoften == 4: # always
update = True
if update:
import urllib2, locale
LOG.debug("Checking for updated addons...")
langs = []
lang = locale.getlocale()[0] # not None
if lang:
langs.append(lang)
if "_" in lang:
lang, variation = lang.split("_", 1)
langs.append(lang)
langs.append("en")
# now we have a list of languages to try:
fp = None
for lang in langs:
URL = "%s/listings/addons-%s.txt" % (config.get("behavior.addons-url"), lang)
LOG.debug(" trying: %s" % URL)
try:
fp = urllib2.urlopen(URL, timeout=10) # wait up to 10 seconds
except: # some error
LOG.debug(" IOError!")
fp = None
if fp and fp.getcode() == 200: # ok
break
addon_update_list = []
if fp and fp.getcode() == 200:
lines = list(fp.readlines())
count = 0
for line in lines:
try:
plugin_dict = safe_eval(line)
if type(plugin_dict) != type({}):
raise TypeError("Line with addon metadata is not "
"a dictionary")
except:
LOG.debug("Skipped a line in the addon listing: " +
str(line))
continue
id = plugin_dict["i"]
plugin = self._pmgr.get_plugin(id)
if plugin:
LOG.debug("Comparing %s > %s" %
(version_str_to_tup(plugin_dict["v"], 3),
version_str_to_tup(plugin.version, 3)))
if (version_str_to_tup(plugin_dict["v"], 3) >
version_str_to_tup(plugin.version, 3)):
LOG.debug(" Downloading '%s'..." % plugin_dict["z"])
if "update" in whattypes:
if (not config.get('behavior.do-not-show-previously-seen-updates') or
plugin_dict["i"] not in config.get('behavior.previously-seen-updates')):
addon_update_list.append((_("Updated"),
"%s/download/%s" %
(config.get("behavior.addons-url"),
plugin_dict["z"]),
plugin_dict))
else:
LOG.debug(" '%s' is ok" % plugin_dict["n"])
else:
LOG.debug(" '%s' is not installed" % plugin_dict["n"])
if "new" in whattypes:
if (not config.get('behavior.do-not-show-previously-seen-updates') or
plugin_dict["i"] not in config.get('behavior.previously-seen-updates')):
addon_update_list.append((_("New"),
"%s/download/%s" %
(config.get("behavior.addons-url"),
plugin_dict["z"]),
plugin_dict))
config.set("behavior.last-check-for-updates",
datetime.date.today().strftime("%Y/%m/%d"))
count += 1
else:
from QuestionDialog import OkDialog
OkDialog(_("Checking Addons Failed"),
_("The addon repository appears to be unavailable. Please try again later."),
self.window)
if fp:
fp.close()
return
LOG.debug("Done checking!")
# List of translated strings used here
# Dead code for l10n
_('new'), _('update')
if addon_update_list:
self.update_addons(addon_update_list)
elif force:
from QuestionDialog import OkDialog
OkDialog(_("There are no available addons of this type"),
_("Checked for '%s'") %
_("' and '").join([_(t) for t in config.get('behavior.check-for-update-types')]),
self.window)
AvailableUpdates(self.uistate).start()
def update_addons(self, addon_update_list):
from glade import Glade
import ManagedWindow
from ListModel import ListModel, NOSORT, TOGGLE
glade = Glade("updateaddons.glade")
self.update_dialog = glade.toplevel
ManagedWindow.set_titles(self.update_dialog,
glade.get_object('title'),
_('Available Gramps Updates for Addons'))
apply_button = glade.get_object('apply')
cancel_button = glade.get_object('cancel')
select_all = glade.get_object('select_all')
select_all.connect("clicked", self.select_all_clicked)
select_none = glade.get_object('select_none')
select_none.connect("clicked", self.select_none_clicked)
apply_button.connect("clicked", self.install_addons)
cancel_button.connect("clicked",
lambda obj: self.update_dialog.destroy())
self.list = ListModel(glade.get_object("list"), [
# name, click?, width, toggle
{"name": _('Select'),
"width": 60,
"type": TOGGLE,
"visible_col": 6,
"editable": True}, # 0 selected?
(_('Type'), 1, 180), # 1 new gramplet
(_('Name'), 2, 200), # 2 name (version)
(_('Description'), 3, 200), # 3 description
('', NOSORT, 0), # 4 url
('', NOSORT, 0), # 5 id
{"name": '', "type": TOGGLE}, # 6 visible? bool
], list_mode="tree")
pos = None
addon_update_list.sort(key=lambda x: "%s %s" % (x[0], x[2]["t"]))
last_category = None
for (status,plugin_url,plugin_dict) in addon_update_list:
count = get_count(addon_update_list, plugin_dict["t"])
category = _("%(adjective)s: %(addon)s") % {
"adjective": status,
"addon": _(plugin_dict["t"])}
if last_category != category:
last_category = category
node = self.list.add([False, # initially selected?
category,
"",
"",
"",
"",
False]) # checkbox visible?
iter = self.list.add([False, # initially selected?
"%s %s" % (status, _(plugin_dict["t"])),
"%s (%s)" % (plugin_dict["n"],
plugin_dict["v"]),
plugin_dict["d"],
plugin_url,
plugin_dict["i"],
True], node=node)
if pos is None:
pos = iter
if pos:
self.list.selection.select_iter(pos)
self.update_dialog.run()
def select_all_clicked(self, widget):
def process_updates(self, addon_update_list):
"""
Select all of the addons for download.
Called when add-on updates are available.
"""
self.list.model.foreach(update_rows, True)
self.list.tree.expand_all()
try:
PluginWindows.UpdateAddons(self.uistate, [], addon_update_list)
except WindowActiveError:
pass
def select_none_clicked(self, widget):
"""
Select none of the addons for download.
"""
self.list.model.foreach(update_rows, False)
self.list.tree.expand_all()
def install_addons(self, obj):
"""
Process all of the selected addons.
"""
from QuestionDialog import OkDialog
from gui.widgets.progressdialog import LongOpStatus
self.update_dialog.hide()
model = self.list.model
iter = model.get_iter_first()
length = 0
while iter:
iter = model.iter_next(iter)
if iter:
length += model.iter_n_children(iter)
longop = LongOpStatus(
_("Downloading and installing selected addons..."),
length, 1, # total, increment-by
can_cancel=True)
pm = ProgressMonitor(GtkProgressDialog,
("Title", self.window, gtk.DIALOG_MODAL))
pm.add_op(longop)
count = 0
if not config.get('behavior.do-not-show-previously-seen-updates'):
# reset list
config.get('behavior.previously-seen-updates')[:] = []
iter = model.get_iter_first()
while iter:
for rowcnt in range(model.iter_n_children(iter)):
child = model.iter_nth_child(iter, rowcnt)
row = [model.get_value(child, n) for n in range(6)]
if longop.should_cancel():
break
elif row[0]: # toggle on
load_addon_file(row[4], callback=LOG.debug)
count += 1
else: # add to list of previously seen, but not installed
if row[5] not in config.get('behavior.previously-seen-updates'):
config.get('behavior.previously-seen-updates').append(row[5])
longop.heartbeat()
pm._get_dlg()._process_events()
iter = model.iter_next(iter)
if not longop.was_cancelled():
longop.end()
if count:
self.do_reg_plugins(self.dbstate, self.uistate)
OkDialog(_("Done downloading and installing addons"),
"%s %s" % (ngettext("%d addon was installed.",
"%d addons were installed.",
count) % count,
_("You need to restart Gramps to see new views.")),
self.window)
else:
OkDialog(_("Done downloading and installing addons"),
_("No addons were installed."),
self.window)
self.update_dialog.destroy()
self.do_reg_plugins(self.dbstate, self.uistate)
def _errordialog(self, title, errormessage):
"""