import os from os.path import dirname, join import sys import threading from traceback import print_exc import semantic_version from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: import tkinter as tk # ensure registry is set up on Windows before we start from config import appname, appversion_nobuild, config, update_feed class EDMCVersion(object): """ Hold all the information about an EDMC version. Attributes ---------- version : str Full version string title: str Title of the release sv: semantic_version.base.Version semantic_version object for this version """ def __init__(self, version: str, title: str, sv: semantic_version.base.Version): self.version: str = version self.title: str = title self.sv: semantic_version.base.Version = sv class Updater(object): """ Updater class to handle checking for updates, whether using internal code or an external library such as WinSparkle on win32. """ def shutdown_request(self) -> None: """ Receive (Win)Sparkle shutdown request and send it to parent. :rtype: None """ if not config.shutting_down: self.root.event_generate('<>', when="tail") def use_internal(self) -> bool: """ :return: if internal update checks should be used. :rtype: bool """ if self.provider == 'internal': return True return False def __init__(self, tkroot: 'tk.Tk'=None, provider: str='internal'): """ :param tkroot: reference to the root window of the GUI :param provider: 'internal' or other string if not """ self.root: 'tk.Tk' = tkroot self.provider: str = provider self.thread: threading.Thread = None if self.use_internal(): return if sys.platform == 'win32': import ctypes try: self.updater = ctypes.cdll.WinSparkle # Set the appcast URL self.updater.win_sparkle_set_appcast_url(update_feed.encode()) # Set the appversion *without* build metadata, as WinSparkle # doesn't do proper Semantic Version checks. # NB: It 'accidentally' supports pre-release due to how it # splits and compares strings: # self.updater.win_sparkle_set_app_build_version(str(appversion_nobuild())) # set up shutdown callback global root root = tkroot self.callback_t = ctypes.CFUNCTYPE(None) # keep reference self.callback_fn = self.callback_t(self.shutdown_request) self.updater.win_sparkle_set_shutdown_request_callback(self.callback_fn) # Get WinSparkle running self.updater.win_sparkle_init() except Exception as ex: print_exc() self.updater = None return if sys.platform == 'darwin': import objc try: objc.loadBundle('Sparkle', globals(), join(dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework')) self.updater = SUUpdater.sharedUpdater() except: # can't load framework - not frozen or not included in app bundle? print_exc() self.updater = None def setAutomaticUpdatesCheck(self, onoroff: bool) -> None: """ Helper to set (Win)Sparkle to perform automatic update checks, or not. :param onoroff: bool for if we should have the library check or not. :return: None """ if self.use_internal(): return if sys.platform == 'win32' and self.updater: self.updater.win_sparkle_set_automatic_check_for_updates(onoroff) if sys.platform == 'darwin' and self.updater: self.updater.SUEnableAutomaticChecks(onoroff) def checkForUpdates(self) -> None: """ Trigger the requisite method to check for an update. :return: None """ if self.use_internal(): self.thread = threading.Thread(target = self.worker, name = 'update worker') self.thread.daemon = True self.thread.start() elif sys.platform == 'win32' and self.updater: self.updater.win_sparkle_check_update_with_ui() elif sys.platform == 'darwin' and self.updater: self.updater.checkForUpdates_(None) def check_appcast(self) -> Optional[EDMCVersion]: """ Manually (no Sparkle or WinSparkle) check the update_feed appcast file to see if any listed version is semantically greater than the current running version. :return: EDMCVersion or None if no newer version found """ import requests from xml.etree import ElementTree newversion = None items = {} try: r = requests.get(update_feed, timeout=10) except requests.RequestException as ex: print('Error retrieving update_feed file: {}'.format(str(ex)), file=sys.stderr) return None try: feed = ElementTree.fromstring(r.text) except SyntaxError as ex: print('Syntax error in update_feed file: {}'.format(str(ex)), file=sys.stderr) return None for item in feed.findall('channel/item'): ver = item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}version') # This will change A.B.C.D to A.B.C+D sv = semantic_version.Version.coerce(ver) items[sv] = EDMCVersion(version=ver, # sv might have mangled version title=item.find('title').text, sv=sv ) # Look for any remaining version greater than appversion simple_spec = semantic_version.SimpleSpec(f'>{appversion_nobuild()}') newversion = simple_spec.select(items.keys()) if newversion: return items[newversion] return None def worker(self) -> None: """ Thread worker to perform internal update checking and update GUI status if a newer version is found. :return: None """ newversion = self.check_appcast() if newversion: # TODO: Surely we can do better than this # nametowidget('.{}.status'.format(appname.lower()))['text'] self.root.nametowidget('.{}.status'.format(appname.lower()))['text'] = newversion.title + ' is available' self.root.update_idletasks() def close(self) -> None: """ Handles the EDMarketConnector.AppWindow.onexit() request. NB: We just 'pass' here because: 1) We might have a worker() going, but no way to make that co-operative to respond to a "please stop now" message. 2) If we're running frozen then we're using (Win)Sparkle to check and *it* might have asked this whole application to quit, in which case we don't want to ask *it* to quit :return: None """ pass