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
52 ###############################################################################
59 ###############################################################################
62 Global cache of DBus connexions and signal handlers
65 self.dbusConnexions = {}
66 self.signalHandlers = {}
71 Disconnect signal handlers before resetting cache.
73 self.dbusConnexions = {}
74 # disconnect signal handlers
75 for key in self.signalHandlers:
76 self.signalHandlers[key].disconnect()
77 self.signalHandlers = {}
80 def dbusConnexion(self, busName):
81 if not self.dbusConnexions.has_key(busName):
82 if busName == "session":
83 self.dbusConnexions[busName] = dbus.SessionBus()
84 elif busName == "system":
85 self.dbusConnexions[busName] = dbus.SystemBus()
87 raise Exception("Error: invalid bus: %s" % busName)
88 return self.dbusConnexions[busName]
92 ###############################################################################
93 class DbusSignalHandler:
95 signal hash id as busName#senderName#objectName#interfaceName#signalName
97 def __init__(self, busName, senderName, objectName, interfaceName, signalName):
98 self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
99 # connect handler to signal
100 self.bus = cache.dbusConnexion(busName)
101 self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
104 def disconnect(self):
105 names = self.id.split("#")
106 self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
109 def handleSignal(self, *args):
111 publish dbus args under topic hash id
113 factory.dispatch(self.id, json.dumps(args))
117 ###############################################################################
118 class DbusCallHandler:
120 deferred reply to return dbus results
122 def __init__(self, method, args):
124 self.request = defer.Deferred()
129 def callMethod(self):
131 dbus method async call
134 self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
138 def dbusSuccess(self, *result):
140 return JSON string result array
142 self.request.callback(json.dumps(result))
146 def dbusError(self, error):
148 return dbus error message
150 self.request.errback(Exception(error.get_dbus_message()))
155 ################################################################################
157 def __init__(self, globalCtx, localCtx) :
158 self.exec_string = ""
159 self.exec_code = None
160 self.exec_code_valid = 1
161 self.indent_level = 0
162 self.indent_increment = 1
164 self.localCtx = localCtx
165 self.globalCtx = globalCtx
168 # __str__ : Return a string representation of the object, for
171 return self.exec_string
176 def append_stmt(self, stmt) :
177 self.exec_code_valid = 0
180 for x in range(0,self.indent_level):
181 self.exec_string = self.exec_string + ' '
182 self.exec_string = self.exec_string + stmt + "\t\t# l:" + str(self.line) + '\n'
185 self.exec_string = self.exec_string + "# l:" + str(self.line) + '\n'
187 self.exec_string = self.exec_string + stmt + "\t\t# l:" + str(self.line) + '\n'
190 self.indent_level = self.indent_level + self.indent_increment
193 self.indent_level = self.indent_level - self.indent_increment
195 # compile : Compile exec_string into exec_code using the builtin
196 # compile function. Skip if already in sync.
198 if not self.exec_code_valid :
199 self.exec_code = compile(self.exec_string, "<string>", "exec")
200 self.exec_code_valid = True
203 if not self.exec_code_valid :
205 exec(self.exec_code, self.globalCtx, self.localCtx)
209 ################################################################################
210 class XmlCb_Parser: # The target object of the parser
213 def __init__(self, dynDBusClass):
214 self.dynDBusClass = dynDBusClass
216 def start(self, tag, attrib): # Called for each opening tag.
220 if (tag == 'interface'):
221 self.dynDBusClass.set_interface(attrib['name'])
224 if (tag == 'method'):
226 self.dynDBusClass.def_method(attrib['name'])
228 if (tag == 'signal'):
230 self.dynDBusClass.def_signal(attrib['name'])
233 # Set signature (in/out & name) for method
235 if (self.current == 'method'):
236 if (attrib.has_key('direction') == False):
237 attrib['direction'] = "in"
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" % (str(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()
644 ## Class already exist, instanciate it if not already instanciated
645 if (self.serviceAgents.has_key(self.className) == False):
646 self.serviceAgents[self.className] = eval(self.className + "(self.bus, callback=self.srvCB, objName=self.agentObjectPath, busName=self.srvName)", self.globalCtx, self.localCtx)
648 self.serviceAgents[self.className].add_to_connection()
649 return (self.agentObjectPath)
652 def serviceDelAgent(self, list):
654 arguments: objectPath, xmlTemplate
656 agentObjectPath = list[0]
657 className = re.sub('/', '_', agentObjectPath[1:])
659 if (self.serviceAgents.has_key(className)):
660 self.serviceAgents[self.className].remove_from_connection()
661 self.serviceAgents.pop(self.className)
663 raise Exception(agentObjectPath + " doesn't exist!")
665 return (agentObjectPath)
668 def getVersion(self):
670 return current version string
676 ###############################################################################
677 class CloudeebusServerProtocol(WampCraServerProtocol):
679 connexion and session authentication management
682 def onSessionOpen(self):
683 # CRA authentication options
684 self.clientAuthTimeout = 0
685 self.clientAuthAllowAnonymous = OPENDOOR
686 # CRA authentication init
687 WampCraServerProtocol.onSessionOpen(self)
690 def getAuthPermissions(self, key, extra):
691 return json.loads(extra.get("permissions", "[]"))
694 def getAuthSecret(self, key):
695 secret = CREDENTIALS.get(key, None)
698 # secret must be of str type to be hashed
699 return secret.encode('utf-8')
702 def onAuthenticated(self, key, permissions):
704 # check authentication key
706 raise Exception("Authentication failed")
707 # check permissions, array.index throws exception
708 for req in permissions:
710 # create cloudeebus service instance
711 self.cloudeebusService = CloudeebusService(permissions)
712 # register it for RPC
713 self.registerForRpc(self.cloudeebusService)
714 # register for Publish / Subscribe
715 self.registerForPubSub("", True)
718 def connectionLost(self, reason):
719 WampCraServerProtocol.connectionLost(self, reason)
720 if factory.getConnectionCount() == 0:
725 ###############################################################################
727 if __name__ == '__main__':
731 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
732 parser.add_argument('-v', '--version', action='store_true',
733 help='print version and exit')
734 parser.add_argument('-d', '--debug', action='store_true',
735 help='log debug info on standard output')
736 parser.add_argument('-o', '--opendoor', action='store_true',
737 help='allow anonymous access to all services')
738 parser.add_argument('-p', '--port', default='9000',
740 parser.add_argument('-c', '--credentials',
741 help='path to credentials file')
742 parser.add_argument('-w', '--whitelist',
743 help='path to whitelist file')
745 args = parser.parse_args(sys.argv[1:])
748 print("Cloudeebus version " + VERSION)
752 log.startLogging(sys.stdout)
754 OPENDOOR = args.opendoor
757 jfile = open(args.credentials)
758 CREDENTIALS = json.load(jfile)
762 jfile = open(args.whitelist)
763 WHITELIST = json.load(jfile)
766 uri = "ws://localhost:" + args.port
768 factory = WampServerFactory(uri, debugWamp = args.debug)
769 factory.protocol = CloudeebusServerProtocol
770 factory.setProtocolOptions(allowHixie76 = True)
774 DBusGMainLoop(set_as_default=True)