Cloudeebus via Crosswalk extension
authorPatrick Ohly <patrick.ohly@intel.com>
Thu, 12 Jun 2014 14:09:18 +0000 (16:09 +0200)
committerPatrick Ohly <patrick.ohly@intel.com>
Mon, 30 Jun 2014 09:55:48 +0000 (11:55 +0200)
The basic idea is this:
- run the Python side inside the Crosswalk extension process
- load the JavaScript side into Crosswalk from the extension
- replace the WebSocket communication with Crosswalk internal
  message passing

Because each app gets its own instance of Cloudeebus and thus its own
private connection to D-Bus, this solution does not suffer from the
security issues of the WebSocket-based Cloudeebus (circumventing D-Bus
access controls, services not seeing the real app).

On Tizen, the services see a normal app process talking to them via
D-Bus.

The Python side gets split it into the main executable using WebSocket
and the core engine which only uses objects providing certain
WebSocket APIs. The Crosswalk extension uses the engine with dummy
objects that map the WebSocket APIs to Crosswalk extension message
passing.

cloudeebus/.gitignore [new file with mode: 0644]
cloudeebus/cloudeebus.py
cloudeebus/engine.py [new file with mode: 0644]
cloudeebus/examples/crosswalk/example.html [new file with mode: 0644]
cloudeebus/xwalkcloudeebus.py [new file with mode: 0644]

diff --git a/cloudeebus/.gitignore b/cloudeebus/.gitignore
new file mode 100644 (file)
index 0000000..c616c62
--- /dev/null
@@ -0,0 +1 @@
+libxwalkcloudeebus.so
index c7109b9..b360387 100755 (executable)
@@ -28,16 +28,14 @@ from twisted.internet import glib2reactor
 # Configure the twisted mainloop to be run inside the glib mainloop.
 # This must be done before importing the other twisted modules
 glib2reactor.install()
-from twisted.internet import reactor, defer
+from twisted.internet import reactor
 
 from autobahn.websocket import listenWS
-from autobahn.wamp import exportRpc, WampServerFactory, WampCraServerProtocol
+from autobahn.wamp import WampServerFactory, WampCraServerProtocol
 
 from dbus.mainloop.glib import DBusGMainLoop
 
 import gobject
-import re
-import dbus.service
 gobject.threads_init()
 
 from dbus import glib
@@ -46,16 +44,14 @@ glib.init_threads()
 # enable debug log
 from twisted.python import log
 
-# XML parser module
-from xml.etree.ElementTree import XMLParser
-
 ###############################################################################
 
-VERSION = "0.6.0"
+from engine import VERSION, SERVICELIST, CloudeebusService, cache
+import engine
+
 OPENDOOR = False
 CREDENTIALS = {}
 WHITELIST = []
-SERVICELIST = []
 NETMASK =  []
 
 ###############################################################################
@@ -85,588 +81,6 @@ def ipV4ToHex(mask):
     return maskHex
 
 ###############################################################################
