From e6fa02a75eee350ea6f2dff72a2adb144ee8068d Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 17 Jun 2020 14:49:31 +0100 Subject: [PATCH] Upgrade and cleanup WebSocket code --- openlp/core/api/poll.py | 97 +++---------------- openlp/core/api/websockets.py | 72 +++++++++----- openlp/core/ui/mainwindow.py | 5 - openlp/core/ui/slidecontroller.py | 2 - .../openlp_core/api/test_websockets.py | 22 +---- 5 files changed, 64 insertions(+), 134 deletions(-) diff --git a/openlp/core/api/poll.py b/openlp/core/api/poll.py index e84fcca7c..69f7f05ea 100644 --- a/openlp/core/api/poll.py +++ b/openlp/core/api/poll.py @@ -18,10 +18,6 @@ # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## -import json - -from openlp.core.common.registry import Registry -from openlp.core.common.httputils import get_web_page from openlp.core.common.mixins import RegistryProperties @@ -34,13 +30,7 @@ class Poller(RegistryProperties): Constructor for the poll builder class. """ super(Poller, self).__init__() - self.live_cache = None - self.stage_cache = None - self.chords_cache = None - settings = Registry().get('settings_thread') - self.address = settings.value('api/ip address') - self.ws_port = settings.value('api/websocket port') - self.http_port = settings.value('api/port') + self.previous = {} def raw_poll(self): return { @@ -53,81 +43,22 @@ class Poller(RegistryProperties): 'display': self.live_controller.desktop_screen.isChecked(), 'version': 3, 'isSecure': self.settings.value('api/authentication enabled'), - 'isAuthorised': False, - 'chordNotation': self.settings.value('songs/chord notation'), - 'isStageActive': self.is_stage_active(), - 'isLiveActive': self.is_live_active(), - 'isChordsActive': self.is_chords_active() + 'chordNotation': self.settings.value('songs/chord notation') } def poll(self): """ - Poll OpenLP to determine the current slide number and item name. + Poll OpenLP to determine current state if it has changed. + """ + current = self.raw_poll() + if self.previous != current: + self.previous = current + return {'results': current} + else: + return None + + def poll_first_time(self): + """ + Poll OpenLP to determine the current state. """ return {'results': self.raw_poll()} - - def main_poll(self): - """ - Poll OpenLP to determine the current slide count. - """ - result = { - 'slide_count': self.live_controller.slide_count - } - return json.dumps({'results': result}).encode() - - def reset_cache(self): - """ - Reset the caches as the web has changed - :return: - """ - self.stage_cache = None - self.live_cache = None - self.chords.cache = None - - def is_stage_active(self): - """ - Is stage active - call it and see but only once - :return: if stage is active or not - """ - if self.stage_cache is None: - try: - page = get_web_page(f'http://{self.address}:{self.http_port}/stage') - except Exception: - page = None - if page: - self.stage_cache = True - else: - self.stage_cache = False - return self.stage_cache - - def is_live_active(self): - """ - Is main active - call it and see but only once - :return: if live is active or not - """ - if self.live_cache is None: - try: - page = get_web_page(f'http://{self.address}:{self.http_port}/main') - except Exception: - page = None - if page: - self.live_cache = True - else: - self.live_cache = False - return self.live_cache - - def is_chords_active(self): - """ - Is chords active - call it and see but only once - :return: if live is active or not - """ - if self.chords_cache is None: - try: - page = get_web_page(f'http://{self.address}:{self.http_port}/chords') - except Exception: - page = None - if page: - self.chords_cache = True - else: - self.chords_cache = False - return self.chords_cache diff --git a/openlp/core/api/websockets.py b/openlp/core/api/websockets.py index f053cfc1c..46135e2d2 100644 --- a/openlp/core/api/websockets.py +++ b/openlp/core/api/websockets.py @@ -33,38 +33,64 @@ from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry from openlp.core.threading import ThreadWorker, run_thread +USERS = set() + log = logging.getLogger(__name__) -async def handle_websocket(request, path): +async def handle_websocket(websocket, path): """ - Handle web socket requests and return the poll information + Handle web socket requests and return the state information + Check every 0.2 seconds to get the latest position and send if it changed. - Check every 0.2 seconds to get the latest position and send if it changed. This only gets triggered when the first - client connects. - - :param request: request from client - :param path: determines the endpoints supported + :param websocket: request from client + :param path: determines the endpoints supported - Not needed """ - log.debug('WebSocket handler registered with client') - previous_poll = None - previous_main_poll = None - poller = Registry().get('poller') - if path == '/state': + log.debug('WebSocket handle_websocket connection') + await register(websocket) + reply = Registry().get('poller').poll_first_time() + if reply: + json_reply = json.dumps(reply).encode() + await websocket.send(json_reply) + try: while True: - current_poll = poller.poll() - if current_poll != previous_poll: - await request.send(json.dumps(current_poll).encode()) - previous_poll = current_poll - await asyncio.sleep(0.2) - elif path == '/live_changed': - while True: - main_poll = poller.main_poll() - if main_poll != previous_main_poll: - await request.send(main_poll) - previous_main_poll = main_poll + await notify_users() await asyncio.sleep(0.2) + finally: + await unregister(websocket) + + +async def register(websocket): + """ + Register Clients + :param websocket: The client details + :return: + """ + log.debug('WebSocket handler register') + USERS.add(websocket) + + +async def unregister(websocket): + """ + Unregister Clients + :param websocket: The client details + :return: + """ + log.debug('WebSocket handler unregister') + USERS.remove(websocket) + + +async def notify_users(): + """ + Dispatch state to all registered users if we have any changes + :return: + """ + if USERS: # asyncio.wait doesn't accept an empty list + reply = Registry().get('poller').poll() + if reply: + json_reply = json.dumps(reply).encode() + await asyncio.wait([user.send(json_reply) for user in USERS]) class WebSocketWorker(ThreadWorker, RegistryProperties, LogMixin): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 9100bedfe..b28abffe6 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -1037,11 +1037,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert event.ignore() else: event.accept() - # Included here as it is only needed to help force the logging rollover not for general logging use. - # Rollover require as when WebSocket exits having been used it will destroy the application log on exit. - import logging - log_man = logging.getLogger() - log_man.handlers[0].doRollover() if event.isAccepted(): # Wait for all the threads to complete self._wait_for_threads() diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 8ff7197af..79f64246d 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -202,7 +202,6 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): self.update_slide_limits() self.panel = QtWidgets.QWidget(self.main_window.control_splitter) self.slide_list = {} - self.slide_count = 0 self.controller_width = -1 # Layout for holding panel self.panel_layout = QtWidgets.QVBoxLayout(self.panel) @@ -1201,7 +1200,6 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): self.preview_display.set_single_image('#000', image_path) else: self.preview_display.go_to_slide(self.selected_row) - self.slide_count += 1 def display_maindisplay(self): """ diff --git a/tests/functional/openlp_core/api/test_websockets.py b/tests/functional/openlp_core/api/test_websockets.py index c8156a95f..5ecfbc07d 100644 --- a/tests/functional/openlp_core/api/test_websockets.py +++ b/tests/functional/openlp_core/api/test_websockets.py @@ -67,19 +67,6 @@ def test_serverstart_not_required(mocked_run_thread, MockWebSocketWorker, regist assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called' -def test_main_poll(poller): - """ - Test the main_poll function returns the correct JSON - """ - # WHEN: the live controller has 5 slides - mocked_live_controller = MagicMock() - mocked_live_controller.slide_count = 5 - Registry().register('live_controller', mocked_live_controller) - # THEN: the live json should be generated - main_json = poller.main_poll() - assert b'{"results": {"slide_count": 5}}' == main_json, 'The return value should match the defined json' - - def test_poll(poller, settings): """ Test the poll function returns the correct JSON @@ -97,19 +84,12 @@ def test_poll(poller, settings): Registry().register('live_controller', mocked_live_controller) Registry().register('service_manager', mocked_service_manager) # WHEN: The poller polls - with patch.object(poller, 'is_stage_active') as mocked_is_stage_active, \ - patch.object(poller, 'is_live_active') as mocked_is_live_active, \ - patch.object(poller, 'is_chords_active') as mocked_is_chords_active: - mocked_is_stage_active.return_value = True - mocked_is_live_active.return_value = True - mocked_is_chords_active.return_value = True - poll_json = poller.poll() + poll_json = poller.poll() # THEN: the live json should be generated and match expected results assert poll_json['results']['blank'] is True, 'The blank return value should be True' assert poll_json['results']['theme'] is False, 'The theme return value should be False' assert poll_json['results']['display'] is False, 'The display return value should be False' assert poll_json['results']['isSecure'] is False, 'The isSecure return value should be False' - assert poll_json['results']['isAuthorised'] is False, 'The isAuthorised return value should be False' assert poll_json['results']['twelve'] is True, 'The twelve return value should be True' assert poll_json['results']['version'] == 3, 'The version return value should be 3' assert poll_json['results']['slide'] == 5, 'The slide return value should be 5'