Browse Source

prepare glib loop to receive dbus signals

Fabian Peter Hammerle 3 years ago
parent
commit
aee870c751
6 changed files with 51 additions and 8 deletions
  1. 7 3
      .github/workflows/python.yml
  2. 12 0
      Pipfile.lock
  3. 1 1
      README.md
  4. 1 1
      setup.py
  5. 15 1
      systemctl_mqtt/__init__.py
  6. 15 2
      tests/test_mqtt.py

+ 7 - 3
.github/workflows/python.yml

@@ -26,8 +26,10 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - run: pip install --upgrade pipenv>=2018.10.9
     - run: sudo apt-get update
-    # TODO exclude dbus-python from pipenv install
-    - run: sudo apt-get install --yes --no-install-recommends libdbus-1-dev
+    # TODO exclude dbus-python & PyGObject from pipenv install
+    - run: sudo apt-get install --yes --no-install-recommends
+        libdbus-1-dev
+        libgirepository1.0-dev
     - run: pipenv install --python "$PYTHON_VERSION" --deploy --dev
       env:
         PYTHON_VERSION: ${{ matrix.python-version }}
@@ -50,7 +52,9 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - run: pip install --upgrade pipenv>=2018.10.9
     - run: sudo apt-get update
-    - run: sudo apt-get install --yes --no-install-recommends libdbus-1-dev
+    - run: sudo apt-get install --yes --no-install-recommends
+        libdbus-1-dev
+        libgirepository1.0-dev
     - run: pipenv install --python "$PYTHON_VERSION" --deploy --dev
       env:
         PYTHON_VERSION: ${{ matrix.python-version }}

+ 12 - 0
Pipfile.lock

@@ -28,6 +28,18 @@
             ],
             "version": "==1.5.0"
         },
+        "pycairo": {
+            "hashes": [
+                "sha256:2c143183280feb67f5beb4e543fd49990c28e7df427301ede04fc550d3562e84"
+            ],
+            "version": "==1.19.1"
+        },
+        "pygobject": {
+            "hashes": [
+                "sha256:012a589aec687bfa809a1ff9f5cd775dc7f6fcec1a6bc7fe88e1002a68f8ba34"
+            ],
+            "version": "==3.36.1"
+        },
         "systemctl-mqtt": {
             "editable": true,
             "path": "."

+ 1 - 1
README.md

@@ -17,7 +17,7 @@ $ systemctl-mqtt --mqtt-host HOSTNAME_OR_IP_ADDRESS
 
 On debian-based systems, dependencies can optionally be installed via:
 ```sh
-$ sudo apt-get install --no-install-recommends python3-dbus python3-paho-mqtt
+$ sudo apt-get install --no-install-recommends python3-dbus python3-gi python3-paho-mqtt
 ```
 
 Schedule poweroff by sending a MQTT message to topic `systemctl/hostname/poweroff`.

+ 1 - 1
setup.py

@@ -62,7 +62,7 @@ setuptools.setup(
     ],
     entry_points={"console_scripts": ["systemctl-mqtt = systemctl_mqtt:_main",]},
     # https://dbus.freedesktop.org/doc/dbus-python/news.html
-    install_requires=["dbus-python<2", "paho-mqtt<2"],
+    install_requires=["PyGObject<4", "dbus-python<2", "paho-mqtt<2"],
     setup_requires=["setuptools_scm"],
     tests_require=["pytest"],
 )

+ 15 - 1
systemctl_mqtt/__init__.py

@@ -25,6 +25,8 @@ import typing
 
 import dbus
 
+# https://pygobject.readthedocs.io/en/latest/getting_started.html#ubuntu-logo-ubuntu-debian-logo-debian
+import gi.repository.GLib
 import paho.mqtt.client
 
 _LOGGER = logging.getLogger(__name__)
@@ -166,7 +168,19 @@ def _run(
     elif mqtt_password:
         raise ValueError("Missing MQTT username")
     mqtt_client.connect(host=mqtt_host, port=mqtt_port)
-    mqtt_client.loop_forever()
+    # loop_start runs loop_forever in a new thread (daemon)
+    # https://github.com/eclipse/paho.mqtt.python/blob/v1.5.0/src/paho/mqtt/client.py#L1814
+    # loop_forever attempts to reconnect if disconnected
+    # https://github.com/eclipse/paho.mqtt.python/blob/v1.5.0/src/paho/mqtt/client.py#L1744
+    mqtt_client.loop_start()
+    try:
+        # https://dbus.freedesktop.org/doc/dbus-python/tutorial.html#setting-up-an-event-loop
+        gi.repository.GLib.MainLoop().run()
+    finally:
+        # blocks until loop_forever stops
+        _LOGGER.debug("waiting for MQTT loop to stop")
+        mqtt_client.loop_stop()
+        _LOGGER.debug("MQTT loop stopped")
 
 
 def _get_hostname() -> str:

+ 15 - 2
tests/test_mqtt.py

@@ -16,6 +16,8 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import logging
+import threading
+import time
 import unittest.mock
 
 import pytest
@@ -37,7 +39,9 @@ def test__run(caplog, mqtt_host, mqtt_port, mqtt_topic_prefix):
         "ssl.SSLContext.wrap_socket", autospec=True,
     ) as ssl_wrap_socket_mock, unittest.mock.patch(
         "paho.mqtt.client.Client.loop_forever", autospec=True,
-    ) as mqtt_loop_forever_mock:
+    ) as mqtt_loop_forever_mock, unittest.mock.patch(
+        "gi.repository.GLib.MainLoop.run"
+    ) as glib_loop_mock:
         ssl_wrap_socket_mock.return_value.send = len
         systemctl_mqtt._run(
             mqtt_host=mqtt_host,
@@ -60,6 +64,8 @@ def test__run(caplog, mqtt_host, mqtt_port, mqtt_topic_prefix):
     assert ssl_context.check_hostname is True
     assert ssl_wrap_socket_mock.call_args[1]["server_hostname"] == mqtt_host
     # loop started?
+    while threading.active_count() > 1:
+        time.sleep(0.01)
     assert mqtt_loop_forever_mock.call_count == 1
     (mqtt_client,) = mqtt_loop_forever_mock.call_args[0]
     assert mqtt_client._tls_insecure is False
@@ -109,6 +115,11 @@ def test__run(caplog, mqtt_host, mqtt_port, mqtt_topic_prefix):
     )
     assert caplog.records[1].message.startswith("executing action poweroff")
     assert caplog.records[2].message.startswith("completed action poweroff")
+    # dbus loop started?
+    glib_loop_mock.assert_called_once_with()
+    # waited for mqtt loop to stop?
+    assert mqtt_client._thread_terminate
+    assert mqtt_client._thread is None
 
 
 @pytest.mark.parametrize("mqtt_host", ["mqtt-broker.local"])
@@ -123,7 +134,9 @@ def test__run_authentication(
         "ssl.SSLContext.wrap_socket"
     ) as ssl_wrap_socket_mock, unittest.mock.patch(
         "paho.mqtt.client.Client.loop_forever", autospec=True,
-    ) as mqtt_loop_forever_mock:
+    ) as mqtt_loop_forever_mock, unittest.mock.patch(
+        "gi.repository.GLib.MainLoop.run"
+    ):
         ssl_wrap_socket_mock.return_value.send = len
         systemctl_mqtt._run(
             mqtt_host=mqtt_host,