From 4e065a96242fbbf7dc09a4bd004d6239303ffc9a Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 2 Oct 2025 16:25:58 +0200 Subject: [PATCH 1/4] vici: Add decorators to Python bindings to simplify listening for events --- src/libcharon/plugins/vici/python/README.rst | 27 +++++- .../plugins/vici/python/vici/__init__.py | 1 + .../vici/python/vici/event_listener.py | 85 +++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/libcharon/plugins/vici/python/vici/event_listener.py diff --git a/src/libcharon/plugins/vici/python/README.rst b/src/libcharon/plugins/vici/python/README.rst index 3990f6300..b9c0cebcb 100644 --- a/src/libcharon/plugins/vici/python/README.rst +++ b/src/libcharon/plugins/vici/python/README.rst @@ -7,8 +7,8 @@ side implementation of the VICI protocol, well suited to script automated tasks in a reliable way. -Example Usage -------------- +Basic Usage +----------- .. code-block:: python @@ -22,3 +22,26 @@ Example Usage >>> s.get_pools() OrderedDict([('p1', OrderedDict([('base', b'10.0.0.0'), ('size', b'254'), ('online', b'0'), ('offline', b'0')]))]) + +Event Handling +-------------- + +Either use the convenient decorators provided by EventListener or directly call +listen() on a Session object to loop over received events and dispatch them +manually. + +.. code-block:: python + + >>> import vici + >>> s = vici.Session() + >>> l = vici.EventListener(s) + >>> @l.on_events(['ike-updown', 'ike-rekey']) + ... def ike_events(name, data): + ... """Handle event with given 'name' and 'data'.""" + ... print(name, data) + ... + >>> @l.on_events(['child-updown', 'child-rekey']) + ... def child_events(name, data): + ... print(name, data) + ... + >>> l.listen() diff --git a/src/libcharon/plugins/vici/python/vici/__init__.py b/src/libcharon/plugins/vici/python/vici/__init__.py index d314325b6..12f84b366 100644 --- a/src/libcharon/plugins/vici/python/vici/__init__.py +++ b/src/libcharon/plugins/vici/python/vici/__init__.py @@ -1 +1,2 @@ +from .event_listener import EventListener from .session import Session diff --git a/src/libcharon/plugins/vici/python/vici/event_listener.py b/src/libcharon/plugins/vici/python/vici/event_listener.py new file mode 100644 index 000000000..45030bbb6 --- /dev/null +++ b/src/libcharon/plugins/vici/python/vici/event_listener.py @@ -0,0 +1,85 @@ +from functools import wraps + + +class EventListener(object): + def __init__(self, session=None): + """Create an event listener instance, which provides decorator methods + to make listening for events and the disconnection of the vici session + more convenient. + + The session is optional here, but one must be set via + :func:`~set_session()` before calling :func:`~listen()`. + + :param session: optional vici session to use + :type session: :class:`~vici.session.Session` or None + """ + self.event_map = {} + self.disconnect_list = [] + self.session = session + + def set_session(self, session): + """Set the session that's used to listen for events. Only has an effect + when set before calling :func:`~listen()`. + + :param session: vici session to use + :type session: :class:`~vici.session.Session` + """ + self.session = session + + def on_events(self, events): + """Decorator to mark a function as a listener for specific events. + + The decorated function is expected to receive the name of the event and + the data as arguments. + + :param events: events to register and call decorated function for + :type events: list + :return: decorator function + :rtype: any + """ + def decorator(func): + self.event_map.update({event: func for event in events}) + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorator + + def on_disconnected(self): + """Decorator to mark a function as a listener for when the daemon + disconnects the vici session. This listener instance is passed to the + decorated function. + + :return: decorator function + :rtype: any + """ + def decorator(func): + self.disconnect_list.append(func) + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorator + + def listen(self): + """Dispatch events registered via decorators of this instance. + + This method does not return unless the daemon disconnects or an + exception occurs. + + An active session has to be set before calling this. After getting + disconnected, a new session may be set via :func:`~set_session()` + before calling this again. + """ + try: + if self.session is None: + return + for label, event in self.session.listen(self.event_map.keys()): + name = label.decode() + if name in self.event_map: + self.event_map[name](name, event) + except IOError: + for func in self.disconnect_list: + func(self) From 79da1172831a43f044fe6ea1a03c0799aeb55e02 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 3 Oct 2025 10:59:03 +0200 Subject: [PATCH 2/4] vici: Provide a way to stop listening and re-connect in Python bindings This allows re-connecting to a new session in a disconnect listener and continue listening without having to return from listen(). The exception can also be used to stop listening after some condition (e.g. to wait until a specific SA got created and then stop). --- .../plugins/vici/python/vici/__init__.py | 2 +- .../vici/python/vici/event_listener.py | 52 ++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/libcharon/plugins/vici/python/vici/__init__.py b/src/libcharon/plugins/vici/python/vici/__init__.py index 12f84b366..3ba21a89b 100644 --- a/src/libcharon/plugins/vici/python/vici/__init__.py +++ b/src/libcharon/plugins/vici/python/vici/__init__.py @@ -1,2 +1,2 @@ -from .event_listener import EventListener +from .event_listener import EventListener, StopListening from .session import Session diff --git a/src/libcharon/plugins/vici/python/vici/event_listener.py b/src/libcharon/plugins/vici/python/vici/event_listener.py index 45030bbb6..2d82619a7 100644 --- a/src/libcharon/plugins/vici/python/vici/event_listener.py +++ b/src/libcharon/plugins/vici/python/vici/event_listener.py @@ -1,6 +1,10 @@ from functools import wraps +class StopListening(Exception): + """Exception that may be raised to stop listening for events.""" + + class EventListener(object): def __init__(self, session=None): """Create an event listener instance, which provides decorator methods @@ -30,7 +34,8 @@ class EventListener(object): """Decorator to mark a function as a listener for specific events. The decorated function is expected to receive the name of the event and - the data as arguments. + the data as arguments. It may raise :class:`~StopListening` to stop + listening and let :func:`~listen()` return. :param events: events to register and call decorated function for :type events: list @@ -48,8 +53,12 @@ class EventListener(object): def on_disconnected(self): """Decorator to mark a function as a listener for when the daemon - disconnects the vici session. This listener instance is passed to the - decorated function. + disconnects the vici session. + + This listener instance is passed to the decorated function, which may + be used to set a new session and continue listening. If no session is + set, :func:`~listen()` will return after the decorated function has + been called. :return: decorator function :rtype: any @@ -66,20 +75,27 @@ class EventListener(object): def listen(self): """Dispatch events registered via decorators of this instance. - This method does not return unless the daemon disconnects or an - exception occurs. - An active session has to be set before calling this. After getting - disconnected, a new session may be set via :func:`~set_session()` - before calling this again. + disconnected, a new session may be set via :func:`~set_session()` in + a function decorated with :func:`~on_disconnected()` to resume + listening for events. + + This method does not return unless :class:`~StopListening` or an + unexpected exception is raised or if the current session is disconnected + and no new session is set in a listener. """ - try: - if self.session is None: - return - for label, event in self.session.listen(self.event_map.keys()): - name = label.decode() - if name in self.event_map: - self.event_map[name](name, event) - except IOError: - for func in self.disconnect_list: - func(self) + while True: + try: + if self.session is None: + break + for label, event in self.session.listen(self.event_map.keys()): + name = label.decode() + if name in self.event_map: + self.event_map[name](name, event) + except IOError: + self.session = None + for func in self.disconnect_list: + func(self) + continue + except StopListening: + break From 8bfdf2fb6011124c1e30b049b934c26938f5c45e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 3 Oct 2025 11:42:11 +0200 Subject: [PATCH 3/4] vici: Export timeout in event listener of Python bindings This allows running periodic tasks (e.g. check some outside condition) and stop listening by raising the StopListening exception. --- .../vici/python/vici/event_listener.py | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/libcharon/plugins/vici/python/vici/event_listener.py b/src/libcharon/plugins/vici/python/vici/event_listener.py index 2d82619a7..f659362a1 100644 --- a/src/libcharon/plugins/vici/python/vici/event_listener.py +++ b/src/libcharon/plugins/vici/python/vici/event_listener.py @@ -1,4 +1,7 @@ from functools import wraps +import inspect + +from .protocol import RECV_TIMEOUT_DEFAULT class StopListening(Exception): @@ -19,6 +22,7 @@ class EventListener(object): """ self.event_map = {} self.disconnect_list = [] + self.timeout_list = [] self.session = session def set_session(self, session): @@ -72,7 +76,30 @@ class EventListener(object): return wrapper return decorator - def listen(self): + def on_timeout(self): + """Decorator to mark a function as a listener for when a timeout occurs + while waiting for events. Only has an effect if :func:`~listen()` is + called with a timeout. + + The decorated function may either take no or two arguments (both will be + set to `None`). So this may be applied to a function that's also + decorated with :func:`~on_events()`. It may raise + :class:`~StopListening` to stop listening and let :func:`~listen()` + return. + + :return: decorator function + :rtype: any + """ + def decorator(func): + self.timeout_list.append(func) + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorator + + def listen(self, timeout=RECV_TIMEOUT_DEFAULT): """Dispatch events registered via decorators of this instance. An active session has to be set before calling this. After getting @@ -83,12 +110,28 @@ class EventListener(object): This method does not return unless :class:`~StopListening` or an unexpected exception is raised or if the current session is disconnected and no new session is set in a listener. + + The optional timeout allows calling functions decorated with + :func:`~on_timeout()` if no event has been received for that time. Which + may be used to abort listening or perform periodic tasks while + continuing to listen for events. + + :param timeout: timeout to wait for events, in fractions of a second + :type timeout: float """ while True: try: if self.session is None: break - for label, event in self.session.listen(self.event_map.keys()): + for label, event in self.session.listen(self.event_map.keys(), + timeout): + if label is None and event is None: + for func in self.timeout_list: + if len(inspect.signature(func).parameters) > 0: + func(label, event) + else: + func() + continue name = label.decode() if name in self.event_map: self.event_map[name](name, event) From 4840507d7aeec03845f4cecb7cdea73b9a0c2021 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 8 Oct 2025 16:21:33 +0200 Subject: [PATCH 4/4] vici: Allow callers of listen() to distinguish between disconnects and intended breaks --- src/libcharon/plugins/vici/python/vici/event_listener.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libcharon/plugins/vici/python/vici/event_listener.py b/src/libcharon/plugins/vici/python/vici/event_listener.py index f659362a1..f2cc6c8cf 100644 --- a/src/libcharon/plugins/vici/python/vici/event_listener.py +++ b/src/libcharon/plugins/vici/python/vici/event_listener.py @@ -118,11 +118,13 @@ class EventListener(object): :param timeout: timeout to wait for events, in fractions of a second :type timeout: float + :return: True if StopListening was raised, False if no session available + :rtype: bool """ while True: try: if self.session is None: - break + return False for label, event in self.session.listen(self.event_map.keys(), timeout): if label is None and event is None: @@ -141,4 +143,4 @@ class EventListener(object): func(self) continue except StopListening: - break + return True