1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-12 15:27:14 +03:00
EDMarketConnector/update.py

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