blob: ece0e4ee52dcc2ef491452b84c18695199f97a67 [file] [log] [blame]
Ole Troana03f4ef2016-12-02 12:53:55 +01001#!/usr/bin/env python
Ole Troan5f9dcff2016-08-01 04:59:13 +02002#
3# Copyright (c) 2016 Cisco and/or its affiliates.
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at:
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
Ole Troana03f4ef2016-12-02 12:53:55 +010015#
Ole Troan5f9dcff2016-08-01 04:59:13 +020016
Ole Troan5f9dcff2016-08-01 04:59:13 +020017from __future__ import print_function
Ole Troan4df97162017-07-07 16:06:08 +020018import sys
19import os
20import logging
21import collections
22import struct
23import json
24import threading
Chris Luke52bf22e2017-11-03 23:32:38 -040025import fnmatch
Klement Sekera180402d2018-02-17 10:58:37 +010026import weakref
Ole Troan4df97162017-07-07 16:06:08 +020027import atexit
Ole Troan3cc49712017-03-08 12:02:24 +010028from cffi import FFI
Ole Troana74b7412017-11-01 10:49:03 +010029import cffi
Ole Troan4df97162017-07-07 16:06:08 +020030
31if sys.version[0] == '2':
32 import Queue as queue
33else:
34 import queue as queue
35
Ole Troan3cc49712017-03-08 12:02:24 +010036ffi = FFI()
37ffi.cdef("""
Damjan Marion5fec1e82017-04-13 19:13:47 +020038typedef void (*vac_callback_t)(unsigned char * data, int len);
39typedef void (*vac_error_callback_t)(void *, unsigned char *, int);
40int vac_connect(char * name, char * chroot_prefix, vac_callback_t cb,
Ole Troan3cc49712017-03-08 12:02:24 +010041 int rx_qlen);
Damjan Marion5fec1e82017-04-13 19:13:47 +020042int vac_disconnect(void);
43int vac_read(char **data, int *l, unsigned short timeout);
44int vac_write(char *data, int len);
45void vac_free(void * msg);
Ole Troan5f9dcff2016-08-01 04:59:13 +020046
Damjan Marion5fec1e82017-04-13 19:13:47 +020047int vac_get_msg_index(unsigned char * name);
48int vac_msg_table_size(void);
49int vac_msg_table_max_index(void);
Ole Troan3cc49712017-03-08 12:02:24 +010050
Damjan Marion5fec1e82017-04-13 19:13:47 +020051void vac_rx_suspend (void);
52void vac_rx_resume (void);
53void vac_set_error_handler(vac_error_callback_t);
Ole Troan3cc49712017-03-08 12:02:24 +010054 """)
55
56# Barfs on failure, no need to check success.
Damjan Marion5fec1e82017-04-13 19:13:47 +020057vpp_api = ffi.dlopen('libvppapiclient.so')
Ole Troan1732fc12016-08-30 21:03:51 +020058
Ole Troanafddd832018-02-28 14:55:20 +010059
Klement Sekera180402d2018-02-17 10:58:37 +010060def vpp_atexit(vpp_weakref):
Ole Troan5016f992017-01-19 09:44:44 +010061 """Clean up VPP connection on shutdown."""
Klement Sekera180402d2018-02-17 10:58:37 +010062 vpp_instance = vpp_weakref()
63 if vpp_instance.connected:
64 vpp_instance.logger.debug('Cleaning up VPP on exit')
65 vpp_instance.disconnect()
Ole Troan5016f992017-01-19 09:44:44 +010066
Ole Troanafddd832018-02-28 14:55:20 +010067
Ole Troan3cc49712017-03-08 12:02:24 +010068vpp_object = None
69
Ole Troan4df97162017-07-07 16:06:08 +020070
71def vpp_iterator(d):
72 if sys.version[0] == '2':
73 return d.iteritems()
74 else:
75 return d.items()
76
77
Ole Troan3cc49712017-03-08 12:02:24 +010078@ffi.callback("void(unsigned char *, int)")
Damjan Marion5fec1e82017-04-13 19:13:47 +020079def vac_callback_sync(data, len):
Ole Troan3cc49712017-03-08 12:02:24 +010080 vpp_object.msg_handler_sync(ffi.buffer(data, len))
Ole Troan4df97162017-07-07 16:06:08 +020081
82
Ole Troan3cc49712017-03-08 12:02:24 +010083@ffi.callback("void(unsigned char *, int)")
Damjan Marion5fec1e82017-04-13 19:13:47 +020084def vac_callback_async(data, len):
Ole Troan3cc49712017-03-08 12:02:24 +010085 vpp_object.msg_handler_async(ffi.buffer(data, len))
Ole Troan4df97162017-07-07 16:06:08 +020086
87
Ole Troan3cc49712017-03-08 12:02:24 +010088@ffi.callback("void(void *, unsigned char *, int)")
Damjan Marion5fec1e82017-04-13 19:13:47 +020089def vac_error_handler(arg, msg, msg_len):
Ole Troan4df97162017-07-07 16:06:08 +020090 vpp_object.logger.warning("VPP API client:: %s", ffi.string(msg, msg_len))
91
Klement Sekera7112c542017-03-01 09:53:19 +010092
93class Empty(object):
94 pass
95
96
97class FuncWrapper(object):
98 def __init__(self, func):
99 self._func = func
100 self.__name__ = func.__name__
101
102 def __call__(self, **kwargs):
103 return self._func(**kwargs)
104
105
Ole Troana03f4ef2016-12-02 12:53:55 +0100106class VPP():
Ole Troan5016f992017-01-19 09:44:44 +0100107 """VPP interface.
108
109 This class provides the APIs to VPP. The APIs are loaded
110 from provided .api.json files and makes functions accordingly.
111 These functions are documented in the VPP .api files, as they
112 are dynamically created.
113
114 Additionally, VPP can send callback messages; this class
115 provides a means to register a callback function to receive
116 these messages in a background thread.
117 """
Ole Troan4df97162017-07-07 16:06:08 +0200118 def __init__(self, apifiles=None, testmode=False, async_thread=True,
Ian Wellsd0e812f2018-06-06 14:12:27 +0100119 logger=None, loglevel=None,
Ole Troanafddd832018-02-28 14:55:20 +0100120 read_timeout=0):
Ole Troan5016f992017-01-19 09:44:44 +0100121 """Create a VPP API object.
122
123 apifiles is a list of files containing API
124 descriptions that will be loaded - methods will be
125 dynamically created reflecting these APIs. If not
126 provided this will load the API files from VPP's
127 default install location.
Ian Wellsd0e812f2018-06-06 14:12:27 +0100128
129 logger, if supplied, is the logging logger object to log to.
130 loglevel, if supplied, is the log level this logger is set
131 to report at (from the loglevels in the logging module).
Ole Troan5016f992017-01-19 09:44:44 +0100132 """
Ole Troan3cc49712017-03-08 12:02:24 +0100133 global vpp_object
134 vpp_object = self
Ian Wellsd0e812f2018-06-06 14:12:27 +0100135
136 if logger is None:
137 logger = logging.getLogger(__name__)
138 if loglevel is not None:
139 logger.setLevel(loglevel)
140
Ole Troan3cc49712017-03-08 12:02:24 +0100141 self.logger = logger
Ole Troan3cc49712017-03-08 12:02:24 +0100142
Ole Troana03f4ef2016-12-02 12:53:55 +0100143 self.messages = {}
144 self.id_names = []
145 self.id_msgdef = []
Ole Troana03f4ef2016-12-02 12:53:55 +0100146 self.connected = False
147 self.header = struct.Struct('>HI')
Ole Troan5016f992017-01-19 09:44:44 +0100148 self.apifiles = []
Ole Troan3d31f002017-01-26 11:13:00 +0100149 self.event_callback = None
Ole Troan4df97162017-07-07 16:06:08 +0200150 self.message_queue = queue.Queue()
dongjuan84937522017-11-09 14:46:36 +0800151 self.read_timeout = read_timeout
Ole Troandfc9b7c2017-03-06 23:51:57 +0100152 self.vpp_api = vpp_api
Klement Sekera180402d2018-02-17 10:58:37 +0100153 self.async_thread = async_thread
Ole Troan5f9dcff2016-08-01 04:59:13 +0200154
Ole Troanf5984bd2016-12-18 13:15:08 +0100155 if not apifiles:
156 # Pick up API definitions from default directory
Chris Luke52bf22e2017-11-03 23:32:38 -0400157 try:
158 apifiles = self.find_api_files()
159 except RuntimeError:
160 # In test mode we don't care that we can't find the API files
161 if testmode:
162 apifiles = []
163 else:
164 raise
Ole Troanf5984bd2016-12-18 13:15:08 +0100165
Ole Troana03f4ef2016-12-02 12:53:55 +0100166 for file in apifiles:
Ole Troana03f4ef2016-12-02 12:53:55 +0100167 with open(file) as apidef_file:
168 api = json.load(apidef_file)
169 for t in api['types']:
170 self.add_type(t[0], t[1:])
Ole Troan5f9dcff2016-08-01 04:59:13 +0200171
Ole Troana03f4ef2016-12-02 12:53:55 +0100172 for m in api['messages']:
173 self.add_message(m[0], m[1:])
Ole Troan4df97162017-07-07 16:06:08 +0200174 self.apifiles = apifiles
Ole Troan5f9dcff2016-08-01 04:59:13 +0200175
Ole Troana03f4ef2016-12-02 12:53:55 +0100176 # Basic sanity check
Ole Troanf5984bd2016-12-18 13:15:08 +0100177 if len(self.messages) == 0 and not testmode:
178 raise ValueError(1, 'Missing JSON message definitions')
Ole Troan5f9dcff2016-08-01 04:59:13 +0200179
Ole Troan5016f992017-01-19 09:44:44 +0100180 # Make sure we allow VPP to clean up the message rings.
Klement Sekera180402d2018-02-17 10:58:37 +0100181 atexit.register(vpp_atexit, weakref.ref(self))
Ole Troan5f9dcff2016-08-01 04:59:13 +0200182
Ole Troan3cc49712017-03-08 12:02:24 +0100183 # Register error handler
Damjan Marion5fec1e82017-04-13 19:13:47 +0200184 vpp_api.vac_set_error_handler(vac_error_handler)
Ole Troan3cc49712017-03-08 12:02:24 +0100185
Ole Troana74b7412017-11-01 10:49:03 +0100186 # Support legacy CFFI
187 # from_buffer supported from 1.8.0
Ole Troanafddd832018-02-28 14:55:20 +0100188 (major, minor, patch) = [int(s) for s in
189 cffi.__version__.split('.', 3)]
Ole Troana74b7412017-11-01 10:49:03 +0100190 if major >= 1 and minor >= 8:
191 self._write = self._write_new_cffi
192 else:
193 self._write = self._write_legacy_cffi
194
Ole Troana03f4ef2016-12-02 12:53:55 +0100195 class ContextId(object):
Ole Troan5016f992017-01-19 09:44:44 +0100196 """Thread-safe provider of unique context IDs."""
Ole Troana03f4ef2016-12-02 12:53:55 +0100197 def __init__(self):
198 self.context = 0
Ole Troan4df97162017-07-07 16:06:08 +0200199 self.lock = threading.Lock()
200
Ole Troana03f4ef2016-12-02 12:53:55 +0100201 def __call__(self):
Ole Troan5016f992017-01-19 09:44:44 +0100202 """Get a new unique (or, at least, not recently used) context."""
Ole Troan4df97162017-07-07 16:06:08 +0200203 with self.lock:
204 self.context += 1
205 return self.context
Ole Troana03f4ef2016-12-02 12:53:55 +0100206 get_context = ContextId()
Ole Troan5f9dcff2016-08-01 04:59:13 +0200207
Chris Luke52bf22e2017-11-03 23:32:38 -0400208 @classmethod
209 def find_api_dir(cls):
210 """Attempt to find the best directory in which API definition
211 files may reside. If the value VPP_API_DIR exists in the environment
212 then it is first on the search list. If we're inside a recognized
213 location in a VPP source tree (src/scripts and src/vpp-api/python)
214 then entries from there to the likely locations in build-root are
215 added. Finally the location used by system packages is added.
216
217 :returns: A single directory name, or None if no such directory
218 could be found.
219 """
220 dirs = []
221
222 if 'VPP_API_DIR' in os.environ:
223 dirs.append(os.environ['VPP_API_DIR'])
224
225 # perhaps we're in the 'src/scripts' or 'src/vpp-api/python' dir;
226 # in which case, plot a course to likely places in the src tree
227 import __main__ as main
228 if hasattr(main, '__file__'):
229 # get the path of the calling script
230 localdir = os.path.dirname(os.path.realpath(main.__file__))
231 else:
232 # use cwd if there is no calling script
Andrey "Zed" Zaikin68e2ffb2018-04-24 14:50:02 +0300233 localdir = os.getcwd()
Chris Luke52bf22e2017-11-03 23:32:38 -0400234 localdir_s = localdir.split(os.path.sep)
235
236 def dmatch(dir):
237 """Match dir against right-hand components of the script dir"""
238 d = dir.split('/') # param 'dir' assumes a / separator
Ole Troanafddd832018-02-28 14:55:20 +0100239 length = len(d)
240 return len(localdir_s) > length and localdir_s[-length:] == d
Chris Luke52bf22e2017-11-03 23:32:38 -0400241
242 def sdir(srcdir, variant):
243 """Build a path from srcdir to the staged API files of
244 'variant' (typically '' or '_debug')"""
245 # Since 'core' and 'plugin' files are staged
246 # in separate directories, we target the parent dir.
247 return os.path.sep.join((
248 srcdir,
249 'build-root',
250 'install-vpp%s-native' % variant,
251 'vpp',
252 'share',
253 'vpp',
254 'api',
255 ))
256
257 srcdir = None
258 if dmatch('src/scripts'):
259 srcdir = os.path.sep.join(localdir_s[:-2])
260 elif dmatch('src/vpp-api/python'):
261 srcdir = os.path.sep.join(localdir_s[:-3])
262 elif dmatch('test'):
263 # we're apparently running tests
264 srcdir = os.path.sep.join(localdir_s[:-1])
265
266 if srcdir:
267 # we're in the source tree, try both the debug and release
268 # variants.
Chris Luke52bf22e2017-11-03 23:32:38 -0400269 dirs.append(sdir(srcdir, '_debug'))
270 dirs.append(sdir(srcdir, ''))
271
272 # Test for staged copies of the scripts
273 # For these, since we explicitly know if we're running a debug versus
274 # release variant, target only the relevant directory
275 if dmatch('build-root/install-vpp_debug-native/vpp/bin'):
276 srcdir = os.path.sep.join(localdir_s[:-4])
277 dirs.append(sdir(srcdir, '_debug'))
278 if dmatch('build-root/install-vpp-native/vpp/bin'):
279 srcdir = os.path.sep.join(localdir_s[:-4])
280 dirs.append(sdir(srcdir, ''))
281
282 # finally, try the location system packages typically install into
283 dirs.append(os.path.sep.join(('', 'usr', 'share', 'vpp', 'api')))
284
285 # check the directories for existance; first one wins
286 for dir in dirs:
287 if os.path.isdir(dir):
288 return dir
289
290 return None
291
292 @classmethod
293 def find_api_files(cls, api_dir=None, patterns='*'):
294 """Find API definition files from the given directory tree with the
295 given pattern. If no directory is given then find_api_dir() is used
296 to locate one. If no pattern is given then all definition files found
297 in the directory tree are used.
298
299 :param api_dir: A directory tree in which to locate API definition
300 files; subdirectories are descended into.
301 If this is None then find_api_dir() is called to discover it.
302 :param patterns: A list of patterns to use in each visited directory
303 when looking for files.
304 This can be a list/tuple object or a comma-separated string of
305 patterns. Each value in the list will have leading/trialing
306 whitespace stripped.
307 The pattern specifies the first part of the filename, '.api.json'
308 is appended.
309 The results are de-duplicated, thus overlapping patterns are fine.
310 If this is None it defaults to '*' meaning "all API files".
311 :returns: A list of file paths for the API files found.
312 """
313 if api_dir is None:
314 api_dir = cls.find_api_dir()
315 if api_dir is None:
316 raise RuntimeError("api_dir cannot be located")
317
318 if isinstance(patterns, list) or isinstance(patterns, tuple):
319 patterns = [p.strip() + '.api.json' for p in patterns]
320 else:
321 patterns = [p.strip() + '.api.json' for p in patterns.split(",")]
322
323 api_files = []
324 for root, dirnames, files in os.walk(api_dir):
325 # iterate all given patterns and de-dup the result
326 files = set(sum([fnmatch.filter(files, p) for p in patterns], []))
327 for filename in files:
328 api_files.append(os.path.join(root, filename))
329
330 return api_files
331
Ole Troana03f4ef2016-12-02 12:53:55 +0100332 def status(self):
Ole Troan5016f992017-01-19 09:44:44 +0100333 """Debug function: report current VPP API status to stdout."""
Ole Troana03f4ef2016-12-02 12:53:55 +0100334 print('Connected') if self.connected else print('Not Connected')
Ole Troan5016f992017-01-19 09:44:44 +0100335 print('Read API definitions from', ', '.join(self.apifiles))
Ole Troan5f9dcff2016-08-01 04:59:13 +0200336
Ole Troan4df97162017-07-07 16:06:08 +0200337 def __struct(self, t, n=None, e=-1, vl=None):
Ole Troan5016f992017-01-19 09:44:44 +0100338 """Create a packing structure for a message."""
Ole Troan4df97162017-07-07 16:06:08 +0200339 base_types = {'u8': 'B',
340 'u16': 'H',
341 'u32': 'I',
342 'i32': 'i',
343 'u64': 'Q',
344 'f64': 'd', }
Ole Troanafddd832018-02-28 14:55:20 +0100345
Ole Troana03f4ef2016-12-02 12:53:55 +0100346 if t in base_types:
Ole Troana03f4ef2016-12-02 12:53:55 +0100347 if not vl:
Ole Troanf5984bd2016-12-18 13:15:08 +0100348 if e > 0 and t == 'u8':
Ole Troana03f4ef2016-12-02 12:53:55 +0100349 # Fixed byte array
Ole Troan895b6e82017-10-20 13:28:20 +0200350 s = struct.Struct('>' + str(e) + 's')
351 return s.size, s
Ole Troanf5984bd2016-12-18 13:15:08 +0100352 if e > 0:
Ole Troana03f4ef2016-12-02 12:53:55 +0100353 # Fixed array of base type
Ole Troan895b6e82017-10-20 13:28:20 +0200354 s = struct.Struct('>' + base_types[t])
355 return s.size, [e, s]
Ole Troanf5984bd2016-12-18 13:15:08 +0100356 elif e == 0:
357 # Old style variable array
Ole Troan895b6e82017-10-20 13:28:20 +0200358 s = struct.Struct('>' + base_types[t])
359 return s.size, [-1, s]
Ole Troana03f4ef2016-12-02 12:53:55 +0100360 else:
361 # Variable length array
Ole Troan895b6e82017-10-20 13:28:20 +0200362 if t == 'u8':
363 s = struct.Struct('>s')
364 return s.size, [vl, s]
365 else:
366 s = struct.Struct('>' + base_types[t])
367 return s.size, [vl, s]
Ole Troan57c3d662016-09-12 22:00:32 +0200368
Ole Troan895b6e82017-10-20 13:28:20 +0200369 s = struct.Struct('>' + base_types[t])
370 return s.size, s
Ole Troan57c3d662016-09-12 22:00:32 +0200371
Ole Troana03f4ef2016-12-02 12:53:55 +0100372 if t in self.messages:
Ole Troan895b6e82017-10-20 13:28:20 +0200373 size = self.messages[t]['sizes'][0]
374
Ole Troan4df97162017-07-07 16:06:08 +0200375 # Return a list in case of array
Ole Troanf5984bd2016-12-18 13:15:08 +0100376 if e > 0 and not vl:
Ole Troan895b6e82017-10-20 13:28:20 +0200377 return size, [e, lambda self, encode, buf, offset, args: (
Ole Troanf5984bd2016-12-18 13:15:08 +0100378 self.__struct_type(encode, self.messages[t], buf, offset,
379 args))]
Ole Troana03f4ef2016-12-02 12:53:55 +0100380 if vl:
Ole Troan895b6e82017-10-20 13:28:20 +0200381 return size, [vl, lambda self, encode, buf, offset, args: (
Ole Troanf5984bd2016-12-18 13:15:08 +0100382 self.__struct_type(encode, self.messages[t], buf, offset,
383 args))]
384 elif e == 0:
385 # Old style VLA
Ole Troan4df97162017-07-07 16:06:08 +0200386 raise NotImplementedError(1,
387 'No support for compound types ' + t)
Ole Troan895b6e82017-10-20 13:28:20 +0200388 return size, lambda self, encode, buf, offset, args: (
Ole Troana03f4ef2016-12-02 12:53:55 +0100389 self.__struct_type(encode, self.messages[t], buf, offset, args)
390 )
Ole Troanb8602b52016-10-05 11:10:50 +0200391
Ole Troanf5984bd2016-12-18 13:15:08 +0100392 raise ValueError(1, 'Invalid message type: ' + t)
Ole Troan5f9dcff2016-08-01 04:59:13 +0200393
Ole Troana03f4ef2016-12-02 12:53:55 +0100394 def __struct_type(self, encode, msgdef, buf, offset, kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100395 """Get a message packer or unpacker."""
Ole Troana03f4ef2016-12-02 12:53:55 +0100396 if encode:
397 return self.__struct_type_encode(msgdef, buf, offset, kwargs)
398 else:
399 return self.__struct_type_decode(msgdef, buf, offset)
Ole Troan5f9dcff2016-08-01 04:59:13 +0200400
Ole Troana03f4ef2016-12-02 12:53:55 +0100401 def __struct_type_encode(self, msgdef, buf, offset, kwargs):
402 off = offset
403 size = 0
Ole Troan7e3a8752016-12-05 10:27:09 +0100404
405 for k in kwargs:
406 if k not in msgdef['args']:
Ole Troanafddd832018-02-28 14:55:20 +0100407 raise ValueError(1, 'Non existing argument [' + k + ']' +
408 ' used in call to: ' +
409 self.id_names[kwargs['_vl_msg_id']] + '()')
Ole Troan68ec9402017-08-31 13:18:44 +0200410
Ole Troan4df97162017-07-07 16:06:08 +0200411 for k, v in vpp_iterator(msgdef['args']):
Ole Troana03f4ef2016-12-02 12:53:55 +0100412 off += size
413 if k in kwargs:
414 if type(v) is list:
415 if callable(v[1]):
Ole Troanf5984bd2016-12-18 13:15:08 +0100416 e = kwargs[v[0]] if v[0] in kwargs else v[0]
Ole Troan895b6e82017-10-20 13:28:20 +0200417 if e != len(kwargs[k]):
Ole Troanafddd832018-02-28 14:55:20 +0100418 raise (ValueError(1,
419 'Input list length mismatch: '
420 '%s (%s != %s)' %
421 (k, e, len(kwargs[k]))))
Ole Troana03f4ef2016-12-02 12:53:55 +0100422 size = 0
423 for i in range(e):
424 size += v[1](self, True, buf, off + size,
425 kwargs[k][i])
426 else:
427 if v[0] in kwargs:
Ole Troanafddd832018-02-28 14:55:20 +0100428 kwargslen = kwargs[v[0]]
429 if kwargslen != len(kwargs[k]):
430 raise ValueError(1,
431 'Input list length mismatch:'
432 ' %s (%s != %s)' %
433 (k, kwargslen,
434 len(kwargs[k])))
Ole Troana03f4ef2016-12-02 12:53:55 +0100435 else:
Ole Troanafddd832018-02-28 14:55:20 +0100436 kwargslen = len(kwargs[k])
Ole Troana03f4ef2016-12-02 12:53:55 +0100437 if v[1].size == 1:
Ole Troanafddd832018-02-28 14:55:20 +0100438 buf[off:off + kwargslen] = bytearray(kwargs[k])
439 size = kwargslen
Ole Troana03f4ef2016-12-02 12:53:55 +0100440 else:
441 size = 0
442 for i in kwargs[k]:
443 v[1].pack_into(buf, off + size, i)
444 size += v[1].size
445 else:
446 if callable(v):
447 size = v(self, True, buf, off, kwargs[k])
448 else:
Ole Troan895b6e82017-10-20 13:28:20 +0200449 if type(kwargs[k]) is str and v.size < len(kwargs[k]):
Ole Troanafddd832018-02-28 14:55:20 +0100450 raise ValueError(1,
451 'Input list length mismatch: '
452 '%s (%s < %s)' %
453 (k, v.size, len(kwargs[k])))
Ole Troana03f4ef2016-12-02 12:53:55 +0100454 v.pack_into(buf, off, kwargs[k])
455 size = v.size
456 else:
Ole Troan7e3a8752016-12-05 10:27:09 +0100457 size = v.size if not type(v) is list else 0
Ole Troan5f9dcff2016-08-01 04:59:13 +0200458
Ole Troana03f4ef2016-12-02 12:53:55 +0100459 return off + size - offset
Ole Troan5f9dcff2016-08-01 04:59:13 +0200460
Ole Troana03f4ef2016-12-02 12:53:55 +0100461 def __getitem__(self, name):
462 if name in self.messages:
463 return self.messages[name]
464 return None
Ole Troan5f9dcff2016-08-01 04:59:13 +0200465
Ole Troan895b6e82017-10-20 13:28:20 +0200466 def get_size(self, sizes, kwargs):
467 total_size = sizes[0]
468 for e in sizes[1]:
469 if e in kwargs and type(kwargs[e]) is list:
470 total_size += len(kwargs[e]) * sizes[1][e]
471 return total_size
472
Ole Troana03f4ef2016-12-02 12:53:55 +0100473 def encode(self, msgdef, kwargs):
474 # Make suitably large buffer
Chris Luke52bf22e2017-11-03 23:32:38 -0400475 size = self.get_size(msgdef['sizes'], kwargs)
Ole Troan895b6e82017-10-20 13:28:20 +0200476 buf = bytearray(size)
Ole Troana03f4ef2016-12-02 12:53:55 +0100477 offset = 0
478 size = self.__struct_type(True, msgdef, buf, offset, kwargs)
479 return buf[:offset + size]
480
481 def decode(self, msgdef, buf):
482 return self.__struct_type(False, msgdef, buf, 0, None)[1]
483
484 def __struct_type_decode(self, msgdef, buf, offset):
485 res = []
486 off = offset
487 size = 0
Ole Troan4df97162017-07-07 16:06:08 +0200488 for k, v in vpp_iterator(msgdef['args']):
Ole Troana03f4ef2016-12-02 12:53:55 +0100489 off += size
490 if type(v) is list:
491 lst = []
Ole Troan4df97162017-07-07 16:06:08 +0200492 if callable(v[1]): # compound type
Ole Troana03f4ef2016-12-02 12:53:55 +0100493 size = 0
Ole Troan4df97162017-07-07 16:06:08 +0200494 if v[0] in msgdef['args']: # vla
Ole Troanf5984bd2016-12-18 13:15:08 +0100495 e = res[v[2]]
Ole Troan4df97162017-07-07 16:06:08 +0200496 else: # fixed array
Ole Troana03f4ef2016-12-02 12:53:55 +0100497 e = v[0]
498 res.append(lst)
499 for i in range(e):
Ole Troan4df97162017-07-07 16:06:08 +0200500 (s, l) = v[1](self, False, buf, off + size, None)
Ole Troana03f4ef2016-12-02 12:53:55 +0100501 lst.append(l)
502 size += s
503 continue
Ole Troana03f4ef2016-12-02 12:53:55 +0100504 if v[1].size == 1:
Ole Troanf5984bd2016-12-18 13:15:08 +0100505 if type(v[0]) is int:
506 size = len(buf) - off
507 else:
508 size = res[v[2]]
Ole Troana03f4ef2016-12-02 12:53:55 +0100509 res.append(buf[off:off + size])
510 else:
Ole Troanf5984bd2016-12-18 13:15:08 +0100511 e = v[0] if type(v[0]) is int else res[v[2]]
512 if e == -1:
513 e = (len(buf) - off) / v[1].size
Ole Troana03f4ef2016-12-02 12:53:55 +0100514 lst = []
515 res.append(lst)
516 size = 0
517 for i in range(e):
518 lst.append(v[1].unpack_from(buf, off + size)[0])
519 size += v[1].size
520 else:
521 if callable(v):
Ole Troanca3b6f12017-10-31 14:50:13 +0100522 size = 0
Ole Troan4df97162017-07-07 16:06:08 +0200523 (s, l) = v(self, False, buf, off, None)
Ole Troana03f4ef2016-12-02 12:53:55 +0100524 res.append(l)
525 size += s
526 else:
527 res.append(v.unpack_from(buf, off)[0])
528 size = v.size
529
530 return off + size - offset, msgdef['return_tuple']._make(res)
531
532 def ret_tup(self, name):
533 if name in self.messages and 'return_tuple' in self.messages[name]:
534 return self.messages[name]['return_tuple']
535 return None
536
Ole Troan2eaf2d22018-01-25 09:53:31 +0100537 def duplicate_check_ok(self, name, msgdef):
538 crc = None
539 for c in msgdef:
540 if type(c) is dict and 'crc' in c:
541 crc = c['crc']
542 break
543 if crc:
544 # We can get duplicates because of imports
545 if crc == self.messages[name]['crc']:
546 return True
547 return False
548
Ole Troan4df97162017-07-07 16:06:08 +0200549 def add_message(self, name, msgdef, typeonly=False):
Ole Troana03f4ef2016-12-02 12:53:55 +0100550 if name in self.messages:
Ole Troan2eaf2d22018-01-25 09:53:31 +0100551 if typeonly:
552 if self.duplicate_check_ok(name, msgdef):
553 return
Ole Troana03f4ef2016-12-02 12:53:55 +0100554 raise ValueError('Duplicate message name: ' + name)
555
556 args = collections.OrderedDict()
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100557 argtypes = collections.OrderedDict()
Ole Troana03f4ef2016-12-02 12:53:55 +0100558 fields = []
559 msg = {}
Ole Troan895b6e82017-10-20 13:28:20 +0200560 total_size = 0
561 sizes = {}
Ole Troanf5984bd2016-12-18 13:15:08 +0100562 for i, f in enumerate(msgdef):
Ole Troana03f4ef2016-12-02 12:53:55 +0100563 if type(f) is dict and 'crc' in f:
564 msg['crc'] = f['crc']
565 continue
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100566 field_type = f[0]
Ole Troana03f4ef2016-12-02 12:53:55 +0100567 field_name = f[1]
Ole Troanf5984bd2016-12-18 13:15:08 +0100568 if len(f) == 3 and f[2] == 0 and i != len(msgdef) - 2:
569 raise ValueError('Variable Length Array must be last: ' + name)
Ole Troan895b6e82017-10-20 13:28:20 +0200570 size, s = self.__struct(*f)
571 args[field_name] = s
Ole Troanafddd832018-02-28 14:55:20 +0100572 if type(s) == list and type(s[0]) == int and \
573 type(s[1]) == struct.Struct:
Ole Troan895b6e82017-10-20 13:28:20 +0200574 if s[0] < 0:
575 sizes[field_name] = size
576 else:
577 sizes[field_name] = size
578 total_size += s[0] * size
579 else:
580 sizes[field_name] = size
581 total_size += size
582
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100583 argtypes[field_name] = field_type
Ole Troan4df97162017-07-07 16:06:08 +0200584 if len(f) == 4: # Find offset to # elements field
585 idx = list(args.keys()).index(f[3]) - i
586 args[field_name].append(idx)
Ole Troana03f4ef2016-12-02 12:53:55 +0100587 fields.append(field_name)
588 msg['return_tuple'] = collections.namedtuple(name, fields,
Ole Troan4df97162017-07-07 16:06:08 +0200589 rename=True)
Ole Troana03f4ef2016-12-02 12:53:55 +0100590 self.messages[name] = msg
591 self.messages[name]['args'] = args
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100592 self.messages[name]['argtypes'] = argtypes
Ole Troan3cc49712017-03-08 12:02:24 +0100593 self.messages[name]['typeonly'] = typeonly
Ole Troan895b6e82017-10-20 13:28:20 +0200594 self.messages[name]['sizes'] = [total_size, sizes]
Ole Troana03f4ef2016-12-02 12:53:55 +0100595 return self.messages[name]
596
597 def add_type(self, name, typedef):
Ole Troan4df97162017-07-07 16:06:08 +0200598 return self.add_message('vl_api_' + name + '_t', typedef,
599 typeonly=True)
Ole Troana03f4ef2016-12-02 12:53:55 +0100600
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100601 def make_function(self, name, i, msgdef, multipart, async):
Wojciech Dec64bc6122016-12-12 11:32:25 +0100602 if (async):
Ole Troanafddd832018-02-28 14:55:20 +0100603 def f(**kwargs):
604 return self._call_vpp_async(i, msgdef, **kwargs)
Wojciech Dec64bc6122016-12-12 11:32:25 +0100605 else:
Ole Troanafddd832018-02-28 14:55:20 +0100606 def f(**kwargs):
607 return self._call_vpp(i, msgdef, multipart, **kwargs)
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100608 args = self.messages[name]['args']
609 argtypes = self.messages[name]['argtypes']
610 f.__name__ = str(name)
Ole Troan4df97162017-07-07 16:06:08 +0200611 f.__doc__ = ", ".join(["%s %s" %
612 (argtypes[k], k) for k in args.keys()])
Christophe Fontaine04f4b782016-12-09 15:53:47 +0100613 return f
Ole Troana03f4ef2016-12-02 12:53:55 +0100614
Klement Sekera7112c542017-03-01 09:53:19 +0100615 @property
616 def api(self):
617 if not hasattr(self, "_api"):
618 raise Exception("Not connected, api definitions not available")
619 return self._api
620
Wojciech Dec64bc6122016-12-12 11:32:25 +0100621 def _register_functions(self, async=False):
Ole Troana03f4ef2016-12-02 12:53:55 +0100622 self.id_names = [None] * (self.vpp_dictionary_maxid + 1)
623 self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1)
Klement Sekera7112c542017-03-01 09:53:19 +0100624 self._api = Empty()
Ole Troan4df97162017-07-07 16:06:08 +0200625 for name, msgdef in vpp_iterator(self.messages):
626 if self.messages[name]['typeonly']:
627 continue
Ole Troan3cc49712017-03-08 12:02:24 +0100628 crc = self.messages[name]['crc']
629 n = name + '_' + crc[2:]
Ole Troan4df97162017-07-07 16:06:08 +0200630 i = vpp_api.vac_get_msg_index(n.encode())
Ole Troan3cc49712017-03-08 12:02:24 +0100631 if i > 0:
Ole Troana03f4ef2016-12-02 12:53:55 +0100632 self.id_msgdef[i] = msgdef
633 self.id_names[i] = name
634 multipart = True if name.find('_dump') > 0 else False
Klement Sekera7112c542017-03-01 09:53:19 +0100635 f = self.make_function(name, i, msgdef, multipart, async)
636 setattr(self._api, name, FuncWrapper(f))
Ole Troan3cc49712017-03-08 12:02:24 +0100637 else:
Ole Troan4df97162017-07-07 16:06:08 +0200638 self.logger.debug(
639 'No such message type or failed CRC checksum: %s', n)
Ole Troana03f4ef2016-12-02 12:53:55 +0100640
Ole Troana74b7412017-11-01 10:49:03 +0100641 def _write_new_cffi(self, buf):
Ole Troan5016f992017-01-19 09:44:44 +0100642 """Send a binary-packed message to VPP."""
Ole Troana03f4ef2016-12-02 12:53:55 +0100643 if not self.connected:
644 raise IOError(1, 'Not connected')
Ole Troan4df97162017-07-07 16:06:08 +0200645 return vpp_api.vac_write(ffi.from_buffer(buf), len(buf))
Ole Troana03f4ef2016-12-02 12:53:55 +0100646
Ole Troana74b7412017-11-01 10:49:03 +0100647 def _write_legacy_cffi(self, buf):
648 """Send a binary-packed message to VPP."""
649 if not self.connected:
650 raise IOError(1, 'Not connected')
Andrey "Zed" Zaikin7fe930b2018-04-12 12:14:02 +0300651 return vpp_api.vac_write(bytes(buf), len(buf))
Ole Troana74b7412017-11-01 10:49:03 +0100652
Ole Troan4df97162017-07-07 16:06:08 +0200653 def _read(self):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100654 if not self.connected:
655 raise IOError(1, 'Not connected')
Ole Troan3cc49712017-03-08 12:02:24 +0100656 mem = ffi.new("char **")
657 size = ffi.new("int *")
Damjan Marion5fec1e82017-04-13 19:13:47 +0200658 rv = vpp_api.vac_read(mem, size, self.read_timeout)
Ole Troan3cc49712017-03-08 12:02:24 +0100659 if rv:
Ole Troanb0856b42017-08-17 12:48:08 +0200660 raise IOError(rv, 'vac_read failed')
Ole Troan3cc49712017-03-08 12:02:24 +0100661 msg = bytes(ffi.buffer(mem[0], size[0]))
Damjan Marion5fec1e82017-04-13 19:13:47 +0200662 vpp_api.vac_free(mem[0])
Ole Troan3cc49712017-03-08 12:02:24 +0100663 return msg
Ole Troana03f4ef2016-12-02 12:53:55 +0100664
Ole Troan4df97162017-07-07 16:06:08 +0200665 def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen,
666 async):
Ole Troan6bf177c2017-08-17 10:34:32 +0200667 pfx = chroot_prefix.encode() if chroot_prefix else ffi.NULL
668 rv = vpp_api.vac_connect(name.encode(), pfx, msg_handler, rx_qlen)
Ole Troana03f4ef2016-12-02 12:53:55 +0100669 if rv != 0:
670 raise IOError(2, 'Connect failed')
Ole Troan7e3a8752016-12-05 10:27:09 +0100671 self.connected = True
Ole Troana03f4ef2016-12-02 12:53:55 +0100672
Damjan Marion5fec1e82017-04-13 19:13:47 +0200673 self.vpp_dictionary_maxid = vpp_api.vac_msg_table_max_index()
Wojciech Dec64bc6122016-12-12 11:32:25 +0100674 self._register_functions(async=async)
Ole Troana03f4ef2016-12-02 12:53:55 +0100675
676 # Initialise control ping
Ole Troan3cc49712017-03-08 12:02:24 +0100677 crc = self.messages['control_ping']['crc']
Ole Troan4df97162017-07-07 16:06:08 +0200678 self.control_ping_index = vpp_api.vac_get_msg_index(
679 ('control_ping' + '_' + crc[2:]).encode())
Ole Troana03f4ef2016-12-02 12:53:55 +0100680 self.control_ping_msgdef = self.messages['control_ping']
Klement Sekera180402d2018-02-17 10:58:37 +0100681 if self.async_thread:
682 self.event_thread = threading.Thread(
683 target=self.thread_msg_handler)
684 self.event_thread.daemon = True
685 self.event_thread.start()
Ole Troan4df97162017-07-07 16:06:08 +0200686 return rv
Ole Troana03f4ef2016-12-02 12:53:55 +0100687
Ole Troan6bf177c2017-08-17 10:34:32 +0200688 def connect(self, name, chroot_prefix=None, async=False, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100689 """Attach to VPP.
690
691 name - the name of the client.
692 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
693 async - if true, messages are sent without waiting for a reply
694 rx_qlen - the length of the VPP message receive queue between
695 client and server.
696 """
Ole Troan4df97162017-07-07 16:06:08 +0200697 msg_handler = vac_callback_sync if not async else vac_callback_async
Ole Troandfc9b7c2017-03-06 23:51:57 +0100698 return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen,
699 async)
700
Ole Troan6bf177c2017-08-17 10:34:32 +0200701 def connect_sync(self, name, chroot_prefix=None, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100702 """Attach to VPP in synchronous mode. Application must poll for events.
703
704 name - the name of the client.
705 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
706 rx_qlen - the length of the VPP message receive queue between
707 client and server.
708 """
709
Ole Troan3cc49712017-03-08 12:02:24 +0100710 return self.connect_internal(name, ffi.NULL, chroot_prefix, rx_qlen,
Ole Troandfc9b7c2017-03-06 23:51:57 +0100711 async=False)
712
Ole Troana03f4ef2016-12-02 12:53:55 +0100713 def disconnect(self):
Ole Troan5016f992017-01-19 09:44:44 +0100714 """Detach from VPP."""
Damjan Marion5fec1e82017-04-13 19:13:47 +0200715 rv = vpp_api.vac_disconnect()
Ole Troan5016f992017-01-19 09:44:44 +0100716 self.connected = False
Klement Sekera180402d2018-02-17 10:58:37 +0100717 self.message_queue.put("terminate event thread")
Ole Troana03f4ef2016-12-02 12:53:55 +0100718 return rv
719
Ole Troan5016f992017-01-19 09:44:44 +0100720 def msg_handler_sync(self, msg):
721 """Process an incoming message from VPP in sync mode.
722
723 The message may be a reply or it may be an async notification.
724 """
725 r = self.decode_incoming_msg(msg)
726 if r is None:
Ole Troana03f4ef2016-12-02 12:53:55 +0100727 return
728
Ole Troan5016f992017-01-19 09:44:44 +0100729 # If we have a context, then use the context to find any
730 # request waiting for a reply
731 context = 0
732 if hasattr(r, 'context') and r.context > 0:
733 context = r.context
Ole Troan5f9dcff2016-08-01 04:59:13 +0200734
Ole Troan5016f992017-01-19 09:44:44 +0100735 if context == 0:
736 # No context -> async notification that we feed to the callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100737 self.message_queue.put_nowait(r)
Ole Troana03f4ef2016-12-02 12:53:55 +0100738 else:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100739 raise IOError(2, 'RPC reply message received in event handler')
Ole Troan5016f992017-01-19 09:44:44 +0100740
741 def decode_incoming_msg(self, msg):
742 if not msg:
Ole Troan3cc49712017-03-08 12:02:24 +0100743 self.logger.warning('vpp_api.read failed')
Ole Troan5016f992017-01-19 09:44:44 +0100744 return
745
746 i, ci = self.header.unpack_from(msg, 0)
747 if self.id_names[i] == 'rx_thread_exit':
748 return
749
750 #
751 # Decode message and returns a tuple.
752 #
753 msgdef = self.id_msgdef[i]
754 if not msgdef:
755 raise IOError(2, 'Reply message undefined')
756
757 r = self.decode(msgdef, msg)
758
Ole Troana03f4ef2016-12-02 12:53:55 +0100759 return r
760
Ole Troan5016f992017-01-19 09:44:44 +0100761 def msg_handler_async(self, msg):
762 """Process a message from VPP in async mode.
763
764 In async mode, all messages are returned to the callback.
765 """
766 r = self.decode_incoming_msg(msg)
767 if r is None:
768 return
769
770 msgname = type(r).__name__
771
Ole Troan4df97162017-07-07 16:06:08 +0200772 if self.event_callback:
773 self.event_callback(msgname, r)
Ole Troan5016f992017-01-19 09:44:44 +0100774
775 def _control_ping(self, context):
776 """Send a ping command."""
777 self._call_vpp_async(self.control_ping_index,
Ole Troan4df97162017-07-07 16:06:08 +0200778 self.control_ping_msgdef,
Ole Troan5016f992017-01-19 09:44:44 +0100779 context=context)
780
781 def _call_vpp(self, i, msgdef, multipart, **kwargs):
782 """Given a message, send the message and await a reply.
783
784 msgdef - the message packing definition
785 i - the message type index
786 multipart - True if the message returns multiple
787 messages in return.
788 context - context number - chosen at random if not
789 supplied.
790 The remainder of the kwargs are the arguments to the API call.
791
792 The return value is the message or message array containing
793 the response. It will raise an IOError exception if there was
794 no response within the timeout window.
795 """
796
Ole Troan4df97162017-07-07 16:06:08 +0200797 if 'context' not in kwargs:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100798 context = self.get_context()
799 kwargs['context'] = context
800 else:
801 context = kwargs['context']
802 kwargs['_vl_msg_id'] = i
803 b = self.encode(msgdef, kwargs)
Ole Troan5016f992017-01-19 09:44:44 +0100804
Damjan Marion5fec1e82017-04-13 19:13:47 +0200805 vpp_api.vac_rx_suspend()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100806 self._write(b)
Ole Troan5016f992017-01-19 09:44:44 +0100807
808 if multipart:
809 # Send a ping after the request - we use its response
810 # to detect that we have seen all results.
811 self._control_ping(context)
812
813 # Block until we get a reply.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100814 rl = []
815 while (True):
816 msg = self._read()
817 if not msg:
Ole Troan4df97162017-07-07 16:06:08 +0200818 raise IOError(2, 'VPP API client: read failed')
Ole Troan5016f992017-01-19 09:44:44 +0100819
Ole Troandfc9b7c2017-03-06 23:51:57 +0100820 r = self.decode_incoming_msg(msg)
821 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200822 if context not in r or r.context == 0 or context != r.context:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100823 self.message_queue.put_nowait(r)
824 continue
825
826 if not multipart:
827 rl = r
828 break
829 if msgname == 'control_ping_reply':
830 break
831
832 rl.append(r)
833
Damjan Marion5fec1e82017-04-13 19:13:47 +0200834 vpp_api.vac_rx_resume()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100835
836 return rl
Ole Troan5016f992017-01-19 09:44:44 +0100837
838 def _call_vpp_async(self, i, msgdef, **kwargs):
839 """Given a message, send the message and await a reply.
840
841 msgdef - the message packing definition
842 i - the message type index
843 context - context number - chosen at random if not
844 supplied.
845 The remainder of the kwargs are the arguments to the API call.
846 """
Ole Troan4df97162017-07-07 16:06:08 +0200847 if 'context' not in kwargs:
Ole Troan7e3a8752016-12-05 10:27:09 +0100848 context = self.get_context()
849 kwargs['context'] = context
850 else:
851 context = kwargs['context']
852 kwargs['_vl_msg_id'] = i
853 b = self.encode(msgdef, kwargs)
854
855 self._write(b)
856
Ole Troana03f4ef2016-12-02 12:53:55 +0100857 def register_event_callback(self, callback):
Ole Troan5016f992017-01-19 09:44:44 +0100858 """Register a callback for async messages.
Ole Troana03f4ef2016-12-02 12:53:55 +0100859
Ole Troan5016f992017-01-19 09:44:44 +0100860 This will be called for async notifications in sync mode,
861 and all messages in async mode. In sync mode, replies to
862 requests will not come here.
863
864 callback is a fn(msg_type_name, msg_type) that will be
865 called when a message comes in. While this function is
866 executing, note that (a) you are in a background thread and
867 may wish to use threading.Lock to protect your datastructures,
868 and (b) message processing from VPP will stop (so if you take
869 a long while about it you may provoke reply timeouts or cause
870 VPP to fill the RX buffer). Passing None will disable the
871 callback.
872 """
873 self.event_callback = callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100874
875 def thread_msg_handler(self):
876 """Python thread calling the user registerd message handler.
877
878 This is to emulate the old style event callback scheme. Modern
879 clients should provide their own thread to poll the event
880 queue.
881 """
882 while True:
883 r = self.message_queue.get()
Klement Sekera180402d2018-02-17 10:58:37 +0100884 if r == "terminate event thread":
885 break
Ole Troandfc9b7c2017-03-06 23:51:57 +0100886 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200887 if self.event_callback:
888 self.event_callback(msgname, r)
Chris Luke52bf22e2017-11-03 23:32:38 -0400889
890
891# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4