1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-06-12 21:32:29 +03:00

Updater class now used for all updates checking

* Updater.__init__() now takes 'provider' argument to specify if we use
 the internal checking code, or the available external code.
* EDMC.py changed to utilise this with internal provider.
* EDMarketConnector.py changed to use internal provider if not frozen,
 else the internal provider.
* Corrected the darwin/MacOS toggling of auto updates checking to actually
 use the Sparkle, not WinSparkle, API call.
* Updater.check_appcast() does the internal checking:
    * class EDMCVersion to hold the information.
    * Returns None on any error, or if it didn't find a newer version.
    * Returns an EDMCVersion object if it found a newer version.
This commit is contained in:
Athanasius 2020-07-18 16:45:06 +01:00
parent 76a6eec69d
commit 35f573bc14
3 changed files with 178 additions and 156 deletions

44
EDMC.py
View File

@ -12,11 +12,9 @@ import os
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
os.environ["EDMC_NO_UI"] = "1"
from os.path import dirname, getmtime, join
from os.path import getmtime, join
from time import time, sleep
from xml.etree import ElementTree
import re
import semantic_version
import l10n
l10n.Translations.install_dummy()
@ -30,7 +28,8 @@ import loadout
import edshipyard
import shipyard
import stats
from config import appcmdname, appversion, update_feed, config
from config import appcmdname, appversion, config
from update import Updater, EDMCVersion
from monitor import monitor
sys.path.append(config.internal_plugin_dir)
@ -63,41 +62,12 @@ try:
args = parser.parse_args()
if args.version:
newversion = None
try:
r = requests.get(update_feed, timeout = 10)
except requests.RequestException as ex:
sys.stderr.write('Error retrieving update_feed file: {}\n'.format(str(ex)))
else:
try:
feed = ElementTree.fromstring(r.text)
except SyntaxError as ex:
sys.stderr.write('Syntax error in update_feed file: {}\n'.format(str(ex)))
else:
# Want to make items['<version>'] = {'os': '...', 'title': '...' }
items = dict(
[
(
item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}version'),
{'title': item.find('title').text,
'os': item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}os'),
}
) for item in feed.findall('channel/item')
]
)
# Filter on the sparkle:os attribute
os_map = {'darwin': 'macos', 'win32': 'windows', 'linux': 'linux'} # Map sys.platform to sparkle:os
items = { k:v for (k,v) in items.items() if v['os'] == os_map[sys.platform]}
# Look for any remaining version greater than appversion
simple_spec = semantic_version.SimpleSpec('>' + appversion)
newversion = simple_spec.select([semantic_version.Version.coerce(x) for x in items])
updater = Updater(provider='internal')
newversion: EDMCVersion = updater.check_appcast()
if newversion:
print('{CURRENT} ("{UPDATE}" is available)'.format(
CURRENT=appversion,
UPDATE=items[str(newversion)]['title']))
UPDATE=newversion.title))
else:
print(appversion)
sys.exit(EXIT_SUCCESS)
@ -121,7 +91,7 @@ try:
if __debug__:
print('Invalid journal entry "%s"' % repr(line))
except Exception as e:
sys.stderr.write("Can't read Journal file: {}\n".format(str(e)))
print("Can't read Journal file: {}\n".format(str(e)), file=sys.stderr)
sys.exit(EXIT_SYS_ERR)
if not monitor.cmdr:

View File

@ -283,9 +283,12 @@ class AppWindow(object):
# Load updater after UI creation (for WinSparkle)
import update
self.updater = update.Updater(self.w)
if not getattr(sys, 'frozen', False):
self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps
if getattr(sys, 'frozen', False):
# Running in frozen .exe, so use WinSparkle
self.updater = update.Updater(tkroot=self.w, provider='external')
else:
self.updater = update.Updater(tkroot=self.w, provider='internal')
self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps
try:
config.get_password('') # Prod SecureStorage on Linux to initialise

281
update.py
View File

