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(error.get_dbus_message())
157 ################################################################################
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
167 # __str__ : Return a string representation of the object, for
170 return self.exec_string
175 def append_stmt(self, stmt) :
176 self.exec_code_valid = 0
179 for x in range(0,self.indent_level):
180 self.exec_string = self.exec_string + ' '
181 self.exec_string = self.exec_string + stmt + "\t\t# l:" + str(self.line) + '\n'
184 self.exec_string = self.exec_string + "# l:" + str(self.line) + '\n'
186 self.exec_string = self.exec_string + stmt + "\t\t# l:" + str(self.line) + '\n'
189 self.indent_level = self.indent_level + self.indent_increment
192 self.indent_level = self.indent_level - self.indent_increment
194 # compile : Compile exec_string into exec_code using the builtin
195 # compile function. Skip if already in sync.
197 if not self.exec_code_valid :
198 self.exec_code = compile(self.exec_string, "<string>", "exec")
199 self.exec_code_valid = 1
202 if not self.exec_code_valid :
208 ################################################################################
209 class XmlCb_Parser: # The target object of the parser
212 def __init__(self, dynDBusClass):
213 self.dynDBusClass = dynDBusClass
215 def start(self, tag, attrib): # Called for each opening tag.
219 if (tag == 'interface'):
220 self.dynDBusClass.set_interface(attrib['name'])
223 if (tag == 'method'):
225 self.dynDBusClass.def_method(attrib['name'])
227 if (tag == 'signal'):
229 self.dynDBusClass.def_signal(attrib['name'])
232 # Set signature (in/out & name) for method
234 if (self.current == 'method'):
235 self.dynDBusClass.add_signature(attrib['name'],
239 if (self.current == 'signal'):
240 self.dynDBusClass.add_signature(attrib['name'], 'in',
243 def end(self, tag): # Called for each closing tag.
244 if (tag == 'method'):
245 self.dynDBusClass.add_dbus_method()
246 self.dynDBusClass.add_body_method()
247 self.dynDBusClass.end_method()
248 if (tag == 'signal'):
249 self.dynDBusClass.add_dbus_signal()
250 self.dynDBusClass.add_body_signal()
251 self.dynDBusClass.end_method()
253 def data(self, data):
254 pass # We do not need to do anything with data.
255 def close(self): # Called when all data has been parsed.
260 ################################################################################
261 class dynDBusClass():
262 def __init__(self, className, globalCtx, localCtx):
263 self.className = className
264 self.xmlCB = XmlCb_Parser(self)
265 self.localCtx = localCtx
266 self.globalCtx = globalCtx
268 self.class_code = exec_code()
269 self.class_code.indent_increment = 4
270 self.class_code.append_stmt("import dbus")
271 self.class_code.append_stmt("\n")
272 self.class_code.append_stmt("\n")
273 self.class_code.append_stmt("class " + self.className + "(dbus.service.Object):")
274 self.class_code.indent()
276 ## Overload of __init__ method
277 self.def_method("__init__")
278 self.add_method("bus, callback=None, objName='/sample', busName='org.cloudeebus'")
279 self.add_stmt("self.bus = bus")
280 self.add_stmt("self.objName = objName")
281 self.add_stmt("self.callback = callback")
282 self.add_stmt("dbus.service.Object.__init__(self, conn=bus, object_path=objName, bus_name=busName)")
285 def createDBusServiceFromXML(self, xml):
286 self.parser = XMLParser(target=self.xmlCB)
287 self.parser.feed(xml)
290 def set_interface(self, ifName):
293 def def_method(self, methodName):
294 self.methodToAdd = methodName
295 self.signalToAdd = None
296 self.args_str = str()
298 self.signature['name'] = str()
299 self.signature['in'] = str()
300 self.signature['out'] = str()
302 def def_signal(self, signalName):
303 self.methodToAdd = None
304 self.signalToAdd = signalName
305 self.args_str = str()
307 self.signature['name'] = str()
308 self.signature['in'] = str()
309 self.signature['out'] = str()
311 def add_signature(self, name, direction, signature):
312 if (direction == 'in'):
313 self.signature['in'] += signature
314 if (self.signature['name'] != str()):
315 self.signature['name'] += ", "
316 self.signature['name'] += name
317 if (direction == 'out'):
318 self.signature['out'] = signature
320 def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
322 if (self.methodToAdd != None):
323 name = self.methodToAdd
325 name = self.signalToAdd
328 if (async_success_cb != None):
329 async_cb_str = async_success_cb
330 if (async_err_cb != None):
331 if (async_cb_str != str()):
333 async_cb_str += async_err_cb
335 parameters = self.args_str
336 if (async_cb_str != str()):
337 if (parameters != str()):
339 parameters +=async_cb_str
341 if (parameters != str()):
342 self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)
344 self.class_code.append_stmt("def " + name + "(self):")
345 self.class_code.indent()
347 def end_method(self):
348 self.class_code.append_stmt("\n")
349 self.class_code.append_stmt("\n")
350 self.class_code.dedent()
352 def add_dbus_method(self):
353 decorator = '@dbus.service.method("' + self.ifName + '"'
354 if (self.signature.has_key('in') and self.signature['in'] != str()):
355 decorator += ", in_signature='" + self.signature['in'] + "'"
356 if (self.signature.has_key('out') and self.signature['out'] != str()):
357 decorator += ", out_signature='" + self.signature['out'] + "'"
358 decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"
360 self.class_code.append_stmt(decorator)
361 if (self.signature.has_key('name') and self.signature['name'] != str()):
362 self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
364 self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
366 def add_dbus_signal(self):
367 decorator = '@dbus.service.signal("' + self.ifName + '"'
368 if (self.signature.has_key('in') and self.signature['in'] != str()):
369 decorator += ", signature='" + self.signature['in'] + "'"
371 self.class_code.append_stmt(decorator)
372 if (self.signature.has_key('name') and self.signature['name'] != str()):
373 self.add_method(self.signature['name'])
377 def add_body_method(self):
378 if (self.methodToAdd != None):
379 self.class_code.append_stmt("print 'In " + self.methodToAdd + "()'")
380 if (self.args_str != str()):
381 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
383 self.class_code.append_stmt("self.callback('" + self.methodToAdd + "', dbus_async_cb, dbus_async_err_cb)")
385 def add_body_signal(self):
386 self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
387 self.class_code.append_stmt("\n")
389 def add_stmt(self, stmt) :
390 self.class_code.append_stmt(stmt)
393 self.class_code.compile()
394 exec(self.class_code.exec_string, self.globalCtx, self.localCtx)
397 return self.class_code.exec_string
399 # p : Since it is often useful to be able to look at the code
400 # that is generated interactively, this function provides
401 # a shorthand for "print str(some_exec_code_instance)", which
402 # gives a reasonable nice look at the contents of the
409 ###############################################################################
410 class CloudeebusService:
412 support for sending DBus messages and registering for DBus signals
414 def __init__(self, permissions):
415 self.permissions = permissions;
416 self.proxyObjects = {}
417 self.proxyMethods = {}
418 self.pendingCalls = []
419 self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
420 self.services = {} # DBus service created
421 self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
424 def proxyObject(self, busName, serviceName, objectName):
426 object hash id as busName#serviceName#objectName
428 id = "#".join([busName, serviceName, objectName])
429 if not self.proxyObjects.has_key(id):
431 # check permissions, array.index throws exception
432 self.permissions.index(serviceName)
433 bus = cache.dbusConnexion(busName)
434 self.proxyObjects[id] = bus.get_object(serviceName, objectName)
435 return self.proxyObjects[id]
438 def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
440 method hash id as busName#serviceName#objectName#interfaceName#methodName
442 id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
443 if not self.proxyMethods.has_key(id):
444 obj = self.proxyObject(busName, serviceName, objectName)
445 self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
446 return self.proxyMethods[id]
450 def dbusRegister(self, list):
452 arguments: bus, sender, object, interface, signal
455 raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
458 # check permissions, array.index throws exception
459 self.permissions.index(list[1])
461 # check if a handler exists
462 sigId = "#".join(list)
463 if cache.signalHandlers.has_key(sigId):
466 # create a handler that will publish the signal
467 dbusSignalHandler = DbusSignalHandler(*list)
468 cache.signalHandlers[sigId] = dbusSignalHandler
470 return dbusSignalHandler.id
474 def dbusSend(self, list):
476 arguments: bus, destination, object, interface, message, [args]
478 # clear pending calls
479 for call in self.pendingCalls:
481 self.pendingCalls.remove(call)
484 raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
486 # parse JSON arg list
489 args = json.loads(list[5])
491 # get dbus proxy method
492 method = self.proxyMethod(*list[0:5])
494 # use a deferred call handler to manage dbus results
495 dbusCallHandler = DbusCallHandler(method, args)
496 self.pendingCalls.append(dbusCallHandler)
497 return dbusCallHandler.callMethod()
500 def srvCB(self, name, async_succes_cb, async_error_cb, *args):
501 print "self.srvCB(name='%s', args=%s')\n\n" % (name, str(args))
502 methodId = self.srvName + "#" + self.agentObjectPath + "#" + name
503 print "factory.dispatch(methodId='%s', json.dumps(args)=%s')\n\n" % (methodId, json.dumps(args))
504 factory.dispatch(methodId, json.dumps(args))
507 def serviceAdd(self, list):
509 arguments: busName, srvName
512 self.bus = cache.dbusConnexion( busName['name'] )
513 self.srvName = list[1]
514 if (self.services.has_key(self.srvName) == False):
515 self.services[self.srvName] = dbus.service.BusName(name = self.srvName, bus = self.bus)
519 def serviceAddAgent(self, list):
521 arguments: objectPath, xmlTemplate
523 self.agentObjectPath = list[0]
524 xmlTemplate = list[1]
525 className = re.sub('/', '_', self.agentObjectPath[1:])
526 if (self.dynDBusClasses.has_key(className) == False):
527 self.dynDBusClasses[className] = dynDBusClass(className, globals(), locals())
528 self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
532 if (1): ## Force deletion
533 if os.access('./MyDbusClass.py', os.R_OK) == True:
534 os.remove('./MyDbusClass.py')
536 if os.access('./MyDbusClass.py', os.R_OK) == False:
537 f = open('./MyDbusClass.py', 'w')
538 f.write(self.dynDBusClasses[className].class_code.exec_string)
540 # self.dynDBusClass[className].p()
541 self.dynDBusClasses[className].declare()
543 if (self.serviceAgents.has_key(className) == False):
544 exe_str = "self.serviceAgents[" + className +"] = " + className + "(self.bus, callback=self.srvCB, objName=self.agentObjectPath, busName=self.srvName)"
545 exec (exe_str, globals(), locals())
548 def getVersion(self):
550 return current version string
556 ###############################################################################
557 class CloudeebusServerProtocol(WampCraServerProtocol):
559 connexion and session authentication management
562 def onSessionOpen(self):
563 # CRA authentication options
564 self.clientAuthTimeout = 0
565 self.clientAuthAllowAnonymous = OPENDOOR
566 # CRA authentication init
567 WampCraServerProtocol.onSessionOpen(self)
570 def getAuthPermissions(self, key, extra):
571 return json.loads(extra.get("permissions", "[]"))
574 def getAuthSecret(self, key):
575 secret = CREDENTIALS.get(key, None)
578 # secret must be of str type to be hashed
579 return secret.encode('utf-8')
582 def onAuthenticated(self, key, permissions):
584 # check authentication key
586 raise Exception("Authentication failed")
587 # check permissions, array.index throws exception
588 for req in permissions:
590 # create cloudeebus service instance
591 self.cloudeebusService = CloudeebusService(permissions)
592 # register it for RPC
593 self.registerForRpc(self.cloudeebusService)
594 # register for Publish / Subscribe
595 self.registerForPubSub("", True)
598 def connectionLost(self, reason):
599 WampCraServerProtocol.connectionLost(self, reason)
600 if factory.getConnectionCount() == 0:
605 ###############################################################################
607 if __name__ == '__main__':
611 parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
612 parser.add_argument('-v', '--version', action='store_true',
613 help='print version and exit')
614 parser.add_argument('-d', '--debug', action='store_true',
615 help='log debug info on standard output')
616 parser.add_argument('-o', '--opendoor', action='store_true',
617 help='allow anonymous access to all services')
618 parser.add_argument('-p', '--port', default='9000',
620 parser.add_argument('-c', '--credentials',
621 help='path to credentials file')
622 parser.add_argument('-w', '--whitelist',
623 help='path to whitelist file')
625 args = parser.parse_args(sys.argv[1:])
628 print("Cloudeebus version " + VERSION)
632 log.startLogging(sys.stdout)
634 OPENDOOR = args.opendoor
637 jfile = open(args.credentials)
638 CREDENTIALS = json.load(jfile)
642 jfile = open(args.whitelist)
643 WHITELIST = json.load(jfile)
646 uri = "ws://localhost:" + args.port
648 factory = WampServerFactory(uri, debugWamp = args.debug)
649 factory.protocol = CloudeebusServerProtocol
650 factory.setProtocolOptions(allowHixie76 = True)
654 DBusGMainLoop(set_as_default=True)