forked from openlp/openlp
Merge branch 'websockets' into 'master'
Upgrade and cleanup WebSocket code See merge request openlp/openlp!208
This commit is contained in:
commit
d117430175
@ -18,10 +18,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License #
|
# You should have received a copy of the GNU General Public License #
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
##########################################################################
|
##########################################################################
|
||||||
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
|
from openlp.core.common.mixins import RegistryProperties
|
||||||
|
|
||||||
|
|
||||||
@ -34,13 +30,7 @@ class Poller(RegistryProperties):
|
|||||||
Constructor for the poll builder class.
|
Constructor for the poll builder class.
|
||||||
"""
|
"""
|
||||||
super(Poller, self).__init__()
|
super(Poller, self).__init__()
|
||||||
self.live_cache = None
|
self.previous = {}
|
||||||
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')
|
|
||||||
|
|
||||||
def raw_poll(self):
|
def raw_poll(self):
|
||||||
return {
|
return {
|
||||||
@ -53,81 +43,22 @@ class Poller(RegistryProperties):
|
|||||||
'display': self.live_controller.desktop_screen.isChecked(),
|
'display': self.live_controller.desktop_screen.isChecked(),
|
||||||
'version': 3,
|
'version': 3,
|
||||||
'isSecure': self.settings.value('api/authentication enabled'),
|
'isSecure': self.settings.value('api/authentication enabled'),
|
||||||
'isAuthorised': False,
|
'chordNotation': self.settings.value('songs/chord notation')
|
||||||
'chordNotation': self.settings.value('songs/chord notation'),
|
|
||||||
'isStageActive': self.is_stage_active(),
|
|
||||||
'isLiveActive': self.is_live_active(),
|
|
||||||
'isChordsActive': self.is_chords_active()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def poll(self):
|
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()}
|
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
|
|
||||||
|
@ -33,38 +33,64 @@ from openlp.core.common.mixins import LogMixin, RegistryProperties
|
|||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
from openlp.core.threading import ThreadWorker, run_thread
|
from openlp.core.threading import ThreadWorker, run_thread
|
||||||
|
|
||||||
|
USERS = set()
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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
|
:param websocket: request from client
|
||||||
client connects.
|
:param path: determines the endpoints supported - Not needed
|
||||||
|
|
||||||
:param request: request from client
|
|
||||||
:param path: determines the endpoints supported
|
|
||||||
"""
|
"""
|
||||||
log.debug('WebSocket handler registered with client')
|
log.debug('WebSocket handle_websocket connection')
|
||||||
previous_poll = None
|
await register(websocket)
|
||||||
previous_main_poll = None
|
reply = Registry().get('poller').poll_first_time()
|
||||||
poller = Registry().get('poller')
|
if reply:
|
||||||
if path == '/state':
|
json_reply = json.dumps(reply).encode()
|
||||||
|
await websocket.send(json_reply)
|
||||||
|
try:
|
||||||
while True:
|
while True:
|
||||||
current_poll = poller.poll()
|
await notify_users()
|
||||||
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 asyncio.sleep(0.2)
|
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):
|
class WebSocketWorker(ThreadWorker, RegistryProperties, LogMixin):
|
||||||
|
@ -1037,11 +1037,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
else:
|
else:
|
||||||
event.accept()
|
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():
|
if event.isAccepted():
|
||||||
# Wait for all the threads to complete
|
# Wait for all the threads to complete
|
||||||
self._wait_for_threads()
|
self._wait_for_threads()
|
||||||
|
@ -202,7 +202,6 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
|
|||||||
self.update_slide_limits()
|
self.update_slide_limits()
|
||||||
self.panel = QtWidgets.QWidget(self.main_window.control_splitter)
|
self.panel = QtWidgets.QWidget(self.main_window.control_splitter)
|
||||||
self.slide_list = {}
|
self.slide_list = {}
|
||||||
self.slide_count = 0
|
|
||||||
self.controller_width = -1
|
self.controller_width = -1
|
||||||
# Layout for holding panel
|
# Layout for holding panel
|
||||||
self.panel_layout = QtWidgets.QVBoxLayout(self.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)
|
self.preview_display.set_single_image('#000', image_path)
|
||||||
else:
|
else:
|
||||||
self.preview_display.go_to_slide(self.selected_row)
|
self.preview_display.go_to_slide(self.selected_row)
|
||||||
self.slide_count += 1
|
|
||||||
|
|
||||||
def display_maindisplay(self):
|
def display_maindisplay(self):
|
||||||
"""
|
"""
|
||||||
|
@ -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'
|
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):
|
def test_poll(poller, settings):
|
||||||
"""
|
"""
|
||||||
Test the poll function returns the correct JSON
|
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('live_controller', mocked_live_controller)
|
||||||
Registry().register('service_manager', mocked_service_manager)
|
Registry().register('service_manager', mocked_service_manager)
|
||||||
# WHEN: The poller polls
|
# 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
|
# 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']['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']['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']['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']['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']['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']['version'] == 3, 'The version return value should be 3'
|
||||||
assert poll_json['results']['slide'] == 5, 'The slide return value should be 5'
|
assert poll_json['results']['slide'] == 5, 'The slide return value should be 5'
|
||||||
|
Loading…
Reference in New Issue
Block a user