← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~raoul-snyman/openlp/zeroconf into lp:openlp

 

Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/zeroconf into lp:openlp.

Commit message:
Add Zeroconf services to OpenLP so that external devices can find OpenLP on the network.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~raoul-snyman/openlp/zeroconf/+merge/369554

Add Zeroconf services to OpenLP so that external devices can find OpenLP on the network.
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~raoul-snyman/openlp/zeroconf into lp:openlp.
=== added file 'openlp/core/api/zeroconf.py'
--- openlp/core/api/zeroconf.py	1970-01-01 00:00:00 +0000
+++ openlp/core/api/zeroconf.py	2019-07-01 22:48:21 +0000
@@ -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')

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2019-05-24 18:50:51 +0000
+++ openlp/core/ui/mainwindow.py	2019-07-01 22:48:21 +0000
@@ -33,8 +33,9 @@
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.state import State
-from openlp.core.api import websockets
-from openlp.core.api.http import server
+from openlp.core.api.websockets import WebSocketServer
+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.actions import ActionList, CategoryOrder
 from openlp.core.common.applocation import AppLocation
@@ -495,8 +496,9 @@
         self.copy_data = False
         Settings().set_up_default_values()
         self.about_form = AboutForm(self)
-        self.ws_server = websockets.WebSocketServer()
-        self.http_server = server.HttpServer(self)
+        self.ws_server = WebSocketServer()
+        self.http_server = HttpServer(self)
+        start_zeroconf()
         SettingsForm(self)
         self.formatting_tag_form = FormattingTagForm(self)
         self.shortcut_form = ShortcutListForm(self)

=== modified file 'tests/functional/openlp_core/common/test_json.py'
--- tests/functional/openlp_core/common/test_json.py	2019-05-22 06:47:00 +0000
+++ tests/functional/openlp_core/common/test_json.py	2019-07-01 22:48:21 +0000
@@ -31,7 +31,7 @@
 from openlp.core.common.json import JSONMixin, OpenLPJSONDecoder, OpenLPJSONEncoder, PathSerializer, _registered_classes
 
 
-class TestClassBase(object):
+class BaseTestClass(object):
     """
     Simple class to avoid repetition
     """
@@ -81,7 +81,7 @@
         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
-        class TestClass(TestClassBase, JSONMixin):
+        class TestClass(BaseTestClass, JSONMixin):
             _json_keys = ['a', 'b']
 
         instance = TestClass(a=1, c=2)
@@ -97,7 +97,7 @@
         Test that an instance of a JSONMixin subclass is properly deserialized from a JSON string
         """
         # GIVEN: A subclass of the JSONMixin class
-        class TestClass(TestClassBase, JSONMixin):
+        class TestClass(BaseTestClass, JSONMixin):
             _json_keys = ['a', 'b']
 
         # WHEN: Deserializing a JSON representation of the TestClass
@@ -115,7 +115,7 @@
         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
-        class TestClass(TestClassBase, JSONMixin, register_names=('AltName', )):
+        class TestClass(BaseTestClass, JSONMixin, register_names=('AltName', )):
             _json_keys = ['a', 'b']
             _name = 'AltName'
             _version = 2
@@ -134,7 +134,7 @@
         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']
             _name = 'AltName'
             _version = 2

=== modified file 'tests/interfaces/openlp_core/ui/test_mainwindow.py'
--- tests/interfaces/openlp_core/ui/test_mainwindow.py	2019-04-13 13:00:22 +0000
+++ tests/interfaces/openlp_core/ui/test_mainwindow.py	2019-07-01 22:48:21 +0000
@@ -62,9 +62,10 @@
                 patch('openlp.core.ui.mainwindow.ServiceManager'), \
                 patch('openlp.core.ui.mainwindow.ThemeManager'), \
                 patch('openlp.core.ui.mainwindow.ProjectorManager'), \
-                patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
-                patch('openlp.core.ui.mainwindow.PluginForm'), \
-                patch('openlp.core.ui.mainwindow.server.HttpServer'):
+                patch('openlp.core.ui.mainwindow.HttpServer'), \
+                patch('openlp.core.ui.mainwindow.WebSocketServer'), \
+                patch('openlp.core.ui.mainwindow.start_zeroconf'), \
+                patch('openlp.core.ui.mainwindow.PluginForm'):
             self.main_window = MainWindow()
 
     def tearDown(self):

=== added directory 'tests/openlp_core/api'
=== added file 'tests/openlp_core/api/test_zeroconf.py'
--- tests/openlp_core/api/test_zeroconf.py	1970-01-01 00:00:00 +0000
+++ tests/openlp_core/api/test_zeroconf.py	2019-07-01 22:48:21 +0000
@@ -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')

=== modified file 'tests/openlp_core/projectors/test_projector_db.py'
--- tests/openlp_core/projectors/test_projector_db.py	2019-04-13 13:00:22 +0000
+++ tests/openlp_core/projectors/test_projector_db.py	2019-07-01 22:48:21 +0000
@@ -153,8 +153,9 @@
                 patch('openlp.core.ui.mainwindow.ServiceManager'), \
                 patch('openlp.core.ui.mainwindow.ThemeManager'), \
                 patch('openlp.core.ui.mainwindow.ProjectorManager'), \
-                patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
-                patch('openlp.core.ui.mainwindow.server.HttpServer'), \
+                patch('openlp.core.ui.mainwindow.WebSocketServer'), \
+                patch('openlp.core.ui.mainwindow.HttpServer'), \
+                patch('openlp.core.ui.mainwindow.start_zeroconf'), \
                 patch('openlp.core.state.State.list_plugins') as mock_plugins:
             mock_plugins.return_value = []
             self.main_window = MainWindow()


References