From a5d01e4e49d499dd9b65b3ab29fe7faae91faf78 Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Sun, 20 Aug 2023 23:00:16 +0200 Subject: [PATCH 1/6] Add mypy to CI tests --- .github/workflows/build-test.yml | 6 ++++-- pyproject.toml | 9 ++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9b77020..2b79fbb 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -54,12 +54,14 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest + pip install mypy - name: Install built packages run: | pip install -U dist/*.whl - name: Test with pytest - run: | - pytest + run: pytest + - name: Test with mypy + run: mypy --install-types --non-interactive src/trick17 tests pypi-publish: needs: test diff --git a/pyproject.toml b/pyproject.toml index 68d0615..8d69e3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ path = "src/trick17/__about__.py" [tool.hatch.envs.default] dependencies = [ "coverage[toml]>=6.5", + "mypy>=1.0.0", "pytest", ] [tool.hatch.envs.default.scripts] @@ -53,6 +54,7 @@ cov = [ "test-cov", "cov-report", ] +typing = "mypy --install-types --non-interactive {args:src/trick17 tests}" [[tool.hatch.envs.all.matrix]] python = ["3.7", "3.8", "3.9", "3.10", "3.11"] @@ -61,12 +63,9 @@ python = ["3.7", "3.8", "3.9", "3.10", "3.11"] detached = true dependencies = [ "black>=23.1.0", - "mypy>=1.0.0", "ruff>=0.0.243", - "pytest", ] [tool.hatch.envs.lint.scripts] -typing = "mypy --install-types --non-interactive {args:src/trick17 tests}" style = [ "ruff {args:.}", "black --check --diff {args:.}", @@ -76,10 +75,6 @@ fmt = [ "ruff --fix {args:.}", "style", ] -all = [ - "style", - "typing", -] [tool.black] target-version = ["py37"] From 2c3355564da5448b8e6c3247fcad2246cd0c292b Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Sun, 20 Aug 2023 23:24:31 +0200 Subject: [PATCH 2/6] Drop support for python 3.7 --- .github/workflows/build-test.yml | 4 ++-- .pre-commit-config.yaml | 4 +--- pyproject.toml | 9 ++++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2b79fbb..2a405d0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,7 +1,7 @@ name: Build package, run test matrix, and publish on PyPi env: - BUILD_PYTHON_VERSION: "3.7" + BUILD_PYTHON_VERSION: "3.8" on: [push] @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b0657d..7997ff8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,3 @@ -default_language_version: - python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 @@ -15,6 +13,6 @@ repos: hooks: - id: black - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.284' + rev: 'v0.0.285' hooks: - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 8d69e3f..871cfbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "trick17" dynamic = ["version"] description = "Systemd utility functions in pure python" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = "MIT" keywords = [ "systemd", @@ -20,7 +20,6 @@ classifiers = [ "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -57,7 +56,7 @@ cov = [ typing = "mypy --install-types --non-interactive {args:src/trick17 tests}" [[tool.hatch.envs.all.matrix]] -python = ["3.7", "3.8", "3.9", "3.10", "3.11"] +python = ["3.8", "3.9", "3.10", "3.11"] [tool.hatch.envs.lint] detached = true @@ -77,11 +76,11 @@ fmt = [ ] [tool.black] -target-version = ["py37"] +target-version = ["py38"] line-length = 84 [tool.ruff] -target-version = "py37" +target-version = "py38" line-length = 84 select = [ "A", From a7b6da745d7133ba4f95fa9d8502709d7ae5c5d2 Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Sun, 20 Aug 2023 23:32:24 +0200 Subject: [PATCH 3/6] Start devel of version 0.0.3 --- src/trick17/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trick17/__about__.py b/src/trick17/__about__.py index 8982866..0c03560 100644 --- a/src/trick17/__about__.py +++ b/src/trick17/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present Stefano Miccoli # # SPDX-License-Identifier: MIT -__version__ = "0.0.2" +__version__ = "0.0.3.dev0" From fa3c58fbfaf0569a42f2dbbc7209bb2b67848c4d Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Sun, 20 Aug 2023 22:36:24 +0200 Subject: [PATCH 4/6] Add listen_fds support, for socket activation --- src/trick17/__about__.py | 2 +- src/trick17/__init__.py | 4 ++++ src/trick17/daemon.py | 47 ++++++++++++++++++++++++++++++++++++++++ tests/test_daemon.py | 7 ++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/trick17/__about__.py b/src/trick17/__about__.py index 0c03560..ac17b16 100644 --- a/src/trick17/__about__.py +++ b/src/trick17/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present Stefano Miccoli # # SPDX-License-Identifier: MIT -__version__ = "0.0.3.dev0" +__version__ = "0.0.3.dev1" diff --git a/src/trick17/__init__.py b/src/trick17/__init__.py index 78968cb..8452480 100644 --- a/src/trick17/__init__.py +++ b/src/trick17/__init__.py @@ -5,7 +5,11 @@ # systemd notable paths SD_BOOTED_PATH = "/run/systemd/system" SD_JOURNAL_SOCKET_PATH = "/run/systemd/journal/socket" +SD_LISTEN_FDS_START = 3 # environmet variables possibly set by systemd SD_JOURNAL_STREAM_ENV = "JOURNAL_STREAM" SD_NOTIFY_SOCKET_ENV = "NOTIFY_SOCKET" +SD_LISTEN_FDS_PID_ENV = "LISTEN_PID" +SD_LISTEN_FDS_ENV = "LISTEN_FDS" +SD_LISTEN_FDS_NAMES_ENV = "LISTEN_FDNAMES" diff --git a/src/trick17/daemon.py b/src/trick17/daemon.py index 6e1f976..dc9054b 100644 --- a/src/trick17/daemon.py +++ b/src/trick17/daemon.py @@ -4,6 +4,7 @@ import os from pathlib import Path +from typing import Iterator, List, Tuple import trick17 from trick17 import util @@ -16,6 +17,52 @@ def booted() -> bool: return Path(trick17.SD_BOOTED_PATH).is_dir() +def listen_fds() -> Iterator[Tuple[int, str]]: + """listen_fds() returns an iterator over (fd, name) tuples, where + - fd is an open file descriptor intialized by systemd socket-activation + - name is an optional name, '' if undefined. + """ + + # check pid + if trick17.SD_LISTEN_FDS_PID_ENV not in os.environ: + return iter(()) + try: + pid = int(os.environ[trick17.SD_LISTEN_FDS_PID_ENV]) + except ValueError as err: + msg = f"Unable to get pid from environment: {err}" + raise RuntimeError(msg) from err + if os.getpid() != pid: + return iter(()) + + # check FDS + nfds: int + try: + nfds = int(os.environ[trick17.SD_LISTEN_FDS_ENV]) + except KeyError as err: + msg = f"Unable to get number of fd's from environment: {err}" + raise RuntimeError(msg) from err + except ValueError as err: + msg = f"Nvalid number of fd's in environment: {err}" + raise RuntimeError(msg) from err + fds = range(trick17.SD_LISTEN_FDS_START, trick17.SD_LISTEN_FDS_START + nfds) + assert len(fds) == nfds + + # check names + names: List[str] + if trick17.SD_LISTEN_FDS_NAMES_ENV not in os.environ: + names = [ + "", + ] * nfds + else: + names = os.environ[trick17.SD_LISTEN_FDS_NAMES_ENV].split(os.pathsep) + if len(names) > nfds: + names = names[:nfds] + elif len(names) < nfds: + names.extend("" for _ in range(nfds - len(names))) + + return zip(fds, names) + + def notify(state: str) -> bool: """notify 'state' to systemd; returns - True if notification sent to socket, diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 0d0f0cf..9977104 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -1,3 +1,5 @@ +import pytest + from trick17 import daemon @@ -8,3 +10,8 @@ def test_booted(): def test_notify(): ret = daemon.notify("READY=1") assert ret is False + + +def test_listen_fds(): + with pytest.raises(StopIteration): + next(daemon.listen_fds()) From c70eae980ad9a54a85659c5d6228154a09a9821a Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Tue, 22 Aug 2023 18:26:17 +0200 Subject: [PATCH 5/6] Add tsock and small refactor --- src/trick17/daemon.py | 10 ++++++---- tests/tsock.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100755 tests/tsock.py diff --git a/src/trick17/daemon.py b/src/trick17/daemon.py index dc9054b..4e5ec66 100644 --- a/src/trick17/daemon.py +++ b/src/trick17/daemon.py @@ -42,23 +42,25 @@ def listen_fds() -> Iterator[Tuple[int, str]]: msg = f"Unable to get number of fd's from environment: {err}" raise RuntimeError(msg) from err except ValueError as err: - msg = f"Nvalid number of fd's in environment: {err}" + msg = f"Parsing of fd's from environment failed: {err}" raise RuntimeError(msg) from err + if nfds < 1: + msg = f"Invalid number of fd's in environment: {nfds:d}" + raise RuntimeError(msg) fds = range(trick17.SD_LISTEN_FDS_START, trick17.SD_LISTEN_FDS_START + nfds) assert len(fds) == nfds # check names names: List[str] if trick17.SD_LISTEN_FDS_NAMES_ENV not in os.environ: - names = [ - "", - ] * nfds + names = [""] * nfds else: names = os.environ[trick17.SD_LISTEN_FDS_NAMES_ENV].split(os.pathsep) if len(names) > nfds: names = names[:nfds] elif len(names) < nfds: names.extend("" for _ in range(nfds - len(names))) + assert len(names) == len(fds) return zip(fds, names) diff --git a/tests/tsock.py b/tests/tsock.py new file mode 100755 index 0000000..2fe233d --- /dev/null +++ b/tests/tsock.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +import logging +import os +import socket +import stat + +from trick17 import daemon + +LISTEN_FDS_START = 3 + +logging.basicConfig(level=logging.INFO) +for fd, name in daemon.listen_fds(): + fstat = os.fstat(fd) + if not stat.S_ISSOCK(fstat.st_mode): + logging.error("fd %d ('%s') not a socket", fd, name) + continue + with socket.socket(fileno=fd) as sock: + logging.info("fd %d ('%s') bound to '%s'", fd, name, sock.getsockname()) + if sock.type == socket.SOCK_STREAM: + while True: + conn, address = sock.accept() + logging.info(" remote '%s'", address) + with conn: + data = conn.recv(4096) + logging.info(" -> %s", data) + if data.startswith(b"STOP"): + break + elif sock.type == socket.SOCK_DGRAM: + while True: + data = sock.recv(4096) + logging.info(" -> %s", data) + if data.startswith(b"STOP"): + break + else: + msg = f"Unknown socket type {sock.type}" + raise NotImplementedError(msg) From d94cca8c7c467a91f542f4f94ec316238ab9e818 Mon Sep 17 00:00:00 2001 From: Stefano Miccoli Date: Tue, 22 Aug 2023 18:38:16 +0200 Subject: [PATCH 6/6] Modify README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5aceefb..8eb9e3b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ pip install trick17 - `daemon.booted()` returns `True` if system was booted by systemd. - `daemon.notify(state)` sends a notification to systemd. +- `listen_fds()` returns an iterator over (fd, name) tuples in case of socket activation, + see [systemd.socket](https://www.freedesktop.org/software/systemd/man/systemd.socket.html) ### trick17.journal