Refactor version check threading

This commit is contained in:
Raoul Snyman 2017-09-08 22:19:22 -07:00
parent cd158b63fd
commit a6324b6b7f
9 changed files with 171 additions and 124 deletions

View File

@ -20,13 +20,17 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import sys
"""
The entrypoint for OpenLP
"""
import faulthandler
import multiprocessing
import sys
from openlp.core.common import is_win, is_macosx
from openlp.core import main
faulthandler.enable()
if __name__ == '__main__':
"""

View File

@ -26,21 +26,19 @@ The :mod:`core` module provides all core application functions
All the core functions of the OpenLP application including the GUI, settings,
logging and a plugin framework are contained within the openlp.core module.
"""
import argparse
import logging
import os
import shutil
import sys
import time
from pathlib import Path
from traceback import format_exception
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
check_directory_exists, is_macosx, is_win, translate
from openlp.core.common.versionchecker import VersionThread, get_application_version
from openlp.core.version import check_for_update, get_version
from openlp.core.lib import ScreenList
from openlp.core.resources import qInitResources
from openlp.core.ui import SplashScreen
@ -154,8 +152,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
self.processEvents()
if not has_run_wizard:
self.main_window.first_time()
version = VersionThread(self.main_window)
version.start()
if Settings().value('core/update check'):
check_for_update(self.main_window)
self.main_window.is_display_blank()
self.main_window.app_startup()
return self.exec()
@ -183,22 +181,18 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
data_folder_path = str(AppLocation.get_data_path())
if not os.path.exists(data_folder_path):
log.critical('Database was not found in: ' + data_folder_path)
status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'),
translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}'
'\n\nThe location of the data folder was '
'previously changed from the OpenLP\'s '
'default location. If the data was stored on '
'removable device, that device needs to be '
'made available.\n\nYou may reset the data '
'location back to the default location, '
'or you can try to make the current location '
'available.\n\nDo you want to reset to the '
'default data location? If not, OpenLP will be '
'closed so you can try to fix the the problem.')
.format(path=data_folder_path),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No)
status = QtWidgets.QMessageBox.critical(
None, translate('OpenLP', 'Data Directory Error'),
translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}\n\nThe location of the data '
'folder was previously changed from the OpenLP\'s default location. If the data was '
'stored on removable device, that device needs to be made available.\n\nYou may reset '
'the data location back to the default location, or you can try to make the current '
'location available.\n\nDo you want to reset to the default data location? If not, '
'OpenLP will be closed so you can try to fix the the problem.').format(
path=data_folder_path),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No
)
if status == QtWidgets.QMessageBox.No:
# If answer was "No", return "True", it will shutdown OpenLP in def main
log.info('User requested termination')
@ -239,7 +233,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
:param can_show_splash: Should OpenLP show the splash screen
"""
data_version = Settings().value('core/application version')
openlp_version = get_application_version()['version']
openlp_version = get_version()['version']
# New installation, no need to create backup
if not has_run_wizard:
Settings().setValue('core/application version', openlp_version)
@ -415,7 +409,7 @@ def main(args=None):
Registry.create()
Registry().register('application', application)
Registry().set_flag('no_web_server', args.no_web_server)
application.setApplicationVersion(get_application_version()['version'])
application.setApplicationVersion(get_version()['version'])
# Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one
if application.is_already_running():
sys.exit()

View File

@ -27,7 +27,7 @@ import logging
from PyQt5 import QtCore
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings
from openlp.core.common.versionchecker import get_application_version
from openlp.core.version import get_version
log = logging.getLogger(__name__)
@ -139,7 +139,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
if version:
self.version = version
else:
self.version = get_application_version()['version']
self.version = get_version()['version']
self.settings_section = self.name
self.icon = None
self.media_item_class = media_item_class

55
openlp/core/threading.py Normal file
View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`openlp.core.threading` module contains some common threading code
"""
from PyQt5 import QtCore
def run_thread(parent, worker, prefix='', auto_start=True):
"""
Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase.
:param object parent: The parent object so that the thread and worker are not orphaned.
:param QObject worker: A QObject-based worker object which does the actual work.
:param str prefix: A prefix to be applied to the attribute names.
:param bool auto_start: Automatically start the thread. Defaults to True.
"""
# Set up attribute names
thread_name = 'thread'
worker_name = 'worker'
if prefix:
thread_name = '_'.join([prefix, thread_name])
worker_name = '_'.join([prefix, worker_name])
# Create the thread and add the thread and the worker to the parent
thread = QtCore.QThread()
setattr(parent, thread_name, thread)
setattr(parent, worker_name, worker)
# Move the worker into the thread's context
worker.moveToThread(thread)
# Connect slots and signals
parent.version_thread.started.connect(parent.version_worker.start)
parent.version_worker.quit.connect(parent.version_thread.quit)
parent.version_worker.quit.connect(parent.version_worker.deleteLater)
parent.version_thread.finished.connect(parent.version_thread.deleteLater)
if auto_start:
parent.version_thread.start()

