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 ################################################################################
159 def __init__(self, globalCtx, localCtx) :
160 self.exec_string = ""
161 self.exec_code = None
162 self.exec_code_valid = 1
163 self.indent_level = 0
164 self.indent_increment = 1
166 self.localCtx = localCtx
167 self.globalCtx = globalCtx
170 # __str__ : Return a string representation of the object, for
173 return self.exec_string
178 def append_stmt(self, stmt) :
179 self.exec_code_valid = 0
182 for x in range(0,self.indent_level):
183 self.exec_string = self.exec_string + ' '
184 self.exec_string = self.exec_string + stmt + "\t\t# l:" + str(self.line) + '\n'
187 self.exec_string = self.exec_string + "# l:" + str(self.line) + '\n'
189 self.exec_string = self.exec_string + stmt + "\t\t# l:" + str(self.line) + '\n'
192 self.indent_level = self.indent_level + self.indent_increment
195 self.indent_level = self.indent_level - self.indent_increment
197 # compile : Compile exec_string into exec_code using the builtin
198 # compile function. Skip if already in sync.
200 if not self.exec_code_valid :
201 self.exec_code = compile(self.exec_string, "<string>", "exec")
202 self.exec_code_valid = True
205 if not self.exec_code_valid :
207 exec(self.exec_code, self.globalCtx, self.localCtx)
211 ################################################################################
212 class XmlCb_Parser: # The target object of the parser
215 def __init__(self, dynDBusClass):
216 self.dynDBusClass = dynDBusClass
218 def start(self, tag, attrib): # Called for each opening tag.
222 if (tag == 'interface'):
223 self.dynDBusClass.set_interface(attrib['name'])
226 if (tag == 'method'):
228 self.dynDBusClass.def_method(attrib['name'])
230 if (tag == 'signal'):
232 self.dynDBusClass.def_signal(attrib['name'])
235 # Set signature (in/out & name) for method
237 if (self.current == 'method'):
238 self.dynDBusClass.add_signature(attrib['name'],
242 if (self.current == 'signal'):
243 self.dynDBusClass.add_signature(attrib['name'], 'in',
246 def end(self, tag): # Called for each closing tag.
247 if (tag == 'method'):
248 self.dynDBusClass.add_dbus_method()
249 self.dynDBusClass.add_body_method()
250 self.dynDBusClass.end_method()
251 if (tag == 'signal'):
252 self.dynDBusClass.add_dbus_signal()
253 self.dynDBusClass.add_body_signal()
254 self.dynDBusClass.end_method()
256 def data(self, data):
257 pass # We do not need to do anything with data.
258 def close(self): # Called when all data has been parsed.
263 ################################################################################
264 class dynDBusClass():
265 def __init__(self, className, globalCtx, localCtx):
266 self.className = className
267 self.xmlCB = XmlCb_Parser(self)
269 self.class_code = exec_code(globalCtx, localCtx)
270 self.class_code.indent_increment = 4
271 self.class_code.append_stmt("import dbus")
272 self.class_code.append_stmt("\n")
273 self.class_code.append_stmt("\n")
274 self.class_code.append_stmt("class " + self.className + "(dbus.service.Object):")
275 self.class_code.indent()
277 ## Overload of __init__ method
278 self.def_method("__init__")
279 self.add_method("bus, callback=None, objName='/sample', busName='org.cloudeebus'")
280 self.add_stmt("self.bus = bus")
281 self.add_stmt("self.objName = objName")
282 self.add_stmt("self.callback = callback")
283 # self.add_stmt("dbus.service.Object.__init__(self, conn=bus, object_path=objName, bus_name=busName)")
284 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=busName)")
287 ## Create 'add_to_connection' method
288 self.def_method("add_to_connection")
289 self.add_method("connection=None, path=None")
290 self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objName)")
293 ## Create 'remove_from_connection' method
294 self.def_method("remove_from_connection")
295 self.add_method("connection=None, path=None")
296 self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objName)")
299 def createDBusServiceFromXML(self, xml):
300 self.parser = XMLParser(target=self.xmlCB)
301 self.parser.feed(xml)
304 def set_interface(self, ifName):
307 def def_method(self, methodName):
308 self.methodToAdd = methodName
309 self.signalToAdd = None
310 self.args_str = str()
312 self.signature['name'] = str()
313 self.signature['in'] = str()
314 self.signature['out'] = str()
316 def def_signal(self, signalName):
317 self.methodToAdd = None
318 self.signalToAdd = signalName
319 self.args_str = str()
321 self.signature['name'] = str()
322 self.signature['in'] = str()
323 self.signature['out'] = str()
325 def add_signature(self, name, direction, signature):
326 if (direction == 'in'):
327 self.signature['in'] += signature
328 if (self.signature['name'] != str()):
329 self.signature['name'] += ", "
330 self.signature['name'] += name
331 if (direction == 'out'):
332 self.signature['out'] = signature
334 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
336 if (self.methodToAdd != None):
337 name = self.methodToAdd
339 name = self.signalToAdd
342 if (async_success_cb != None):
343 async_cb_str = async_success_cb
344 if (async_err_cb != None):
345 if (async_cb_str != str()):
347 async_cb_str += async_err_cb
349 parameters = self.args_str
350 if (async_cb_str != str()):
351 if (parameters != str()):
353 parameters +=async_cb_str
355 if (parameters != str()):
356 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
358 self.class_code.append_stmt("def " + name + "(self):")
359 self.class_code.indent()
361 def end_method(self):
362 self.class_code.append_stmt("\n")
363 self.class_code.append_stmt("\n")
364 self.class_code.dedent()
366 def add_dbus_method(self):
367 decorator = '@dbus.service.method("' + self.ifName + '"'
368 if (self.signature.has_key('in') and self.signature['in'] != str()):
369 decorator += ", in_signature='" + self.signature['in'] + "'"
370 if (self.signature.has_key('out') and self.signature['out'] != str()):
371 decorator += ", out_signature='" + self.signature['out'] + "'"
372 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
374 self.class_code.append_stmt(decorator)
375 if (self.signature.has_key('name') and self.signature['name'] != str()):
376 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
378 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
380 def add_dbus_signal(self):
381 decorator = '@dbus.service.signal("' + self.ifName + '"'
382 if (self.signature.has_key('in') and self.signature['in'] != str()):
383 decorator += ", signature='" + self.signature['in'] + "'"
385 self.class_code.append_stmt(decorator)
386 if (self.signature.has_key('name') and self.signature['name'] != str()):
387 self.add_method(self.signature['name'])
391 def add_body_method(self):
392 if (self.methodToAdd != None):
393 self.class_code.append_stmt("print 'In " + self.methodToAdd + "()'")
394 if (self.args_str != str()):
395 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
397 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb)")
399 def add_body_signal(self):
400 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
401 self.class_code.append_stmt("\n")
403 def add_stmt(self, stmt) :
404 self.class_code.append_stmt(stmt)
407 self.class_code.execute()
410 return self.class_code.exec_string
412 # p : Since it is often useful to be able to look at the code
413 # that is generated interactively, this function provides
414 # a shorthand for "print str(some_exec_code_instance)", which
415 # gives a reasonable nice look at the contents of the
422 ###############################################################################
423 class CloudeebusService:
425 support for sending DBus messages and registering for DBus signals
427 def __init__(self, permissions):
428 self.permissions = permissions;
429 self.proxyObjects = {}
430 self.proxyMethods = {}
431 self.pendingCalls = []
432 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
433 self.services = {} # DBus service created
434 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
435 self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
436 self.localCtx = locals()
437 self.globalCtx = globals()
440 def proxyObject(self, busName, serviceName, objectName):
442 object hash id as busName#serviceName#objectName
444 id = "#".join([busName, serviceName, objectName])
445 if not self.proxyObjects.has_key(id):
447 # check permissions, array.index throws exception
448 self.permissions.index(serviceName)
449 bus = cache.dbusConnexion(busName)
450 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
451 return self.proxyObjects[id]
454 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
456 method hash id as busName#serviceName#objectName#interfaceName#methodName
458 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
459 if not self.proxyMethods.has_key(id):
460 obj = self.proxyObject(busName, serviceName, objectName)
461 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
462 return self.proxyMethods[id]
466 def dbusRegister(self, list):
468 arguments: bus, sender, object, interface, signal
471 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
474 # check permissions, array.index throws exception
475 self.permissions.index(list[1])
477 # check if a handler exists
478 sigId = "#".join(list)
479 if cache.signalHandlers.has_key(sigId):
482 # create a handler that will publish the signal
483 dbusSignalHandler = DbusSignalHandler(*list)
484 cache.signalHandlers[sigId] = dbusSignalHandler
486 return dbusSignalHandler.id
490 def dbusSend(self, list):
492 arguments: bus, destination, object, interface, message, [args]
494 # clear pending calls
495 for call in self.pendingCalls:
497 self.pendingCalls.remove(call)
500 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
502 # parse JSON arg list
505 args = json.loads(list[5])
507 # get dbus proxy method
508 method = self.proxyMethod(*list[0:5])
510 # use a deferred call handler to manage dbus results
511 dbusCallHandler = DbusCallHandler(method, args)
512 self.pendingCalls.append(dbusCallHandler)
513 return dbusCallHandler.callMethod()
517 def returnMethod(self, list):
519 arguments: methodId, success (=true, error otherwise), result (to return)
524 if (self.servicePendingCalls.has_key(methodId)):
525 cb = self.servicePendingCalls[methodId]
527 successCB = cb["successCB"]
533 errorCB = cb["errorCB"]
538 self.servicePendingCalls[methodId] = None
540 print "No methodID %s !!" % (methodId)
542 def jsonEncodeTupleKeyDict(self, data):
544 # creates new dictionary with the original tuple converted to json string
547 for index in range(dataLen):
548 for key in data[index]:
549 value = data[index][key]
551 print "value=" + str(value)
554 print "JSON key=" + nkey
555 if (isinstance(value, dbus.Array)):
556 # Searching dbus byte in array...
557 ValueLen = len(value)
559 for indexValue in range(ValueLen):
560 a = value[indexValue]
561 if (isinstance(a, dbus.Byte)):
562 a = int(value[indexValue])
565 nvalue = str(value[indexValue])
567 print "JSON value=" + str(nvalue)
572 def srvCB(self, name, async_succes_cb, async_error_cb, *args):
573 methodId = self.srvName + "#" + self.agentObjectPath + "#" + name
574 cb = { 'successCB': async_succes_cb,
575 'errorCB': async_error_cb}
576 self.servicePendingCalls[methodId] = cb
579 print "Received args=%s" % (args)
581 print "No args received"
584 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, json.dumps(args))
585 factory.dispatch(methodId, json.dumps(args))
587 except Exception, e :
588 print "Error=%s" % (str(e))
590 print "Trying to decode dbus.Dictionnary..."
592 params = self.jsonEncodeTupleKeyDict(args)
593 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, params)
594 factory.dispatch(methodId, params)
596 except Exception, e :
597 print "Error=%s" % (str(e))
599 print "Trying to pass args as string..."
601 print "factory.dispatch(methodId=%s, args=%s)" % (methodId, str(args))
602 factory.dispatch(methodId, str(args))
604 except Exception, e :
605 print "Error=%s" % (str(e))
608 def serviceAdd(self, list):
610 arguments: busName, srvName
613 self.bus = cache.dbusConnexion( busName['name'] )
614 self.srvName = list[1]
615 if (self.services.has_key(self.srvName) == False):
616 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
620 def serviceRelease(self, list):
622 arguments: busName, srvName
624 self.srvName = list[0]
625 if (self.services.has_key(self.srvName) == True):
626 self.services.pop(self.srvName)
629 raise Exception(self.srvName + " do not exist")
632 def serviceAddAgent(self, list):
634 arguments: objectPath, xmlTemplate
636 self.agentObjectPath = list[0]
637 xmlTemplate = list[1]
638 self.className = re.sub('/', '_', self.agentObjectPath[1:])
639 if (self.dynDBusClasses.has_key(self.className) == False):
640 self.dynDBusClasses[self.className] = dynDBusClass(self.className, self.globalCtx, self.localCtx)
641 self.dynDBusClasses[self.className].createDBusServiceFromXML(xmlTemplate)
642 self.dynDBusClasses[self.className].declare()
646 if (1): ## Force deletion
647 if os.access('./MyDbusClass.py', os.R_OK) == True:
648 os.remove('./MyDbusClass.py')
650 if os.access('./MyDbusClass.py', os.R_OK) == False:
651 f = open('./MyDbusClass.py', 'w')
652 f.write(self.dynDBusClasses[self.className].class_code.exec_string)
655 ## Class already exist, instanciate it if not already instanciated
656 if (self.serviceAgents.has_key(self.className) == False):
657 # self.dynDBusClasses[self.className].p()
658 # self.dynDBusClasses[self.className].declare()
659 self.serviceAgents[self.className] = eval(self.className + "(self.bus, callback=self.srvCB, objName=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
661 self.serviceAgents[self.className].add_to_connection()
662 # exe_str = "self.serviceAgents['" + self.className +"'].add_to_connection()"
663 # exec (exe_str, self.globalCtx, self.localCtx)
664 return (self.agentObjectPath)
667 def serviceDelAgent(self, list):
669 arguments: objectPath, xmlTemplate
671 agentObjectPath = list[0]
672 className = re.sub('/', '_', agentObjectPath[1:])
674 if (self.serviceAgents.has_key(className)):
675 self.serviceAgents[self.className].remove_from_connection()
676 self.serviceAgents.pop(self.className)
678 raise Exception(agentObjectPath + " doesn't exist!")
680 return (agentObjectPath)
683 def getVersion(self):
685 return current version string
691 ###############################################################################
692 class CloudeebusServerProtocol(WampCraServerProtocol):
694 connexion and session authentication management
697 def onSessionOpen(self):
698 # CRA authentication options
699 self.clientAuthTimeout = 0
700 self.clientAuthAllowAnonymous = OPENDOOR
701 # CRA authentication init
702 WampCraServerProtocol.onSessionOpen(self)
705 def getAuthPermissions(self, key, extra):
706 return json.loads(extra.get("permissions", "[]"))
709 def getAuthSecret(self, key):
710 secret = CREDENTIALS.get(key, None)
713 # secret must be of str type to be hashed
714 return secret.encode('utf-8')
717 def onAuthenticated(self, key, permissions):
719 # check authentication key
721 raise Exception("Authentication failed")
722 # check permissions, array.index throws exception
723 for req in permissions:
725 # create cloudeebus service instance
726 self.cloudeebusService = CloudeebusService(permissions)
727 # register it for RPC
728 self.registerForRpc(self.cloudeebusService)
729 # register for Publish / Subscribe
730 self.registerForPubSub("", True)
733 def connectionLost(self, reason):
734 WampCraServerProtocol.connectionLost(self, reason)
735 if factory.getConnectionCount() == 0:
740 ###############################################################################
742 if __name__ == '__main__':
746 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
747 parser.add_argument('-v', '--version', action='store_true',
748 help='print version and exit')
749 parser.add_argument('-d', '--debug', action='store_true',
750 help='log debug info on standard output')
751 parser.add_argument('-o', '--opendoor', action='store_true',
752 help='allow anonymous access to all services')
753 parser.add_argument('-p', '--port', default='9000',
755 parser.add_argument('-c', '--credentials',
756 help='path to credentials file')
757 parser.add_argument('-w', '--whitelist',
758 help='path to whitelist file')
760 args = parser.parse_args(sys.argv[1:])
763 print("Cloudeebus version " + VERSION)
767 log.startLogging(sys.stdout)
769 OPENDOOR = args.opendoor
772 jfile = open(args.credentials)
773 CREDENTIALS = json.load(jfile)
777 jfile = open(args.whitelist)
778 WHITELIST = json.load(jfile)
781 uri = "ws://localhost:" + args.port
783 factory = WampServerFactory(uri, debugWamp = args.debug)
784 factory.protocol = CloudeebusServerProtocol
785 factory.setProtocolOptions(allowHixie76 = True)
789 DBusGMainLoop(set_as_default=True)