diff --git a/openlp.py b/openlp.py index 7ede25519..68001f2d1 100755 --- a/openlp.py +++ b/openlp.py @@ -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__': """ diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index af29cddae..d7c21026e 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -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() diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index b06e0fbd4..d06385864 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -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 diff --git a/openlp/core/threading.py b/openlp/core/threading.py new file mode 100644 index 000000000..3eda2e436 --- /dev/null +++ b/openlp/core/threading.py @@ -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() diff --git a/openlp/core/ui/aboutform.py b/openlp/core/ui/aboutform.py index e1768b127..bed83785b 100644 --- a/openlp/core/ui/aboutform.py +++ b/openlp/core/ui/aboutform.py @@ -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('', application_version['version']) if application_version['build']: diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 349352d92..c8a1753c2 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -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()) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index dc084eb2b..d3c44ff6c 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -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 diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 54f70ccb2..a8f7d91c7 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -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) diff --git a/openlp/core/common/versionchecker.py b/openlp/core/version.py similarity index 57% rename from openlp/core/common/versionchecker.py rename to openlp/core/version.py index 6129ee2aa..3fa6c006c 100644 --- a/openlp/core/common/versionchecker.py +++ b/openlp/core/version.py @@ -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