forked from openlp/openlp
Added a Zeroconf service to aid devices detecting OpenLP on the network
This commit is contained in:
parent
28da80ecf9
commit
0618f63ede
101
openlp/core/api/zeroconf.py
Normal file
101
openlp/core/api/zeroconf.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>. #
|
||||||
|
##########################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`~openlp.core.api.zeroconf` module runs a Zerconf server so that OpenLP can advertise the
|
||||||
|
RESTful API for devices on the network to discover.
|
||||||
|
"""
|
||||||
|
import socket
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from zeroconf import ServiceInfo, Zeroconf
|
||||||
|
|
||||||
|
from openlp.core.common import get_local_ip4
|
||||||
|
from openlp.core.common.registry import Registry
|
||||||
|
from openlp.core.common.settings import Settings
|
||||||
|
from openlp.core.threading import ThreadWorker, run_thread
|
||||||
|
|
||||||
|
|
||||||
|
class ZeroconfWorker(ThreadWorker):
|
||||||
|
"""
|
||||||
|
This thread worker runs a Zeroconf service
|
||||||
|
"""
|
||||||
|
address = None
|
||||||
|
http_port = 4316
|
||||||
|
ws_port = 4317
|
||||||
|
_can_run = False
|
||||||
|
|
||||||
|
def __init__(self, ip_address, http_port=4316, ws_port=4317):
|
||||||
|
"""
|
||||||
|
Create the worker for the Zeroconf service
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.address = socket.inet_aton(ip_address)
|
||||||
|
self.http_port = http_port
|
||||||
|
self.ws_port = ws_port
|
||||||
|
|
||||||
|
def can_run(self):
|
||||||
|
"""
|
||||||
|
Check if the worker can continue to run. This is mostly so that we can override this method
|
||||||
|
and test the class.
|
||||||
|
"""
|
||||||
|
return self._can_run
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
Start the service
|
||||||
|
"""
|
||||||
|
http_info = ServiceInfo('_http._tcp.local.', 'OpenLP._http._tcp.local.',
|
||||||
|
address=self.address, port=self.http_port, properties={})
|
||||||
|
ws_info = ServiceInfo('_ws._tcp.local.', 'OpenLP._ws._tcp.local.',
|
||||||
|
address=self.address, port=self.ws_port, properties={})
|
||||||
|
zc = Zeroconf()
|
||||||
|
zc.register_service(http_info)
|
||||||
|
zc.register_service(ws_info)
|
||||||
|
self._can_run = True
|
||||||
|
while self.can_run():
|
||||||
|
sleep(0.1)
|
||||||
|
zc.unregister_service(http_info)
|
||||||
|
zc.unregister_service(ws_info)
|
||||||
|
zc.close()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stop the service
|
||||||
|
"""
|
||||||
|
self._can_run = False
|
||||||
|
|
||||||
|
|
||||||
|
def start_zeroconf():
|
||||||
|
"""
|
||||||
|
Start the Zeroconf service
|
||||||
|
"""
|
||||||
|
# When we're running tests, just skip this set up if this flag is set
|
||||||
|
if Registry().get_flag('no_web_server'):
|
||||||
|
return
|
||||||
|
ifaces = get_local_ip4()
|
||||||
|
for key in iter(ifaces):
|
||||||
|
address = ifaces.get(key)['ip']
|
||||||
|
break
|
||||||
|
http_port = Settings().value('api/port')
|
||||||
|
ws_port = Settings().value('api/websocket port')
|
||||||
|
worker = ZeroconfWorker(address, http_port, ws_port)
|
||||||
|
run_thread(worker, 'api_zeroconf')
|
@ -33,8 +33,9 @@ from tempfile import gettempdir
|
|||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from openlp.core.state import State
|
from openlp.core.state import State
|
||||||
from openlp.core.api import websockets
|
from openlp.core.api.websockets import WebSocketServer
|
||||||
from openlp.core.api.http import server
|
from openlp.core.api.http.server import HttpServer
|
||||||
|
from openlp.core.api.zeroconf import start_zeroconf
|
||||||
from openlp.core.common import add_actions, is_macosx, is_win
|
from openlp.core.common import add_actions, is_macosx, is_win
|
||||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||||
from openlp.core.common.applocation import AppLocation
|
from openlp.core.common.applocation import AppLocation
|
||||||
@ -495,8 +496,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
|||||||
self.copy_data = False
|
self.copy_data = False
|
||||||
Settings().set_up_default_values()
|
Settings().set_up_default_values()
|
||||||
self.about_form = AboutForm(self)
|
self.about_form = AboutForm(self)
|
||||||
self.ws_server = websockets.WebSocketServer()
|
self.ws_server = WebSocketServer()
|
||||||
self.http_server = server.HttpServer(self)
|
self.http_server = HttpServer(self)
|
||||||
|
start_zeroconf()
|
||||||
SettingsForm(self)
|
SettingsForm(self)
|
||||||
self.formatting_tag_form = FormattingTagForm(self)
|
self.formatting_tag_form = FormattingTagForm(self)
|
||||||
self.shortcut_form = ShortcutListForm(self)
|
self.shortcut_form = ShortcutListForm(self)
|
||||||
|
@ -31,7 +31,7 @@ from unittest.mock import patch
|
|||||||
from openlp.core.common.json import JSONMixin, OpenLPJSONDecoder, OpenLPJSONEncoder, PathSerializer, _registered_classes
|
from openlp.core.common.json import JSONMixin, OpenLPJSONDecoder, OpenLPJSONEncoder, PathSerializer, _registered_classes
|
||||||
|
|
||||||
|
|
||||||
class TestClassBase(object):
|
class BaseTestClass(object):
|
||||||
"""
|
"""
|
||||||
Simple class to avoid repetition
|
Simple class to avoid repetition
|
||||||
"""
|
"""
|
||||||
@ -81,7 +81,7 @@ class TestJSONMixin(TestCase):
|
|||||||
Test that an instance of a JSONMixin subclass is properly serialized to a JSON string
|
Test that an instance of a JSONMixin subclass is properly serialized to a JSON string
|
||||||
"""
|
"""
|
||||||
# GIVEN: A instance of a subclass of the JSONMixin class
|
# GIVEN: A instance of a subclass of the JSONMixin class
|
||||||
class TestClass(TestClassBase, JSONMixin):
|
class TestClass(BaseTestClass, JSONMixin):
|
||||||
_json_keys = ['a', 'b']
|
_json_keys = ['a', 'b']
|
||||||
|
|
||||||
instance = TestClass(a=1, c=2)
|
instance = TestClass(a=1, c=2)
|
||||||
@ -97,7 +97,7 @@ class TestJSONMixin(TestCase):
|
|||||||
Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string
|
Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string
|
||||||
"""
|
"""
|
||||||
# GIVEN: A subclass of the JSONMixin class
|
# GIVEN: A subclass of the JSONMixin class
|
||||||
class TestClass(TestClassBase, JSONMixin):
|
class TestClass(BaseTestClass, JSONMixin):
|
||||||
_json_keys = ['a', 'b']
|
_json_keys = ['a', 'b']
|
||||||
|
|
||||||
# WHEN: Deserializing a JSON representation of the TestClass
|
# WHEN: Deserializing a JSON representation of the TestClass
|
||||||
@ -115,7 +115,7 @@ class TestJSONMixin(TestCase):
|
|||||||
Test that an instance of a JSONMixin subclass is properly serialized to a JSON string when using a custom name
|
Test that an instance of a JSONMixin subclass is properly serialized to a JSON string when using a custom name
|
||||||
"""
|
"""
|
||||||
# GIVEN: A instance of a subclass of the JSONMixin class with a custom name
|
# GIVEN: A instance of a subclass of the JSONMixin class with a custom name
|
||||||
class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
|
class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )):
|
||||||
_json_keys = ['a', 'b']
|
_json_keys = ['a', 'b']
|
||||||
_name = 'AltName'
|
_name = 'AltName'
|
||||||
_version = 2
|
_version = 2
|
||||||
@ -134,7 +134,7 @@ class TestJSONMixin(TestCase):
|
|||||||
name
|
name
|
||||||
"""
|
"""
|
||||||
# GIVEN: A instance of a subclass of the JSONMixin class with a custom name
|
# GIVEN: A instance of a subclass of the JSONMixin class with a custom name
|
||||||
class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
|
class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )):
|
||||||
_json_keys = ['a', 'b']
|
_json_keys = ['a', 'b']
|
||||||
_name = 'AltName'
|
_name = 'AltName'
|
||||||
_version = 2
|
_version = 2
|
||||||
|
@ -62,9 +62,10 @@ class TestMainWindow(TestCase, TestMixin):
|
|||||||
patch('openlp.core.ui.mainwindow.ServiceManager'), \
|
patch('openlp.core.ui.mainwindow.ServiceManager'), \
|
||||||
patch('openlp.core.ui.mainwindow.ThemeManager'), \
|
patch('openlp.core.ui.mainwindow.ThemeManager'), \
|
||||||
patch('openlp.core.ui.mainwindow.ProjectorManager'), \
|
patch('openlp.core.ui.mainwindow.ProjectorManager'), \
|
||||||
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
|
patch('openlp.core.ui.mainwindow.HttpServer'), \
|
||||||
patch('openlp.core.ui.mainwindow.PluginForm'), \
|
patch('openlp.core.ui.mainwindow.WebSocketServer'), \
|
||||||
patch('openlp.core.ui.mainwindow.server.HttpServer'):
|
patch('openlp.core.ui.mainwindow.start_zeroconf'), \
|
||||||
|
patch('openlp.core.ui.mainwindow.PluginForm'):
|
||||||
self.main_window = MainWindow()
|
self.main_window = MainWindow()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
102
tests/openlp_core/api/test_zeroconf.py
Normal file
102
tests/openlp_core/api/test_zeroconf.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>. #
|
||||||
|
##########################################################################
|
||||||
|
from unittest.mock import MagicMock, call, patch
|
||||||
|
|
||||||
|
from openlp.core.api.zeroconf import ZeroconfWorker, start_zeroconf
|
||||||
|
|
||||||
|
|
||||||
|
@patch('openlp.core.api.zeroconf.socket.inet_aton')
|
||||||
|
def test_zeroconf_worker_constructor(mocked_inet_aton):
|
||||||
|
"""Test creating the Zeroconf worker object"""
|
||||||
|
# GIVEN: A ZeroconfWorker class and a mocked inet_aton
|
||||||
|
mocked_inet_aton.return_value = 'processed_ip'
|
||||||
|
|
||||||
|
# WHEN: An instance of the ZeroconfWorker is created
|
||||||
|
worker = ZeroconfWorker('127.0.0.1', 8000, 8001)
|
||||||
|
|
||||||
|
# THEN: The inet_aton function should have been called and the attrs should be set
|
||||||
|
mocked_inet_aton.assert_called_once_with('127.0.0.1')
|
||||||
|
assert worker.address == 'processed_ip'
|
||||||
|
assert worker.http_port == 8000
|
||||||
|
assert worker.ws_port == 8001
|
||||||
|
|
||||||
|
|
||||||
|
@patch('openlp.core.api.zeroconf.ServiceInfo')
|
||||||
|
@patch('openlp.core.api.zeroconf.Zeroconf')
|
||||||
|
def test_zeroconf_worker_start(MockedZeroconf, MockedServiceInfo):
|
||||||
|
"""Test the start() method of ZeroconfWorker"""
|
||||||
|
# GIVEN: A few mocks and a ZeroconfWorker instance with a mocked can_run method
|
||||||
|
mocked_http_info = MagicMock()
|
||||||
|
mocked_ws_info = MagicMock()
|
||||||
|
mocked_zc = MagicMock()
|
||||||
|
MockedServiceInfo.side_effect = [mocked_http_info, mocked_ws_info]
|
||||||
|
MockedZeroconf.return_value = mocked_zc
|
||||||
|
worker = ZeroconfWorker('127.0.0.1', 8000, 8001)
|
||||||
|
|
||||||
|
# WHEN: The start() method is called
|
||||||
|
with patch.object(worker, 'can_run') as mocked_can_run:
|
||||||
|
mocked_can_run.side_effect = [True, False]
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
# THEN: The correct calls are made
|
||||||
|
assert MockedServiceInfo.call_args_list == [
|
||||||
|
call('_http._tcp.local.', 'OpenLP._http._tcp.local.', address=b'\x7f\x00\x00\x01', port=8000, properties={}),
|
||||||
|
call('_ws._tcp.local.', 'OpenLP._ws._tcp.local.', address=b'\x7f\x00\x00\x01', port=8001, properties={})
|
||||||
|
]
|
||||||
|
assert MockedZeroconf.call_count == 1
|
||||||
|
assert mocked_zc.register_service.call_args_list == [call(mocked_http_info), call(mocked_ws_info)]
|
||||||
|
assert mocked_can_run.call_count == 2
|
||||||
|
assert mocked_zc.unregister_service.call_args_list == [call(mocked_http_info), call(mocked_ws_info)]
|
||||||
|
assert mocked_zc.close.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_zeroconf_worker_stop():
|
||||||
|
"""Test that the ZeroconfWorker.stop() method correctly stops the service"""
|
||||||
|
# GIVEN: A worker object with _can_run set to True
|
||||||
|
worker = ZeroconfWorker('127.0.0.1', 8000, 8001)
|
||||||
|
worker._can_run = True
|
||||||
|
|
||||||
|
# WHEN: stop() is called
|
||||||
|
worker.stop()
|
||||||
|
|
||||||
|
# THEN: _can_run should be False
|
||||||
|
assert worker._can_run is False
|
||||||
|
|
||||||
|
|
||||||
|
# @patch('openlp.core.api.zeroconf.get_local_ip4')
|
||||||
|
@patch('openlp.core.api.zeroconf.Registry')
|
||||||
|
@patch('openlp.core.api.zeroconf.Settings')
|
||||||
|
@patch('openlp.core.api.zeroconf.ZeroconfWorker')
|
||||||
|
@patch('openlp.core.api.zeroconf.run_thread')
|
||||||
|
def test_start_zeroconf(mocked_run_thread, MockedZeroconfWorker, MockedSettings, MockedRegistry):
|
||||||
|
"""Test the start_zeroconf() function"""
|
||||||
|
# GIVEN: A whole bunch of stuff that's mocked out
|
||||||
|
MockedRegistry.return_value.get_flag.return_value = False
|
||||||
|
MockedSettings.return_value.value.side_effect = [8000, 8001]
|
||||||
|
mocked_worker = MagicMock()
|
||||||
|
MockedZeroconfWorker.return_value = mocked_worker
|
||||||
|
|
||||||
|
# WHEN: start_zeroconf() is called
|
||||||
|
start_zeroconf()
|
||||||
|
|
||||||
|
# THEN: A worker is added to the list of threads
|
||||||
|
mocked_run_thread.assert_called_once_with(mocked_worker, 'api_zeroconf')
|
@ -153,8 +153,9 @@ class TestProjectorDB(TestCase, TestMixin):
|
|||||||
patch('openlp.core.ui.mainwindow.ServiceManager'), \
|
patch('openlp.core.ui.mainwindow.ServiceManager'), \
|
||||||
patch('openlp.core.ui.mainwindow.ThemeManager'), \
|
patch('openlp.core.ui.mainwindow.ThemeManager'), \
|
||||||
patch('openlp.core.ui.mainwindow.ProjectorManager'), \
|
patch('openlp.core.ui.mainwindow.ProjectorManager'), \
|
||||||
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
|
patch('openlp.core.ui.mainwindow.WebSocketServer'), \
|
||||||
patch('openlp.core.ui.mainwindow.server.HttpServer'), \
|
patch('openlp.core.ui.mainwindow.HttpServer'), \
|
||||||
|
patch('openlp.core.ui.mainwindow.start_zeroconf'), \
|
||||||
patch('openlp.core.state.State.list_plugins') as mock_plugins:
|
patch('openlp.core.state.State.list_plugins') as mock_plugins:
|
||||||
mock_plugins.return_value = []
|
mock_plugins.return_value = []
|
||||||
self.main_window = MainWindow()
|
self.main_window = MainWindow()
|
||||||
|
Loading…
Reference in New Issue
Block a user