mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-12 15:27:14 +03:00
215 lines
7.2 KiB
Python
215 lines
7.2 KiB
Python
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('<<Quit>>', 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:
|
|
# <https://github.com/vslavik/winsparkle/issues/214>
|
|
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
|