5 # Copyright 2012 Intel Corporation.
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # Luc Yriarte <luc.yriarte@intel.com>
20 # Christophe Guiraud <christophe.guiraud@intel.com>
24 import argparse, dbus, json, sys
26 from twisted.internet import glib2reactor
27 # Configure the twisted mainloop to be run inside the glib mainloop.
28 # This must be done before importing the other twisted modules
29 glib2reactor.install()
30 from twisted.internet import reactor, defer
32 from autobahn.websocket import listenWS
33 from autobahn.wamp import exportRpc, WampServerFactory, WampCraServerProtocol
35 from dbus.mainloop.glib import DBusGMainLoop
40 gobject.threads_init()
46 from twisted.python import log
49 from xml.etree.ElementTree import XMLParser
54 ###############################################################################
61 ###############################################################################
64 Global cache of DBus connexions and signal handlers
67 self.dbusConnexions = {}
68 self.signalHandlers = {}
73 Disconnect signal handlers before resetting cache.
75 self.dbusConnexions = {}
76 # disconnect signal handlers
77 for key in self.signalHandlers:
78 self.signalHandlers[key].disconnect()
79 self.signalHandlers = {}
82 def dbusConnexion(self, busName):
83 if not self.dbusConnexions.has_key(busName):
84 if busName == "session":
85 self.dbusConnexions[busName] = dbus.SessionBus()
86 elif busName == "system":
87 self.dbusConnexions[busName] = dbus.SystemBus()
89 raise Exception("Error: invalid bus: %s" % busName)
90 return self.dbusConnexions[busName]
94 ###############################################################################
95 class DbusSignalHandler:
97 signal hash id as busName#senderName#objectName#interfaceName#signalName
99 def __init__(self, busName, senderName, objectName, interfaceName, signalName):
100 self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
101 # connect handler to signal
102 self.bus = cache.dbusConnexion(busName)
103 self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
106 def disconnect(self):
107 names = self.id.split("#")
108 self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
111 def handleSignal(self, *args):
113 publish dbus args under topic hash id
115 factory.dispatch(self.id, json.dumps(args))
119 ###############################################################################
120 class DbusCallHandler:
122 deferred reply to return dbus results
124 def __init__(self, method, args):
126 self.request = defer.Deferred()
131 def callMethod(self):
133 dbus method async call
136 self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
140 def dbusSuccess(self, *result):
142 return JSON string result array
144 self.request.callback(json.dumps(result))
148 def dbusError(self, error):
150 return dbus error message
152 self.request.errback(Exception(error.get_dbus_message()))
157 ################################################################################
160 Execute DynDBusClass generated code
162 def __init__(self, globalCtx, localCtx) :
163 self.exec_string = ""
164 self.exec_code = None
165 self.exec_code_valid = 1
166 self.indent_level = 0
167 self.indent_increment = 1
169 self.localCtx = localCtx
170 self.globalCtx = globalCtx
173 def append_stmt(self, stmt) :
174 self.exec_code_valid = 0
176 for x in range(0,self.indent_level):
177 self.exec_string = self.exec_string + ' '
178 self.exec_string = self.exec_string + stmt + '\n'
181 self.indent_level = self.indent_level + self.indent_increment
184 self.indent_level = self.indent_level - self.indent_increment
186 # compile : Compile exec_string into exec_code using the builtin
187 # compile function. Skip if already in sync.
189 if not self.exec_code_valid :
190 self.exec_code = compile(self.exec_string, "<string>", "exec")
191 self.exec_code_valid = True
194 if not self.exec_code_valid :
196 exec(self.exec_code, self.globalCtx, self.localCtx)
200 ################################################################################
201 class XmlCbParser: # The target object of the parser
204 def __init__(self, dynDBusClass):
205 self.dynDBusClass = dynDBusClass
207 def start(self, tag, attrib): # Called for each opening tag.
211 if (tag == 'interface'):
212 self.dynDBusClass.set_interface(attrib['name'])
215 if (tag == 'method'):
217 self.dynDBusClass.def_method(attrib['name'])
219 if (tag == 'signal'):
221 self.dynDBusClass.def_signal(attrib['name'])
224 # Set signature (in/out & name) for method
226 if (self.current == 'method'):
227 if (attrib.has_key('direction') == False):
228 attrib['direction'] = "in"
229 self.dynDBusClass.add_signature(attrib['name'],
233 if (self.current == 'signal'):
234 if (attrib.has_key('name') == False):
235 attrib['name'] = 'value'
236 self.dynDBusClass.add_signature(attrib['name'], 'in',
239 def end(self, tag): # Called for each closing tag.
240 if (tag == 'method'):
241 self.dynDBusClass.add_dbus_method()
242 self.dynDBusClass.add_body_method()
243 self.dynDBusClass.end_method()
244 if (tag == 'signal'):
245 self.dynDBusClass.add_dbus_signal()
246 self.dynDBusClass.add_body_signal()
247 self.dynDBusClass.end_method()
249 def data(self, data):
250 pass # We do not need to do anything with data.
251 def close(self): # Called when all data has been parsed.
256 ################################################################################
257 class DynDBusClass():
258 def __init__(self, className, globalCtx, localCtx):
259 self.className = className
260 self.xmlCB = XmlCbParser(self)
262 self.class_code = ExecCode(globalCtx, localCtx)
263 self.class_code.indent_increment = 4
264 self.class_code.append_stmt("import dbus")
265 self.class_code.append_stmt("\n")
266 self.class_code.append_stmt("class " + self.className + "(dbus.service.Object):")
267 self.class_code.indent()
269 ## Overload of __init__ method
270 self.def_method("__init__")
271 self.add_method("bus, callback=None, objPath='/sample', busName='org.cloudeebus'")
272 self.add_stmt("self.bus = bus")
273 self.add_stmt("self.objPath = objPath")
274 self.add_stmt("self.callback = callback")
275 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=busName)")
278 ## Create 'add_to_connection' method
279 self.def_method("add_to_connection")
280 self.add_method("connection=None, path=None")
281 self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objPath)")
284 ## Create 'remove_from_connection' method
285 self.def_method("remove_from_connection")
286 self.add_method("connection=None, path=None")
287 self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objPath)")
290 def createDBusServiceFromXML(self, xml):
291 self.parser = XMLParser(target=self.xmlCB)
292 self.parser.feed(xml)
295 def set_interface(self, ifName):
298 def def_method(self, methodName):
299 self.methodToAdd = methodName
300 self.signalToAdd = None
301 self.args_str = str()
303 self.signature['name'] = str()
304 self.signature['in'] = str()
305 self.signature['out'] = str()
307 def def_signal(self, signalName):
308 self.methodToAdd = None
309 self.signalToAdd = signalName
310 self.args_str = str()
312 self.signature['name'] = str()
313 self.signature['in'] = str()
314 self.signature['out'] = str()
316 def add_signature(self, name, direction, signature):
317 if (direction == 'in'):
318 self.signature['in'] += signature
319 if (self.signature['name'] != str()):
320 self.signature['name'] += ", "
321 self.signature['name'] += name
322 if (direction == 'out'):
323 self.signature['out'] = signature
325 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
327 if (self.methodToAdd != None):
328 name = self.methodToAdd
330 name = self.signalToAdd
333 if (async_success_cb != None):
334 async_cb_str = async_success_cb
335 if (async_err_cb != None):
336 if (async_cb_str != str()):
338 async_cb_str += async_err_cb
340 parameters = self.args_str
341 if (async_cb_str != str()):
342 if (parameters != str()):
344 parameters +=async_cb_str
346 if (parameters != str()):
347 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
349 self.class_code.append_stmt("def " + name + "(self):")
350 self.class_code.indent()
352 def end_method(self):
353 self.class_code.append_stmt("\n")
354 self.class_code.dedent()
356 def add_dbus_method(self):
357 decorator = '@dbus.service.method("' + self.ifName + '"'
358 if (self.signature.has_key('in') and self.signature['in'] != str()):
359 decorator += ", in_signature='" + self.signature['in'] + "'"
360 if (self.signature.has_key('out') and self.signature['out'] != str()):
361 decorator += ", out_signature='" + self.signature['out'] + "'"
362 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
364 self.class_code.append_stmt(decorator)
365 if (self.signature.has_key('name') and self.signature['name'] != str()):
366 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
368 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
370 def add_dbus_signal(self):
371 decorator = '@dbus.service.signal("' + self.ifName + '"'
372 if (self.signature.has_key('in') and self.signature['in'] != str()):
373 decorator += ", signature='" + self.signature['in'] + "'"
375 self.class_code.append_stmt(decorator)
376 if (self.signature.has_key('name') and self.signature['name'] != str()):
377 self.add_method(self.signature['name'])
381 def add_body_method(self):
382 if (self.methodToAdd != None):
383 if (self.args_str != str()):
384 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', self.objPath, '" + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
386 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', self.objPath, '" + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb)")
388 def add_body_signal(self):
389 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
390 self.class_code.append_stmt("\n")
392 def add_stmt(self, stmt) :
393 self.class_code.append_stmt(stmt)
396 self.class_code.execute()
400 ###############################################################################
401 class CloudeebusService:
403 support for sending DBus messages and registering for DBus signals
405 def __init__(self, permissions):
406 self.permissions = permissions;
407 self.proxyObjects = {}
408 self.proxyMethods = {}
409 self.pendingCalls = []
410 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
411 self.services = {} # DBus service created
412 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
413 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
414 self.localCtx = locals()
415 self.globalCtx = globals()
418 def proxyObject(self, busName, serviceName, objectName):
420 object hash id as busName#serviceName#objectName
422 id = "#".join([busName, serviceName, objectName])
423 if not self.proxyObjects.has_key(id):
425 # check permissions, array.index throws exception
426 self.permissions.index(serviceName)
427 bus = cache.dbusConnexion(busName)
428 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
429 return self.proxyObjects[id]
432 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
434 method hash id as busName#serviceName#objectName#interfaceName#methodName
436 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
437 if not self.proxyMethods.has_key(id):
438 obj = self.proxyObject(busName, serviceName, objectName)
439 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
440 return self.proxyMethods[id]
444 def dbusRegister(self, list):
446 arguments: bus, sender, object, interface, signal
449 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
452 # check permissions, array.index throws exception
453 self.permissions.index(list[1])
455 # check if a handler exists
456 sigId = "#".join(list)
457 if cache.signalHandlers.has_key(sigId):
460 # create a handler that will publish the signal
461 dbusSignalHandler = DbusSignalHandler(*list)
462 cache.signalHandlers[sigId] = dbusSignalHandler
464 return dbusSignalHandler.id
468 def dbusSend(self, list):
470 arguments: bus, destination, object, interface, message, [args]
472 # clear pending calls
473 for call in self.pendingCalls:
475 self.pendingCalls.remove(call)
478 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
480 # parse JSON arg list
483 args = json.loads(list[5])
485 # get dbus proxy method
486 method = self.proxyMethod(*list[0:5])
488 # use a deferred call handler to manage dbus results
489 dbusCallHandler = DbusCallHandler(method, args)
490 self.pendingCalls.append(dbusCallHandler)
491 return dbusCallHandler.callMethod()
495 def emitSignal(self, list):
497 arguments: agentObjectPath, signalName, result (to emit)
500 className = re.sub('/', '_', objectPath[1:])
503 if (self.serviceAgents.has_key(className) == True):
504 exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "(" + str(result) + ")"
505 eval(exe_str, self.globalCtx, self.localCtx)
507 raise Exception("No object path " + objectPath)
510 def returnMethod(self, list):
512 arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
518 if (self.servicePendingCalls.has_key(methodId)):
519 cb = self.servicePendingCalls[methodId]['calls'][callIndex]
521 raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
523 successCB = cb["successCB"]
529 errorCB = cb["errorCB"]
534 self.servicePendingCalls[methodId]['calls'][callIndex] = None
535 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
536 if self.servicePendingCalls[methodId]['count'] == 0:
537 del self.servicePendingCalls[methodId]
539 raise Exception("No methodID " + methodId)
541 def srvCB(self, name, objPath, ifName, async_succes_cb, async_error_cb, *args):
542 methodId = self.srvName + "#" + objPath + "#" + ifName + "#" + name
543 cb = { 'successCB': async_succes_cb,
544 'errorCB': async_error_cb}
545 if methodId not in self.servicePendingCalls:
546 self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
547 pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
548 self.servicePendingCalls[methodId]['calls'].append(cb)
549 self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
550 factory.dispatch(methodId, pendingCallStr)
553 def serviceAdd(self, list):
555 arguments: busName, srvName
558 self.bus = cache.dbusConnexion( busName['name'] )
559 self.srvName = list[1]
560 if (self.services.has_key(self.srvName) == False):
561 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
565 def serviceRelease(self, list):
567 arguments: busName, srvName
569 self.srvName = list[0]
570 if (self.services.has_key(self.srvName) == True):
571 self.services.pop(self.srvName)
574 raise Exception(self.srvName + " do not exist")
577 def serviceAddAgent(self, list):
579 arguments: objectPath, xmlTemplate
581 self.agentObjectPath = list[0]
582 xmlTemplate = list[1]
583 self.className = re.sub('/', '_', self.agentObjectPath[1:])
584 if (self.dynDBusClasses.has_key(self.className) == False):
585 self.dynDBusClasses[self.className] = DynDBusClass(self.className, self.globalCtx, self.localCtx)
586 self.dynDBusClasses[self.className].createDBusServiceFromXML(xmlTemplate)
590 if (1): ## Force deletion
591 if os.access('./MyDbusClass.py', os.R_OK) == True:
592 os.remove('./MyDbusClass.py')
594 if os.access('./MyDbusClass.py', os.R_OK) == False:
595 f = open('./MyDbusClass.py', 'w')
596 f.write(self.dynDBusClasses[self.className].class_code.exec_string)
599 self.dynDBusClasses[self.className].declare()
601 ## Class already exist, instanciate it if not already instanciated
602 if (self.serviceAgents.has_key(self.className) == False):
603 self.serviceAgents[self.className] = eval(self.className + "(self.bus, callback=self.srvCB, objPath=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
605 self.serviceAgents[self.className].add_to_connection()
606 return (self.agentObjectPath)
609 def serviceDelAgent(self, list):
611 arguments: objectPath, xmlTemplate
613 agentObjectPath = list[0]
614 className = re.sub('/', '_', agentObjectPath[1:])
616 if (self.serviceAgents.has_key(className)):
617 self.serviceAgents[self.className].remove_from_connection()
618 self.serviceAgents.pop(self.className)
620 raise Exception(agentObjectPath + " doesn't exist!")
622 return (agentObjectPath)
625 def getVersion(self):
627 return current version string
633 ###############################################################################
634 class CloudeebusServerProtocol(WampCraServerProtocol):
636 connexion and session authentication management
639 def onSessionOpen(self):
640 # CRA authentication options
641 self.clientAuthTimeout = 0
642 self.clientAuthAllowAnonymous = OPENDOOR
643 # CRA authentication init
644 WampCraServerProtocol.onSessionOpen(self)
647 def getAuthPermissions(self, key, extra):
648 return json.loads(extra.get("permissions", "[]"))
651 def getAuthSecret(self, key):
652 secret = CREDENTIALS.get(key, None)
655 # secret must be of str type to be hashed
656 return secret.encode('utf-8')
659 def onAuthenticated(self, key, permissions):
661 # check authentication key
663 raise Exception("Authentication failed")
664 # check permissions, array.index throws exception
665 for req in permissions:
667 # create cloudeebus service instance
668 self.cloudeebusService = CloudeebusService(permissions)
669 # register it for RPC
670 self.registerForRpc(self.cloudeebusService)
671 # register for Publish / Subscribe
672 self.registerForPubSub("", True)
675 def connectionLost(self, reason):
676 WampCraServerProtocol.connectionLost(self, reason)
677 if factory.getConnectionCount() == 0:
682 ###############################################################################
684 if __name__ == '__main__':
688 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
689 parser.add_argument('-v', '--version', action='store_true',
690 help='print version and exit')
691 parser.add_argument('-d', '--debug', action='store_true',
692 help='log debug info on standard output')
693 parser.add_argument('-o', '--opendoor', action='store_true',
694 help='allow anonymous access to all services')
695 parser.add_argument('-p', '--port', default='9000',
697 parser.add_argument('-c', '--credentials',
698 help='path to credentials file')
699 parser.add_argument('-w', '--whitelist',
700 help='path to whitelist file')
702 args = parser.parse_args(sys.argv[1:])
705 print("Cloudeebus version " + VERSION)
709 log.startLogging(sys.stdout)
711 OPENDOOR = args.opendoor
714 jfile = open(args.credentials)
715 CREDENTIALS = json.load(jfile)
719 jfile = open(args.whitelist)
720 WHITELIST = json.load(jfile)
723 uri = "ws://localhost:" + args.port
725 factory = WampServerFactory(uri, debugWamp = args.debug)
726 factory.protocol = CloudeebusServerProtocol
727 factory.setProtocolOptions(allowHixie76 = True)
731 DBusGMainLoop(set_as_default=True)