Skip to content

Commit

Permalink
migrate from dbus-python to pure-python jeepney
Browse files Browse the repository at this point in the history
fixes #39
  • Loading branch information
fphammerle committed Dec 29, 2024
1 parent 47af807 commit 73513ea
Show file tree
Hide file tree
Showing 15 changed files with 504 additions and 296 deletions.
9 changes: 0 additions & 9 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- run: pip install --upgrade pipenv==2024.1.0
- run: sudo apt-get update
# 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 }}
Expand Down Expand Up @@ -57,10 +52,6 @@ jobs:
# > [...]/coverage/inorout.py:507: CoverageWarning:
# . Module was never imported. (module-not-imported)
- run: pip install --upgrade pipenv==2023.6.18
- run: sudo apt-get update
- run: sudo apt-get install --yes --no-install-recommends
libdbus-1-dev
libgirepository1.0-dev
# by default pipenv picks the latest version in PATH
- run: pipenv install --python "$PYTHON_VERSION" --deploy --dev
env:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- declare compatibility with `python3.11`, `python3.12` & `python3.13`

### Changed
- migrate from [dbus-python](https://gitlab.freedesktop.org/dbus/dbus-python/)
to pure-python [jeepney](https://gitlab.com/takluyver/jeepney)
(removes indirect dependency on libdbus, glib,
[PyGObject](https://gitlab.gnome.org/GNOME/pygobject) and
[pycairo](https://github.com/pygobject/pycairo),
fixes https://github.com/fphammerle/systemctl-mqtt/issues/39)
- automatic discovery in home assistant:
- replace component-based (topic:
`<discovery_prefix>/binary_sensor/<node_id>/preparing-for-shutdown/config`)
Expand Down
12 changes: 0 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,11 @@ ARG SOURCE_DIR_PATH=/systemctl-mqtt
FROM $BASE_IMAGE AS build

RUN apk add --no-cache \
cairo-dev `# PyGObject > pycairo` \
dbus `# dbus-run-session for dbus-python's build` \
dbus-dev \
gcc \
git `# setuptools_scm` \
glib-dev `# dbus-python` \
gobject-introspection-dev `# PyGObject` \
jq `# edit Pipfile.lock` \
make `# dbus-python` \
musl-dev `# dbus-python` \
py3-certifi `# pipenv` \
py3-pip `# pipenv install` \
py3-virtualenv `# pipenv` \
python3-dev `# dbus-python` \
&& adduser -S build

USER build
Expand Down Expand Up @@ -60,9 +51,6 @@ FROM $BASE_IMAGE

RUN apk add --no-cache \
ca-certificates \
dbus-libs \
glib `# PyGObject` \
gobject-introspection `# PyGObject` \
python3 \
tini \
&& find / -xdev -type f -perm /u+s -exec chmod -c u-s {} \; \
Expand Down
4 changes: 0 additions & 4 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ name = "pypi"
[packages]
systemctl-mqtt = {editable = true, path = "."}

# > ImportError: [...]/python3.10/site-packages/gi/_gi.cpython-310-x86_64-linux-gnu.so:
# . undefined symbol: _PyUnicode_AsStringAndSize
PyGObject = "!=3.30.5"

[dev-packages]
black = "*"
mypy = "*"
Expand Down
34 changes: 5 additions & 29 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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-gi python3-paho-mqtt
$ sudo apt-get install --no-install-recommends python3-jeepney python3-paho-mqtt
```

## Usage
Expand Down
4 changes: 0 additions & 4 deletions docker-apparmor-profile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ profile systemctl-mqtt flags=(attach_disconnected) {
# https://jlk.fjfi.cvut.cz/arch/manpages/man/apparmor.d.5#Access_Modes
/systemctl-mqtt/ r,
/systemctl-mqtt/** r,
/systemctl-mqtt/.venv/lib/python3.12/site-packages/_dbus_bindings.cpython-312-*-linux-musl.so m,
/systemctl-mqtt/.venv/lib/python3.12/site-packages/_dbus_glib_bindings.cpython-312-*-linux-musl.so m,
/systemctl-mqtt/.venv/lib/python3.12/site-packages/gi/_gi.cpython-312-*-linux-musl.so m,
/systemctl-mqtt/.venv/lib/python3.12/site-packages/gi/_gi_cairo.cpython-312-*-linux-musl.so m,
# https://presentations.nordisch.org/apparmor/#/25
/systemctl-mqtt/.venv/bin/systemctl-mqtt rix,
/etc/** r,
Expand Down
13 changes: 11 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,17 @@
# >=3.6 variable type hints, f-strings & * to force keyword-only arguments
# >=3.8 importlib.metadata
python_requires=">=3.9", # <3.9 untested
# https://dbus.freedesktop.org/doc/dbus-python/news.html
install_requires=["PyGObject<4", "dbus-python<2", "paho-mqtt<2"],
# > Currently, the only main loop supported by dbus-python is GLib.
# https://web.archive.org/web/20241228081405/https://dbus.freedesktop.org/doc/dbus-python/tutorial.html#setting-up-an-event-loop
# PyGObject depends on pycairo
# > When pip-installing systemctl-mqtt on a system without graphics it
# > fails as pycairo fails building.
# https://web.archive.org/web/20241228083145/https://github.com/fphammerle/systemctl-mqtt/issues/39
# > Jeepney is a pure Python D-Bus module. It consists of an IO-free core
# > implementing the protocol, and integrations for both blocking I/O and
# > for different asynchronous frameworks.
# https://web.archive.org/web/20241206000411/https://www.freedesktop.org/wiki/Software/DBusBindings/
install_requires=["jeepney>=0.8,<0.9", "paho-mqtt<2"],
setup_requires=["setuptools_scm"],
tests_require=["pytest"],
)
97 changes: 46 additions & 51 deletions systemctl_mqtt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,9 @@
import threading
import typing

import dbus
import dbus.mainloop.glib

# black keeps inserting a blank line above
# https://pygobject.readthedocs.io/en/latest/getting_started.html#ubuntu-logo-ubuntu-debian-logo-debian
import gi.repository.GLib # pylint-import-requirements: imports=PyGObject
import jeepney
import jeepney.bus_messages
import jeepney.io.blocking
import paho.mqtt.client

import systemctl_mqtt._dbus
Expand All @@ -58,10 +55,8 @@ def __init__(
self._mqtt_topic_prefix = mqtt_topic_prefix
self._homeassistant_discovery_prefix = homeassistant_discovery_prefix
self._homeassistant_discovery_object_id = homeassistant_discovery_object_id
self._login_manager: dbus.proxies.Interface = (
systemctl_mqtt._dbus.get_login_manager()
)
self._shutdown_lock: typing.Optional[dbus.types.UnixFd] = None
self._login_manager = systemctl_mqtt._dbus.get_login_manager_proxy()
self._shutdown_lock: typing.Optional[jeepney.fds.FileDescriptor] = None
self._shutdown_lock_mutex = threading.Lock()
self.poweroff_delay = poweroff_delay

Expand All @@ -77,16 +72,21 @@ def acquire_shutdown_lock(self) -> None:
with self._shutdown_lock_mutex:
assert self._shutdown_lock is None
# https://www.freedesktop.org/wiki/Software/systemd/inhibit/
self._shutdown_lock = self._login_manager.Inhibit(
"shutdown", "systemctl-mqtt", "Report shutdown via MQTT", "delay"
(self._shutdown_lock,) = self._login_manager.Inhibit(
what="shutdown",
who="systemctl-mqtt",
why="Report shutdown via MQTT",
mode="delay",
)
assert isinstance(
self._shutdown_lock, jeepney.fds.FileDescriptor
), self._shutdown_lock
_LOGGER.debug("acquired shutdown inhibitor lock")

def release_shutdown_lock(self) -> None:
with self._shutdown_lock_mutex:
if self._shutdown_lock:
# https://dbus.freedesktop.org/doc/dbus-python/dbus.types.html#dbus.types.UnixFd.take
os.close(self._shutdown_lock.take())
self._shutdown_lock.close()
_LOGGER.debug("released shutdown inhibitor lock")
self._shutdown_lock = None

Expand All @@ -113,10 +113,9 @@ def _publish_preparing_for_shutdown(
"failed to publish on %s (return code %d)", topic, msg_info.rc
)

def _prepare_for_shutdown_handler(
self, active: dbus.Boolean, mqtt_client: paho.mqtt.client.Client
def preparing_for_shutdown_handler(
self, active: bool, mqtt_client: paho.mqtt.client.Client
) -> None:
assert isinstance(active, dbus.Boolean)
active = bool(active)
self._publish_preparing_for_shutdown(
mqtt_client=mqtt_client, active=active, block=True
Expand All @@ -126,35 +125,21 @@ def _prepare_for_shutdown_handler(
else:
self.acquire_shutdown_lock()

def register_prepare_for_shutdown_handler(
self, mqtt_client: paho.mqtt.client.Client
) -> None:
self._login_manager.connect_to_signal(
signal_name="PrepareForShutdown",
handler_function=functools.partial(
self._prepare_for_shutdown_handler, mqtt_client=mqtt_client
),
)

def publish_preparing_for_shutdown(
self, mqtt_client: paho.mqtt.client.Client
) -> None:
try:
active = self._login_manager.Get(
"org.freedesktop.login1.Manager",
"PreparingForShutdown",
dbus_interface="org.freedesktop.DBus.Properties",
)
except dbus.DBusException as exc:
((return_type, active),) = self._login_manager.Get("PreparingForShutdown")
except jeepney.wrappers.DBusErrorResponse as exc:
_LOGGER.error(
"failed to read logind's PreparingForShutdown property: %s",
exc.get_dbus_message(),
"failed to read logind's PreparingForShutdown property: %s", exc
)
return
assert isinstance(active, dbus.Boolean), active
assert return_type == "b", return_type
assert isinstance(active, bool), active
self._publish_preparing_for_shutdown(
mqtt_client=mqtt_client,
active=bool(active),
active=active,
# https://github.com/eclipse/paho.mqtt.python/issues/439#issuecomment-565514393
block=False,
)
Expand Down Expand Up @@ -278,7 +263,6 @@ def _mqtt_on_connect(
_LOGGER.debug("connected to MQTT broker %s:%d", mqtt_broker_host, mqtt_broker_port)
if not state.shutdown_lock_acquired:
state.acquire_shutdown_lock()
state.register_prepare_for_shutdown_handler(mqtt_client=mqtt_client)
state.publish_preparing_for_shutdown(mqtt_client=mqtt_client)
state.publish_homeassistant_device_config(mqtt_client=mqtt_client)
for topic_suffix, action in _MQTT_TOPIC_SUFFIX_ACTION_MAPPING.items():
Expand All @@ -293,7 +277,7 @@ def _mqtt_on_connect(
)


def _run(
def _run( # pylint: disable=too-many-arguments
*,
mqtt_host: str,
mqtt_port: int,
Expand All @@ -305,18 +289,24 @@ def _run(
poweroff_delay: datetime.timedelta,
mqtt_disable_tls: bool = False,
) -> None:
# pylint: disable=too-many-arguments
# https://dbus.freedesktop.org/doc/dbus-python/tutorial.html#setting-up-an-event-loop
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
# https://pypi.org/project/paho-mqtt/
mqtt_client = paho.mqtt.client.Client(
userdata=_State(
mqtt_topic_prefix=mqtt_topic_prefix,
homeassistant_discovery_prefix=homeassistant_discovery_prefix,
homeassistant_discovery_object_id=homeassistant_discovery_object_id,
poweroff_delay=poweroff_delay,
)
# pylint: disable=too-many-locals; will be split up when switching to async mqtt
dbus_connection = jeepney.io.blocking.open_dbus_connection(bus="SYSTEM")
bus_proxy = jeepney.io.blocking.Proxy(
msggen=jeepney.bus_messages.message_bus, connection=dbus_connection
)
preparing_for_shutdown_match_rule = (
# pylint: disable=protected-access
systemctl_mqtt._dbus.get_login_manager_signal_match_rule("PrepareForShutdown")
)
assert bus_proxy.AddMatch(preparing_for_shutdown_match_rule) == ()
state = _State(
mqtt_topic_prefix=mqtt_topic_prefix,
homeassistant_discovery_prefix=homeassistant_discovery_prefix,
homeassistant_discovery_object_id=homeassistant_discovery_object_id,
poweroff_delay=poweroff_delay,
)
# https://pypi.org/project/paho-mqtt/
mqtt_client = paho.mqtt.client.Client(userdata=state)
mqtt_client.on_connect = _mqtt_on_connect
if not mqtt_disable_tls:
mqtt_client.tls_set(ca_certs=None) # enable tls trusting default system certs
Expand All @@ -337,7 +327,12 @@ def _run(
# https://github.com/eclipse/paho.mqtt.python/blob/v1.5.0/src/paho/mqtt/client.py#L1744
mqtt_client.loop_start()
try:
gi.repository.GLib.MainLoop().run()
with dbus_connection.filter(preparing_for_shutdown_match_rule) as queue:
while True:
(preparing_for_sleep,) = dbus_connection.recv_until_filtered(queue).body
state.preparing_for_shutdown_handler(
active=preparing_for_sleep, mqtt_client=mqtt_client
)
finally:
# blocks until loop_forever stops
_LOGGER.debug("waiting for MQTT loop to stop")
Expand Down
Loading

0 comments on commit 73513ea

Please sign in to comment.