add multiplexer/router
in case a remote is disconnected and can be reconnected quickly, a seamless routing is possible. However, when after reconnection the description has changed, the server is restarted. + more information in error messages from dispatcher Change-Id: I0837e0254aee3d12a26481f6fd697081a53aabba Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22506 Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch> Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
This commit is contained in:
7
cfg/multiplexer.cfg
Normal file
7
cfg/multiplexer.cfg
Normal file
@ -0,0 +1,7 @@
|
||||
[node router]
|
||||
type = router
|
||||
description = router node
|
||||
nodes = ['localhost:5000', 'localhost:10769']
|
||||
|
||||
[interface tcp]
|
||||
bindto = 0.0.0.0:5001
|
7
cfg/router.cfg
Normal file
7
cfg/router.cfg
Normal file
@ -0,0 +1,7 @@
|
||||
[node router]
|
||||
type = router
|
||||
description = router node
|
||||
node = localhost:5000
|
||||
|
||||
[interface tcp]
|
||||
bindto = 0.0.0.0:5001
|
83
secop/lib/multievent.py
Normal file
83
secop/lib/multievent.py
Normal file
@ -0,0 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
class MultiEvent(threading.Event):
|
||||
"""Class implementing multi event objects.
|
||||
|
||||
meth:`new` creates Event like objects
|
||||
meth:'wait` waits for all of them being set
|
||||
"""
|
||||
|
||||
class SingleEvent:
|
||||
"""Single Event
|
||||
|
||||
remark: :meth:`wait` is not implemented on purpose
|
||||
"""
|
||||
def __init__(self, multievent):
|
||||
self.multievent = multievent
|
||||
self.multievent._clear(self)
|
||||
|
||||
def clear(self):
|
||||
self.multievent._clear(self)
|
||||
|
||||
def set(self):
|
||||
self.multievent._set(self)
|
||||
|
||||
def is_set(self):
|
||||
return self in self.multievent.events
|
||||
|
||||
def __init__(self):
|
||||
self.events = set()
|
||||
self._lock = threading.Lock()
|
||||
super().__init__()
|
||||
|
||||
def new(self):
|
||||
"""create a new SingleEvent"""
|
||||
return self.SingleEvent(self)
|
||||
|
||||
def set(self):
|
||||
raise ValueError('a multievent must not be set directly')
|
||||
|
||||
def clear(self):
|
||||
raise ValueError('a multievent must not be cleared directly')
|
||||
|
||||
def _set(self, event):
|
||||
"""internal: remove event from the event list"""
|
||||
with self._lock:
|
||||
self.events.discard(event)
|
||||
if self.events:
|
||||
return
|
||||
super().set()
|
||||
|
||||
def _clear(self, event):
|
||||
"""internal: add event to the event list"""
|
||||
with self._lock:
|
||||
self.events.add(event)
|
||||
super().clear()
|
||||
|
||||
def wait(self, timeout=None):
|
||||
if not self.events: # do not wait if events are empty
|
||||
return
|
||||
super().wait(timeout)
|
@ -42,9 +42,9 @@ import threading
|
||||
from collections import OrderedDict
|
||||
from time import time as currenttime
|
||||
|
||||
from secop.errors import SECoPServerError as InternalError
|
||||
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPError
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError, InternalError,\
|
||||
SECoPError
|
||||
from secop.params import Parameter
|
||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||
@ -82,6 +82,7 @@ class Dispatcher:
|
||||
# eventname is <modulename> or <modulename>:<parametername>
|
||||
self._subscriptions = {}
|
||||
self._lock = threading.RLock()
|
||||
self.restart = srv.restart
|
||||
|
||||
def broadcast_event(self, msg, reallyall=False):
|
||||
"""broadcasts a msg to all active connections
|
||||
@ -157,7 +158,7 @@ class Dispatcher:
|
||||
return self._modules[modulename]
|
||||
if modulename in list(self._modules.values()):
|
||||
return modulename
|
||||
raise NoSuchModuleError('Module does not exist on this SEC-Node!')
|
||||
raise NoSuchModuleError('Module %r does not exist on this SEC-Node!' % modulename)
|
||||
|
||||
def remove_module(self, modulename_or_obj):
|
||||
moduleobj = self.get_module(modulename_or_obj)
|
||||
@ -209,17 +210,17 @@ class Dispatcher:
|
||||
def _execute_command(self, modulename, exportedname, argument=None):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module does not exist on this SEC-Node!')
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
cmdname = moduleobj.commands.exported.get(exportedname, None)
|
||||
if cmdname is None:
|
||||
raise NoSuchCommandError('Module has no command %r on this SEC-Node!' % exportedname)
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname))
|
||||
cmdspec = moduleobj.commands[cmdname]
|
||||
if argument is None and cmdspec.datatype.argument is not None:
|
||||
raise BadValueError('Command needs an argument!')
|
||||
raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname))
|
||||
|
||||
if argument is not None and cmdspec.datatype.argument is None:
|
||||
raise BadValueError('Command takes no argument!')
|
||||
raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname))
|
||||
|
||||
if cmdspec.datatype.argument:
|
||||
# validate!
|
||||
@ -239,16 +240,18 @@ class Dispatcher:
|
||||
def _setParameterValue(self, modulename, exportedname, value):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module does not exist on this SEC-Node!')
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module has no parameter %r on this SEC-Node!' % exportedname)
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
if pobj.constant is not None:
|
||||
raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
||||
% (modulename, pname))
|
||||
if pobj.readonly:
|
||||
raise ReadOnlyError('This parameter can not be changed remotely.')
|
||||
raise ReadOnlyError("Parameter %s:%s can not be changed remotely"
|
||||
% (modulename, pname))
|
||||
|
||||
# validate!
|
||||
value = pobj.datatype(value)
|
||||
@ -264,11 +267,11 @@ class Dispatcher:
|
||||
def _getParameterValue(self, modulename, exportedname):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module does not exist on this SEC-Node!')
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module has no parameter %r on this SEC-Node!' % exportedname)
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
if pobj.constant is not None:
|
||||
# really needed? we could just construct a readreply instead....
|
||||
@ -306,7 +309,7 @@ class Dispatcher:
|
||||
|
||||
if handler:
|
||||
return handler(conn, specifier, data)
|
||||
raise InternalError('unhandled message!')
|
||||
raise SECoPServerError('unhandled message: %s' % repr(msg))
|
||||
|
||||
# now the (defined) handlers for the different requests
|
||||
def handle_help(self, conn, specifier, data):
|
||||
@ -328,7 +331,7 @@ class Dispatcher:
|
||||
return (READREPLY, specifier, list(self._getParameterValue(modulename, pname)))
|
||||
|
||||
def handle_change(self, conn, specifier, data):
|
||||
modulename, pname = specifier, 'value'
|
||||
modulename, pname = specifier, 'target'
|
||||
if ':' in specifier:
|
||||
modulename, pname = specifier.split(':', 1)
|
||||
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
|
||||
@ -352,13 +355,13 @@ class Dispatcher:
|
||||
if ':' in specifier:
|
||||
modulename, exportedname = specifier.split(':', 1)
|
||||
if modulename not in self._export:
|
||||
raise NoSuchModuleError('Module does not exist on this SEC-Node!')
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
moduleobj = self.get_module(modulename)
|
||||
if exportedname is not None:
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname, True)
|
||||
if pname and pname not in moduleobj.accessibles:
|
||||
# what if we try to subscribe a command here ???
|
||||
raise NoSuchParameterError('Module has no such parameter on this SEC-Node!')
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname))
|
||||
modules = [(modulename, pname)]
|
||||
else:
|
||||
modules = [(modulename, None)]
|
||||
|
@ -221,6 +221,7 @@ class TCPServer(HasProperties, socketserver.ThreadingTCPServer):
|
||||
self.setProperty('detailed_errors', detailed_errors)
|
||||
self.checkProperties()
|
||||
|
||||
self.allow_reuse_address = True
|
||||
self.log.info("TCPServer %s binding to %s:%d" % (name, self.bindto, self.bindport))
|
||||
socketserver.ThreadingTCPServer.__init__(
|
||||
self, (self.bindto, self.bindport), TCPRequestHandler, bind_and_activate=True)
|
||||
|
187
secop/protocol/router.py
Normal file
187
secop/protocol/router.py
Normal file
@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""Secop Router
|
||||
|
||||
this is a replacement for the standard dispatcher, with the
|
||||
additional functionality of routing message from/to several other SEC nodes
|
||||
|
||||
simplifications:
|
||||
- module wise activation not supported
|
||||
- on connection, the description from all nodes are cached and all nodes are activated
|
||||
- on 'describe' and on 'activate', cached values are returned
|
||||
- ping is not forwarded
|
||||
- what to do on a change of descriptive data is not yet implemented
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import secop.protocol.dispatcher
|
||||
import secop.errors
|
||||
from secop.protocol.messages import DESCRIPTIONREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY,\
|
||||
READREQUEST, WRITEREQUEST, COMMANDREQUEST
|
||||
import secop.client
|
||||
from secop.lib.multievent import MultiEvent
|
||||
|
||||
|
||||
class SecopClient(secop.client.SecopClient):
|
||||
DISCONNECTED = ('Communication failed', 'remote SEC node disconnected')
|
||||
|
||||
def __init__(self, uri, log, dispatcher):
|
||||
self.dispatcher = dispatcher
|
||||
super().__init__(uri, log)
|
||||
|
||||
def internalize_name(self, name):
|
||||
"""do not modify names"""
|
||||
return name
|
||||
|
||||
def updateEvent(self, module, parameter, value, timestamp, readerror):
|
||||
specifier = '%s:%s' % (module, parameter)
|
||||
if readerror:
|
||||
msg = ERRORPREFIX + EVENTREPLY, specifier, (readerror.name, str(readerror), dict(t=timestamp))
|
||||
else:
|
||||
msg = EVENTREPLY, specifier, (value, dict(t=timestamp))
|
||||
self.dispatcher.broadcast_event(msg)
|
||||
|
||||
def nodeStateChange(self, online, state):
|
||||
t = time.time()
|
||||
if not online:
|
||||
for key, (value, _, readerror) in self.cache.items():
|
||||
if not readerror:
|
||||
self.cache[key] = value, t, self.DISCONNECTED
|
||||
self.updateEvent(*key, *self.cache[key])
|
||||
|
||||
def descriptiveDataChange(self, module, data):
|
||||
print('CHANGE', self.nodename)
|
||||
self.dispatcher.restart()
|
||||
#if module is None:
|
||||
# self.log.error('descriptive data for node %r has changed', self.nodename)
|
||||
|
||||
|
||||
class Router(secop.protocol.dispatcher.Dispatcher):
|
||||
singlenode = None
|
||||
|
||||
def __init__(self, name, logger, options, srv):
|
||||
"""initialize router
|
||||
|
||||
Use the option node = <uri> for a single node or
|
||||
nodes = ["<uri1>", "<uri2>" ...] for multiple nodes.
|
||||
If a single node is given, the node properties are forwarded transparently,
|
||||
else the description property is a merge from all client node properties.
|
||||
"""
|
||||
uri = options.pop('node', None)
|
||||
uris = options.pop('nodes', None)
|
||||
if uri and uris:
|
||||
raise secop.errors.ConfigError('can not specify node _and_ nodes')
|
||||
super().__init__(name, logger, options, srv)
|
||||
if uri:
|
||||
self.nodes = [SecopClient(uri, logger.getChild('routed'), self)]
|
||||
self.singlenode = self.nodes[0]
|
||||
else:
|
||||
self.nodes = [SecopClient(uri, logger.getChild('routed%d' % i), self) for i, uri in enumerate(uris)]
|
||||
# register callbacks
|
||||
for node in self.nodes:
|
||||
node.register(None, node)
|
||||
self.node_by_module = {}
|
||||
multievent = MultiEvent()
|
||||
for node in self.nodes:
|
||||
node.spawn_connect(multievent.new().set)
|
||||
multievent.wait(10) # wait for all nodes started
|
||||
nodes = []
|
||||
for node in self.nodes:
|
||||
if node.online:
|
||||
for module in node.modules:
|
||||
self.node_by_module[module] = node
|
||||
nodes.append(node)
|
||||
else:
|
||||
|
||||
def check_new_node(online, state, self=self, node=node):
|
||||
if online:
|
||||
for module in node.modules:
|
||||
self.node_by_module[module] = node
|
||||
self.nodes.append(node)
|
||||
self.restart()
|
||||
return secop.client.UNREGISTER
|
||||
return None
|
||||
|
||||
node.register(None, nodeStateChange=check_new_node)
|
||||
logger.warning('can not connect to node %r', node.nodename)
|
||||
|
||||
def handle_describe(self, conn, specifier, data):
|
||||
if self.singlenode:
|
||||
return DESCRIPTIONREPLY, specifier, self.singlenode.descriptive_data
|
||||
reply = super().handle_describe(conn, specifier, data)
|
||||
result = reply[2]
|
||||
allmodules = result.get('modules', {})
|
||||
node_description = [result['description']]
|
||||
for node in self.nodes:
|
||||
data = node.descriptive_data.copy()
|
||||
modules = data.pop('modules')
|
||||
equipment_id = data.pop('equipment_id', 'unknown')
|
||||
node_description.append('--- %s ---\n%s' % (equipment_id, data.pop('description', '')))
|
||||
node_description.append('\n'.join('%s: %r' % kv for kv in data.items()))
|
||||
for modname, moddesc in modules.items():
|
||||
if modname in allmodules:
|
||||
self.log.info('module %r is already present', modname)
|
||||
else:
|
||||
allmodules[modname] = moddesc
|
||||
result['modules'] = allmodules
|
||||
result['description'] = '\n\n'.join(node_description)
|
||||
return DESCRIPTIONREPLY, specifier, result
|
||||
|
||||
def handle_activate(self, conn, specifier, data):
|
||||
super().handle_activate(conn, specifier, data)
|
||||
for node in self.nodes:
|
||||
for (module, parameter), (value, t, readerror) in node.cache.items():
|
||||
spec = '%s:%s' % (module, parameter)
|
||||
if readerror:
|
||||
reply = ERRORPREFIX + EVENTREPLY, spec, (readerror.name, str(readerror), dict(t=t))
|
||||
else:
|
||||
datatype = node.modules[module]['parameters'][parameter]['datatype']
|
||||
reply = EVENTREPLY, spec, [datatype.export_value(value), dict(t=t)]
|
||||
self.broadcast_event(reply)
|
||||
return ENABLEEVENTSREPLY, None, None
|
||||
|
||||
def handle_deactivate(self, conn, specifier, data):
|
||||
if specifier:
|
||||
raise secop.errors.NotImplementedError('module wise activation not implemented')
|
||||
super().handle_deactivate(conn, specifier, data)
|
||||
|
||||
def handle_read(self, conn, specifier, data):
|
||||
module = specifier.split(':')[0]
|
||||
if module in self._modules:
|
||||
return super().handle_read(conn, specifier, data)
|
||||
node = self.node_by_module[module]
|
||||
if node.online:
|
||||
return node.request(READREQUEST, specifier, data)
|
||||
return ERRORPREFIX + READREQUEST, specifier, SecopClient.DISCONNECTED + (dict(t=node.disconnect_time),)
|
||||
|
||||
def handle_change(self, conn, specifier, data):
|
||||
module = specifier.split(':')[0]
|
||||
if module in self._modules:
|
||||
return super().handle_change(conn, specifier, data)
|
||||
return self.node_by_module[module].request(WRITEREQUEST, specifier, data)
|
||||
|
||||
def handle_do(self, conn, specifier, data):
|
||||
module = specifier.split(':')[0]
|
||||
if module in self._modules:
|
||||
return super().handle_do(conn, specifier, data)
|
||||
return self.node_by_module[module].request(COMMANDREQUEST, specifier, data)
|
@ -40,7 +40,7 @@ except ImportError:
|
||||
DaemonContext = None
|
||||
|
||||
from secop.errors import ConfigError
|
||||
from secop.lib import formatException, get_class, getGeneralConfig
|
||||
from secop.lib import formatException, get_class, getGeneralConfig, mkthread
|
||||
from secop.modules import Attached
|
||||
|
||||
|
||||
@ -53,10 +53,12 @@ class Server:
|
||||
# IMPORTANT: keep he order! (node MUST be first, as the others are referencing it!)
|
||||
CFGSECTIONS = [
|
||||
# section_prefix, default type, mapping of selectable classes
|
||||
('node', None, {None: "protocol.dispatcher.Dispatcher"}),
|
||||
('node', 'std', {'std': "protocol.dispatcher.Dispatcher",
|
||||
'router': 'protocol.router.Router'}),
|
||||
('module', None, None),
|
||||
('interface', "tcp", {"tcp": "protocol.interface.tcp.TCPServer"}),
|
||||
]
|
||||
_restart = True
|
||||
|
||||
def __init__(self, name, parent_logger=None):
|
||||
cfg = getGeneralConfig()
|
||||
@ -78,6 +80,7 @@ class Server:
|
||||
|
||||
self._dispatcher = None
|
||||
self._interface = None
|
||||
self._restart_event = threading.Event()
|
||||
|
||||
def start(self):
|
||||
if not DaemonContext:
|
||||
@ -96,6 +99,8 @@ class Server:
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
while self._restart:
|
||||
self._restart = False
|
||||
try:
|
||||
self._processCfg()
|
||||
except Exception:
|
||||
@ -103,21 +108,20 @@ class Server:
|
||||
raise
|
||||
|
||||
self.log.info('startup done, handling transport messages')
|
||||
self._threads = set()
|
||||
threads = []
|
||||
for ifname, ifobj in self.interfaces.items():
|
||||
self.log.debug('starting thread for interface %r' % ifname)
|
||||
t = threading.Thread(target=ifobj.serve_forever)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
self._threads.add(t)
|
||||
while self._threads:
|
||||
time.sleep(1)
|
||||
for t in self._threads:
|
||||
if not t.is_alive():
|
||||
self.log.debug('thread %r died (%d still running)' %
|
||||
(t, len(self._threads)))
|
||||
threads.append((ifname, mkthread(ifobj.serve_forever)))
|
||||
for ifname, t in threads:
|
||||
t.join()
|
||||
self._threads.discard(t)
|
||||
self.log.debug('thread for %r died' % ifname)
|
||||
|
||||
def restart(self):
|
||||
if not self._restart:
|
||||
self._restart = True
|
||||
for ifobj in self.interfaces.values():
|
||||
ifobj.shutdown()
|
||||
ifobj.server_close()
|
||||
|
||||
def _processCfg(self):
|
||||
self.log.debug('Parse config file %s ...' % self._cfgfile)
|
||||
@ -129,7 +133,7 @@ class Server:
|
||||
self.log.error('Couldn\'t read cfg file !')
|
||||
raise ConfigError('Couldn\'t read cfg file %r' % self._cfgfile)
|
||||
|
||||
for kind, devtype, classmapping in self.CFGSECTIONS:
|
||||
for kind, default_type, classmapping in self.CFGSECTIONS:
|
||||
kinds = '%ss' % kind
|
||||
objs = OrderedDict()
|
||||
self.__dict__[kinds] = objs
|
||||
@ -145,7 +149,7 @@ class Server:
|
||||
self.log.error('%s %s needs a class option!' % (kind.title(), name))
|
||||
raise ConfigError('cfgfile %r: %s %s needs a class option!' %
|
||||
(self._cfgfile, kind.title(), name))
|
||||
type_ = opts.pop('type', devtype)
|
||||
type_ = opts.pop('type', default_type)
|
||||
cls = classmapping.get(type_, None)
|
||||
if not cls:
|
||||
self.log.error('%s %s needs a type option (select one of %s)!' %
|
||||
@ -185,6 +189,8 @@ class Server:
|
||||
for modname, modobj in self.modules.items():
|
||||
self.log.info('registering module %r' % modname)
|
||||
self.dispatcher.register_module(modobj, modname, modobj.properties['export'])
|
||||
if modobj.pollerClass is not None:
|
||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||
# also call earlyInit on the modules
|
||||
modobj.earlyInit()
|
||||
|
Reference in New Issue
Block a user