Add context menu support

This patch adds support for adding context menu entries
to connected users. As these callbacks are - in contrast
to all others in mumo - user specific they do not fit
well with the existing mumo architecture. To cope with
this creating these callbacks is now handled directly
inside the manager on behalf of the remote manager.
The remote manager then handles forwarding any context
menu actions to a user selected handler.

This patch also removes the vestigial parts of context
menu handling already present but unused in mumo as they
were an architectural dead end.
This commit is contained in:
Stefan Hacker
2015-05-17 22:25:39 +02:00
parent 373708a435
commit cc68e673f1
2 changed files with 271 additions and 209 deletions

159
mumo.py
View File

@@ -63,7 +63,7 @@ default.update({'ice':(('host', str, '127.0.0.1'),
('watchdog', int, 30),
('callback_host', str, '127.0.0.1'),
('callback_port', int, -1)),
'iceraw':None,
'murmur':(('servers', commaSeperatedIntegers, []),),
'system':(('pidfile', str, 'mumo.pid'),),
@@ -71,13 +71,13 @@ default.update({'ice':(('host', str, '127.0.0.1'),
('file', str, 'mumo.log'))})
def load_slice(slice):
#
#
#--- Loads a given slicefile, used by dynload_slice and fsload_slice
# This function works around a number of differences between Ice python
# versions and distributions when it comes to slice include directories.
#
fallback_slicedirs = ["-I" + sdir for sdir in cfg.ice.slicedirs.split(';')]
if not hasattr(Ice, "getSliceDir"):
Ice.loadSlice('-I%s %s' % (" ".join(fallback_slicedirs), slice))
else:
@@ -86,7 +86,7 @@ def load_slice(slice):
slicedirs = fallback_slicedirs
else:
slicedirs = ['-I' + slicedir]
Ice.loadSlice('', slicedirs + [slice])
def dynload_slice(prx):
@@ -139,7 +139,7 @@ def do_main_program():
initdata.properties = Ice.createProperties([], initdata.properties)
for prop, val in cfg.iceraw:
initdata.properties.setProperty(prop, val)
initdata.properties.setProperty('Ice.ImplicitContext', 'Shared')
initdata.properties.setProperty('Ice.Default.EncodingVersion', '1.0')
initdata.logger = CustomLogger()
@@ -147,80 +147,81 @@ def do_main_program():
ice = Ice.initialize(initdata)
prxstr = 'Meta:tcp -h %s -p %d' % (cfg.ice.host, cfg.ice.port)
prx = ice.stringToProxy(prxstr)
if not cfg.ice.slice:
dynload_slice(prx)
else:
fsload_slice(cfg.ice.slice)
import Murmur
class mumoIceApp(Ice.Application):
def __init__(self, manager):
Ice.Application.__init__(self)
self.manager = manager
def run(self, args):
self.shutdownOnInterrupt()
if not self.initializeIceConnection():
return 1
if cfg.ice.watchdog > 0:
self.metaUptime = -1
self.checkConnection()
# Serve till we are stopped
self.communicator().waitForShutdown()
self.watchdog.cancel()
if self.interrupted():
warning('Caught interrupt, shutting down')
return 0
def initializeIceConnection(self):
"""
Establishes the two-way Ice connection and adds MuMo to the
configured servers
"""
ice = self.communicator()
if cfg.ice.secret:
debug('Using shared ice secret')
ice.getImplicitContext().put("secret", cfg.ice.secret)
else:
warning('Consider using an ice secret to improve security')
info('Connecting to Ice server (%s:%d)', cfg.ice.host, cfg.ice.port)
base = ice.stringToProxy(prxstr)
self.meta =Murmur.MetaPrx.uncheckedCast(base)
self.meta = Murmur.MetaPrx.uncheckedCast(base)
if cfg.ice.callback_port > 0:
cbp = ' -p %d' % cfg.ice.callback_port
else:
cbp = ''
adapter = ice.createObjectAdapterWithEndpoints('Callback.Client', 'tcp -h %s%s' % (cfg.ice.callback_host, cbp))
adapter.activate()
self.adapter = adapter
self.manager.setClientAdapter(adapter)
metacbprx = adapter.addWithUUID(metaCallback(self))
self.metacb = Murmur.MetaCallbackPrx.uncheckedCast(metacbprx)
return self.attachCallbacks()
def attachCallbacks(self):
"""
Attaches all callbacks
"""
# Ice.ConnectionRefusedException
debug('Attaching callbacks')
try:
info('Attaching meta callback')
self.meta.addCallback(self.metacb)
for server in self.meta.getBootedServers():
sid = server.id()
if not cfg.murmur.servers or sid in cfg.murmur.servers:
@@ -228,7 +229,7 @@ def do_main_program():
servercbprx = self.adapter.addWithUUID(serverCallback(self.manager, server, sid))
servercb = Murmur.ServerCallbackPrx.uncheckedCast(servercbprx)
server.addCallback(servercb)
except (Murmur.InvalidSecretException, Ice.UnknownUserException, Ice.ConnectionRefusedException), e:
if isinstance(e, Ice.ConnectionRefusedException):
error('Server refused connection')
@@ -238,15 +239,15 @@ def do_main_program():
else:
# We do not actually want to handle this one, re-raise it
raise e
self.connected = False
self.manager.announceDisconnected()
return False
self.connected = True
self.manager.announceConnected(self.meta)
return True
def checkConnection(self):
"""
Tries to retrieve the server uptime to determine wheter the server is
@@ -255,7 +256,7 @@ def do_main_program():
#debug('Watchdog run')
try:
uptime = self.meta.getUptime()
if self.metaUptime > 0:
if self.metaUptime > 0:
# Check if the server didn't restart since we last checked, we assume
# since the last time we ran this check the watchdog interval +/- 5s
# have passed. This should be replaced by implementing a Keepalive in
@@ -263,17 +264,17 @@ def do_main_program():
if not ((uptime - 5) <= (self.metaUptime + cfg.ice.watchdog) <= (uptime + 5)):
# Seems like the server restarted, re-attach the callbacks
self.attachCallbacks()
self.metaUptime = uptime
except Ice.Exception, e:
error('Connection to server lost, will try to reestablish callbacks in next watchdog run (%ds)', cfg.ice.watchdog)
debug(str(e))
self.attachCallbacks()
# Renew the timer
self.watchdog = Timer(cfg.ice.watchdog, self.checkConnection)
self.watchdog.start()
def checkSecret(func):
"""
Decorator that checks whether the server transmitted the right secret
@@ -281,28 +282,28 @@ def do_main_program():
"""
if not cfg.ice.secret:
return func
def newfunc(*args, **kws):
if 'current' in kws:
current = kws["current"]
else:
current = args[-1]
if not current or 'secret' not in current.ctx or current.ctx['secret'] != cfg.ice.secret:
error('Server transmitted invalid secret. Possible injection attempt.')
raise Murmur.InvalidSecretException()
return func(*args, **kws)
return newfunc
def fortifyIceFu(retval=None, exceptions=(Ice.Exception,)):
"""
Decorator that catches exceptions,logs them and returns a safe retval
value. This helps to prevent getting stuck in
critical code paths. Only exceptions that are instances of classes
given in the exceptions list are not caught.
The default is to catch all non-Ice exceptions.
"""
def newdec(func):
@@ -315,13 +316,13 @@ def do_main_program():
if isinstance(e, ex):
catch = False
break
if catch:
critical('Unexpected exception caught')
exception(e)
return retval
raise
return newfunc
return newdec
@@ -329,7 +330,7 @@ def do_main_program():
def __init__(self, app):
Murmur.MetaCallback.__init__(self)
self.app = app
@fortifyIceFu()
@checkSecret
def started(self, server, current=None):
@@ -344,20 +345,20 @@ def do_main_program():
servercbprx = self.app.adapter.addWithUUID(serverCallback(self.app.manager, server, sid))
servercb = Murmur.ServerCallbackPrx.uncheckedCast(servercbprx)
server.addCallback(servercb)
# Apparently this server was restarted without us noticing
except (Murmur.InvalidSecretException, Ice.UnknownUserException), e:
if hasattr(e, "unknown") and e.unknown != "Murmur::InvalidSecretException":
# Special handling for Murmur 1.2.2 servers with invalid slice files
raise e
error('Invalid ice secret')
return
else:
debug('Virtual server %d got started', sid)
self.app.manager.announceMeta(sid, "started", server, current)
@fortifyIceFu()
@checkSecret
def stopped(self, server, current=None):
@@ -378,10 +379,10 @@ def do_main_program():
except Ice.ConnectionRefusedException:
self.app.connected = False
self.app.manager.announceDisconnected()
debug('Server shutdown stopped a virtual server')
def forwardServer(fu):
def new_fu(self, *args, **kwargs):
self.manager.announceServer(self.sid, fu.__name__, self.server, *args, **kwargs)
@@ -393,12 +394,12 @@ def do_main_program():
self.manager = manager
self.sid = sid
self.server = server
# Hack to prevent every call to server.id() from the client callbacks
# from having to go over Ice
def id_replacement():
return self.sid
server.id = id_replacement
@checkSecret
@@ -412,7 +413,7 @@ def do_main_program():
def userConnected(self, u, current=None): pass
@checkSecret
@forwardServer
def channelCreated(self, c, current=None): pass
def channelCreated(self, c, current=None): pass
@checkSecret
@forwardServer
def channelRemoved(self, c, current=None): pass
@@ -422,56 +423,56 @@ def do_main_program():
@checkSecret
@forwardServer
def userTextMessage(self, u, m, current=None) : pass
class contextCallback(Murmur.ServerContextCallback):
def __init__(self, manager, server, sid):
class customContextCallback(Murmur.ServerContextCallback):
def __init__(self, contextActionCallback, *ctx):
Murmur.ServerContextCallback.__init__(self)
self.manager = manager
self.server = server
self.sid = sid
self.cb = contextActionCallback
self.ctx = ctx
@checkSecret
def contextAction(self, action, p, session, chanid, current=None):
self.manager.announceContext(self.sid, "contextAction", self.server, action, p, session, chanid, current)
def contextAction(self, *args, **argv):
# (action, user, target_session, target_chanid, current=None)
self.cb(*(self.ctx + args), **argv)
#
#--- Start of moderator
#
info('Starting mumble moderator')
debug('Initializing manager')
manager = MumoManager(Murmur)
manager = MumoManager(Murmur, customContextCallback)
manager.start()
manager.loadModules()
manager.startModules()
debug("Initializing mumoIceApp")
debug("Initializing mumoIceApp")
app = mumoIceApp(manager)
state = app.main(sys.argv[:1], initData=initdata)
manager.stopModules()
manager.stop()
info('Shutdown complete')
return state
class CustomLogger(Ice.Logger):
"""
Logger implementation to pipe Ice log messages into
our own log
"""
def __init__(self):
Ice.Logger.__init__(self)
self._log = getLogger('Ice')
def _print(self, message):
self._log.info(message)
def trace(self, category, message):
self._log.debug('Trace %s: %s', category, message)
def warning(self, message):
self._log.warning(message)
def error(self, message):
self._log.error(message)
@@ -492,11 +493,11 @@ if __name__ == '__main__':
parser.add_option('-a', '--app', action='store_true', dest='force_app',
help='do not run as daemon', default=False)
(option, args) = parser.parse_args()
if option.force_daemon and option.force_app:
parser.print_help()
sys.exit(1)
# Load configuration
try:
cfg = Config(option.ini, default)
@@ -504,7 +505,7 @@ if __name__ == '__main__':
print >> sys.stderr, 'Fatal error, could not load config file from "%s"' % cfgfile
print >> sys.stderr, e
sys.exit(1)
# Initialise logger
if cfg.log.file:
try:
@@ -515,19 +516,19 @@ if __name__ == '__main__':
sys.exit(1)
else:
logfile = logging.sys.stderr
if option.verbose:
level = cfg.log.level
else:
level = logging.ERROR
logging.basicConfig(level=level,
format='%(asctime)s %(levelname)s %(name)s %(message)s',
stream=logfile)
# As the default try to run as daemon. Silently degrade to running as a normal application if this fails
# unless the user explicitly defined what he expected with the -a / -d parameter.
# unless the user explicitly defined what he expected with the -a / -d parameter.
try:
if option.force_app:
raise ImportError # Pretend that we couldn't import the daemon lib