View File

@ -26,7 +26,7 @@ import webbrowser
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.versionchecker import get_application_version
from openlp.core.version import get_version
from openlp.core.lib import translate
from .aboutdialog import UiAboutDialog
@ -49,7 +49,7 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
Set up the dialog. This method is mocked out in tests.
"""
self.setup_ui(self)
application_version = get_application_version()
application_version = get_version()
about_text = self.about_text_edit.toPlainText()
about_text = about_text.replace('<version>', application_version['version'])
if application_version['build']:

View File

@ -71,7 +71,7 @@ except ImportError:
VLC_VERSION = '-'
from openlp.core.common import Settings, UiStrings, translate
from openlp.core.common.versionchecker import get_application_version
from openlp.core.version import get_version
from openlp.core.common import RegistryProperties, is_linux
from .exceptiondialog import Ui_ExceptionDialog
@ -110,7 +110,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
"""
Create an exception report.
"""
openlp_version = get_application_version()
openlp_version = get_version()
description = self.description_text_edit.toPlainText()
traceback = self.exception_text_edit.toPlainText()
system = translate('OpenLP.ExceptionForm', 'Platform: {platform}\n').format(platform=platform.platform())

View File

@ -164,7 +164,6 @@ class GeneralTab(SettingsTab):
self.startup_layout.addWidget(self.show_splash_check_box)
self.check_for_updates_check_box = QtWidgets.QCheckBox(self.startup_group_box)
self.check_for_updates_check_box.setObjectName('check_for_updates_check_box')
self.check_for_updates_check_box.setVisible(False)
self.startup_layout.addWidget(self.check_for_updates_check_box)
self.right_layout.addWidget(self.startup_group_box)
# Logo

View File

@ -40,7 +40,6 @@ from openlp.core.api.http import server
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
check_directory_exists, translate, is_win, is_macosx, add_actions
from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.versionchecker import get_application_version
from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
from openlp.core.lib.ui import create_action
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
@ -51,6 +50,7 @@ from openlp.core.ui.printserviceform import PrintServiceForm
from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
from openlp.core.version import get_version
log = logging.getLogger(__name__)
@ -487,7 +487,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
The main window.
"""
openlp_version_check = QtCore.pyqtSignal(QtCore.QVariant)
log.info('MainWindow loaded')
def __init__(self):
@ -561,7 +560,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.application.set_busy_cursor()
# Simple message boxes
Registry().register_function('theme_update_global', self.default_theme_changed)
self.openlp_version_check.connect(self.version_notice)
Registry().register_function('config_screen_changed', self.screen_changed)
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
# Reset the cursor
@ -587,6 +585,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if saved_plugin_id != -1:
self.media_tool_box.setCurrentIndex(saved_plugin_id)
def on_new_version_number(self, version_number):
"""
Called when the version check thread completes and we need to check the version number
:param str version_number: The version number downloaded from the OpenLP server.
"""
def on_search_shortcut_triggered(self):
"""
Called when the search shortcut has been pressed.
@ -606,7 +611,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if widget:
widget.on_focus()
def version_notice(self, version):
def on_new_version(self, version):
"""
Notifies the user that a newer version of OpenLP is available.
Triggered by delay thread and cannot display popup.
@ -616,7 +621,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.debug('version_notice')
version_text = translate('OpenLP.MainWindow', 'Version {new} of OpenLP is now available for download (you are '
'currently running version {current}). \n\nYou can download the latest version from '
'http://openlp.org/.').format(new=version, current=get_application_version()[u'full'])
'http://openlp.org/.').format(new=version, current=get_version()[u'full'])
QtWidgets.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Version Updated'), version_text)
def show(self):
@ -973,7 +978,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# Add a header section.
# This is to insure it's our conf file for import.
now = datetime.now()
application_version = get_application_version()
application_version = get_version()
# Write INI format using Qsettings.
# Write our header.
export_settings.beginGroup(self.header_section)

View File

