sysrepo-python-plugind 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. sysrepo_python_plugind-1.0.0/LICENSE +28 -0
  2. sysrepo_python_plugind-1.0.0/PKG-INFO +200 -0
  3. sysrepo_python_plugind-1.0.0/README.rst +186 -0
  4. sysrepo_python_plugind-1.0.0/pyproject.toml +32 -0
  5. sysrepo_python_plugind-1.0.0/setup.cfg +4 -0
  6. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/__init__.py +15 -0
  7. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/__main__.py +8 -0
  8. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/cli.py +158 -0
  9. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/daemon.py +306 -0
  10. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/install_yang.py +82 -0
  11. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/pid.py +92 -0
  12. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/plugin.py +56 -0
  13. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/sort.py +91 -0
  14. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/yang/sysrepo-python-plugind.yang +54 -0
  15. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/PKG-INFO +200 -0
  16. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/SOURCES.txt +21 -0
  17. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/dependency_links.txt +1 -0
  18. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/entry_points.txt +3 -0
  19. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/requires.txt +4 -0
  20. sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/top_level.txt +1 -0
  21. sysrepo_python_plugind-1.0.0/tests/test_daemon.py +209 -0
  22. sysrepo_python_plugind-1.0.0/tests/test_pid.py +79 -0
  23. sysrepo_python_plugind-1.0.0/tests/test_sort.py +88 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 EntryPoint Communications, LLC
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: sysrepo-python-plugind
3
+ Version: 1.0.0
4
+ Summary: Python plugin daemon for sysrepo (replacement for sysrepo-plugind)
5
+ License-Expression: BSD-3-Clause
6
+ Project-URL: Repository, https://github.com/entrypoint-communications/sysrepo-python-plugind
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/x-rst
9
+ License-File: LICENSE
10
+ Requires-Dist: sysrepo~=2.0
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest>=7; extra == "test"
13
+ Dynamic: license-file
14
+
15
+ sysrepo-python-plugind
16
+ ======================
17
+
18
+ A Python reimplementation of ``sysrepo-plugind`` — the sysrepo plugin daemon.
19
+
20
+ The C daemon loads compiled ``.so`` plugins via ``dlopen``. This daemon loads
21
+ **Python plugins** discovered via entry points (``importlib.metadata``), giving
22
+ plugin authors a pure-Python development experience against the existing
23
+ `sysrepo-python`_ CFFI bindings.
24
+
25
+ The two daemons can run **simultaneously**: the Python daemon manages its own
26
+ YANG subtree (``python-plugin-order``, ``python-loaded-plugins``) and never
27
+ touches the C daemon's nodes.
28
+
29
+ .. _sysrepo-python: https://github.com/sysrepo/sysrepo-python
30
+
31
+
32
+ Requirements
33
+ ------------
34
+
35
+ - Python ≥ 3.10
36
+ - sysrepo-python_ (and the underlying ``libsysrepo.so``)
37
+ - sysrepo core 4.2.10 (ships ``sysrepo-plugind@2022-08-26.yang``)
38
+
39
+
40
+ Installation
41
+ ------------
42
+
43
+ .. code-block:: console
44
+
45
+ pip install sysrepo-python-plugind
46
+
47
+ Then register the YANG augmentation module with sysrepo (requires write
48
+ access to the sysrepo repository, typically run as root):
49
+
50
+ .. code-block:: console
51
+
52
+ sysrepo-python-plugind-setup
53
+
54
+ This is idempotent — safe to run on every deployment or system boot. Pass
55
+ ``--force`` to upgrade the module after a package update.
56
+
57
+
58
+ Writing a plugin
59
+ ----------------
60
+
61
+ Subclass ``SysrepoPlugin`` and implement ``init()``. Register the class via
62
+ an entry point in your package's ``pyproject.toml``:
63
+
64
+ .. code-block:: toml
65
+
66
+ [project.entry-points."sysrepo_python.plugins"]
67
+ my-plugin = "my_package.plugin:MyPlugin"
68
+
69
+ Installing your package makes the plugin visible to the daemon immediately —
70
+ no copying of files or environment variables required.
71
+
72
+ Minimal example:
73
+
74
+ .. code-block:: python
75
+
76
+ import logging
77
+ import sysrepo
78
+ from sysrepo_python_plugind import SysrepoPlugin
79
+
80
+ LOG = logging.getLogger(__name__)
81
+
82
+ class MyPlugin(SysrepoPlugin):
83
+
84
+ def init(self, session: sysrepo.SysrepoSession) -> None:
85
+ self._counter = 0
86
+ session.subscribe_module_change(
87
+ "my-module", None, self._on_change, done_only=True
88
+ )
89
+ session.subscribe_oper_data_request(
90
+ "my-module", "/my-module:stats", self._on_oper
91
+ )
92
+ LOG.info("MyPlugin ready")
93
+
94
+ def cleanup(self, session: sysrepo.SysrepoSession) -> None:
95
+ LOG.info("MyPlugin stopping (counter=%d)", self._counter)
96
+
97
+ def _on_change(self, event, req_id, changes, priv):
98
+ self._counter += 1
99
+
100
+ def _on_oper(self, xpath, priv):
101
+ return {"my-module": {"stats": {"change-count": self._counter}}}
102
+
103
+ State that the C daemon stored in ``void *private_data`` is stored as instance
104
+ attributes (``self._counter`` above).
105
+
106
+ Async callbacks
107
+ ~~~~~~~~~~~~~~~
108
+
109
+ The daemon starts an asyncio event loop in a background thread before calling
110
+ any ``init()``, so plugins can use ``asyncio_register=True`` subscriptions
111
+ without managing a loop themselves:
112
+
113
+ .. code-block:: python
114
+
115
+ class AsyncMyPlugin(SysrepoPlugin):
116
+
117
+ def init(self, session: sysrepo.SysrepoSession) -> None:
118
+ session.subscribe_module_change(
119
+ "my-module", None, self._on_change,
120
+ done_only=True, asyncio_register=True,
121
+ )
122
+
123
+ async def _on_change(self, event, req_id, changes, priv):
124
+ ...
125
+
126
+
127
+ Running the daemon
128
+ ------------------
129
+
130
+ .. code-block:: console
131
+
132
+ # Foreground, INFO-level logging to stderr:
133
+ sysrepo-python-plugind -d -v 2
134
+
135
+ # Background (daemonize), write PID file:
136
+ sysrepo-python-plugind -p /run/sysrepo-python-plugind.pid
137
+
138
+ CLI flags mirror those of the C ``sysrepo-plugind``:
139
+
140
+ ============ ===================================================================
141
+ ``-d`` Stay in foreground; log to stderr instead of syslog
142
+ ``-v LEVEL`` Verbosity: 0=ERROR 1=WARNING 2=INFO 3=DEBUG (default: 2)
143
+ ``-p PATH`` Write PID file at PATH
144
+ ``-f`` Exit if any plugin fails to initialise (default: skip and continue)
145
+ ============ ===================================================================
146
+
147
+
148
+ Plugin ordering
149
+ ---------------
150
+
151
+ By default plugins are initialised in entry-point discovery order. To
152
+ override this, set the ``python-plugin-order`` leaf-list in sysrepo's running
153
+ datastore:
154
+
155
+ .. code-block:: console
156
+
157
+ sysrepocfg -d running --edit
158
+
159
+ Add under ``/sysrepo-plugind:sysrepo-plugind``:
160
+
161
+ .. code-block:: xml
162
+
163
+ <python-plugin-order xmlns="urn:sysrepo:python-plugind">
164
+ <plugin>my-plugin</plugin>
165
+ <plugin>other-plugin</plugin>
166
+ </python-plugin-order>
167
+
168
+ Plugins absent from the list are appended after the ordered set, preserving
169
+ their discovery order. A trailing file extension in a configured name (e.g.
170
+ ``my-plugin.so``) is stripped before comparison, so operator configs written
171
+ for the C daemon can be reused unchanged.
172
+
173
+ The list of successfully initialised plugins is published to the operational
174
+ datastore at ``/sysrepo-plugind:sysrepo-plugind/sysrepo-python-plugind:python-loaded-plugins/plugin``.
175
+
176
+
177
+ Systemd deployment
178
+ ------------------
179
+
180
+ A service file is provided in the ``systemd/`` directory of the source
181
+ distribution:
182
+
183
+ .. code-block:: console
184
+
185
+ cp systemd/sysrepo-python-plugind.service /etc/systemd/system/
186
+ systemctl daemon-reload
187
+ systemctl enable --now sysrepo-python-plugind
188
+
189
+ The service runs ``sysrepo-python-plugind-setup`` as ``ExecStartPre=``,
190
+ ensuring the YANG module is present before the daemon starts.
191
+
192
+ If the daemon binary is not at ``/usr/bin/sysrepo-python-plugind`` (e.g. when
193
+ installed into a virtual environment), update the ``ExecStartPre=`` and
194
+ ``ExecStart=`` paths in the service file accordingly.
195
+
196
+
197
+ License
198
+ -------
199
+
200
+ BSD-3-Clause — same as sysrepo-python.
@@ -0,0 +1,186 @@
1
+ sysrepo-python-plugind
2
+ ======================
3
+
4
+ A Python reimplementation of ``sysrepo-plugind`` — the sysrepo plugin daemon.
5
+
6
+ The C daemon loads compiled ``.so`` plugins via ``dlopen``. This daemon loads
7
+ **Python plugins** discovered via entry points (``importlib.metadata``), giving
8
+ plugin authors a pure-Python development experience against the existing
9
+ `sysrepo-python`_ CFFI bindings.
10
+
11
+ The two daemons can run **simultaneously**: the Python daemon manages its own
12
+ YANG subtree (``python-plugin-order``, ``python-loaded-plugins``) and never
13
+ touches the C daemon's nodes.
14
+
15
+ .. _sysrepo-python: https://github.com/sysrepo/sysrepo-python
16
+
17
+
18
+ Requirements
19
+ ------------
20
+
21
+ - Python ≥ 3.10
22
+ - sysrepo-python_ (and the underlying ``libsysrepo.so``)
23
+ - sysrepo core 4.2.10 (ships ``sysrepo-plugind@2022-08-26.yang``)
24
+
25
+
26
+ Installation
27
+ ------------
28
+
29
+ .. code-block:: console
30
+
31
+ pip install sysrepo-python-plugind
32
+
33
+ Then register the YANG augmentation module with sysrepo (requires write
34
+ access to the sysrepo repository, typically run as root):
35
+
36
+ .. code-block:: console
37
+
38
+ sysrepo-python-plugind-setup
39
+
40
+ This is idempotent — safe to run on every deployment or system boot. Pass
41
+ ``--force`` to upgrade the module after a package update.
42
+
43
+
44
+ Writing a plugin
45
+ ----------------
46
+
47
+ Subclass ``SysrepoPlugin`` and implement ``init()``. Register the class via
48
+ an entry point in your package's ``pyproject.toml``:
49
+
50
+ .. code-block:: toml
51
+
52
+ [project.entry-points."sysrepo_python.plugins"]
53
+ my-plugin = "my_package.plugin:MyPlugin"
54
+
55
+ Installing your package makes the plugin visible to the daemon immediately —
56
+ no copying of files or environment variables required.
57
+
58
+ Minimal example:
59
+
60
+ .. code-block:: python
61
+
62
+ import logging
63
+ import sysrepo
64
+ from sysrepo_python_plugind import SysrepoPlugin
65
+
66
+ LOG = logging.getLogger(__name__)
67
+
68
+ class MyPlugin(SysrepoPlugin):
69
+
70
+ def init(self, session: sysrepo.SysrepoSession) -> None:
71
+ self._counter = 0
72
+ session.subscribe_module_change(
73
+ "my-module", None, self._on_change, done_only=True
74
+ )
75
+ session.subscribe_oper_data_request(
76
+ "my-module", "/my-module:stats", self._on_oper
77
+ )
78
+ LOG.info("MyPlugin ready")
79
+
80
+ def cleanup(self, session: sysrepo.SysrepoSession) -> None:
81
+ LOG.info("MyPlugin stopping (counter=%d)", self._counter)
82
+
83
+ def _on_change(self, event, req_id, changes, priv):
84
+ self._counter += 1
85
+
86
+ def _on_oper(self, xpath, priv):
87
+ return {"my-module": {"stats": {"change-count": self._counter}}}
88
+
89
+ State that the C daemon stored in ``void *private_data`` is stored as instance
90
+ attributes (``self._counter`` above).
91
+
92
+ Async callbacks
93
+ ~~~~~~~~~~~~~~~
94
+
95
+ The daemon starts an asyncio event loop in a background thread before calling
96
+ any ``init()``, so plugins can use ``asyncio_register=True`` subscriptions
97
+ without managing a loop themselves:
98
+
99
+ .. code-block:: python
100
+
101
+ class AsyncMyPlugin(SysrepoPlugin):
102
+
103
+ def init(self, session: sysrepo.SysrepoSession) -> None:
104
+ session.subscribe_module_change(
105
+ "my-module", None, self._on_change,
106
+ done_only=True, asyncio_register=True,
107
+ )
108
+
109
+ async def _on_change(self, event, req_id, changes, priv):
110
+ ...
111
+
112
+
113
+ Running the daemon
114
+ ------------------
115
+
116
+ .. code-block:: console
117
+
118
+ # Foreground, INFO-level logging to stderr:
119
+ sysrepo-python-plugind -d -v 2
120
+
121
+ # Background (daemonize), write PID file:
122
+ sysrepo-python-plugind -p /run/sysrepo-python-plugind.pid
123
+
124
+ CLI flags mirror those of the C ``sysrepo-plugind``:
125
+
126
+ ============ ===================================================================
127
+ ``-d`` Stay in foreground; log to stderr instead of syslog
128
+ ``-v LEVEL`` Verbosity: 0=ERROR 1=WARNING 2=INFO 3=DEBUG (default: 2)
129
+ ``-p PATH`` Write PID file at PATH
130
+ ``-f`` Exit if any plugin fails to initialise (default: skip and continue)
131
+ ============ ===================================================================
132
+
133
+
134
+ Plugin ordering
135
+ ---------------
136
+
137
+ By default plugins are initialised in entry-point discovery order. To
138
+ override this, set the ``python-plugin-order`` leaf-list in sysrepo's running
139
+ datastore:
140
+
141
+ .. code-block:: console
142
+
143
+ sysrepocfg -d running --edit
144
+
145
+ Add under ``/sysrepo-plugind:sysrepo-plugind``:
146
+
147
+ .. code-block:: xml
148
+
149
+ <python-plugin-order xmlns="urn:sysrepo:python-plugind">
150
+ <plugin>my-plugin</plugin>
151
+ <plugin>other-plugin</plugin>
152
+ </python-plugin-order>
153
+
154
+ Plugins absent from the list are appended after the ordered set, preserving
155
+ their discovery order. A trailing file extension in a configured name (e.g.
156
+ ``my-plugin.so``) is stripped before comparison, so operator configs written
157
+ for the C daemon can be reused unchanged.
158
+
159
+ The list of successfully initialised plugins is published to the operational
160
+ datastore at ``/sysrepo-plugind:sysrepo-plugind/sysrepo-python-plugind:python-loaded-plugins/plugin``.
161
+
162
+
163
+ Systemd deployment
164
+ ------------------
165
+
166
+ A service file is provided in the ``systemd/`` directory of the source
167
+ distribution:
168
+
169
+ .. code-block:: console
170
+
171
+ cp systemd/sysrepo-python-plugind.service /etc/systemd/system/
172
+ systemctl daemon-reload
173
+ systemctl enable --now sysrepo-python-plugind
174
+
175
+ The service runs ``sysrepo-python-plugind-setup`` as ``ExecStartPre=``,
176
+ ensuring the YANG module is present before the daemon starts.
177
+
178
+ If the daemon binary is not at ``/usr/bin/sysrepo-python-plugind`` (e.g. when
179
+ installed into a virtual environment), update the ``ExecStartPre=`` and
180
+ ``ExecStart=`` paths in the service file accordingly.
181
+
182
+
183
+ License
184
+ -------
185
+
186
+ BSD-3-Clause — same as sysrepo-python.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sysrepo-python-plugind"
7
+ version = "1.0.0"
8
+ description = "Python plugin daemon for sysrepo (replacement for sysrepo-plugind)"
9
+ readme = "README.rst"
10
+ license = "BSD-3-Clause"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["sysrepo~=2.0"]
13
+
14
+ [project.scripts]
15
+ sysrepo-python-plugind = "sysrepo_python_plugind:main"
16
+ sysrepo-python-plugind-setup = "sysrepo_python_plugind.install_yang:main"
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/entrypoint-communications/sysrepo-python-plugind"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+
24
+ [tool.setuptools.package-data]
25
+ sysrepo_python_plugind = ["yang/*.yang"]
26
+
27
+ [project.optional-dependencies]
28
+ test = ["pytest>=7"]
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["tests"]
32
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ # Copyright (c) 2026 EntryPoint Communications, LLC
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ """sysrepo-python-plugind — Python plugin daemon for sysrepo.
5
+
6
+ Public API:
7
+
8
+ - :class:`SysrepoPlugin`: Abstract base class for Python plugins.
9
+ - :func:`main`: Entry point for the ``sysrepo-python-plugind`` console script.
10
+ """
11
+
12
+ from .cli import main
13
+ from .plugin import SysrepoPlugin
14
+
15
+ __all__ = ["SysrepoPlugin", "main"]
@@ -0,0 +1,8 @@
1
+ # Copyright (c) 2026 EntryPoint Communications, LLC
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ import sys
5
+
6
+ from .cli import main
7
+
8
+ sys.exit(main())
@@ -0,0 +1,158 @@
1
+ # Copyright (c) 2026 EntryPoint Communications, LLC
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ import argparse
5
+ import logging
6
+ import os
7
+ import sys
8
+ from typing import Optional
9
+
10
+ import sysrepo
11
+
12
+ from .daemon import PluginDaemon
13
+ from .pid import PidFile
14
+
15
+ # Map sysrepo-plugind -v levels to Python logging levels, matching the C daemon.
16
+ _LOG_LEVELS = {
17
+ 0: logging.ERROR,
18
+ 1: logging.WARNING,
19
+ 2: logging.INFO,
20
+ 3: logging.DEBUG,
21
+ 4: logging.DEBUG,
22
+ }
23
+
24
+
25
+ def main() -> int:
26
+ """Parse CLI arguments and run the sysrepo Python plugin daemon.
27
+
28
+ Parses ``-d``, ``-v``, ``-p``, and ``-f`` flags (mirroring the C
29
+ sysrepo-plugind), configures logging, creates a PluginDaemon, and
30
+ either daemonizes or runs in the foreground.
31
+
32
+ Returns:
33
+ int: Exit code; 0 on success, 1 on error.
34
+ """
35
+ parser = argparse.ArgumentParser(
36
+ prog="sysrepo-python-plugind",
37
+ description="Sysrepo Python plugin daemon",
38
+ )
39
+ parser.add_argument(
40
+ "-d",
41
+ dest="debug",
42
+ action="store_true",
43
+ help="debug mode: stay in foreground, log to stderr",
44
+ )
45
+ parser.add_argument(
46
+ "-v",
47
+ dest="verbosity",
48
+ type=int,
49
+ default=2,
50
+ metavar="LEVEL",
51
+ choices=range(5),
52
+ help="log verbosity 0-4 (default: 2 = INFO)",
53
+ )
54
+ parser.add_argument(
55
+ "-p",
56
+ dest="pid_file",
57
+ metavar="PATH",
58
+ help="write PID file at PATH",
59
+ )
60
+ parser.add_argument(
61
+ "-f",
62
+ dest="fatal_fail",
63
+ action="store_true",
64
+ help="exit if any plugin fails to initialise",
65
+ )
66
+ args = parser.parse_args()
67
+
68
+ log_level = _LOG_LEVELS.get(args.verbosity, logging.DEBUG)
69
+ _configure_logging(debug=args.debug, level=log_level)
70
+
71
+ daemon = PluginDaemon(fatal_fail=args.fatal_fail)
72
+
73
+ if args.pid_file:
74
+ return _run_with_pid(daemon, args.pid_file, debug=args.debug)
75
+
76
+ if not args.debug:
77
+ _daemonize()
78
+ return daemon.run()
79
+
80
+
81
+ # ------------------------------------------------------------------------------
82
+ def _run_with_pid(daemon: PluginDaemon, pid_path: str, debug: bool) -> int:
83
+ """Run the daemon with a PID file, handling lock failure gracefully.
84
+
85
+ Opens the PID file lock before daemonizing so that a second invocation
86
+ fails immediately with a clear error message.
87
+
88
+ Args:
89
+ daemon (PluginDaemon): Configured daemon instance to run.
90
+ pid_path (str): Filesystem path for the PID file.
91
+ debug (bool): If True, skip daemonization and stay in foreground.
92
+
93
+ Returns:
94
+ int: Exit code from daemon.run(), or 1 if the PID file is already
95
+ locked by another instance.
96
+ """
97
+ try:
98
+ with PidFile(pid_path) as pid_file:
99
+ if not debug:
100
+ _daemonize()
101
+ return daemon.run(pid_file)
102
+ except RuntimeError as exc:
103
+ # PID file lock failure — another instance is running.
104
+ logging.getLogger(__name__).error("%s", exc)
105
+ return 1
106
+
107
+
108
+ def _configure_logging(debug: bool, level: int) -> None:
109
+ """Configure Python and sysrepo logging.
110
+
111
+ In debug mode logs to stderr; otherwise routes to syslog under the
112
+ application name ``'sysrepo-python-plugind'``.
113
+
114
+ Args:
115
+ debug (bool): If True, log to stderr; otherwise log to syslog.
116
+ level (int): Python logging level (e.g. ``logging.INFO``).
117
+ """
118
+ handler = logging.StreamHandler()
119
+ handler.setLevel(level)
120
+ logging.getLogger().addHandler(handler)
121
+ logging.getLogger().setLevel(level)
122
+
123
+ if debug:
124
+ sysrepo.configure_logging(stderr_level=level, py_logging=True)
125
+ else:
126
+ sysrepo.configure_logging(
127
+ syslog_level=level,
128
+ syslog_app_name="sysrepo-python-plugind",
129
+ py_logging=True,
130
+ )
131
+
132
+
133
+ def _daemonize() -> None:
134
+ """Daemonize the current process using the standard double-fork technique.
135
+
136
+ After the second fork the process is in its own session, has no
137
+ controlling terminal, and has stdin/stdout/stderr redirected to
138
+ /dev/null. File descriptors beyond stderr are left open so that a
139
+ PidFile lock opened before this call survives in the child process.
140
+ """
141
+ # First fork: become session leader's child.
142
+ pid = os.fork()
143
+ if pid > 0:
144
+ sys.exit(0)
145
+
146
+ os.setsid()
147
+
148
+ # Second fork: ensure we can never re-acquire a controlling terminal.
149
+ pid = os.fork()
150
+ if pid > 0:
151
+ sys.exit(0)
152
+
153
+ os.chdir("/")
154
+
155
+ devnull = os.open(os.devnull, os.O_RDWR)
156
+ for fd in (sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno()):
157
+ os.dup2(devnull, fd)
158
+ os.close(devnull)