Skip to content

Commit 1203356

Browse files
phlogistonjohnmergify[bot]
authored andcommitted
varlink: add server.py implementing the core varlink server
The sambacc varlink server types wrap the python varlink libraries types allowing it to more flexibly initialize the services (aka endpoints). Signed-off-by: John Mulligan <[email protected]>
1 parent 6db21a0 commit 1203356

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed

sambacc/varlink/server.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#
2+
# sambacc: a samba container configuration tool (and more)
3+
# Copyright (C) 2025 John Mulligan
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>
17+
#
18+
19+
20+
import contextlib
21+
import dataclasses
22+
import importlib.resources
23+
import logging
24+
import threading
25+
import typing
26+
27+
import varlink # type: ignore[import]
28+
import varlink.error # type: ignore[import]
29+
import varlink.server # type: ignore[import]
30+
31+
from .endpoint import VarlinkEndpoint
32+
33+
34+
_logger = logging.getLogger(__name__)
35+
36+
_VET = typing.Type[varlink.error.VarlinkEncoder]
37+
_varlink_VarlinkEncoder: _VET = varlink.error.VarlinkEncoder
38+
39+
40+
class VarlinkEncoder(_varlink_VarlinkEncoder):
41+
"""Custom varlink encoder supporting dataclasses."""
42+
43+
def default(self, obj: typing.Any) -> typing.Any:
44+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
45+
dct = dataclasses.asdict(obj)
46+
return dct
47+
return super().default(obj)
48+
49+
50+
def patch_varlink_encoder() -> None:
51+
"""Monkeypatch varlink encoder to enable dataclass support."""
52+
varlink.error.VarlinkEncoder = VarlinkEncoder
53+
varlink.server.VarlinkEncoder = VarlinkEncoder
54+
55+
56+
class VarlinkServerOptions:
57+
"""Options used to configure the sambacc varlink server."""
58+
59+
address: str
60+
vendor: str = "sambacc"
61+
version: str = "1"
62+
product: str = ""
63+
64+
def __init__(
65+
self,
66+
address: str,
67+
vendor: str = "",
68+
version: str = "",
69+
product: str = "",
70+
) -> None:
71+
self.address = address
72+
if vendor:
73+
self.vendor = vendor
74+
if version:
75+
self.version = version
76+
if product:
77+
self.product = product
78+
79+
80+
class VarlinkServer:
81+
"""Varlink server core.
82+
Use add_endpoint to attach new endpoint objects to the server. The endpoint
83+
objects map to varlink interfaces.
84+
Use serve function to start serving.
85+
"""
86+
87+
def __init__(
88+
self,
89+
options: VarlinkServerOptions,
90+
endpoints: typing.Optional[list[VarlinkEndpoint]] = None,
91+
) -> None:
92+
self.options = options
93+
self.endpoints: list[VarlinkEndpoint] = []
94+
for ep in endpoints or []:
95+
self.add_endpoint(ep)
96+
# internal attributes
97+
self._service: typing.Optional[varlink.Service] = None
98+
self._server: typing.Optional[varlink.Server] = None
99+
100+
def add_endpoint(self, endpoint: VarlinkEndpoint) -> None:
101+
"""Add a new endpoint object to the server object."""
102+
assert endpoint.interface_filename
103+
assert endpoint.interface_name
104+
assert endpoint.interface_cls
105+
self.endpoints.append(endpoint)
106+
107+
def _make_service(self) -> varlink.Service:
108+
svc = varlink.Service(
109+
vendor=self.options.vendor,
110+
product=self.options.product,
111+
version=self.options.version,
112+
)
113+
for ep in self.endpoints:
114+
self._attach(svc, ep)
115+
return svc
116+
117+
def _attach(self, svc: varlink.Service, endpoint: VarlinkEndpoint) -> None:
118+
"""Associate a varlink library service with a sambacc varlink endpoint.
119+
This is the main glue function between the way sambacc does things
120+
and the varlink library style of working.
121+
This is also where we use importlib to get the interface vs. the
122+
library's file-path based mechanism.
123+
"""
124+
if_data = importlib.resources.read_text(
125+
endpoint.location,
126+
endpoint.interface_filename,
127+
)
128+
interface = varlink.Interface(if_data)
129+
_logger.debug("Read interface data for %s", interface.name)
130+
handler = endpoint.interface_cls(**endpoint.interface_kwargs)
131+
svc.interfaces[interface.name] = interface
132+
svc.interfaces_handlers[interface.name] = handler
133+
_logger.debug("Attached handler for %s", interface.name)
134+
135+
def _request_handler_cls(self, svc: varlink.Service) -> typing.Any:
136+
class Handler(varlink.RequestHandler):
137+
service = svc
138+
139+
return Handler
140+
141+
def _make_server(self) -> None:
142+
"""Create the varlink library server object."""
143+
self._service = self._make_service()
144+
self._server = varlink.ThreadingServer(
145+
self.options.address, self._request_handler_cls(self._service)
146+
)
147+
_logger.debug("Created new varlink server")
148+
149+
@contextlib.contextmanager
150+
def serve(self) -> typing.Iterator[None]:
151+
"""Returns a context manager that Runs the varlink server in a thread,
152+
terminating the server when the context manager exits.
153+
"""
154+
if not self._server:
155+
self._make_server()
156+
assert self._server
157+
self._serve_thread = threading.Thread(
158+
target=self._server.serve_forever
159+
)
160+
self._serve_thread.start()
161+
_logger.debug("started server thread")
162+
try:
163+
yield
164+
finally:
165+
_logger.debug("shutting down server...")
166+
self._server.shutdown()
167+
_logger.debug("waiting for thread...")
168+
self._serve_thread.join()

0 commit comments

Comments
 (0)