Client Javascript Connection
From Mugshot Developer Wiki
In various places we want web pages served from the server to interact with the native client; this is done to provide features like live chat previous, the ability to open chat windows, the ability to detect that client isn't running and needs to be launched, and so forth. This page describes a planned rewrite (as of 2006-06-30) of how this connection is done so that it will work on Linux as well as on Windows and with Firefox as well as with Internet Explorer.
Contents |
Historical Internet Explorer model
The initial cut at integration between the Mugshot client and Internet Explorer worked via a couple of ActiveX objects that could be embedded into a web page. The HippoEmbed object has global methods like ShowChatWindow() and OpenBrowserBar(). (The latter opens the mugshot bar that appears at the bottom of a web page shared via Mugshot.) Then there is a separate HippoChatControl object that is initialized with the chat room ID as a property and handles interaction with the that chat room via methods like Join(), Leave(), and SendMessage(). HippoChatRoom also exposes a set of events (the IHippoChatRoomEvents interface), including OnUserJoin(), OnUserLeave(), and so forth that allows the web page to track the state of the chat room.
HippoEmbed and HippoChatControl objects talk over COM to the corresponding objects in the Mugshot client (HippoUI and HippoChatRoom).
Security is managed by only allowing the objects to be created from particular known host names (*.mugshot.org and *.dumbhippo.com)
Principals of the new model
- Reduce Differences between browsers - The more we can make the web page and Javascript code look the same for Internet Explorer and Firefox the better off we are. The embedded Active X control and events used in the old Internet Explorer model are problematical because there is nothing corresponding to them in Firefox.
- Clean layering - The XPCOM vs. COM split in the browser doesn't line up cleanly with the D-BUS vs. COM split for browser communication with the client, so we should avoid having the COM code for browser objects know about the COM code for client communication. (In the old IE model, some shortcuts are taken by using a IHippoChatRoom interface in both places.) A clean split become even more important if we add support for Firefox on other platforms like OS X with their own IPC mechanisms.
- Few objects - all interactions have to be proxied from Javascript via XPCOM or COM to the plugin code, and then from the plugin code via D-BUS or COM to the client and back to the core GObject code. With all this layering in place, having a fine-grained set of objects with separate objects for chat rooms, for users, and so forth is going to be highly annoying. Instead we should have one interface that represents the Mugshot client, and one interface that represents a listener to things going on in the client.
- Simple event routing - since there will be multiple layers involved, and multiple implementations of some of the layers (IE vs. Firefox, D-BUS vs. COM), we shouldn't put too much event routing logic into the layer boundaries. Instead the generic client code should do all the event routing logic, and put an "address" in the event, and intermediate layers should just pass the event along.
How it looks from Javascript
To begin using the integration code, you get create a new MugshotService object for the page. The details of this look different in IE and Firefox. In IE it would look something like:
window.mugshot = new ActiveXObject("Mugshot.MugshotService")
On the other hand, in Firefox, you would have:
window.mugshot = new MugshotService()
(This difference would presumably be hidden behind a Javascript wrapper.), but once you have the object, things can work the same for both browser. The first thing you need to do is to specify which web server the object connects to:
window.mugshot.connect("http://mugshot.org")
This would then look for a Mugshot client running configured to point to that web server and establish a connection to the Mugshot client. If invoked from a web page (as opposed to say, chrome code for a Firefox extension), the URL would have to be the base URL of the connecting page itself, so we might wnat to allow window.mugshot.connect() to default to that for simplicity.
The object has various methods:
window.mugshot.showChatWindow("JLsX9HBd4nldmD")
window.mugshot.joinChat("JLsX9HBd4nldmD")
if (window.mugshot.isConnected()) { ... }
You can also set a listener object to receive events:
dh.chatroom.chatCallbacks = {
onUserJoin: function(userId, userName, photoUrl) { ... },
onUserLeave: function(userId) { ... },
...
}
window.mugshot.setListener(dh.chatroom.chatCallbacks);
Javascript implementation challenges
There are a few things that are tricky about implementing the above scheme:
- Implementing MugshotService - providing the ability for a page to do 'new MugshotService()' really goes beyond the standard things that most Firefox extensions do. It's easy to create custom components from privilege script code, such as that driving the browser chrome; you can just do Components.classes['@mugshot.org/mugshot-service;1'].createInstance(), but that facility isn't available from a web page. There is no way I know of to mark a class "audited". It is, however, also possible for an extension to register a new class globally with some more work. This is how XMLHttpRequest is implemented in Firefox, in fact. The security checks in XMLHttpRequest for preventing cross-site access can also be adapted to provide the lockdown we need for our control.
- XPCOM object lifecycle - In general, there is no notification to Javascript when a page is closed. But if we have a MugshotService object for the page which has joined a chatroom, we really want to get it cleaned up as soon as possible so that other users don't still see this user in the chatroom when they aren't. So will be counting on all the objects created in the scope of the page being finalized predictably when the page is closed and not in some later garbage collection pass.
From Browser to client
The overall setup within the users machine looks something like:
The Mugshot client takes events from the XMPP data driven model and distributes them via D-BUS (Linux) or COM (Windows) to different browser processes running on the system. The browser processes than take those events and distributes them to the individual objects created from Javascript. We can see that there is a repeated pattern that looks something like:
If everything was implemented with the same technology, then it would be no problem to turn this pattern into code. We would have a single HippoController object, a HippoProvider interface with instances like HippoXMPPProvider, HippoDBusProvider, HippoCOMProvider, and a HippoListener interface with instances like HippoDBUSListener, HippoCOMListener, HippoFirefoxJSListenter, HippoIEJSListener. But since we are using different technologies everywhere, this gets a lot more complex; the linux client is C with GLib and GObject; the Windows client is C++ and the STL, Firefox has its own varient of C++ and utility classes, and so forth.
The fact that we can't easily do code reuse and will have to reimplement the controller component multiple times gets us back to the idea of simple event routing described above. What this probably means is that the javascript methods:
connect(url) joinChatRoom(chatId)
Will be mapped at the transport layer to a COM methods:
UINT64 connect(); void joinChatRoom(UINT64 listener, BSTR chatId);
And similarly the event callback:
onUserJoin(chatId, userId)
Will have as a COM equivalent:
void onUserJoin(UINT64 listener, BSTR chatId, BSTR userId);
This means that the intermediate layers don't have to keep track of which of their listeners are subscribed to which chat rooms, or, for user change notification, which users are in which chat rooms, but can simply forward events along based on the listener ID.
Guide to the Code
- common/hippo/hippo-ipc-source.[ch]: HippoIpcSource A GObject hooks into client's data model and has the logic for keeping track of what events need to be sent to what Javascript controls ("sources" ... each source is identified with a 64-bit ID)
- linux/hippo/hippo-dbus-server.c: we export the provider interface via D-Bus from here
- common/hippoipc/hippo-ipc.h: HippoIpcListener, HippoIpcProvider abstract classes defining the "listener" and "provider" roles shown in the diagrams above.
- linux/hippoipc/hippo-dbus-ipc-provider: HippoDBusIpcProvider HippoProvider subclass for hooking up to the D-Bus exported provider interface of the Mugshot client
- common/hippoipc/hippo-ipc-controller.cpp: HippoIpcController concrete controller object that hooks up one provider to multiple listeners
- common/firefox/public/hippoIServer.idl: XPCOM IDL version of the interface exported to interface
- common/firefox/public/hippoIServerProvider.idl: XPCOM IDL version of the interface that the Javascript listener needs to export back
- common/firefox/src/hippoServer.{cpp,h}: The Firefox component
Extending the Listener Interface
In order to add something to the listener interface, you need to modify:
- ../server/web/javascript/dh/control.js
- common/firefox/public/hippoIServiceListener.idl
- common/firefox/src/hippoService.h
- common/firefox/src/hippoService.cpp
- common/hippoipc/hippo-ipc.h
- common/hippoipc/hippo-ipc-controller.cpp (modify both the declaration and implementation of HippoIpcControllerImpl)
- linux/hippoipc/hippo-dbus-ipc-provider.cpp (add it to handleMethod)
- linux/src/hippo-dbus-server.cpp (add the signal handler that bridges the signal from hippo-endpoint-proxy to D-Bus)
- common/hippo/hippo-endpoint-proxy.c Add the signal
- linux/hippoipc/test-hippo-ipc.c



