831d7c49fef86ef6ccead77c91408334071b7cc9
[contrib/cloudeebus.git] / cloudeebus / xwalkcloudeebus.py
1 # Cloudeebus for Crosswalk
2 #
3 # Copyright 2012 Intel Corporation.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17 # Patrick Ohly <patrick.ohly@intel.com>
18 #
19
20 # This is an extension loaded by pycrosswalk. It uses Cloudeebus
21 # Python in the Crosswalk extension process and Cloudeebus JavaScript
22 # in the Crosswalk render process, connected via Crosswalk's extension
23 # message passing instead of the original WebSocket/WAMP.
24 #
25 # Installation:
26 # cloudeebus.js, xwalkcloudeebus.py and engine.py must be installed in
27 # the same directory. xwalkcloudeebus.py (or a symlink to it) and a
28 # symlink to libpycrosswalk.so must be in a directory that Crosswalk
29 # searches for extensions.
30 #
31 # To run some examples directly in the cloudeebus source tree:
32 # ln -s <path to libpycrosswalk.so> cloudeebus/libxwalkcloudeebus.so
33 # xwalk --external-extensions-path=cloudeebus doc/agent/server.html &
34 # xwalk --external-extensions-path=cloudeebus doc/agent/client.html &
35 #
36 # Only one-way messages are used. RPC method calls contain a sequence
37 # number that gets repeated in the reply, so the caller can match
38 # pending calls with their reply.
39 #
40 # The message format is JSON:
41 # [ <type>, ... ]
42 # <type> = "call" | "reply" | "signal"
43 # [ "call", <sequence number>, <method name>, [<parameters>] ]
44 # [ "reply", <call sequence number>, "error string", <result> ]
45 # [ "signal", <topic>, [<parameters>] ]
46 # [ "subscribe", <topic> ]
47 # [ "unsubscribe", <topic> ]
48
49 import gi.repository
50 import sys
51 import inspect
52 import json
53 import time
54 import traceback
55 import os
56 import re
57
58 from gi.repository import GLib
59
60 from dbus.mainloop.glib import DBusGMainLoop
61 DBusGMainLoop(set_as_default=True)
62
63 from twisted.internet import defer
64 from twisted.python import log
65 # enable debug log
66 #log.startLogging(sys.stdout)
67
68 import xwalk
69
70 # Configure engine module. Partly has to be done before importing
71 # because the engine needs to know how it is going to be used.
72 os.environ['CLOUDEEBUS_XWALK'] = '1'
73 import engine
74 engine.OPENDOOR = True # No other process has access, so we need no additional credential checking.
75
76 class Factory:
77   # Mapping from instance ID to hash with all subscribed topics.
78   instances = {}
79   def dispatch(self, topic, event):
80     for instance, topics in Factory.instances.iteritems():
81       if topic in topics:
82         xwalk.PostMessage(instance, json.dumps(['signal', topic, event]))
83
84 engine.factory = Factory()
85
86 service = engine.CloudeebusService({'permissions': [], 'authextra': '', 'services': []})
87 methods = {}
88
89 for method in inspect.getmembers(service.__class__, inspect.ismethod):
90   if method[1].__dict__.has_key("_xwalk_rpc_id"):
91     name = method[1].__dict__["_xwalk_rpc_id"]
92     proc = method[1]
93     methods[name] = proc
94
95 def HandleMessage(instance, message):
96   log.msg('New message: %s' % message)
97   content = json.loads(message)
98   msgtype = content[0]
99   if msgtype == 'call':
100     sequencenr = content[1]
101     try:
102       name = str(content[2])
103       params = content[3]
104       d = defer.maybeDeferred(methods[name], service, params)
105       d.addCallback(lambda result: (log.msg('call %d done: %s' % (sequencenr, result)), xwalk.PostMessage(instance, json.dumps(['reply', sequencenr, '', result]))))
106       d.addErrback(lambda error: (log.msg('call %d failed: %s' % (sequencenr, error)), xwalk.PostMessage(instance, json.dumps(['reply', sequencenr, str(error), []]))))
107     except Exception, ex:
108       log.msg('failed to start call %d: %s' % (sequencenr, traceback.format_exc()));
109       xwalk.PostMessage(instance, json.dumps(['reply', sequencenr, repr(ex), []]))
110   elif msgtype == 'subscribe':
111     topic = content[1]
112     log.msg('Subscribing %d to %s' % (instance, topic))
113     Factory.instances[instance][topic] = True
114   elif msgtype == 'unsubscribe':
115     topic = content[1]
116     log.msg('Unsubscribing %d from %s' % (instance, topic))
117     del Factory.instances[instance][topic]
118
119 def HandleInstanceCreated(instance):
120   Factory.instances[instance] = {}
121   xwalk.SetMessageCallback(instance, HandleMessage)
122
123 def HandleInstanceDestroyed(instance):
124   del Factory.instances[instance]
125
126 def Main():
127   xwalk.SetExtensionName("cloudeebus")
128   xwalk.SetInstanceCreatedCallback(HandleInstanceCreated)
129   xwalk.SetInstanceDestroyedCallback(HandleInstanceDestroyed)
130
131   # cloudeebus.js is expected in the same directory as the actual
132   # xwalkcloudeebus.py file (i.e., after resolving symlinks).
133   modpath = inspect.getsourcefile(Main)
134   modpath = os.path.realpath(modpath)
135   jssource = os.path.join(os.path.dirname(modpath), 'cloudeebus.js')
136
137   js = open(jssource).read()
138
139   js = js + '''
140     var pending_calls = {};
141     var topics = {};
142     var call_counter = 1;
143
144     // A pending call behaves like a Promise: the instance
145     // gets stored in the pending hash, is returned by call(),
146     // and then the caller installs its callbacks with then().
147     var Pending = function() {
148       this.success = null;
149       this.failure = null;
150       return this;
151     };
152     Pending.prototype.then = function(success, failure) {
153       this.success = success;
154       this.failure = failure;
155     };
156
157     // Error instance as used by WAMP error callbacks.
158     // Meant to work with cloudeebus.getError().
159     var Error = function(description) {
160       this.desc = description;
161       this.uri = null;
162       this.name = null;
163       this.message = null;
164       return this;
165     };
166
167     extension.setMessageListener(function(msg) {
168       var msg_content = JSON.parse(msg);
169       if (msg_content[0] == "reply") {
170         // Handle message reply.
171         var pending = pending_calls[msg_content[1]];
172         delete pending_calls[msg_content[1]];
173         if (msg_content[2] != "") {
174           if (pending.failure) {
175             pending.failure(msg_content[2]);
176           }
177         } else {
178           if (pending.success) {
179             pending.success(msg_content[3]);
180           }
181         }
182       }
183       if (msg_content[0] == "signal") {
184         // Handle signal.
185         var topic = msg_content[1];
186         var args = msg_content[2];
187         var handler = topics[topic];
188         if (handler) {
189           handler(topic, args);
190         }
191       }
192     });
193
194     // Emulate WAMPSession.
195     var Session = function() {
196       this.extension = extension;
197       return this;
198     };
199     Session.prototype.call = function(method, args) {
200       var message = [ "call", call_counter, method, args ];
201       var data = JSON.stringify(message);
202       var pending = new Pending();
203       pending_calls[call_counter] = pending;
204       this.extension.postMessage(data);
205       call_counter++;
206       return pending;
207     };
208     Session.prototype.subscribe = function(topic, handler) {
209       var message = [ "subscribe", topic ]
210       var data = JSON.stringify(message);
211       this.extension.postMessage(data);
212       topics[topic] = handler;
213     }
214     Session.prototype.unsubscribe = function(topic) {
215       var message = [ "unsubscribe", topic ]
216       var data = JSON.stringify(message);
217       this.extension.postMessage(data);
218       delete topics[topic];
219     }
220     var session = new Session();
221
222     exports.connect = function(uri, manifest, successCB, errorCB) {
223       cloudeebus.reset();
224       cloudeebus.sessionBus = new cloudeebus.BusConnection("session", session);
225       cloudeebus.systemBus = new cloudeebus.BusConnection("system", session);
226       successCB();
227     };
228     exports.SessionBus = cloudeebus.SessionBus;
229     exports.SystemBus = cloudeebus.SystemBus;
230     exports.reset = cloudeebus.reset;
231     exports.Agent = cloudeebus.Agent;
232     exports.Service = cloudeebus.Service;
233     exports.ProxyObject = cloudeebus.ProxyObject;
234     exports.Promise = cloudeebus.Promise;
235 '''
236
237   xwalk.SetJavaScriptAPI(js)
238
239 Main()