@ -4,120 +4,69 @@ import sys
import threading
from traceback import print_exc
import semantic_version
from typing import Optional
import tkinter as tk
# ensure registry is set up on Windows before we start
from config import appname, appversion, appversion_nobuild, update_feed, update_interval, config
from config import appname, appversion, appversion_nobuild, update_feed
class EDMCVersion(object):
"""
Hold all the information about an EDMC version.
if not getattr(sys, 'frozen', False):
# Running from source
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
#TODO: Update this to use Semantic Version as per EDMC.py args.version check
class Updater(object):
class Updater(object):
"""
Updater class to handle checking for updates, whether using internal code
or an external library such as WinSparkle on win32.
"""
def __init__(self, master):
self.root = master
def shutdown_request(self) -> None:
"""
Receive (Win)Sparkle shutdown request and send it to parent.
:rtype: None
"""
self.root.event_generate('<<Quit>>', when="tail")
def setAutomaticUpdatesCheck(self, onoroff):
return
def use_internal(self) -> bool:
"""
:return: if internal update checks should be used.
:rtype: bool
"""
if self.provider == 'internal':
return True
def checkForUpdates(self):
thread = threading.Thread(target = self.worker, name = 'update worker')
thread.daemon = True
thread.start()
return False
def check_appcast(self) -> dict:
import requests
from xml.etree import ElementTree
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
newversion = None
try:
r = requests.get(update_feed, timeout=10)
except requests.RequestException as ex:
sys.stderr.write('Error retrieving update_feed file: {}\n'.format(str(ex)))
else:
try:
feed = ElementTree.fromstring(r.text)
except SyntaxError as ex:
sys.stderr.write('Syntax error in update_feed file: {}\n'.format(str(ex)))
else:
items = dict()
for item in feed.findall('channel/item'):
ver = item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}version')
sv = semantic_version.Version.coerce(ver)
os = item.find('enclosure').attrib.get('{http://www.andymatuschak.org/xml-namespaces/sparkle}os')
os_map = {'darwin': 'macos', 'win32': 'windows', 'linux' : 'linux'} # Map sys.platform to sparkle:os
if os == os_map[sys.platform]:
items[sv] = {
'version': ver,
'title': item.find('title').text,
}
# Look for any remaining version greater than appversion
simple_spec = semantic_version.SimpleSpec('>' + appversion)
newversion = simple_spec.select(items.keys())
return items[newversion]
def worker(self):
newversion = self.check_appcast()
if newversion:
self.root.nametowidget('.{}.status'.format(appname.lower()))['text'] = newversion['title'] + ' is available'
self.root.update_idletasks()
def close(self):
pass
elif sys.platform=='darwin':
import objc
class Updater(object):
# http://sparkle-project.org/documentation/customization/
def __init__(self, master):
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):
if self.updater:
self.updater.win_sparkle_set_automatic_check_for_updates(onoroff)
def checkForUpdates(self):
if self.updater:
self.updater.checkForUpdates_(None)
def close(self):
self.updater = None
elif sys.platform=='win32':
import ctypes
# https://github.com/vslavik/winsparkle/blob/master/include/winsparkle.h#L272
root = None
def shutdown_request():
root.event_generate('<<Quit>>', when="tail")
class Updater(object):
# https://github.com/vslavik/winsparkle/wiki/Basic-Setup
def __init__(self, master):
try:
sys.frozen # don't want to try updating python.exe
self.updater = ctypes.cdll.WinSparkle
# Set the appcast URL
@ -132,9 +81,9 @@ elif sys.platform=='win32':
# set up shutdown callback
global root
root = master
self.callback_t = ctypes.CFUNCTYPE(None) # keep reference
self.callback_fn = self.callback_t(shutdown_request)
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
@ -144,15 +93,115 @@ elif sys.platform=='win32':
print_exc()
self.updater = None
def setAutomaticUpdatesCheck(self, onoroff):
if self.updater:
self.updater.win_sparkle_set_automatic_check_for_updates(onoroff)
return
def checkForUpdates(self):
if self.updater:
self.updater.win_sparkle_check_update_with_ui()
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 close(self):
if self.updater:
self.updater.win_sparkle_cleanup()
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)
else:
try:
feed = ElementTree.fromstring(r.text)
except SyntaxError as ex:
print('Syntax error in update_feed file: {}'.format(str(ex)), file=sys.stderr)
else:
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('>' + appversion)
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