Nearly finished basic mumo application. config, worker and mumo_module have test coverage. mumo_manager is not yet covered and most likely not right yet.
This commit is contained in:
67
config.py
Normal file
67
config.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
import ConfigParser
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
"""
|
||||||
|
Small abstraction for config loading
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename = None, default = None):
|
||||||
|
if not filename or not default: return
|
||||||
|
cfg = ConfigParser.ConfigParser()
|
||||||
|
cfg.optionxform = str
|
||||||
|
cfg.read(filename)
|
||||||
|
|
||||||
|
for h,v in default.iteritems():
|
||||||
|
if not v:
|
||||||
|
# Output this whole section as a list of raw key/value tuples
|
||||||
|
try:
|
||||||
|
self.__dict__[h] = cfg.items(h)
|
||||||
|
except ConfigParser.NoSectionError:
|
||||||
|
self.__dict__[h] = []
|
||||||
|
else:
|
||||||
|
self.__dict__[h] = Config()
|
||||||
|
for name, val in v.iteritems():
|
||||||
|
conv, vdefault = val
|
||||||
|
try:
|
||||||
|
self.__dict__[h].__dict__[name] = conv(cfg.get(h, name))
|
||||||
|
except (ValueError, ConfigParser.NoSectionError, ConfigParser.NoOptionError):
|
||||||
|
self.__dict__[h].__dict__[name] = vdefault
|
||||||
|
|
||||||
|
def x2bool(s):
|
||||||
|
"""Helper function to convert strings from the config to bool"""
|
||||||
|
if isinstance(s, bool):
|
||||||
|
return s
|
||||||
|
elif isinstance(s, basestring):
|
||||||
|
return s.lower() in ['1', 'true']
|
||||||
|
raise ValueError()
|
111
config_test.py
Normal file
111
config_test.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from config import Config, x2bool
|
||||||
|
from tempfile import mkstemp
|
||||||
|
import os
|
||||||
|
|
||||||
|
def create_file(content = None):
|
||||||
|
"""
|
||||||
|
Creates a temp file filled with 'content' and returns its path.
|
||||||
|
The file has to be manually deleted later on
|
||||||
|
"""
|
||||||
|
fd, path = mkstemp()
|
||||||
|
f = os.fdopen(fd, "wb")
|
||||||
|
if content:
|
||||||
|
f.write(content)
|
||||||
|
f.flush()
|
||||||
|
f.close()
|
||||||
|
return path
|
||||||
|
|
||||||
|
class ConfigTest(unittest.TestCase):
|
||||||
|
cfg_content = """[world]
|
||||||
|
domination = True
|
||||||
|
somestr = Blabla
|
||||||
|
somenum = 10
|
||||||
|
testfallbacknum = asdas
|
||||||
|
"""
|
||||||
|
|
||||||
|
cfg_default = {'world':{'domination':(x2bool, False),
|
||||||
|
'somestr':(str, "fail"),
|
||||||
|
'somenum':(int, 0),
|
||||||
|
'somenumtest':(int, 1)},
|
||||||
|
'somethingelse':{'bla':(str, "test")}}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def testEmpty(self):
|
||||||
|
path = create_file()
|
||||||
|
try:
|
||||||
|
cfg = Config(path, self.cfg_default)
|
||||||
|
assert(cfg.world.domination == False)
|
||||||
|
assert(cfg.world.somestr == "fail")
|
||||||
|
assert(cfg.world.somenum == 0)
|
||||||
|
self.assertRaises(AttributeError, getattr, cfg.world, "testfallbacknum")
|
||||||
|
assert(cfg.somethingelse.bla == "test")
|
||||||
|
finally:
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
def testX2bool(self):
|
||||||
|
assert(x2bool("true") == True)
|
||||||
|
assert(x2bool("false") == False)
|
||||||
|
assert(x2bool("TrUe") == True)
|
||||||
|
assert(x2bool("FaLsE") == False)
|
||||||
|
assert(x2bool("0") == False)
|
||||||
|
assert(x2bool("1") == True)
|
||||||
|
assert(x2bool("10") == False)
|
||||||
|
assert(x2bool("notabool") == False)
|
||||||
|
|
||||||
|
def testConfig(self):
|
||||||
|
path = create_file(self.cfg_content)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
cfg = Config(path, self.cfg_default)
|
||||||
|
except Exception, e:
|
||||||
|
print e
|
||||||
|
assert(cfg.world.domination == True)
|
||||||
|
assert(cfg.world.somestr == "Blabla")
|
||||||
|
assert(cfg.world.somenum == 10)
|
||||||
|
self.assertRaises(AttributeError, getattr, cfg.world, "testfallbacknum")
|
||||||
|
assert(cfg.somethingelse.bla == "test")
|
||||||
|
finally:
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
#import sys;sys.argv = ['', 'Test.testName']
|
||||||
|
unittest.main()
|
107
modules/test.py
Normal file
107
modules/test.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from mumo_module import (x2bool,
|
||||||
|
MumoModule,
|
||||||
|
logModFu)
|
||||||
|
|
||||||
|
class test(MumoModule):
|
||||||
|
default_config = {'testing':{'tvar': (int , 1),
|
||||||
|
'novar': (str, 'no bernd')}}
|
||||||
|
|
||||||
|
def __init__(self, cfg_file, manager):
|
||||||
|
MumoModule.__init__(self, "test", manager, cfg_file)
|
||||||
|
log = self.log()
|
||||||
|
cfg = self.cfg()
|
||||||
|
log.debug("tvar: %s", cfg.testing.tvar)
|
||||||
|
log.debug("novar: %s", cfg.testing.novar)
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def unload(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def connected(self):
|
||||||
|
manager = self.manager()
|
||||||
|
log = self.log()
|
||||||
|
log.debug("Ice connected, register for everything out there")
|
||||||
|
manager.enlistMetaCallbackHandler(self)
|
||||||
|
manager.enlistServerCallbackHandler(self, manager.SERVER_ALL_TRACK)
|
||||||
|
manager.enlistServerContextCallbackHandler(self, manager.SERVER_ALL_TRACK)
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def disconnected(self):
|
||||||
|
self.log().debug("Ice list")
|
||||||
|
#
|
||||||
|
#--- Meta callback functions
|
||||||
|
#
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def started(self, server, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def stopped(self, server, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
#
|
||||||
|
#--- Server callback functions
|
||||||
|
#
|
||||||
|
@logModFu
|
||||||
|
def userConnected(self, state, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def userDisconnected(self, state, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def userStateChanged(self, state, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def channelCreated(self, state, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def channelRemoved(self, state, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@logModFu
|
||||||
|
def channelStateChanged(self, state, context = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
#
|
||||||
|
#--- Server context callback functions
|
||||||
|
#
|
||||||
|
@logModFu
|
||||||
|
def contextAction(self, action, user, session, channelid, context = None):
|
||||||
|
pass
|
466
mumo_manager.py
Normal file
466
mumo_manager.py
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
import Queue
|
||||||
|
from worker import Worker, local_thread, local_thread_blocking
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
class FailedLoadModuleException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FailedLoadModuleConfigException(FailedLoadModuleException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FailedLoadModuleImportException(FailedLoadModuleException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FailedLoadModuleInitializationException(FailedLoadModuleException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def debug_log(fu, enable = True):
|
||||||
|
def new_fu(*args, **kwargs):
|
||||||
|
self = args[0]
|
||||||
|
log = self.log()
|
||||||
|
skwargs = [','.join(['%s=%s' % (karg,repr(arg)) for karg, arg in kwargs])]
|
||||||
|
sargs = [','.join([str(arg) for arg in args[1:]])] + '' if not skwargs else (',' + skwargs)
|
||||||
|
|
||||||
|
call = "%s(%s)" % (fu.__name__, sargs)
|
||||||
|
log.debug()
|
||||||
|
res = fu(*args, **kwargs)
|
||||||
|
log.debug("%s -> %s", call, repr(res))
|
||||||
|
return res
|
||||||
|
return new_fu if enable else fu
|
||||||
|
|
||||||
|
debug_me = True
|
||||||
|
|
||||||
|
class MumoManagerRemote(object):
|
||||||
|
"""
|
||||||
|
Manager object handed to MumoModules. This module
|
||||||
|
acts as a remote for the MumoModule with which it
|
||||||
|
can register/unregister to/from callbacks as well
|
||||||
|
as do other signaling to the master MumoManager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SERVERS_ALL = [-1] ## Applies to all servers
|
||||||
|
|
||||||
|
def __init__(self, master, name, queue):
|
||||||
|
self.__master = master
|
||||||
|
self.__name = name
|
||||||
|
self.__queue = queue
|
||||||
|
|
||||||
|
def getQueue(self):
|
||||||
|
return self.__queue
|
||||||
|
|
||||||
|
def subscribeMetaCallbacks(self, handler, servers = MumoManagerRemote.SERVERS_ALL):
|
||||||
|
"""
|
||||||
|
Subscribe to meta callbacks. Subscribes the given handler to the following
|
||||||
|
callbacks:
|
||||||
|
|
||||||
|
>>> started(self, server, context = None)
|
||||||
|
>>> stopped(self, server, context = None)
|
||||||
|
|
||||||
|
@param servers: List of server IDs for which to subscribe. To subscribe to all
|
||||||
|
servers pass SERVERS_ALL.
|
||||||
|
@param handler: Object on which to call the callback functions
|
||||||
|
"""
|
||||||
|
return self.__master.subscribeMetaCallbacks(self.__queue, handler, servers)
|
||||||
|
|
||||||
|
def unsubscribeMetaCallbacks(self, handler, servers = MumoManagerRemote.SERVERS_ALL):
|
||||||
|
"""
|
||||||
|
Unsubscribe from meta callbacks. Unsubscribes the given handler from callbacks
|
||||||
|
for the given servers.
|
||||||
|
|
||||||
|
@param servers: List of server IDs for which to unsubscribe. To unsubscribe from all
|
||||||
|
servers pass SERVERS_ALL.
|
||||||
|
@param handler: Subscribed handler
|
||||||
|
"""
|
||||||
|
return self.__master.unscubscribeMetaCallbacks(self.__queue, handler, servers)
|
||||||
|
|
||||||
|
def subscribeServerCallbacks(self, handler, servers = MumoManagerRemote.SERVERS_ALL):
|
||||||
|
"""
|
||||||
|
Subscribe to server callbacks. Subscribes the given handler to the following
|
||||||
|
callbacks:
|
||||||
|
|
||||||
|
>>> userConnected(self, state, context = None)
|
||||||
|
>>> userDisconnected(self, state, context = None)
|
||||||
|
>>> userStateChanged(self, state, context = None)
|
||||||
|
>>> channelCreated(self, state, context = None)
|
||||||
|
>>> channelRemoved(self, state, context = None)
|
||||||
|
>>> channelStateChanged(self, state, context = None)
|
||||||
|
|
||||||
|
@param servers: List of server IDs for which to subscribe. To subscribe to all
|
||||||
|
servers pass SERVERS_ALL.
|
||||||
|
@param handler: Object on which to call the callback functions
|
||||||
|
"""
|
||||||
|
return self.__master.subscribeServerCallbacks(self.__queue, handler, servers)
|
||||||
|
|
||||||
|
def unsubscribeServerCallbacks(self, handler, servers = MumoManagerRemote.SERVERS_ALL):
|
||||||
|
"""
|
||||||
|
Unsubscribe from server callbacks. Unsubscribes the given handler from callbacks
|
||||||
|
for the given servers.
|
||||||
|
|
||||||
|
@param servers: List of server IDs for which to unsubscribe. To unsubscribe from all
|
||||||
|
servers pass SERVERS_ALL.
|
||||||
|
@param handler: Subscribed handler
|
||||||
|
"""
|
||||||
|
return self.__master.unsubscribeServerCallbacks(self.__queue, handler, servers)
|
||||||
|
|
||||||
|
def subscribeContextCallbacks(self, handler, servers = MumoManagerRemote.SERVERS_ALL):
|
||||||
|
"""
|
||||||
|
Subscribe to context callbacks. Subscribes the given handler to the following
|
||||||
|
callbacks:
|
||||||
|
|
||||||
|
>>> contextAction(self, action, user, session, channelid, context = None)
|
||||||
|
|
||||||
|
@param servers: List of server IDs for which to subscribe. To subscribe to all
|
||||||
|
servers pass SERVERS_ALL.
|
||||||
|
@param handler: Object on which to call the callback functions
|
||||||
|
"""
|
||||||
|
return self.__master.subscribeContextCallbacks(self.__queue, handler, servers)
|
||||||
|
|
||||||
|
def unsubscribeContextCallbacks(self, handler, servers = MumoManagerRemote.SERVERS_ALL):
|
||||||
|
"""
|
||||||
|
Unsubscribe from context callbacks. Unsubscribes the given handler from callbacks
|
||||||
|
for the given servers.
|
||||||
|
|
||||||
|
@param servers: List of server IDs for which to unsubscribe. To unsubscribe from all
|
||||||
|
servers pass SERVERS_ALL.
|
||||||
|
@param handler: Subscribed handler
|
||||||
|
"""
|
||||||
|
return self.__master.unsubscribeContextCallbacks(self.__queue, handler, servers)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MumoManager(Worker):
|
||||||
|
MAGIC_ALL = -1
|
||||||
|
|
||||||
|
def __init__(self, cfg):
|
||||||
|
Worker.__init__(self, "MumoManager")
|
||||||
|
self.queues = {} # {queue:module}
|
||||||
|
self.modules = {} # {name:module}
|
||||||
|
self.imports = {} # {name:import}
|
||||||
|
self.cfg = cfg
|
||||||
|
|
||||||
|
self.metaCallbacks = {} # {sid:{queue:[handler]}}
|
||||||
|
self.serverCallbacks = {}
|
||||||
|
self.contextCallbacks = {}
|
||||||
|
|
||||||
|
def __add_to_dict(self, mdict, queue, handler, servers):
|
||||||
|
for server in servers:
|
||||||
|
if server in mdict:
|
||||||
|
if queue in mdict[server]:
|
||||||
|
if not handler in mdict[server][queue]:
|
||||||
|
mdict[server][queue].append(handler)
|
||||||
|
else:
|
||||||
|
mdict[server][queue] = [handler]
|
||||||
|
else:
|
||||||
|
mdict[server] = {queue:[handler]}
|
||||||
|
|
||||||
|
def __rem_from_dict(self, mdict, queue, handler, servers):
|
||||||
|
for server in servers:
|
||||||
|
try:
|
||||||
|
mdict[server][queue].remove(handler)
|
||||||
|
except KeyError, ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __announce_to_dict(self, mdict, servers, function, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Call function on handlers for specific servers in one of our handler
|
||||||
|
dictionaries.
|
||||||
|
|
||||||
|
@param mdict Dictionary to announce to
|
||||||
|
@param servers: Servers to announce to, ALL is always implied
|
||||||
|
@param function: Function the handler should call
|
||||||
|
@param args: Arguments for the function
|
||||||
|
@param kwargs: Keyword arguments for the function
|
||||||
|
"""
|
||||||
|
# Announce to all handlers registered to all events
|
||||||
|
for queue, handlers in mdict[self.MAGIC_ALL].iteritems():
|
||||||
|
for handler in handlers:
|
||||||
|
self.__call_remote(queue, handler, args, kwargs)
|
||||||
|
|
||||||
|
# Announce to all handlers of the given serverlist
|
||||||
|
for server in servers:
|
||||||
|
for queue, handler in mdict[server].iteritems():
|
||||||
|
self.__call_remote(queue, handler, args, kwargs)
|
||||||
|
|
||||||
|
def __call_remote(self, queue, handler, *args, **kwargs):
|
||||||
|
queue.put((None, handler, args, kwargs))
|
||||||
|
|
||||||
|
#
|
||||||
|
#--- Module self management functionality
|
||||||
|
#
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def subscribeMetaCallbacks(self, queue, handler, servers):
|
||||||
|
"""
|
||||||
|
@param queue Target worker queue
|
||||||
|
@see MumoManagerRemote
|
||||||
|
"""
|
||||||
|
return self.__add_to_dict(self.metaCallbacks, queue, handler, servers)
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def unsubscribeMetaCallbacks(self, queue, handler, servers):
|
||||||
|
"""
|
||||||
|
@param queue Target worker queue
|
||||||
|
@see MumoManagerRemote
|
||||||
|
"""
|
||||||
|
return self.__rem_from_dict(self.metaCallbacks, queue, handler, servers)
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def subscribeServerCallbacks(self, queue, handler, servers):
|
||||||
|
"""
|
||||||
|
@param queue Target worker queue
|
||||||
|
@see MumoManagerRemote
|
||||||
|
"""
|
||||||
|
return self.__add_to_dict(self.serverCallbacks, queue, handler, servers)
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def unsubscribeServerCallbacks(self, queue, handler, servers):
|
||||||
|
"""
|
||||||
|
@param queue Target worker queue
|
||||||
|
@see MumoManagerRemote
|
||||||
|
"""
|
||||||
|
return self.__rem_from_dict(self.serverCallbacks, queue, handler, servers)
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def subscribeContextCallbacks(self, queue, handler, servers):
|
||||||
|
"""
|
||||||
|
@param queue Target worker queue
|
||||||
|
@see MumoManagerRemote
|
||||||
|
"""
|
||||||
|
return self.__add_to_dict(self.contextCallbacks, queue, handler, servers)
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def unsubscribeContextCallbacks(self, queue, handler, servers):
|
||||||
|
"""
|
||||||
|
@param queue Target worker queue
|
||||||
|
@see MumoManagerRemote
|
||||||
|
"""
|
||||||
|
return self.__rem_from_dict(self.contextCallbacks, queue, handler, servers)
|
||||||
|
|
||||||
|
#
|
||||||
|
#--- Module load/start/stop/unload functionality
|
||||||
|
#
|
||||||
|
@local_thread_blocking
|
||||||
|
@debug_log(debug_me)
|
||||||
|
def loadModules(self, names = None):
|
||||||
|
"""
|
||||||
|
Loads a list of modules from the mumo directory structure by name.
|
||||||
|
|
||||||
|
@param names List of names of modules to load
|
||||||
|
@return: List of modules loaded
|
||||||
|
"""
|
||||||
|
loadedmodules = {}
|
||||||
|
|
||||||
|
if not names:
|
||||||
|
# If no names are given load all modules that have a configuration in the cfg_dir
|
||||||
|
if not os.path.isdir(self.cfg.modules.cfg_dir):
|
||||||
|
msg = "Module directory '%s' not found" % self.cfg.mumo.mod_dir
|
||||||
|
self.log().error(msg)
|
||||||
|
raise FailedLoadModuleImportException(msg)
|
||||||
|
|
||||||
|
names = []
|
||||||
|
for f in os.listdir(self.cfg.modules.cfg_dir):
|
||||||
|
if os.path.isfile(f):
|
||||||
|
base, ext = os.path.splitext(f)
|
||||||
|
if not ext or ext.tolower() == ".ini" or ext.tolower == ".conf":
|
||||||
|
names.append(base)
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
modinst = self._loadModule_noblock(name)
|
||||||
|
loadedmodules[name] = modinst
|
||||||
|
except FailedLoadModuleException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return loadedmodules
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
def loadModuleCls(self, name, modcls, module_cfg = None):
|
||||||
|
return self._loadModuleCls_noblock(name, modcls, module_cfg)
|
||||||
|
|
||||||
|
@debug_log(debug_me)
|
||||||
|
def _loadModuleCls_noblock(self, name, modcls, module_cfg = None):
|
||||||
|
log = self.log()
|
||||||
|
|
||||||
|
if name in self.modules:
|
||||||
|
log.error("Module '%s' already loaded", name)
|
||||||
|
return
|
||||||
|
|
||||||
|
modqueue = Queue.Queue()
|
||||||
|
modmanager = MumoManagerRemote(self, name, modqueue)
|
||||||
|
|
||||||
|
modinst = modcls(name, modmanager, module_cfg)
|
||||||
|
|
||||||
|
# Remember it
|
||||||
|
self.modules[name] = modinst
|
||||||
|
self.queues[modqueue] = modinst
|
||||||
|
|
||||||
|
return modinst
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
def loadModule(self, name):
|
||||||
|
"""
|
||||||
|
Loads a single module either by name
|
||||||
|
|
||||||
|
@param name Name of the module to load
|
||||||
|
@return Module instance
|
||||||
|
"""
|
||||||
|
self._loadModule_noblock(name)
|
||||||
|
|
||||||
|
@debug_log(debug_me)
|
||||||
|
def _loadModule_noblock(self, name):
|
||||||
|
# Make sure this module is not already loaded
|
||||||
|
log = self.log()
|
||||||
|
log.debug("loadModuleByName('%s')", name)
|
||||||
|
|
||||||
|
if name in self.modules:
|
||||||
|
log.warning("Tried to load already loaded module %s", name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check whether there is a configuration file for this module
|
||||||
|
confpath = self.cfg.modules.cfg_dir + name + '.ini'
|
||||||
|
if not os.path.isfile(confpath):
|
||||||
|
msg = "Module configuration file '%s' not found" % confpath
|
||||||
|
log.error(msg)
|
||||||
|
raise FailedLoadModuleConfigException(msg)
|
||||||
|
|
||||||
|
# Make sure the module directory is in our python path and exists
|
||||||
|
if not self.cfg.mumo.mod_dir in sys.path:
|
||||||
|
if not os.path.isdir(self.cfg.mumo.mod_dir):
|
||||||
|
msg = "Module directory '%s' not found" % self.cfg.mumo.mod_dir
|
||||||
|
log.error(msg)
|
||||||
|
raise FailedLoadModuleImportException(msg)
|
||||||
|
sys.path.append(self.cfg.mumo.mod_dir)
|
||||||
|
|
||||||
|
# Import the module and instanciate it
|
||||||
|
try:
|
||||||
|
mod = __import__(name)
|
||||||
|
self.imports[name] = mod
|
||||||
|
except ImportError, e:
|
||||||
|
msg = "Failed to import module '%s', reason: %s" % (name, str(e))
|
||||||
|
log.error(msg)
|
||||||
|
raise FailedLoadModuleImportException(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
modcls = mod.mumo_module_class # First check if there's a magic mumo_module_class variable
|
||||||
|
log.debug("Magic mumo_module_class found")
|
||||||
|
except AttributeError:
|
||||||
|
modcls = getattr(mod, name)
|
||||||
|
except AttributeError:
|
||||||
|
raise FailedLoadModuleInitializationException("Module does not contain required class %s" % name)
|
||||||
|
|
||||||
|
return self._loadModuleCls_noblock(name, modcls, confpath)
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
@debug_log(debug_me)
|
||||||
|
def startModules(self, names = None):
|
||||||
|
"""
|
||||||
|
Start a module by name
|
||||||
|
|
||||||
|
@param names List of names of modules to start
|
||||||
|
@return A dict of started module names and instances
|
||||||
|
"""
|
||||||
|
log = self.log()
|
||||||
|
startedmodules = {}
|
||||||
|
|
||||||
|
if not names:
|
||||||
|
# If no names are given start all models
|
||||||
|
names = self.modules.iterkeys()
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
modinst = self.modules[name]
|
||||||
|
if not modinst.is_alive():
|
||||||
|
modinst.start()
|
||||||
|
log.debug("Module '%s' started", name)
|
||||||
|
else:
|
||||||
|
log.debug("Module '%s' already running", name)
|
||||||
|
startedmodules[name] = modinst
|
||||||
|
except KeyError:
|
||||||
|
log.error("Could not start unknown module '%s'", name)
|
||||||
|
|
||||||
|
return startedmodules
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
@debug_log(debug_me)
|
||||||
|
def stopModules(self, names = None, force = False):
|
||||||
|
"""
|
||||||
|
Stop a list of modules by name. Note that this only works
|
||||||
|
for well behaved modules. At this point if a module is really going
|
||||||
|
rampant you will have to restart mumo.
|
||||||
|
|
||||||
|
@param names List of names of modules to unload
|
||||||
|
@param force Unload the module asap dropping messages queued for it
|
||||||
|
@return A dict of stopped module names and instances
|
||||||
|
"""
|
||||||
|
log = self.log()
|
||||||
|
stoppedmodules = {}
|
||||||
|
|
||||||
|
if not names:
|
||||||
|
# If no names are given start all models
|
||||||
|
names = self.modules.iterkeys()
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
modinst = self.modules[name]
|
||||||
|
stoppedmodules[name] = modinst
|
||||||
|
except KeyError:
|
||||||
|
log.warning("Asked to stop unknown module '%s'", name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if force:
|
||||||
|
# We will have to drain the modules queues
|
||||||
|
for queue, module in self.queues.iteritems():
|
||||||
|
if module in self.modules:
|
||||||
|
try:
|
||||||
|
while queue.get_nowait(): pass
|
||||||
|
except Queue.Empty: pass
|
||||||
|
|
||||||
|
for modinst in stoppedmodules.itervalues():
|
||||||
|
if modinst.is_alive():
|
||||||
|
modinst.stop()
|
||||||
|
log.debug("Module '%s' is being stopped", name)
|
||||||
|
else:
|
||||||
|
log.debug("Module '%s' already stopped", name)
|
||||||
|
|
||||||
|
for modinst in stoppedmodules.itervalues():
|
||||||
|
modinst.join(timeout = self.cfg.modules.timeout)
|
||||||
|
|
||||||
|
return stoppedmodules
|
||||||
|
|
||||||
|
def stop(self, force = True):
|
||||||
|
self.log().debug("Stopping")
|
||||||
|
self.stopModules()
|
||||||
|
Worker.stop(self, force)
|
85
mumo_manager_test.py
Normal file
85
mumo_manager_test.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import Queue
|
||||||
|
from mumo_manager import MumoManager, MumoManagerRemote
|
||||||
|
from mumo_module import MumoModule
|
||||||
|
|
||||||
|
|
||||||
|
class MumoManagerTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class MyModule(MumoModule):
|
||||||
|
def __init__(self, name, manager, configuration = None):
|
||||||
|
MumoModule.__init__(self, name, manager, configuration)
|
||||||
|
self.was_called = False
|
||||||
|
self.par1 = None
|
||||||
|
self.par2 = None
|
||||||
|
self.par3 = None
|
||||||
|
|
||||||
|
def last_call(self):
|
||||||
|
ret = (self.was_called, self.par1, self.par2, self.par3)
|
||||||
|
self.was_called = False
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def call_me(self, par1, par2 = None, par3 = None):
|
||||||
|
self.was_called = True
|
||||||
|
self.par1 = par1
|
||||||
|
self.par2 = par2
|
||||||
|
self.par3 = par3
|
||||||
|
|
||||||
|
self.man = MumoManager()
|
||||||
|
self.man.start()
|
||||||
|
|
||||||
|
class conf(object):
|
||||||
|
pass # Dummy class
|
||||||
|
|
||||||
|
cfg = conf()
|
||||||
|
cfg.test = 10
|
||||||
|
|
||||||
|
self.mod = self.man.loadModuleCls("MyModule", MyModule, cfg)
|
||||||
|
self.man.startModules()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.man.stopModules()
|
||||||
|
self.man.stop()
|
||||||
|
self.man.join(timeout=2)
|
||||||
|
|
||||||
|
|
||||||
|
def testName(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
#import sys;sys.argv = ['', 'Test.testName']
|
||||||
|
unittest.main()
|
94
mumo_module.py
Normal file
94
mumo_module.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from config import Config, x2bool
|
||||||
|
from worker import Worker
|
||||||
|
|
||||||
|
class MumoModule(Worker):
|
||||||
|
default_config = {}
|
||||||
|
|
||||||
|
def __init__(self, name, manager, configuration = None):
|
||||||
|
Worker.__init__(self, name, manager.getQueue())
|
||||||
|
self.__manager = manager
|
||||||
|
|
||||||
|
if isinstance(configuration, basestring):
|
||||||
|
# If we are passed a string expect a config file there
|
||||||
|
if configuration:
|
||||||
|
self.__cfg = Config(configuration, self.default_config)
|
||||||
|
else:
|
||||||
|
self.__cfg = None
|
||||||
|
else:
|
||||||
|
# If we aren't passed a string it will be a config object or None
|
||||||
|
self.__cfg = configuration
|
||||||
|
|
||||||
|
self.log().info("Initialized")
|
||||||
|
|
||||||
|
#--- Accessors
|
||||||
|
def manager(self):
|
||||||
|
return self.__manager
|
||||||
|
|
||||||
|
def cfg(self):
|
||||||
|
return self.__cfg
|
||||||
|
|
||||||
|
#--- Module control
|
||||||
|
|
||||||
|
|
||||||
|
def onStart(self):
|
||||||
|
self.log().info("Start")
|
||||||
|
|
||||||
|
def onStop(self):
|
||||||
|
self.log().info("Stop")
|
||||||
|
|
||||||
|
#--- Events
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
# Called once the Ice connection to the murmur server
|
||||||
|
# is established.
|
||||||
|
#
|
||||||
|
# All event registration should happen here
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def disconnected(self):
|
||||||
|
# Called once a loss of Ice connectivity is detected.
|
||||||
|
#
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def logModFu(fu):
|
||||||
|
def newfu(self, *args, **kwargs):
|
||||||
|
log = self.log()
|
||||||
|
argss = '' if len(args)==0 else ',' + ','.join(['"%s"' % str(arg) for arg in args])
|
||||||
|
kwargss = '' if len(kwargs)==0 else ','.join('%s="%s"' % (kw, str(arg)) for kw, arg in kwargs.iteritems())
|
||||||
|
log.debug("%s(%s%s%s)", fu.__name__, str(self), argss, kwargss)
|
||||||
|
return fu(self, *args, **kwargs)
|
||||||
|
return newfu
|
83
sketches.txt
Normal file
83
sketches.txt
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
********************
|
||||||
|
* Folder structure *
|
||||||
|
********************
|
||||||
|
@@@@@@@@@@
|
||||||
|
/usr/sbin/
|
||||||
|
@@@@@@@@@@
|
||||||
|
mumo
|
||||||
|
|
||||||
|
@@@@@@@@@
|
||||||
|
/usr/lib/
|
||||||
|
@@@@@@@@@
|
||||||
|
|
||||||
|
mumo/
|
||||||
|
mumo.py
|
||||||
|
murmur_derivates.py
|
||||||
|
modules/
|
||||||
|
idlemove.py
|
||||||
|
modbf2/
|
||||||
|
__init__.py
|
||||||
|
engine.py
|
||||||
|
foo.py
|
||||||
|
bar.py
|
||||||
|
|
||||||
|
|
||||||
|
@@@@@
|
||||||
|
/etc/
|
||||||
|
@@@@@
|
||||||
|
|
||||||
|
mumo/
|
||||||
|
mumo
|
||||||
|
modules-available/
|
||||||
|
idlemove
|
||||||
|
modbf2
|
||||||
|
modules-enabled/
|
||||||
|
idlemove
|
||||||
|
|
||||||
|
|
||||||
|
*********************
|
||||||
|
* Config structure: *
|
||||||
|
*********************
|
||||||
|
|
||||||
|
mumo
|
||||||
|
====
|
||||||
|
[modules]
|
||||||
|
mod_dir = /usr/sbin/modules/
|
||||||
|
cfg_dir = /etc/modules-enabled/
|
||||||
|
|
||||||
|
[Ice]
|
||||||
|
ip = 127.0.0.1
|
||||||
|
port = 6502
|
||||||
|
slice = Murmur.ice
|
||||||
|
secret =
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
file = /var/log/mumo/mumo.log
|
||||||
|
level = DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
idlemove
|
||||||
|
========
|
||||||
|
[idlemove]
|
||||||
|
servers = 1,2,3,4,6,7
|
||||||
|
interval = 0.1
|
||||||
|
timeout = 3600
|
||||||
|
mute = True
|
||||||
|
deafen = False
|
||||||
|
channel = 123
|
||||||
|
|
||||||
|
modbf2
|
||||||
|
======
|
||||||
|
[modbf2]
|
||||||
|
bla = blub
|
||||||
|
blib = bernd
|
||||||
|
|
||||||
|
[CoolServer1]
|
||||||
|
vserver = 1
|
||||||
|
root = bf2gaming/CoolServer1Chan
|
||||||
|
sink = True
|
||||||
|
|
||||||
|
[NotSoCoolServer]
|
||||||
|
vserver = 2
|
||||||
|
root = NotSoCoolServerChan
|
||||||
|
sink = False
|
34
test.py
Normal file
34
test.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from worker_test import *
|
||||||
|
from config_test import *
|
||||||
|
from mumo_manager_test import *
|
146
worker.py
Normal file
146
worker.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Queue, Empty
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
def local_thread(fu):
|
||||||
|
"""
|
||||||
|
Decorator which makes a function execute in the local worker thread
|
||||||
|
Return values are discarded
|
||||||
|
"""
|
||||||
|
def new_fu(*args, **kwargs):
|
||||||
|
self = args[0]
|
||||||
|
self.message_queue().put((None, fu, args, kwargs))
|
||||||
|
return new_fu
|
||||||
|
|
||||||
|
def local_thread_blocking(fu, timeout = None):
|
||||||
|
"""
|
||||||
|
Decorator which makes a function execute in the local worker thread
|
||||||
|
The function will block until return values are available or timeout
|
||||||
|
seconds passed.
|
||||||
|
|
||||||
|
@param timeout Timeout in seconds
|
||||||
|
"""
|
||||||
|
def new_fu(*args, **kwargs):
|
||||||
|
self = args[0]
|
||||||
|
out = Queue()
|
||||||
|
self.message_queue().put((out, fu, args, kwargs))
|
||||||
|
ret, ex = out.get(True, timeout)
|
||||||
|
if ex:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return new_fu
|
||||||
|
|
||||||
|
|
||||||
|
class Worker(Thread):
|
||||||
|
def __init__(self, name, message_queue = None):
|
||||||
|
"""
|
||||||
|
Implementation of a basic Queue based Worker thread.
|
||||||
|
|
||||||
|
@param name Name of the thread to run the worker in
|
||||||
|
@param message_queue Message queue on which to receive commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
Thread.__init__(self, name = name)
|
||||||
|
self.daemon = True
|
||||||
|
self.__in = message_queue if message_queue != None else Queue()
|
||||||
|
self.__log = getLogger(name)
|
||||||
|
self.__name = name
|
||||||
|
|
||||||
|
#--- Accessors
|
||||||
|
def log(self):
|
||||||
|
return self.__log
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
def message_queue(self):
|
||||||
|
return self.__in
|
||||||
|
|
||||||
|
#--- Overridable convience stuff
|
||||||
|
def onStart(self):
|
||||||
|
"""
|
||||||
|
Override this function to perform actions on worker startup
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def onStop(self):
|
||||||
|
"""
|
||||||
|
Override this function to perform actions on worker shutdown
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
#--- Thread / Control
|
||||||
|
def run(self):
|
||||||
|
self.log().debug("Enter message loop")
|
||||||
|
self.onStart()
|
||||||
|
while True:
|
||||||
|
msg = self.__in.get()
|
||||||
|
if msg == None:
|
||||||
|
break
|
||||||
|
|
||||||
|
(out, fu, args, kwargs) = msg
|
||||||
|
try:
|
||||||
|
res = fu(*args, **kwargs)
|
||||||
|
ex = None
|
||||||
|
except Exception, e:
|
||||||
|
self.log().exception(e)
|
||||||
|
res = None
|
||||||
|
ex = e
|
||||||
|
finally:
|
||||||
|
if not out is None:
|
||||||
|
out.put((res, ex))
|
||||||
|
|
||||||
|
self.onStop()
|
||||||
|
self.log().debug("Leave message loop")
|
||||||
|
|
||||||
|
def stop(self, force = True):
|
||||||
|
if force:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
self.__in.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.__in.put(None)
|
||||||
|
|
||||||
|
#--- Helpers
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def call_by_name(self, handler, function_name, *args, **kwargs):
|
||||||
|
return getattr(handler, function_name)(*args, **kwargs)
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
def call_by_name_blocking(self, handler, function_name, *args, **kwargs):
|
||||||
|
return getattr(handler, function_name)(*args, **kwargs)
|
159
worker_test.py
Normal file
159
worker_test.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
|
||||||
|
# - Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# - Neither the name of the Mumble Developers nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
|
# software without specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||||
|
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import worker
|
||||||
|
from worker import Worker, local_thread, local_thread_blocking
|
||||||
|
from Queue import Queue
|
||||||
|
from logging.handlers import BufferingHandler
|
||||||
|
from logging import ERROR
|
||||||
|
from threading import Event
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
class WorkerTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
|
||||||
|
def set_ev(fu):
|
||||||
|
def new_fu(*args, **kwargs):
|
||||||
|
s = args[0]
|
||||||
|
s.event.set()
|
||||||
|
s.val = (args, kwargs)
|
||||||
|
return fu(*args, **kwargs)
|
||||||
|
return new_fu
|
||||||
|
|
||||||
|
class ATestWorker(Worker):
|
||||||
|
def __init__(self, name, message_queue):
|
||||||
|
Worker.__init__(self, name, message_queue)
|
||||||
|
self.event = Event()
|
||||||
|
self.val = None
|
||||||
|
self.started = False
|
||||||
|
self.stopped = False
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
@set_ev
|
||||||
|
def echo(self, val):
|
||||||
|
return val
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
@set_ev
|
||||||
|
def echo_block(self, val):
|
||||||
|
return val
|
||||||
|
|
||||||
|
def onStart(self):
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
def onStop(self):
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
@local_thread
|
||||||
|
def raise_(self, ex):
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
@local_thread_blocking
|
||||||
|
def raise_blocking(self, ex):
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
@set_ev
|
||||||
|
def call_me_by_name(self, arg1, arg2):
|
||||||
|
return
|
||||||
|
|
||||||
|
def call_me_by_name_blocking(self, arg1, arg2):
|
||||||
|
return arg1, arg2
|
||||||
|
|
||||||
|
|
||||||
|
self.buha = BufferingHandler(10000)
|
||||||
|
|
||||||
|
q = Queue()
|
||||||
|
self.q = q
|
||||||
|
self.w = ATestWorker("Test", q)
|
||||||
|
l = self.w.log()
|
||||||
|
l.addHandler(self.buha)
|
||||||
|
assert(self.w.started == False)
|
||||||
|
self.w.start()
|
||||||
|
sleep(0.05)
|
||||||
|
assert(self.w.started == True)
|
||||||
|
|
||||||
|
def testName(self):
|
||||||
|
assert(self.w.name() == "Test")
|
||||||
|
|
||||||
|
def testMessageQueue(self):
|
||||||
|
assert(self.w.message_queue() == self.q)
|
||||||
|
|
||||||
|
def testLocalThread(self):
|
||||||
|
s = "Testing"
|
||||||
|
self.w.event.clear()
|
||||||
|
self.w.echo(s)
|
||||||
|
self.w.event.wait(5)
|
||||||
|
args, kwargs = self.w.val
|
||||||
|
|
||||||
|
assert(args[1] == s)
|
||||||
|
|
||||||
|
def testLocalThreadException(self):
|
||||||
|
self.buha.flush()
|
||||||
|
self.w.raise_(Exception())
|
||||||
|
sleep(0.1) # hard delay
|
||||||
|
assert(len(self.buha.buffer) != 0)
|
||||||
|
assert(self.buha.buffer[0].levelno == ERROR)
|
||||||
|
|
||||||
|
def testCallByName(self):
|
||||||
|
self.w.event.clear()
|
||||||
|
self.w.call_by_name(self.w, "call_me_by_name", "arg1", arg2="arg2")
|
||||||
|
self.w.event.wait(5)
|
||||||
|
args, kwargs = self.w.val
|
||||||
|
|
||||||
|
assert(args[1] == "arg1")
|
||||||
|
assert(kwargs["arg2"] == "arg2")
|
||||||
|
|
||||||
|
def testLocalThreadBlocking(self):
|
||||||
|
s = "Testing"
|
||||||
|
assert(s == self.w.echo_block(s))
|
||||||
|
|
||||||
|
def testLocalThreadExceptionBlocking(self):
|
||||||
|
class TestException(Exception): pass
|
||||||
|
self.assertRaises(TestException, self.w.raise_blocking, TestException())
|
||||||
|
|
||||||
|
def testCallByNameBlocking(self):
|
||||||
|
arg1, arg2 = self.w.call_by_name_blocking(self.w, "call_me_by_name_blocking", "arg1", arg2="arg2")
|
||||||
|
|
||||||
|
assert(arg1 == "arg1")
|
||||||
|
assert(arg2 == "arg2")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
assert(self.w.stopped == False)
|
||||||
|
self.w.stop()
|
||||||
|
self.w.join(5)
|
||||||
|
assert(self.w.stopped == True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
#import sys;sys.argv = ['', 'Test.testName']
|
||||||
|
unittest.main()
|
Reference in New Issue
Block a user