@ -20,24 +20,21 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`openlp.core.common` module downloads the version details for OpenLP.
The :mod:`openlp.core.version` module downloads the version details for OpenLP.
"""
import logging
import os
import platform
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime
from distutils.version import LooseVersion
from subprocess import Popen, PIPE
import requests
from PyQt5 import QtCore
from openlp.core.common import AppLocation, Registry, Settings
from openlp.core.common.httputils import ping
from openlp.core.common import AppLocation, Settings
from openlp.core.threading import run_thread
log = logging.getLogger(__name__)
@ -46,42 +43,87 @@ CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2
class VersionThread(QtCore.QThread):
class VersionWorker(QtCore.QObject):
"""
A special Qt thread class to fetch the version of OpenLP from the website.
This is threaded so that it doesn't affect the loading time of OpenLP.
A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it
doesn't affect the loading time of OpenLP.
"""
def __init__(self, main_window):
"""
Constructor for the thread class.
new_version = QtCore.pyqtSignal(dict)
no_internet = QtCore.pyqtSignal()
quit = QtCore.pyqtSignal()
:param main_window: The main window Object.
def __init__(self, last_check_date):
"""
log.debug("VersionThread - Initialise")
super(VersionThread, self).__init__(None)
self.main_window = main_window
Constructor for the version check worker.
def run(self):
:param string last_check_date: The last day we checked for a new version of OpenLP
"""
Run the thread.
log.debug('VersionWorker - Initialise')
super(VersionWorker, self).__init__(None)
self.last_check_date = last_check_date
def start(self):
"""
self.sleep(1)
log.debug('Version thread - run')
found = ping("openlp.io")
Registry().set_flag('internet_present', found)
update_check = Settings().value('core/update check')
if found:
Registry().execute('get_website_version')
if update_check:
app_version = get_application_version()
version = check_latest_version(app_version)
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
version2=LooseVersion(str(app_version['full']))))
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
self.main_window.openlp_version_check.emit('{version}'.format(version=version))
Check the latest version of OpenLP against the version file on the OpenLP site.
**Rules around versions and version files:**
* If a version number has a build (i.e. -bzr1234), then it is a nightly.
* If a version number's minor version is an odd number, it is a development release.
* If a version number's minor version is an even number, it is a stable release.
"""
log.debug('VersionWorker - Start')
# I'm not entirely sure why this was here, I'm commenting it out until I hit the same scenario
# time.sleep(1)
current_version = get_version()
download_url = 'http://www.openlp.org/files/version.txt'
if current_version['build']:
download_url = 'http://www.openlp.org/files/nightly_version.txt'
elif int(current_version['version'].split('.')[1]) % 2 != 0:
download_url = 'http://www.openlp.org/files/dev_version.txt'
headers = {
'User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'],
system=platform.system(),
release=platform.release())
}
remote_version = None
retries = 0
while retries < 3:
try:
response = requests.get(download_url, headers=headers)
remote_version = response.text
log.debug('New version found: %s', remote_version)
break
except requests.exceptions.ConnectionError:
log.exception('Unable to connect to OpenLP server to download version file')
self.no_internet.emit()
retries += 1
except requests.exceptions.RequestException:
log.exception('Error occurred while connecting to OpenLP server to download version file')
retries += 1
if remote_version and LooseVersion(remote_version) > LooseVersion(current_version['full']):
self.new_version.emit(remote_version)
self.quit.emit()
def get_application_version():
def check_for_update(parent):
"""
Run a thread to download and check the version of OpenLP
:param MainWindow parent: The parent object for the thread. Usually the OpenLP main window.
"""
last_check_date = Settings().value('core/last version test')
if datetime.date().strftime('%Y-%m-%d') <= last_check_date:
log.debug('Version check skipped, last checked today')
return
worker = VersionWorker(last_check_date)
worker.new_version.connect(parent.on_new_version)
# TODO: Use this to figure out if there's an Internet connection?
# worker.no_internet.connect(parent.on_no_internet)
run_thread(parent, worker, 'version')
def get_version():
"""
Returns the application version of the running instance of OpenLP::
@ -150,55 +192,3 @@ def get_application_version():
else:
log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['version']))
return APPLICATION_VERSION
def check_latest_version(current_version):
"""
Check the latest version of OpenLP against the version file on the OpenLP
site.
**Rules around versions and version files:**
* If a version number has a build (i.e. -bzr1234), then it is a nightly.
* If a version number's minor version is an odd number, it is a development release.
* If a version number's minor version is an even number, it is a stable release.
:param current_version: The current version of OpenLP.
"""
version_string = current_version['full']
# set to prod in the distribution config file.
settings = Settings()
settings.beginGroup('core')
last_test = settings.value('last version test')
this_test = str(datetime.now().date())
settings.setValue('last version test', this_test)
settings.endGroup()
if last_test != this_test:
if current_version['build']:
req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt')
else:
version_parts = current_version['version'].split('.')
if int(version_parts[1]) % 2 != 0:
req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt')
else:
req = urllib.request.Request('http://www.openlp.org/files/version.txt')
req.add_header('User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'],
system=platform.system(),
release=platform.release()))
remote_version = None
retries = 0
while True:
try:
remote_version = str(urllib.request.urlopen(req, None,
timeout=CONNECTION_TIMEOUT).read().decode()).strip()
except (urllib.error.URLError, ConnectionError):
if retries > CONNECTION_RETRIES:
log.exception('Failed to download the latest OpenLP version file')
else:
retries += 1
time.sleep(0.1)
continue
break
if remote_version:
version_string = remote_version
return version_string