From: Aleš Mrázek Date: Fri, 10 Jul 2026 09:33:53 +0000 (+0200) Subject: controller: notify socket implementation in python X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=db37829d6fa8f89602eb0f2c693e080d9b310206;p=thirdparty%2Fknot-resolver.git controller: notify socket implementation in python --- diff --git a/python/knot_resolver/controller/errors.py b/python/knot_resolver/controller/errors.py new file mode 100644 index 000000000..b49d151a4 --- /dev/null +++ b/python/knot_resolver/controller/errors.py @@ -0,0 +1,20 @@ +from knot_resolver.errors import BaseKresError + + +class ControllerError(BaseKresError): + """Class exception for all errors used in controller submodules.""" + + def __init__(self, msg: str) -> None: + super().__init__() + self._msg = msg + + def __str__(self) -> str: + return self._msg + + +class ControllerNotifySocketError(ControllerError): + """Exception class for notify socket errors.""" + + def __init__(self, msg: str, error_pointer: str = "") -> None: + msg = f"notify socket error: {msg}" + super().__init__(msg, error_pointer) diff --git a/python/knot_resolver/controller/notify/notify_socket.py b/python/knot_resolver/controller/notify/notify_socket.py new file mode 100644 index 000000000..2396f191b --- /dev/null +++ b/python/knot_resolver/controller/notify/notify_socket.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import os +import socket +import struct +from pathlib import Path + +from knot_resolver.controller.errors import ControllerNotifySocketError +from knot_resolver.logging import get_logger + +NOTIFY_SOCKET_NAME = "supervisor-notify-socket" +NOTIFY_SOCKET_ENV_VAR = "NOTIFY_SOCKET" + +CREDENTIALS_FORMAT = "3i" +CREDENTIALS_SIZE = struct.calcsize(CREDENTIALS_FORMAT) +RECEIVE_BUFFER_SIZE = 2048 + +logger = get_logger(__name__) + + +def init_notify_socket() -> socket.socket: + notify_socket_path = Path.cwd() / NOTIFY_SOCKET_NAME + notify_socket_path.unlink(missing_ok=True) + + notify_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_NONBLOCK) + try: + notify_socket.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1) + notify_socket.bind(str(notify_socket_path)) + except OSError as e: + notify_socket.close() + notify_socket_path.unlink(missing_ok=True) + msg = f"failed to initialize notify socket at '{notify_socket_path}'" + raise ControllerNotifySocketError(msg) from e + + os.environ[NOTIFY_SOCKET_ENV_VAR] = str(notify_socket_path) + return notify_socket + + +def read_notify_socket(sock: socket.socket) -> tuple[int, bytes] | None: + try: + data, ancdata, flags, _ = sock.recvmsg( + RECEIVE_BUFFER_SIZE, + socket.CMSG_SPACE(CREDENTIALS_SIZE), + ) + except BlockingIOError: + return None + + if flags & socket.MSG_TRUNC: + raise ControllerNotifySocketError("received datagram was truncated") + + pid = next( + ( + struct.unpack(CREDENTIALS_FORMAT, cdata[:CREDENTIALS_SIZE])[0] + for level, ctype, cdata in ancdata + if level == socket.SOL_SOCKET and ctype == socket.SCM_CREDENTIALS + ), + None, + ) + + if pid is None: + logger.warning("ignoring received data without credentials: %s", data) + return None + + return pid, data + + +def send_notify_message(**values: str) -> None: + notify_socket_path = os.getenv(NOTIFY_SOCKET_ENV_VAR) + + if notify_socket_path is None: + logger.warning("failed to get $NOTIFY_SOCKET environment variable") + return + + if notify_socket_path.startswith("@"): + notify_socket_path = notify_socket_path.replace("@", "\0", 1) + + try: + notify_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + notify_socket.connect(notify_socket_path) + except OSError: + logger.exception("failed to connect to $NOTIFY_SOCKET at '%s'", notify_socket_path) + return + + payload = "\n".join((f"{key}={value}" for key, value in values.items())) + try: + notify_socket.send(payload.encode("utf8")) + except OSError: + logger.exception("failed to send notify message to $NOTIFY_SOCKET at '%s'", notify_socket_path) + + notify_socket.close() diff --git a/tests/python/knot_resolver/controller/notify/test_notify_socket.py b/tests/python/knot_resolver/controller/notify/test_notify_socket.py new file mode 100644 index 000000000..190049b27 --- /dev/null +++ b/tests/python/knot_resolver/controller/notify/test_notify_socket.py @@ -0,0 +1,24 @@ +import os + +import pytest + +from knot_resolver.controller.notify.notify_socket import init_notify_socket, read_notify_socket, send_notify_message + + +@pytest.mark.parametrize( + "msg", + [ + {"READY": 1}, + {"RELOADING": 1}, + {"STOPPING": 1}, + ], +) +def test_notify_socket(msg: dict[str, int]) -> None: + key, value = next(iter(msg.items())) + + notify_socket = init_notify_socket() + send_notify_message(**msg) + pid, message = read_notify_socket(notify_socket) + + assert pid == os.getpid() + assert message.decode() == f"{key}={value}"