From a71cf75a83016099d46c7fb5eb97adb853c1d540 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Mon, 20 Dec 2010 18:13:04 +0100 Subject: [PATCH] Added onjoin module for moving players on join as the first mumo plugin. Numerous fixes/changes in the underlying stuff. Moved sample configuration files from modules-enabled to modules-available. --- modules-available/onjoin.ini | 20 ++++++++ modules-available/test.ini | 14 ++++++ modules-enabled/test.ini | 9 ---- modules/onjoin.py | 93 ++++++++++++++++++++++++++++++++++++ modules/test.py | 14 +++--- mumo.ini | 32 ++++++------- mumo.py | 30 +++++++----- mumo_manager.py | 91 ++++++++++++++++++++++------------- mumo_manager_test.py | 24 +++++++--- mumo_module.py | 4 +- 10 files changed, 247 insertions(+), 84 deletions(-) create mode 100644 modules-available/onjoin.ini create mode 100644 modules-available/test.ini delete mode 100644 modules-enabled/test.ini create mode 100644 modules/onjoin.py diff --git a/modules-available/onjoin.ini b/modules-available/onjoin.ini new file mode 100644 index 0000000..bb4c700 --- /dev/null +++ b/modules-available/onjoin.ini @@ -0,0 +1,20 @@ +; +; This module allows moving players into a specific channel once +; they connect regardless of which channel they were in when they left. +; + +[onjoin] +; Comma seperated list of servers to operate on, leave empty for all +servers = + +[all] +; Id of the channel to move users into once they join. +channel = 2 + +; For every server you want to override the [all] section for create +; a [server_] section. For example: + +; Overriding [all] for server with the id 1 would look like this +;[server_1] +;channel = 4 + diff --git a/modules-available/test.ini b/modules-available/test.ini new file mode 100644 index 0000000..4d601aa --- /dev/null +++ b/modules-available/test.ini @@ -0,0 +1,14 @@ +; This file is a dummy configuration file for the +; test module. The test module has heavy debug output +; and is solely meant for testing the basic framework +; as well as debugging purposes. Usually you don't want +; to enable it. +[testing] +tvar = 1 +tvar2 = Bernd +tvar3 = -1 +tvar4 = True + +[blub] +Bernd = asdad +asdasd = dasdw \ No newline at end of file diff --git a/modules-enabled/test.ini b/modules-enabled/test.ini deleted file mode 100644 index 1a56004..0000000 --- a/modules-enabled/test.ini +++ /dev/null @@ -1,9 +0,0 @@ -[testing] -tvar = 1 -tvar2 = Bernd -tvar3 = -1 -tvar4 = True - -[blub] -Bernd = asdad -asdasd = dasdw \ No newline at end of file diff --git a/modules/onjoin.py b/modules/onjoin.py new file mode 100644 index 0000000..04434d2 --- /dev/null +++ b/modules/onjoin.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 + +# Copyright (C) 2010 Stefan Hacker +# 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, + commaSeperatedIntegers, + MumoModule, + Config) +import re + + +class onjoin(MumoModule): + default_config = {'onjoin':( + ('servers', commaSeperatedIntegers, []), + ), + 'all':( + ('channel', int, 1), + ), + lambda x: re.match('server_\d+', x):( + ('channel', int, 1), + ) + } + + def __init__(self, name, manager, configuration = None): + MumoModule.__init__(self, name, manager, configuration) + self.murmur = manager.getMurmurModule() + + def connected(self): + manager = self.manager() + log = self.log() + log.debug("Register for Server callbacks") + + servers = self.cfg().onjoin.servers + if not servers: + servers = manager.SERVERS_ALL + + manager.subscribeServerCallbacks(self, servers) + + def disconnected(self): pass + + # + #--- Server callback functions + # + + def userConnected(self, server, state, context = None): + log = self.log() + sid = server.id() + try: + scfg = getattr(self.cfg(), 'server_%d' % sid) + except AttributeError: + scfg = self.cfg().all + + if state.channel != scfg.channel: + log.debug("Moving user '%s' from channel %d to %d on server %d", state.name, state.channel, scfg.channel, sid) + state.channel = scfg.channel + + try: + server.setState(state) + except self.murmur.InvalidChannelException: + log.error("Moving user '%s' failed, target channel %d does not exist on server %d", state.name, scfg.channel, sid) + + def userDisconnected(self, server, state, context = None): pass + def userStateChanged(self, server, state, context = None): pass + def channelCreated(self, server, state, context = None): pass + def channelRemoved(self, server, state, context = None): pass + def channelStateChanged(self, server, state, context = None): pass diff --git a/modules/test.py b/modules/test.py index 4e110d1..bb39400 100644 --- a/modules/test.py +++ b/modules/test.py @@ -72,32 +72,32 @@ class test(MumoModule): #--- Server callback functions # @logModFu - def userConnected(self, state, context = None): + def userConnected(self, server, state, context = None): pass @logModFu - def userDisconnected(self, state, context = None): + def userDisconnected(self, server, state, context = None): pass @logModFu - def userStateChanged(self, state, context = None): + def userStateChanged(self, server, state, context = None): pass @logModFu - def channelCreated(self, state, context = None): + def channelCreated(self, server, state, context = None): pass @logModFu - def channelRemoved(self, state, context = None): + def channelRemoved(self, server, state, context = None): pass @logModFu - def channelStateChanged(self, state, context = None): + def channelStateChanged(self, server, state, context = None): pass # #--- Server context callback functions # @logModFu - def contextAction(self, action, user, session, channelid, context = None): + def contextAction(self, server, action, user, session, channelid, context = None): pass \ No newline at end of file diff --git a/mumo.ini b/mumo.ini index fef5608..ec1c48e 100644 --- a/mumo.ini +++ b/mumo.ini @@ -7,39 +7,39 @@ ; Host and port of the Ice interface on ; the target Murmur server. -;host = 127.0.0.1 -;port = 6502 +host = 127.0.0.1 +port = 6502 -; If you do not define a slicefile here MuMo -; will try to automatically retrieve it from -; the server. This forces need_on_startup to -; True +; Slicefile to use -;slice = +slice = Murmur.ice ; Shared secret between the MuMo and the Murmur ; server. For security reason you should always ; use a shared secret. -;secret = +secret = ;Check Ice connection every x seconds -;watchdog = 15 +watchdog = 15 [modules] -;mod_dir = modules/ -;cfg_dir = modules-enabled/ -;timeout = 2 +mod_dir = modules/ +cfg_dir = modules-enabled/ +timeout = 2 [system] - -;pidfile = muauth.pid +pidfile = muauth.pid ; Logging configuration [log] ; Available loglevels: 10 = DEBUG (default) | 20 = INFO | 30 = WARNING | 40 = ERROR -;level = 10 -;file = mumo.log \ No newline at end of file +level = +file = mumo.log + + +[iceraw] +Ice.ThreadPool.Server.Size = 5 \ No newline at end of file diff --git a/mumo.py b/mumo.py index 5281402..66f2195 100644 --- a/mumo.py +++ b/mumo.py @@ -136,7 +136,7 @@ def do_main_program(): sid = server.id() if not cfg.murmur.servers or sid in cfg.murmur.servers: info('Setting callbacks for virtual server %d', sid) - servercbprx = self.adapter.addWithUUID(serverCallback(self.manager, sid)) + servercbprx = self.adapter.addWithUUID(serverCallback(self.manager, server, sid)) servercb = Murmur.ServerCallbackPrx.uncheckedCast(servercbprx) server.addCallback(servercb) @@ -159,7 +159,7 @@ def do_main_program(): return False self.connected = True - self.manager.announceConnected() + self.manager.announceConnected(self.meta) return True def checkConnection(self): @@ -256,7 +256,7 @@ def do_main_program(): if not cfg.murmur.servers or sid in cfg.murmur.servers: info('Setting authenticator for virtual server %d', server.id()) try: - servercbprx = self.app.adapter.addWithUUID(serverCallback(self.app.manager, sid)) + servercbprx = self.app.adapter.addWithUUID(serverCallback(self.app.manager, server, sid)) servercb = Murmur.ServerCallbackPrx.uncheckedCast(servercbprx) server.addCallback(servercb) @@ -301,45 +301,53 @@ def do_main_program(): def forwardServer(fu): - def new_fu(*args, **kwargs): - self = args[0] - self.manager.announceServer([self.sid], fu.__name__, *args, **kwargs) + def new_fu(self, *args, **kwargs): + self.manager.announceServer(self.sid, fu.__name__, self.server, *args, **kwargs) return new_fu class serverCallback(Murmur.ServerCallback): - def __init__(self, manager, sid): + def __init__(self, manager, server, sid): Murmur.ServerCallback.__init__(self) self.manager = manager self.sid = sid - + self.server = server + + @checkSecret @forwardServer def userStateChanged(self, u, current=None): pass + @checkSecret @forwardServer def userDisconnected(self, u, current=None): pass + @checkSecret @forwardServer def userConnected(self, u, current=None): pass + @checkSecret @forwardServer def channelCreated(self, c, current=None): pass + @checkSecret @forwardServer def channelRemoved(self, c, current=None): pass + @checkSecret @forwardServer def channelStateChanged(self, c, current=None): pass class contextCallback(Murmur.ServerContextCallback): - def __init__(self, manager, sid): + def __init__(self, manager, server, sid): Murmur.ServerContextCallback.__init__(self) self.manager = manager + self.server = server self.sid = sid + @checkSecret def contextAction(self, action, p, session, chanid, current=None): - self.manager.announceContext([self.sid], "contextAction", action, p, session, chanid, current) + self.manager.announceContext(self.sid, "contextAction", self.server, action, p, session, chanid, current) # #--- Start of moderator # info('Starting mumble moderator') debug('Initializing manager') - manager = MumoManager() + manager = MumoManager(Murmur) manager.start() manager.loadModules() manager.startModules() diff --git a/mumo_manager.py b/mumo_manager.py index 43d7a42..64a5055 100644 --- a/mumo_manager.py +++ b/mumo_manager.py @@ -47,7 +47,7 @@ class FailedLoadModuleImportException(FailedLoadModuleException): class FailedLoadModuleInitializationException(FailedLoadModuleException): pass -def debug_log(fu, enable = True): +def debug_log(enable = True): def new_dec(fu): def new_fu(*args, **kwargs): self = args[0] @@ -163,6 +163,17 @@ class MumoManagerRemote(object): """ return self.__master.unsubscribeContextCallbacks(self.__queue, handler, servers) + def getMurmurModule(self): + """ + Returns the Murmur module generated from the slice file + """ + return self.__master.getMurmurModule() + + def getMeta(self): + """ + Returns the connected servers meta module or None if it is not available + """ + return self.__master.getMeta() class MumoManager(Worker): @@ -172,13 +183,16 @@ class MumoManager(Worker): ('cfg_dir', str, "modules-enabled/"), ('timeout', int, 2))} - def __init__(self, cfg = Config(default = cfg_default)): + def __init__(self, murmur, cfg = Config(default = cfg_default)): Worker.__init__(self, "MumoManager") self.queues = {} # {queue:module} self.modules = {} # {name:module} self.imports = {} # {name:import} self.cfg = cfg + self.murmur = murmur + self.meta = None + self.metaCallbacks = {} # {sid:{queue:[handler]}} self.serverCallbacks = {} self.contextCallbacks = {} @@ -201,38 +215,37 @@ class MumoManager(Worker): except KeyError, ValueError: pass - def __announce_to_dict(self, mdict, servers, function, *args, **kwargs): + def __announce_to_dict(self, mdict, server, 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 - try: - for queue, handlers in mdict[self.MAGIC_ALL].iteritems(): - for handler in handlers: - self.__call_remote(queue, handler, function, args, kwargs) - except KeyError: - # No handler registered for MAGIC_ALL - pass - + @param server Server 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 of the given serverlist + if server == self.MAGIC_ALL: + servers = mdict.iterkeys() + else: + servers = [self.MAGIC_ALL, server] + for server in servers: try: - for queue, handler in mdict[server].iteritems(): - self.__call_remote(queue, handler, function, args, kwargs) + for queue, handlers in mdict[server].iteritems(): + for handler in handlers: + self.__call_remote(queue, handler, function, *args, **kwargs) except KeyError: # No handler registered for that server pass - + def __call_remote(self, queue, handler, function, *args, **kwargs): try: func = getattr(handler, function) # Find out what to call on target + queue.put((None, func, args, kwargs)) except AttributeError, e: mod = self.queues.get(queue, None) myname = "" @@ -240,20 +253,21 @@ class MumoManager(Worker): if mod == mymod: myname = name if myname: - self.log.error("Handler class registered by module '%s' does not handle function '%s'. Call failed.", myname, function) + self.log().error("Handler class registered by module '%s' does not handle function '%s'. Call failed.", myname, function) else: self.log().exception(e) - queue.put((None, func, args, kwargs)) + # #-- Module multiplexing functionality # @local_thread - def announceConnected(self): + def announceConnected(self, meta = None): """ Call connected handler on all handlers """ + self.meta = meta for queue, module in self.queues.iteritems(): self.__call_remote(queue, module, "connected") @@ -266,40 +280,40 @@ class MumoManager(Worker): self.__call_remote(queue, module, "disconnected") @local_thread - def announceMeta(self, servers, function, *args, **kwargs): + def announceMeta(self, server, function, *args, **kwargs): """ Call a function on the meta handlers - @param servers Servers to announce to + @param server Servers to announce to @param function Name of the function to call on the handler @param args List of arguments @param kwargs List of keyword arguments """ - self.__announce_to_dict(self.metaCallbacks, servers, function, *args, **kwargs) + self.__announce_to_dict(self.metaCallbacks, server, function, *args, **kwargs) @local_thread - def announceServer(self, servers, function, *args, **kwargs): + def announceServer(self, server, function, *args, **kwargs): """ Call a function on the server handlers - @param servers Servers to announce to + @param server Server to announce to @param function Name of the function to call on the handler @param args List of arguments @param kwargs List of keyword arguments """ - self.__announce_to_dict(self.serverCallbacks, servers, function, *args, **kwargs) + self.__announce_to_dict(self.serverCallbacks, server, function, *args, **kwargs) @local_thread - def announceContext(self, servers, function, *args, **kwargs): + def announceContext(self, server, function, *args, **kwargs): """ Call a function on the context handlers - @param servers Servers to announce to + @param server Server to announce to @param function Name of the function to call on the handler @param args List of arguments @param kwargs List of keyword arguments """ - self.__announce_to_dict(self.serverCallbacks, servers, function, *args, **kwargs) + self.__announce_to_dict(self.serverCallbacks, server, function, *args, **kwargs) # #--- Module self management functionality # @@ -351,8 +365,19 @@ class MumoManager(Worker): @see MumoManagerRemote """ return self.__rem_from_dict(self.contextCallbacks, queue, handler, servers) + + def getMurmurModule(self): + """ + Returns the Murmur module generated from the slice file + """ + return self.murmur + + def getMeta(self): + """ + Returns the connected servers meta module or None if it is not available + """ + return self.meta - # #--- Module load/start/stop/unload functionality # @local_thread_blocking diff --git a/mumo_manager_test.py b/mumo_manager_test.py index 6041f54..9ecc0ee 100644 --- a/mumo_manager_test.py +++ b/mumo_manager_test.py @@ -75,11 +75,11 @@ class MumoManagerTest(unittest.TestCase): if arg1 == "arg1" and arg2 == "arg2": self.emeta.set() - def contextCallMe(self, arg1, arg2): + def contextCallMe(self, server, arg1, arg2): if arg1 == "arg1" and arg2 == "arg2": self.econtext.set() - def serverCallMe(self, arg1, arg2): + def serverCallMe(self, server, arg1, arg2): if arg1 == "arg1" and arg2 == "arg2": self.eserver.set() @@ -95,7 +95,7 @@ class MumoManagerTest(unittest.TestCase): #--- Helpers for independent test env creation # def up(self): - man = MumoManager() + man = MumoManager(None) man.start() mod = man.loadModuleCls("MyModule", self.mymod, self.cfg) @@ -144,21 +144,33 @@ class MumoManagerTest(unittest.TestCase): def testMetaCallback(self): man, mod = self.up() man.announceConnected() - man.announceMeta([], "metaCallMe", ["arg1"], {"arg2":"arg2"}) + mod.econnected.wait(timeout=1) + assert(mod.econnected.is_set()) + man.announceMeta(man.MAGIC_ALL, "metaCallMe", "arg1", arg2 = "arg2") + mod.emeta.wait(timeout=1) + assert(mod.emeta.is_set()) man.announceDisconnected() self.down(man, mod) def testContextCallback(self): man, mod = self.up() man.announceConnected() - man.announceContext([], "contextCallMe", ["arg1"], {"arg2":"arg2"}) + mod.econnected.wait(timeout=1) + assert(mod.econnected.is_set()) + man.announceContext(man.MAGIC_ALL, "contextCallMe", "server", "arg1", arg2 = "arg2") + mod.econtext.wait(timeout=1) + assert(mod.econtext.is_set()) man.announceDisconnected() self.down(man, mod) def testServerCallback(self): man, mod = self.up() man.announceConnected() - man.announceServer([], "serverCallMe", ["arg1"], {"arg2":"arg2"}) + mod.econnected.wait(timeout=1) + assert(mod.econnected.is_set()) + man.announceServer(man.MAGIC_ALL, "serverCallMe", "server", "arg1", arg2 = "arg2") + mod.eserver.wait(timeout=1) + assert(mod.eserver.is_set()) man.announceDisconnected() self.down(man, mod) diff --git a/mumo_module.py b/mumo_module.py index fb93acf..b71d432 100644 --- a/mumo_module.py +++ b/mumo_module.py @@ -91,10 +91,10 @@ class MumoModule(Worker): def logModFu(fu): - def newfu(self, *args, **kwargs): + def new_fu(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 \ No newline at end of file + return new_fu \ No newline at end of file