-class DbusCache:
-    '''
-    Global cache of DBus connexions and signal handlers
-    '''
-    def __init__(self):
-        self.dbusConnexions = {}
-        self.signalHandlers = {}
-
-
-    def reset(self):
-        '''
-        Disconnect signal handlers before resetting cache.
-        '''
-        self.dbusConnexions = {}
-        # disconnect signal handlers
-        for key in self.signalHandlers:
-            self.signalHandlers[key].disconnect()
-        self.signalHandlers = {}
-
-
-    def dbusConnexion(self, busName):
-        if not self.dbusConnexions.has_key(busName):
-            if busName == "session":
-                self.dbusConnexions[busName] = dbus.SessionBus()
-            elif busName == "system":
-                self.dbusConnexions[busName] = dbus.SystemBus()
-            else:
-                raise Exception("Error: invalid bus: %s" % busName)
-        return self.dbusConnexions[busName]
-
-
-
-###############################################################################
-class DbusSignalHandler:
-    '''
-    signal hash id as busName#senderName#objectName#interfaceName#signalName
-    '''
-    def __init__(self, busName, senderName, objectName, interfaceName, signalName):
-        self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
-        # connect handler to signal
-        self.bus = cache.dbusConnexion(busName)
-        self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
-        
-    
-    def disconnect(self):
-        names = self.id.split("#")
-        self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
-
-
-    def handleSignal(self, *args):
-        '''
-        publish dbus args under topic hash id
-        '''
-        factory.dispatch(self.id, json.dumps(args))
-
-
-
-###############################################################################
-class DbusCallHandler:
-    '''
-    deferred reply to return dbus results
-    '''
-    def __init__(self, method, args):
-        self.pending = False
-        self.request = defer.Deferred()
-        self.method = method
-        self.args = args
-
-
-    def callMethod(self):
-        '''
-        dbus method async call
-        '''
-        self.pending = True
-        self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
-        return self.request
-
-
-    def dbusSuccess(self, *result):
-        '''
-        return JSON string result array
-        '''
-        self.request.callback(json.dumps(result))
-        self.pending = False
-
-
-    def dbusError(self, error):
-        '''
-        return dbus error message
-        '''
-        self.request.errback(Exception(error.get_dbus_message()))
-        self.pending = False
-
-
-
-################################################################################       
-class ExecCode:
-    '''
-    Execute DynDBusClass generated code
-    '''
-    def __init__(self, globalCtx, localCtx) :
-        self.exec_string = ""
-        self.exec_code = None
-        self.exec_code_valid = 1
-        self.indent_level = 0
-        self.indent_increment = 1
-        self.line = 0
-        self.localCtx = localCtx
-        self.globalCtx = globalCtx
-        
-
-    def append_stmt(self, stmt) :
-        self.exec_code_valid = 0
-        self.line += 1
-        for x in range(0,self.indent_level):
-            self.exec_string = self.exec_string + ' '            
-        self.exec_string = self.exec_string + stmt + '\n'
-
-    def indent(self) :
-        self.indent_level = self.indent_level + self.indent_increment
-
-    def dedent(self) :
-        self.indent_level = self.indent_level - self.indent_increment
-    
-    # compile : Compile exec_string into exec_code using the builtin
-    # compile function. Skip if already in sync.
-    def compile(self) :
-        if not self.exec_code_valid :
-            self.exec_code = compile(self.exec_string, "<string>", "exec")
-        self.exec_code_valid = True
-
-    def execute(self) :
-        if not self.exec_code_valid :
-            self.compile()
-        exec(self.exec_code, self.globalCtx, self.localCtx)
-
-
-
-################################################################################       
-class XmlCbParser: # The target object of the parser
-    maxDepth = 0
-    depth = 0
-    def __init__(self, dynDBusClass):
-        self.dynDBusClass = dynDBusClass
-        
-    def start(self, tag, attrib):   # Called for each opening tag.
-        if (tag == 'node'):
-            return
-        # Set interface name
-        if (tag == 'interface'):
-            self.dynDBusClass.set_interface(attrib['name'])
-            return
-        # Set method name
-        if (tag == 'method'):
-            self.current = tag
-            self.dynDBusClass.def_method(attrib['name'])
-            return
-        if (tag == 'signal'):
-            self.current = tag
-            self.dynDBusClass.def_signal(attrib['name'])
-            return
-
-        # Set signature (in/out & name) for method
-        if (tag == 'arg'):
-            if (self.current == 'method'):
-                if (attrib.has_key('direction') == False):
-                    attrib['direction'] = "in"
-                self.dynDBusClass.add_signature(attrib['name'],
-                                                attrib['direction'],
-                                                attrib['type'])
-                return
-            if (self.current == 'signal'):
-                if (attrib.has_key('name') == False):
-                    attrib['name'] = 'value'
-                self.dynDBusClass.add_signature(attrib['name'], 'in',
-                                                attrib['type'])
-                return
-    def end(self, tag):             # Called for each closing tag.
-        if (tag == 'method'):
-            self.dynDBusClass.add_dbus_method()
-            self.dynDBusClass.add_body_method()
-            self.dynDBusClass.end_method()
-        if (tag == 'signal'):
-            self.dynDBusClass.add_dbus_signal()
-            self.dynDBusClass.add_body_signal()
-            self.dynDBusClass.end_method()
-           
-    def data(self, data):
-        pass            # We do not need to do anything with data.
-    def close(self):    # Called when all data has been parsed.
-        return self.maxDepth
-
-
-       
-###############################################################################
-def createClassName(objectPath):
-    return re.sub('/', '_', objectPath[1:])
-
-################################################################################       
-class DynDBusClass():
-    def __init__(self, className, globalCtx, localCtx):
-        self.xmlCB = XmlCbParser(self)
-        self.signature = {}
-        self.class_code = ExecCode(globalCtx, localCtx)  
-        self.class_code.indent_increment = 4
-        self.class_code.append_stmt("import dbus")
-        self.class_code.append_stmt("\n")
-        self.class_code.append_stmt("class " + className + "(dbus.service.Object):")
-        self.class_code.indent()
-        
-        ## Overload of __init__ method 
-        self.def_method("__init__")
-        self.add_method("bus, callback=None, objPath='/sample', srvName='org.cloudeebus'")
-        self.add_stmt("self.bus = bus")
-        self.add_stmt("self.objPath = objPath")
-        self.add_stmt("self.srvName = srvName")        
-        self.add_stmt("self.callback = callback")        
-        self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=srvName)")
-        self.end_method()
-               
-        ## Create 'add_to_connection' method 
-        self.def_method("add_to_connection")
-        self.add_method("connection=None, path=None")
-        self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objPath)")
-        self.end_method()
-               
-        ## Create 'remove_from_connection' method 
-        self.def_method("remove_from_connection")
-        self.add_method("connection=None, path=None")
-        self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objPath)")
-        self.end_method()
-               
-    def createDBusServiceFromXML(self, xml):
-        self.parser = XMLParser(target=self.xmlCB)
-        self.parser.feed(xml)
-        self.parser.close()
-    
-    def set_interface(self, ifName):
-        self.ifName = ifName
-        
-    def def_method(self, methodName):
-        self.methodToAdd = methodName
-        self.signalToAdd = None
-        self.args_str = str()
-        self.signature = {}
-        self.signature['name'] = str()
-        self.signature['in'] = str()                
-        self.signature['out'] = str()                        
-
-    def def_signal(self, signalName):
-        self.methodToAdd = None
-        self.signalToAdd = signalName
-        self.args_str = str()
-        self.signature = {}
-        self.signature['name'] = str()
-        self.signature['in'] = str()                
-        self.signature['out'] = str()                        
-
-    def add_signature(self, name, direction, signature):
-        if (direction == 'in'):
-            self.signature['in'] += signature
-            if (self.signature['name'] != str()):
-                self.signature['name'] += ", "
-            self.signature['name'] += name
-        if (direction == 'out'):
-            self.signature['out'] = signature                        
-        
-    def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
-        async_cb_str = str()
-        if (self.methodToAdd != None):
-            name = self.methodToAdd
-        else:
-            name = self.signalToAdd
-        if (args != None):
-            self.args_str = args
-        if (async_success_cb != None):
-            async_cb_str = async_success_cb
-        if (async_err_cb != None):
-            if (async_cb_str != str()):
-                async_cb_str += ", "
-            async_cb_str += async_err_cb
-                        
-        parameters = self.args_str
-        if (async_cb_str != str()):
-            if (parameters != str()):
-                parameters += ", "
-            parameters +=async_cb_str       
-        
-        if (parameters != str()):
-            self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)               
-        else:
-            self.class_code.append_stmt("def " + name + "(self):")
-        self.class_code.indent()
-        
-    def end_method(self):
-        self.class_code.append_stmt("\n")
-        self.class_code.dedent()
-        
-    def add_dbus_method(self):
-        decorator = '@dbus.service.method("' + self.ifName + '"'
-        if (self.signature.has_key('in') and self.signature['in'] != str()):
-                decorator += ", in_signature='" + self.signature['in'] + "'"
-        if (self.signature.has_key('out') and self.signature['out'] != str()):
-                decorator += ", out_signature='" + self.signature['out'] + "'"
-        decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"            
-        decorator += ")"
-        self.class_code.append_stmt(decorator)
-        if (self.signature.has_key('name') and self.signature['name'] != str()):
-            self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
-        else:
-            self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
-
-    def add_dbus_signal(self):
-        decorator = '@dbus.service.signal("' + self.ifName + '"'
-        if (self.signature.has_key('in') and self.signature['in'] != str()):
-                decorator += ", signature='" + self.signature['in'] + "'"
-        decorator += ")"            
-        self.class_code.append_stmt(decorator)
-        if (self.signature.has_key('name') and self.signature['name'] != str()):
-            self.add_method(self.signature['name'])
-        else:
-            self.add_method()
-
-    def add_body_method(self):
-        if (self.methodToAdd != None):
-            if (self.args_str != str()):
-                self.class_code.append_stmt("self.callback(self.srvName,'" + self.methodToAdd + "', self.objPath, '"  + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
-            else:        
-                self.class_code.append_stmt("self.callback(self.srvName,'" + self.methodToAdd + "', self.objPath, '"  + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb)")
-
-    def add_body_signal(self):
-        self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
-        self.class_code.append_stmt("\n")
-
-    def add_stmt(self, stmt) :
-        self.class_code.append_stmt(stmt)
-        
-    def declare(self) :
-        self.class_code.execute()
-
-
-
-###############################################################################
-class CloudeebusService:
-    '''
-    support for sending DBus messages and registering for DBus signals
-    '''
-    def __init__(self, permissions):
-        self.permissions = {};
-        self.permissions['permissions'] = permissions['permissions']
-        self.permissions['authextra'] = permissions['authextra']
-        self.permissions['services'] = permissions['services']
-        self.proxyObjects = {}
-        self.proxyMethods = {}
-        self.pendingCalls = []
-        self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
-        self.services = {}  # DBus service created
-        self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
-        self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
-        self.localCtx = locals()
-        self.globalCtx = globals()
-
-
-    def proxyObject(self, busName, serviceName, objectName):
-        '''
-        object hash id as busName#serviceName#objectName
-        '''
-        id = "#".join([busName, serviceName, objectName])
-        if not self.proxyObjects.has_key(id):
-            if not OPENDOOR:
-                # check permissions, array.index throws exception
-                self.permissions['permissions'].index(serviceName)
-            bus = cache.dbusConnexion(busName)
-            self.proxyObjects[id] = bus.get_object(serviceName, objectName)
-        return self.proxyObjects[id]
-
-
-    def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
-        '''
-        method hash id as busName#serviceName#objectName#interfaceName#methodName
-        '''
-        id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
-        if not self.proxyMethods.has_key(id):
-            obj = self.proxyObject(busName, serviceName, objectName)
-            self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
-        return self.proxyMethods[id]
-
-
-    @exportRpc
-    def dbusRegister(self, list):
-        '''
-        arguments: bus, sender, object, interface, signal
-        '''
-        if len(list) < 5:
-            raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
-        
-        if not OPENDOOR:
-            # check permissions, array.index throws exception
-            self.permissions['permissions'].index(list[1])
-        
-        # check if a handler exists
-        sigId = "#".join(list)
-        if cache.signalHandlers.has_key(sigId):
-            return sigId
-        
-        # create a handler that will publish the signal
-        dbusSignalHandler = DbusSignalHandler(*list)
-        cache.signalHandlers[sigId] = dbusSignalHandler
-        
-        return dbusSignalHandler.id
-
-
-    @exportRpc
-    def dbusSend(self, list):
-        '''
-        arguments: bus, destination, object, interface, message, [args]
-        '''
-        # clear pending calls
-        for call in self.pendingCalls:
-            if not call.pending:
-                self.pendingCalls.remove(call)
-        
-        if len(list) < 5:
-            raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
-        
-        # parse JSON arg list
-        args = []
-        if len(list) == 6:
-            args = json.loads(list[5])
-        
-        # get dbus proxy method
-        method = self.proxyMethod(*list[0:5])
-        
-        # use a deferred call handler to manage dbus results
-        dbusCallHandler = DbusCallHandler(method, args)
-        self.pendingCalls.append(dbusCallHandler)
-        return dbusCallHandler.callMethod()
-
-
-    @exportRpc
-    def emitSignal(self, list):
-        '''
-        arguments: agentObjectPath, signalName, args (to emit)
-        '''
-        objectPath = list[0]
-        className = re.sub('/', '_', objectPath[1:])
-        signalName = list[1]
-        args = json.loads(list[2])
-        if (self.serviceAgents.has_key(className) == True):            
-            exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "("
-            if len(args) > 0:
-                exe_str += json.dumps(args[0])
-                for idx in args[1:]:
-                    exe_str += "," + json.dumps(idx)
-            exe_str += ")"               
-            eval(exe_str, self.globalCtx, self.localCtx)
-        else:
-            raise Exception("No object path " + objectPath)
-
-    @exportRpc
-    def returnMethod(self, list):
-        '''
-        arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
-        '''
-        methodId = list[0]
-        callIndex = list[1]
-        success = list[2]
-        result = list[3]
-        if (self.servicePendingCalls.has_key(methodId)):
-            cb = self.servicePendingCalls[methodId]['calls'][callIndex]
-            if cb is None:
-                raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
-            if (success):                
-                successCB = cb["successCB"]
-                if (result != None):
-                    successCB(result)
-                else:
-                    successCB()                    
-            else:     
-                errorCB = cb["errorCB"]        
-                if (result != None):
-                    errorCB(result)
-                else:
-                    errorCB()
-            self.servicePendingCalls[methodId]['calls'][callIndex] = None
-            self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
-            if self.servicePendingCalls[methodId]['count'] == 0:
-                del self.servicePendingCalls[methodId]
-        else:
-            raise Exception("No methodID " + methodId)
-
-    def srvCB(self, srvName, name, objPath, ifName, async_succes_cb, async_error_cb, *args):
-        methodId = srvName + "#" + objPath + "#" + ifName + "#" + name
-        cb = { 'successCB': async_succes_cb, 
-               'errorCB': async_error_cb}
-        if methodId not in self.servicePendingCalls:
-            self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
-            
-        try:
-            pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
-        except Exception, e:                
-            args = eval( str(args).replace("dbus.Byte", "dbus.Int16") )
-            pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
-               
-        self.servicePendingCalls[methodId]['calls'].append(cb)
-        self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
-        factory.dispatch(methodId, pendingCallStr)
-                    
-    @exportRpc
-    def serviceAdd(self, list):
-        '''
-        arguments: busName, srvName
-        '''
-        busName = list[0]
-        self.bus =  cache.dbusConnexion( busName )
-        srvName = list[1]
-        if not OPENDOOR and (SERVICELIST == [] or SERVICELIST != [] and self.permissions['services'] == None):
-            SERVICELIST.index(srvName)
-            
-        if (self.services.has_key(srvName) == False):
-            self.services[srvName] = dbus.service.BusName(name = srvName, bus = self.bus)
-        return srvName
-
-    @exportRpc
-    def serviceRelease(self, list):
-        '''
-        arguments: busName, srvName
-        '''
-        srvName = list[0]
-        if (self.services.has_key(srvName) == True):
-            self.services.pop(srvName)
-            return srvName
-        else:
-            raise Exception(srvName + " does not exist")
-                   
-    @exportRpc
-    def serviceAddAgent(self, list):
-        '''
-        arguments: objectPath, xmlTemplate
-        '''
-        srvName = list[0]
-        agentObjectPath = list[1]
-        xmlTemplate = list[2]
-        className = createClassName(agentObjectPath)
-        if (self.dynDBusClasses.has_key(className) == False):
-            self.dynDBusClasses[className] = DynDBusClass(className, self.globalCtx, self.localCtx)
-            self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
-            self.dynDBusClasses[className].declare()
-
-        ## Class already exist, instanciate it if not already instanciated
-        if (self.serviceAgents.has_key(className) == False):
-            self.serviceAgents[className] = eval(className + "(self.bus, callback=self.srvCB, objPath='" + agentObjectPath + "', srvName='" + srvName + "')", self.globalCtx, self.localCtx)
-            
-        self.serviceAgents[className].add_to_connection()
-        return (agentObjectPath)
-                    
-    @exportRpc
-    def serviceDelAgent(self, list):
-        '''
-        arguments: objectPath, xmlTemplate
-        '''
-        agentObjectPath = list[0]
-        className = createClassName(agentObjectPath)
-        
-        if (self.serviceAgents.has_key(className)):
-            self.serviceAgents[className].remove_from_connection()
-            self.serviceAgents.pop(className)
-        else:
-            raise Exception(agentObjectPath + " doesn't exist!")
-        
-        return (agentObjectPath)
-                    
-    @exportRpc
-    def getVersion(self):
-        '''
-        return current version string
-        '''
-        return VERSION
-
-
-
-###############################################################################
 class CloudeebusServerProtocol(WampCraServerProtocol):
     '''
     connexion and session authentication management
@@ -734,9 +148,6 @@ class CloudeebusServerProtocol(WampCraServerProtocol):
 ###############################################################################
 
 if __name__ == '__main__':
-    
-    cache = DbusCache()
-
     parser = argparse.ArgumentParser(description='Javascript DBus bridge.')
     parser.add_argument('-v', '--version', action='store_true', 
         help='print version and exit')
@@ -773,12 +184,12 @@ if __name__ == '__main__':
     
     if args.whitelist:
         jfile = open(args.whitelist)
-        WHITELIST = json.load(jfile)
+        WHITELIST.extend(json.load(jfile))
         jfile.close()
         
     if args.servicelist:
         jfile = open(args.servicelist)
-        SERVICELIST = json.load(jfile)
+        SERVICELIST.extend(json.load(jfile))
         jfile.close()
         
     if args.netmask:
@@ -798,7 +209,11 @@ if __name__ == '__main__':
     factory = WampServerFactory(uri, debugWamp = args.debug)
     factory.protocol = CloudeebusServerProtocol
     factory.setProtocolOptions(allowHixie76 = True)
-    
+
+    # Configure engine for WAMP.
+    engine.factory = factory
+    engine.OPENDOOR = OPENDOOR
+
     listenWS(factory)
     
     DBusGMainLoop(set_as_default=True)
diff --git a/cloudeebus/engine.py b/cloudeebus/engine.py
new file mode 100644 (file)
index 0000000..a6831fc
--- /dev/null
@@ -0,0 +1,633 @@
+# Cloudeebus
+#
+# Copyright 2012 Intel Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Luc Yriarte <luc.yriarte@intel.com>
+# Christophe Guiraud <christophe.guiraud@intel.com>
+# Frederic Paut <frederic.paut@intel.com>
+#
+
+import dbus
+import dbus.service
+import os
+import re
+import json
+
+# enable debug log
+from twisted.python import log
+
+# XML parser module
+from xml.etree.ElementTree import XMLParser
+
+from twisted.internet import defer
+
+# The user of engine.py must set this to some object
+# providing a dispatch(topicUri, event) method as in WampServerFactory
+factory = None
+
+if os.environ.get('CLOUDEEBUS_XWALK', False):
+    # Same approach as in autobahn.wamp: add the method name to
+    # decorated methods, which then gets used to identify the methods
+    # that can be called from remote.
+    def exportRpc(arg):
+        arg._xwalk_rpc_id = arg.__name__
+        return arg
+else:
+    from autobahn.wamp import exportRpc
+
+VERSION = "0.6.0"
+OPENDOOR = False
+SERVICELIST = []
+
+###############################################################################
+class DbusCache:
+    '''
+    Global cache of DBus connexions and signal handlers
+    '''
+    def __init__(self):
+        self.dbusConnexions = {}
+        self.signalHandlers = {}
+
+
+    def reset(self):
+        '''
+        Disconnect signal handlers before resetting cache.
+        '''
+        self.dbusConnexions = {}
+        # disconnect signal handlers
+        for key in self.signalHandlers:
+            self.signalHandlers[key].disconnect()
+        self.signalHandlers = {}
+
+
+    def dbusConnexion(self, busName):
+        if not self.dbusConnexions.has_key(busName):
+            if busName == "session":
+                self.dbusConnexions[busName] = dbus.SessionBus()
+            elif busName == "system":
+                self.dbusConnexions[busName] = dbus.SystemBus()
+            else:
+                raise Exception("Error: invalid bus: %s" % busName)
+        return self.dbusConnexions[busName]
+
+cache = DbusCache()
+
+
+###############################################################################
+class DbusSignalHandler:
+    '''
+    signal hash id as busName#senderName#objectName#interfaceName#signalName
+    '''
+    def __init__(self, busName, senderName, objectName, interfaceName, signalName):
+        self.id = "#".join([busName, senderName, objectName, interfaceName, signalName])
+        # connect handler to signal
+        self.bus = cache.dbusConnexion(busName)
+        self.bus.add_signal_receiver(self.handleSignal, signalName, interfaceName, senderName, objectName)
+        
+    
+    def disconnect(self):
+        names = self.id.split("#")
+        self.bus.remove_signal_receiver(self.handleSignal, names[4], names[3], names[1], names[2])
+
+
+    def handleSignal(self, *args):
+        '''
+        publish dbus args under topic hash id
+        '''
+        factory.dispatch(self.id, json.dumps(args))
+
+
+
+###############################################################################
+class DbusCallHandler:
+    '''
+    deferred reply to return dbus results
+    '''
+    def __init__(self, method, args):
+        self.pending = False
+        self.request = defer.Deferred()
+        self.method = method
+        self.args = args
+
+
+    def callMethod(self):
+        '''
+        dbus method async call
+        '''
+        self.pending = True
+        self.method(*self.args, reply_handler=self.dbusSuccess, error_handler=self.dbusError)
+        return self.request
+
+
+    def dbusSuccess(self, *result):
+        '''
+        return JSON string result array
+        '''
+        self.request.callback(json.dumps(result))
+        self.pending = False
+
+
+    def dbusError(self, error):
+        '''
+        return dbus error message
+        '''
+        self.request.errback(Exception(error.get_dbus_message()))
+        self.pending = False
+
+
+
+################################################################################       
+class ExecCode:
+    '''
+    Execute DynDBusClass generated code
+    '''
+    def __init__(self, globalCtx, localCtx) :
+        self.exec_string = ""
+        self.exec_code = None
+        self.exec_code_valid = 1
+        self.indent_level = 0
+        self.indent_increment = 1
+        self.line = 0
+        self.localCtx = localCtx
+        self.globalCtx = globalCtx
+        
+
+    def append_stmt(self, stmt) :
+        self.exec_code_valid = 0
+        self.line += 1
+        for x in range(0,self.indent_level):
+            self.exec_string = self.exec_string + ' '            
+        self.exec_string = self.exec_string + stmt + '\n'
+
+    def indent(self) :
+        self.indent_level = self.indent_level + self.indent_increment
+
+    def dedent(self) :
+        self.indent_level = self.indent_level - self.indent_increment
+    
+    # compile : Compile exec_string into exec_code using the builtin
+    # compile function. Skip if already in sync.
+    def compile(self) :
+        if not self.exec_code_valid :
+            self.exec_code = compile(self.exec_string, "<string>", "exec")
+        self.exec_code_valid = True
+
+    def execute(self) :
+        if not self.exec_code_valid :
+            self.compile()
+        exec(self.exec_code, self.globalCtx, self.localCtx)
+
+
+
+################################################################################       
+class XmlCbParser: # The target object of the parser
+    maxDepth = 0
+    depth = 0
+    def __init__(self, dynDBusClass):
+        self.dynDBusClass = dynDBusClass
+        
+    def start(self, tag, attrib):   # Called for each opening tag.
+        if (tag == 'node'):
+            return
+        # Set interface name
+        if (tag == 'interface'):
+            self.dynDBusClass.set_interface(attrib['name'])
+            return
+        # Set method name
+        if (tag == 'method'):
+            self.current = tag
+            self.dynDBusClass.def_method(attrib['name'])
+            return
+        if (tag == 'signal'):
+            self.current = tag
+            self.dynDBusClass.def_signal(attrib['name'])
+            return
+
+        # Set signature (in/out & name) for method
+        if (tag == 'arg'):
+            if (self.current == 'method'):
+                if (attrib.has_key('direction') == False):
+                    attrib['direction'] = "in"
+                self.dynDBusClass.add_signature(attrib['name'],
+                                                attrib['direction'],
+                                                attrib['type'])
+                return
+            if (self.current == 'signal'):
+                if (attrib.has_key('name') == False):
+                    attrib['name'] = 'value'
+                self.dynDBusClass.add_signature(attrib['name'], 'in',
+                                                attrib['type'])
+                return
+    def end(self, tag):             # Called for each closing tag.
+        if (tag == 'method'):
+            self.dynDBusClass.add_dbus_method()
+            self.dynDBusClass.add_body_method()
+            self.dynDBusClass.end_method()
+        if (tag == 'signal'):
+            self.dynDBusClass.add_dbus_signal()
+            self.dynDBusClass.add_body_signal()
+            self.dynDBusClass.end_method()
+           
+    def data(self, data):
+        pass            # We do not need to do anything with data.
+    def close(self):    # Called when all data has been parsed.
+        return self.maxDepth
+
+
+       
+###############################################################################
+def createClassName(objectPath):
+    return re.sub('/', '_', objectPath[1:])
+
+################################################################################       
+class DynDBusClass():
+    def __init__(self, className, globalCtx, localCtx):
+        self.xmlCB = XmlCbParser(self)
+        self.signature = {}
+        self.class_code = ExecCode(globalCtx, localCtx)  
+        self.class_code.indent_increment = 4
+        self.class_code.append_stmt("import dbus")
+        self.class_code.append_stmt("\n")
+        self.class_code.append_stmt("class " + className + "(dbus.service.Object):")
+        self.class_code.indent()
+        
+        ## Overload of __init__ method 
+        self.def_method("__init__")
+        self.add_method("bus, callback=None, objPath='/sample', srvName='org.cloudeebus'")
+        self.add_stmt("self.bus = bus")
+        self.add_stmt("self.objPath = objPath")
+        self.add_stmt("self.srvName = srvName")        
+        self.add_stmt("self.callback = callback")        
+        self.add_stmt("dbus.service.Object.__init__(self, conn=bus, bus_name=srvName)")
+        self.end_method()
+               
+        ## Create 'add_to_connection' method 
+        self.def_method("add_to_connection")
+        self.add_method("connection=None, path=None")
+        self.add_stmt("dbus.service.Object.add_to_connection(self, connection=self.bus, path=self.objPath)")
+        self.end_method()
+               
+        ## Create 'remove_from_connection' method 
+        self.def_method("remove_from_connection")
+        self.add_method("connection=None, path=None")
+        self.add_stmt("dbus.service.Object.remove_from_connection(self, connection=None, path=self.objPath)")
+        self.end_method()
+               
+    def createDBusServiceFromXML(self, xml):
+        self.parser = XMLParser(target=self.xmlCB)
+        self.parser.feed(xml)
+        self.parser.close()
+    
+    def set_interface(self, ifName):
+        self.ifName = ifName
+        
+    def def_method(self, methodName):
+        self.methodToAdd = methodName
+        self.signalToAdd = None
+        self.args_str = str()
+        self.signature = {}
+        self.signature['name'] = str()
+        self.signature['in'] = str()                
+        self.signature['out'] = str()                        
+
+    def def_signal(self, signalName):
+        self.methodToAdd = None
+        self.signalToAdd = signalName
+        self.args_str = str()
+        self.signature = {}
+        self.signature['name'] = str()
+        self.signature['in'] = str()                
+        self.signature['out'] = str()                        
+
+    def add_signature(self, name, direction, signature):
+        if (direction == 'in'):
+            self.signature['in'] += signature
+            if (self.signature['name'] != str()):
+                self.signature['name'] += ", "
+            self.signature['name'] += name
+        if (direction == 'out'):
+            self.signature['out'] = signature                        
+        
+    def add_method(self, args = None, async_success_cb = None, async_err_cb = None):
+        async_cb_str = str()
+        if (self.methodToAdd != None):
+            name = self.methodToAdd
+        else:
+            name = self.signalToAdd
+        if (args != None):
+            self.args_str = args
+        if (async_success_cb != None):
+            async_cb_str = async_success_cb
+        if (async_err_cb != None):
+            if (async_cb_str != str()):
+                async_cb_str += ", "
+            async_cb_str += async_err_cb
+                        
+        parameters = self.args_str
+        if (async_cb_str != str()):
+            if (parameters != str()):
+                parameters += ", "
+            parameters +=async_cb_str       
+        
+        if (parameters != str()):
+            self.class_code.append_stmt("def " + name + "(self, %s):" % parameters)               
+        else:
+            self.class_code.append_stmt("def " + name + "(self):")
+        self.class_code.indent()
+        
+    def end_method(self):
+        self.class_code.append_stmt("\n")
+        self.class_code.dedent()
+        
+    def add_dbus_method(self):
+        decorator = '@dbus.service.method("' + self.ifName + '"'
+        if (self.signature.has_key('in') and self.signature['in'] != str()):
+                decorator += ", in_signature='" + self.signature['in'] + "'"
+        if (self.signature.has_key('out') and self.signature['out'] != str()):
+                decorator += ", out_signature='" + self.signature['out'] + "'"
+        decorator += ", async_callbacks=('dbus_async_cb', 'dbus_async_err_cb')"            
+        decorator += ")"
+        self.class_code.append_stmt(decorator)
+        if (self.signature.has_key('name') and self.signature['name'] != str()):
+            self.add_method(self.signature['name'], async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
+        else:
+            self.add_method(async_success_cb='dbus_async_cb', async_err_cb='dbus_async_err_cb')
+
+    def add_dbus_signal(self):
+        decorator = '@dbus.service.signal("' + self.ifName + '"'
+        if (self.signature.has_key('in') and self.signature['in'] != str()):
+                decorator += ", signature='" + self.signature['in'] + "'"
+        decorator += ")"            
+        self.class_code.append_stmt(decorator)
+        if (self.signature.has_key('name') and self.signature['name'] != str()):
+            self.add_method(self.signature['name'])
+        else:
+            self.add_method()
+
+    def add_body_method(self):
+        if (self.methodToAdd != None):
+            if (self.args_str != str()):
+                self.class_code.append_stmt("self.callback(self.srvName,'" + self.methodToAdd + "', self.objPath, '"  + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb, %s)" % self.args_str)
+            else:        
+                self.class_code.append_stmt("self.callback(self.srvName,'" + self.methodToAdd + "', self.objPath, '"  + self.ifName + "', " + "dbus_async_cb, dbus_async_err_cb)")
+
+    def add_body_signal(self):
+        self.class_code.append_stmt("return") ## TODO: Remove and fix with code ad hoc
+        self.class_code.append_stmt("\n")
+
+    def add_stmt(self, stmt) :
+        self.class_code.append_stmt(stmt)
+        
+    def declare(self) :
+        self.class_code.execute()
+
+
+
+###############################################################################
+class CloudeebusService:
+    '''
+    support for sending DBus messages and registering for DBus signals
+    '''
+    def __init__(self, permissions):
+        self.permissions = {};
+        self.permissions['permissions'] = permissions['permissions']
+        self.permissions['authextra'] = permissions['authextra']
+        self.permissions['services'] = permissions['services']
+        self.proxyObjects = {}
+        self.proxyMethods = {}
+        self.pendingCalls = []
+        self.dynDBusClasses = {} # DBus class source code generated dynamically (a list because one by classname)
+        self.services = {}  # DBus service created
+        self.serviceAgents = {} # Instantiated DBus class previously generated dynamically, for now, one by classname
+        self.servicePendingCalls = {} # JS methods called (and waiting for a Success/error response), containing 'methodId', (successCB, errorCB)
+        self.localCtx = locals()
+        self.globalCtx = globals()
+
+
+    def proxyObject(self, busName, serviceName, objectName):
+        '''
+        object hash id as busName#serviceName#objectName
+        '''
+        id = "#".join([busName, serviceName, objectName])
+        if not self.proxyObjects.has_key(id):
+            if not OPENDOOR:
+                # check permissions, array.index throws exception
+                self.permissions['permissions'].index(serviceName)
+            bus = cache.dbusConnexion(busName)
+            self.proxyObjects[id] = bus.get_object(serviceName, objectName)
+        return self.proxyObjects[id]
+
+
+    def proxyMethod(self, busName, serviceName, objectName, interfaceName, methodName):
+        '''
+        method hash id as busName#serviceName#objectName#interfaceName#methodName
+        '''
+        id = "#".join([busName, serviceName, objectName, interfaceName, methodName])
+        if not self.proxyMethods.has_key(id):
+            obj = self.proxyObject(busName, serviceName, objectName)
+            self.proxyMethods[id] = obj.get_dbus_method(methodName, interfaceName)
+        return self.proxyMethods[id]
+
+
+    @exportRpc
+    def dbusRegister(self, list):
+        '''
+        arguments: bus, sender, object, interface, signal
+        '''
+        if len(list) < 5:
+            raise Exception("Error: expected arguments: bus, sender, object, interface, signal)")
+        
+        if not OPENDOOR:
+            # check permissions, array.index throws exception
+            self.permissions['permissions'].index(list[1])
+        
+        # check if a handler exists
+        sigId = "#".join(list)
+        if cache.signalHandlers.has_key(sigId):
+            return sigId
+        
+        # create a handler that will publish the signal
+        dbusSignalHandler = DbusSignalHandler(*list)
+        cache.signalHandlers[sigId] = dbusSignalHandler
+        
+        return dbusSignalHandler.id
+
+
+    @exportRpc
+    def dbusSend(self, list):
+        '''
+        arguments: bus, destination, object, interface, message, [args]
+        '''
+        # clear pending calls
+        for call in self.pendingCalls:
+            if not call.pending:
+                self.pendingCalls.remove(call)
+        
+        if len(list) < 5:
+            raise Exception("Error: expected arguments: bus, destination, object, interface, message, [args])")
+        
+        # parse JSON arg list
+        args = []
+        if len(list) == 6:
+            args = json.loads(list[5])
+        
+        # get dbus proxy method
+        method = self.proxyMethod(*list[0:5])
+        
+        # use a deferred call handler to manage dbus results
+        dbusCallHandler = DbusCallHandler(method, args)
+        self.pendingCalls.append(dbusCallHandler)
+        return dbusCallHandler.callMethod()
+
+
+    @exportRpc
+    def emitSignal(self, list):
+        '''
+        arguments: agentObjectPath, signalName, args (to emit)
+        '''
+        objectPath = list[0]
+        className = re.sub('/', '_', objectPath[1:])
+        signalName = list[1]
+        args = json.loads(list[2])
+        if (self.serviceAgents.has_key(className) == True):            
+            exe_str = "self.serviceAgents['"+ className +"']."+ signalName + "("
+            if len(args) > 0:
+                exe_str += json.dumps(args[0])
+                for idx in args[1:]:
+                    exe_str += "," + json.dumps(idx)
+            exe_str += ")"               
+            eval(exe_str, self.globalCtx, self.localCtx)
+        else:
+            raise Exception("No object path " + objectPath)
+
+    @exportRpc
+    def returnMethod(self, list):
+        '''
+        arguments: methodId, callIndex, success (=true, error otherwise), result (to return)
+        '''
+        methodId = list[0]
+        callIndex = list[1]
+        success = list[2]
+        result = list[3]
+        if (self.servicePendingCalls.has_key(methodId)):
+            cb = self.servicePendingCalls[methodId]['calls'][callIndex]
+            if cb is None:
+                raise Exception("No pending call " + str(callIndex) + " for methodID " + methodId)
+            if (success):                
+                successCB = cb["successCB"]
+                if (result != None):
+                    successCB(result)
+                else:
+                    successCB()                    
+            else:     
+                errorCB = cb["errorCB"]        
+                if (result != None):
+                    errorCB(result)
+                else:
+                    errorCB()
+            self.servicePendingCalls[methodId]['calls'][callIndex] = None
+            self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] - 1
+            if self.servicePendingCalls[methodId]['count'] == 0:
+                del self.servicePendingCalls[methodId]
+        else:
+            raise Exception("No methodID " + methodId)
+
+    def srvCB(self, srvName, name, objPath, ifName, async_succes_cb, async_error_cb, *args):
+        methodId = srvName + "#" + objPath + "#" + ifName + "#" + name
+        cb = { 'successCB': async_succes_cb, 
+               'errorCB': async_error_cb}
+        if methodId not in self.servicePendingCalls:
+            self.servicePendingCalls[methodId] = {'count': 0, 'calls': []}
+            
+        try:
+            pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
+        except Exception, e:                
+            args = eval( str(args).replace("dbus.Byte", "dbus.Int16") )
+            pendingCallStr = json.dumps({'callIndex': len(self.servicePendingCalls[methodId]['calls']), 'args': args})
+               
+        self.servicePendingCalls[methodId]['calls'].append(cb)
+        self.servicePendingCalls[methodId]['count'] = self.servicePendingCalls[methodId]['count'] + 1
+        factory.dispatch(methodId, pendingCallStr)
+                    
+    @exportRpc
+    def serviceAdd(self, list):
+        '''
+        arguments: busName, srvName
+        '''
+        busName = list[0]
+        self.bus =  cache.dbusConnexion( busName )
+        srvName = list[1]
+        if not OPENDOOR and (SERVICELIST == [] or SERVICELIST != [] and self.permissions['services'] == None):
+            SERVICELIST.index(srvName)
+            
+        if (self.services.has_key(srvName) == False):
+            self.services[srvName] = dbus.service.BusName(name = srvName, bus = self.bus)
+        return srvName
+
+    @exportRpc
+    def serviceRelease(self, list):
+        '''
+        arguments: busName, srvName
+        '''
+        srvName = list[0]
+        if (self.services.has_key(srvName) == True):
+            self.services.pop(srvName)
+            return srvName
+        else:
+            raise Exception(srvName + " does not exist")
+                   
+    @exportRpc
+    def serviceAddAgent(self, list):
+        '''
+        arguments: objectPath, xmlTemplate
+        '''
+        srvName = list[0]
+        agentObjectPath = list[1]
+        xmlTemplate = list[2]
+        className = createClassName(agentObjectPath)
+        if (self.dynDBusClasses.has_key(className) == False):
+            self.dynDBusClasses[className] = DynDBusClass(className, self.globalCtx, self.localCtx)
+            self.dynDBusClasses[className].createDBusServiceFromXML(xmlTemplate)
+            self.dynDBusClasses[className].declare()
+
+        ## Class already exist, instanciate it if not already instanciated
+        if (self.serviceAgents.has_key(className) == False):
+            self.serviceAgents[className] = eval(className + "(self.bus, callback=self.srvCB, objPath='" + agentObjectPath + "', srvName='" + srvName + "')", self.globalCtx, self.localCtx)
+            
+        self.serviceAgents[className].add_to_connection()
+        return (agentObjectPath)
+                    
+    @exportRpc
+    def serviceDelAgent(self, list):
+        '''
+        arguments: objectPath, xmlTemplate
+        '''
+        agentObjectPath = list[0]
+        className = createClassName(agentObjectPath)
+        
+        if (self.serviceAgents.has_key(className)):
+            self.serviceAgents[className].remove_from_connection()
+            self.serviceAgents.pop(className)
+        else:
+            raise Exception(agentObjectPath + " doesn't exist!")
+        
+        return (agentObjectPath)
+                    
+    @exportRpc
+    def getVersion(self):
+        '''
+        return current version string
+        '''
+        return VERSION
diff --git a/cloudeebus/examples/crosswalk/example.html b/cloudeebus/examples/crosswalk/example.html
new file mode 100644 (file)
index 0000000..9bc30b4
--- /dev/null
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <p>Example</p>
+    <div id="output"></div>
+    <script>
+      function log(message) {
+        document.getElementById("output").innerHTML =
+            document.getElementById("output").innerHTML + "<br/>" + message;
+      }
+
+      log(cloudeebus);
+      cloudeebus.connect('', null, function() {}, function() {})
+      log(cloudeebus.SessionBus)
+      log(cloudeebus.SessionBus())
+      var trackerReady = function(status_proxy) {
+        status_proxy.GetProgress().then(function(result) { log("Tracker progress:" + result); },
+                                        function(result) { log("Tracker error:" + result); })
+      }
+      var tracker = cloudeebus.SessionBus().getObject("org.freedesktop.Tracker1", 
+                                                     "/org/freedesktop/Tracker1/Status",
+                                                     trackerReady,
+                                                     log);
+    </script>
+  </body>
+</html>
diff --git a/cloudeebus/xwalkcloudeebus.py b/cloudeebus/xwalkcloudeebus.py
new file mode 100644 (file)
index 0000000..831d7c4
--- /dev/null
@@ -0,0 +1,239 @@
+# Cloudeebus for Crosswalk
+#
+# Copyright 2012 Intel Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Patrick Ohly <patrick.ohly@intel.com>
+#
+
+# This is an extension loaded by pycrosswalk. It uses Cloudeebus
+# Python in the Crosswalk extension process and Cloudeebus JavaScript
+# in the Crosswalk render process, connected via Crosswalk's extension
+# message passing instead of the original WebSocket/WAMP.
+#
+# Installation:
+# cloudeebus.js, xwalkcloudeebus.py and engine.py must be installed in
+# the same directory. xwalkcloudeebus.py (or a symlink to it) and a
+# symlink to libpycrosswalk.so must be in a directory that Crosswalk
+# searches for extensions.
+#
+# To run some examples directly in the cloudeebus source tree:
+# ln -s <path to libpycrosswalk.so> cloudeebus/libxwalkcloudeebus.so
+# xwalk --external-extensions-path=cloudeebus doc/agent/server.html &
+# xwalk --external-extensions-path=cloudeebus doc/agent/client.html &
+#
+# Only one-way messages are used. RPC method calls contain a sequence
+# number that gets repeated in the reply, so the caller can match
+# pending calls with their reply.
+#
+# The message format is JSON:
+# [ <type>, ... ]
+# <type> = "call" | "reply" | "signal"
+# [ "call", <sequence number>, <method name>, [<parameters>] ]
+# [ "reply", <call sequence number>, "error string", <result> ]
+# [ "signal", <topic>, [<parameters>] ]
+# [ "subscribe", <topic> ]
+# [ "unsubscribe", <topic> ]
+
+import gi.repository
+import sys
+import inspect
+import json
+import time
+import traceback
+import os
+import re
+
+from gi.repository import GLib
+
+from dbus.mainloop.glib import DBusGMainLoop
+DBusGMainLoop(set_as_default=True)
+
+from twisted.internet import defer
+from twisted.python import log
+# enable debug log
+#log.startLogging(sys.stdout)
+
+import xwalk
+
+# Configure engine module. Partly has to be done before importing
+# because the engine needs to know how it is going to be used.
+os.environ['CLOUDEEBUS_XWALK'] = '1'
+import engine
+engine.OPENDOOR = True # No other process has access, so we need no additional credential checking.
+
+class Factory:
+  # Mapping from instance ID to hash with all subscribed topics.
+  instances = {}
+  def dispatch(self, topic, event):
+    for instance, topics in Factory.instances.iteritems():
+      if topic in topics:
+        xwalk.PostMessage(instance, json.dumps(['signal', topic, event]))
+
+engine.factory = Factory()
+
+service = engine.CloudeebusService({'permissions': [], 'authextra': '', 'services': []})
+methods = {}
+
+for method in inspect.getmembers(service.__class__, inspect.ismethod):
+  if method[1].__dict__.has_key("_xwalk_rpc_id"):
+    name = method[1].__dict__["_xwalk_rpc_id"]
+    proc = method[1]
+    methods[name] = proc
+
+def HandleMessage(instance, message):
+  log.msg('New message: %s' % message)
+  content = json.loads(message)
+  msgtype = content[0]
+  if msgtype == 'call':
+    sequencenr = content[1]
+    try:
+      name = str(content[2])
+      params = content[3]
+      d = defer.maybeDeferred(methods[name], service, params)
+      d.addCallback(lambda result: (log.msg('call %d done: %s' % (sequencenr, result)), xwalk.PostMessage(instance, json.dumps(['reply', sequencenr, '', result]))))
+      d.addErrback(lambda error: (log.msg('call %d failed: %s' % (sequencenr, error)), xwalk.PostMessage(instance, json.dumps(['reply', sequencenr, str(error), []]))))
+    except Exception, ex:
+      log.msg('failed to start call %d: %s' % (sequencenr, traceback.format_exc()));
+      xwalk.PostMessage(instance, json.dumps(['reply', sequencenr, repr(ex), []]))
+  elif msgtype == 'subscribe':
+    topic = content[1]
+    log.msg('Subscribing %d to %s' % (instance, topic))
+    Factory.instances[instance][topic] = True
+  elif msgtype == 'unsubscribe':
+    topic = content[1]
+    log.msg('Unsubscribing %d from %s' % (instance, topic))
+    del Factory.instances[instance][topic]
+
+def HandleInstanceCreated(instance):
+  Factory.instances[instance] = {}
+  xwalk.SetMessageCallback(instance, HandleMessage)
+
+def HandleInstanceDestroyed(instance):
+  del Factory.instances[instance]
+
+def Main():
+  xwalk.SetExtensionName("cloudeebus")
+  xwalk.SetInstanceCreatedCallback(HandleInstanceCreated)
+  xwalk.SetInstanceDestroyedCallback(HandleInstanceDestroyed)
+
+  # cloudeebus.js is expected in the same directory as the actual
+  # xwalkcloudeebus.py file (i.e., after resolving symlinks).
+  modpath = inspect.getsourcefile(Main)
+  modpath = os.path.realpath(modpath)
+  jssource = os.path.join(os.path.dirname(modpath), 'cloudeebus.js')
+
+  js = open(jssource).read()
+
+  js = js + '''
+    var pending_calls = {};
+    var topics = {};
+    var call_counter = 1;
+
+    // A pending call behaves like a Promise: the instance
+    // gets stored in the pending hash, is returned by call(),
+    // and then the caller installs its callbacks with then().
+    var Pending = function() {
+      this.success = null;
+      this.failure = null;
+      return this;
+    };
+    Pending.prototype.then = function(success, failure) {
+      this.success = success;
+      this.failure = failure;
+    };
+
+    // Error instance as used by WAMP error callbacks.
+    // Meant to work with cloudeebus.getError().
+    var Error = function(description) {
+      this.desc = description;
+      this.uri = null;
+      this.name = null;
+      this.message = null;
+      return this;
+    };
+
+    extension.setMessageListener(function(msg) {
+      var msg_content = JSON.parse(msg);
+      if (msg_content[0] == "reply") {
+        // Handle message reply.
+        var pending = pending_calls[msg_content[1]];
+        delete pending_calls[msg_content[1]];
+        if (msg_content[2] != "") {
+          if (pending.failure) {
+            pending.failure(msg_content[2]);
+          }
+        } else {
+          if (pending.success) {
+            pending.success(msg_content[3]);
+          }
+        }
+      }
+      if (msg_content[0] == "signal") {
+        // Handle signal.
+        var topic = msg_content[1];
+        var args = msg_content[2];
+        var handler = topics[topic];
+        if (handler) {
+          handler(topic, args);
+        }
+      }
+    });
+
+    // Emulate WAMPSession.
+    var Session = function() {
+      this.extension = extension;
+      return this;
+    };
+    Session.prototype.call = function(method, args) {
+      var message = [ "call", call_counter, method, args ];
+      var data = JSON.stringify(message);
+      var pending = new Pending();
+      pending_calls[call_counter] = pending;
+      this.extension.postMessage(data);
+      call_counter++;
+      return pending;
+    };
+    Session.prototype.subscribe = function(topic, handler) {
+      var message = [ "subscribe", topic ]
+      var data = JSON.stringify(message);
+      this.extension.postMessage(data);
+      topics[topic] = handler;
+    }
+    Session.prototype.unsubscribe = function(topic) {
+      var message = [ "unsubscribe", topic ]
+      var data = JSON.stringify(message);
+      this.extension.postMessage(data);
+      delete topics[topic];
+    }
+    var session = new Session();
+
+    exports.connect = function(uri, manifest, successCB, errorCB) {
+      cloudeebus.reset();
+      cloudeebus.sessionBus = new cloudeebus.BusConnection("session", session);
+      cloudeebus.systemBus = new cloudeebus.BusConnection("system", session);
+      successCB();
+    };
+    exports.SessionBus = cloudeebus.SessionBus;
+    exports.SystemBus = cloudeebus.SystemBus;
+    exports.reset = cloudeebus.reset;
+    exports.Agent = cloudeebus.Agent;
+    exports.Service = cloudeebus.Service;
+    exports.ProxyObject = cloudeebus.ProxyObject;
+    exports.Promise = cloudeebus.Promise;
+'''
+
+  xwalk.SetJavaScriptAPI(js)
+
+Main()