croczilla.com 
 home   stratified   bits&pieces   blog   
  home > bits&pieces > jssh

JSSh - a TCP/IP JavaScript Shell Server for Mozilla

JSSh is a Mozilla C++ extension module that allows other programs (such as telnet) to establish JavaScript shell connections to a running Mozilla process via TCP/IP. This functionality is useful for interactive debugging/development of Mozilla applications, remotely controlling Mozilla, or for automated testing purposes.

Note that JSSh is a binary component, which makes it a bit awkward to distribute as a firefox extension. There is a also a javascript-only component that apparently pretty much does what JSSh does. For details see Shane Caraveo's blog post here: jssh replacement? SD Connector. I haven't tried it out yet, but it looks promising!


Download

CVS: The JSSh code is checked into Mozilla trunk CVS in directory mozilla/extensions/jssh/.

Building from source

When building Mozilla (Firefox, Xulrunner, etc.), add the following to your .mozconfig:
ac_add_options --enable-extensions=default,jssh,webservices

The webservices extension is only required if you want to use the dumpIDL() function defined in jssh-debug.js (see below).

Running it

Start Mozilla (firefox, Xulrunner, etc.) with the command line option
  -jssh
JSSh should now start up automatically and start listening on port 9997 (only on localhost). You should be able to connect to it with telnet:
telnet localhost 9997 
Note that the telnet client shipping with MS Windows seems to have some CR/LF problems. As an alternative you can use (x)emacs (see below).
You can also start a JSSh server manually from code; see below under 'Running a server'.

Binary distribution; installing as an extension

Installing JSSh as an extension is currently somewhat broken. There are some pointers here.

Description (quite outdated - refers to version jssh-20030213)

The JSSh module provides an XPCOM object with
CID:        {A1764959-87D8-4249-A432-8005DE1372FC}
ContractID: "@mozilla.org/jssh-server;1"
implementing the following interface:
[scriptable, uuid(f8b2b6bc-4f1d-42e2-af46-9a2d6ca627bf)]
interface nsIJSShServer : nsISupports
{
  /* start listening for jssh connections on the given port.
     'startupURI' specifies an optional script that will be executed for
     new connections */
  void startServerSocket(in unsigned long port, in AUTF8String startupURI);

  /* stop listening for connections */
  void stopServerSocket();

  /* run a jssh session with the given input and output streams.
     'startupURI' specifies an optional script that will be executed
     on session startup.
     if 'interactive' is false, this will be a non-interactive session,
     with no input being collected from the input stream (i.e. input should
     be 'null'). The idea is that the session input is taken from startupURI.
     Even for non-interactive sessions, output (via 'print') can
     still be collected with the output stream object. */
  void runShell(in nsIInputStream input, in nsIOutputStream output,
                in string startupURI, in boolean interactive);
};

Running a server

Method 1: Automatically on startup
As described above under 'Running it'
Method 2: From code
A JSSh server is started with code like this:
var server = Components.classes["@mozilla.org/jssh-server;1"]
         .createInstance()
         .QueryInterface(Components.interfaces.nsIJSShServer);
server.startServerSocket(9997, "");

This code will create a server socket listening for connections on port 9997. When a client, such as e.g. telnet, establishes a connection, the JSSh server creates a new fully-trusted JavaScript context for the global JavaScript runtime and drops into a read-eval-print loop.

Each connections runs in its own thread, and many clients can connect simultaneously. Since Mozilla is not fully threadsafe, however, execution of JavaScript commands is being proxied onto the main UI thread.

The nsIJSShServer::startServerSocket() method takes the URL of a startup script as an optional argument. The JSSh module comes with one such startup script, chrome://jssh/content/jssh-debug.js, which defines some functions useful for debugging purposes (see below).

Method 3: From Tools Menu (probably broken)

As an alternative to starting a server from code, the JSSh module can also be started from the Tools menu in the Mozilla browser:

JSSh configuration dialog

Shell features

There are no security restrictions on the JavaScript context exposed to clients. It contains the usual standard JS objects, such as Array, Date, Math, as well as the XPConnect Components object. Furthermore, the following global functions are defined:

print(str)         : Print str

dump(str)          : *deprecated*, synonym for print()

exit()             : Terminate shell

quit()             : *deprecated*, synonym for exit()

load(url)          : Execute the script at 'url'

suspend()          : Suspend shell I/O until a call to resume().
                     Used in conjunction with resume() to provide
                     synchronization to asynchronous processes such
                     as e.g. document loads.

resume()           : Resume from a suspend()

addressOf(obj)     : Provide string uniquely identifying the given object.
                     This function can be used to generate keys for 
                     associative arrays.

setProtocol(p)     : One of 'interactive' (the default) or 'synchronous'.
                     'interactive': - shows prompt ("\n> ") to prompt for input
                     'synchronous': - shows prompt ("\n> ") to prompt for input
                                    - all output is either a prompt or of the 
                                      form "[n]str", where n is the number of 
                                      characters in str.

getProtocol()      : Returns currently used protocol, see setProtocol(p)

setContextObj(obj) : Change to a different JavaScript context. 
                     Can be used for executing code in a different context.
                     Switching back to the original context has to be done 
                     by installing suitable switching code on the new 
                     context prior to switching.

jssh-debug.js startup script

The JSSh module comes with the startup script chrome://jssh/content/jssh-debug.js, which defines some functions useful for debugging or interactively developing js code. The following functions are available:
help(func)             : Show help for one of the functions defined in 
                         jssh-debug.js

inspect(obj)           : Dumps information about 'obj'.

subobjs(obj, type)     : List all subobjects of object 'obj' that have type 
                         'type' (default='object')

dumpIDL(iid)           : Dumps idl code for interface 'iid'. 'iid' can either 
                         be an interface, or a string of form 
                         'Components.interfaces.nsIFoo' or just 'nsIFoo'

getWindows()           : Returns an array of all currently open windows.

findClass(/regex/)     : Finds registered classes matching /regex/

findInterface(/regex/) : Finds registered interfaces matching /regex/

dumpStack(offset, max) : Displays a stack trace up to depth 'max' (default: 10),
                         starting 'offset' frames below the current one.

getInstrumented(obj, func)
                       : Returns an object with instrumentation data for the
                         given (obj, func). It can be used to change
                         instrumentation functions or operation-flags for the
                         instrumentation functions . Members include:
                           'old': old method that will be called.
                           'before', 'after': before and after methods
                         The default before and after methods recognise the
                         flags (set by defining/unset by deleting):
                           'stack' : show stack trace
                           'nargs' : don't show args
                           'nresult' : don't show result/exception status 
                                       on exiting.

instrument(obj, func, beforeHook, afterHook) 
                       : Instruments func on obj. obj and func need to be
                         strings. Optional beforeHook and afterHook are the
                         methods called before and after the instrumented
                         method is being called. Signature for before hook
                         functions is hook(obj, func, argv). Signature for
                         after hook functions is hook(obj, func, argv, retval,
                         exception). Return value of after hook will be taken
                         as return value for function call

dumpInstrumented()     : Prints a list of all currently instrumented functions.

uninstrument(obj, fun) : Uninstruments a previously instrumented function 'fun' 
                         on object 'obj'. 'fun' and 'obj' both need to be given
                         as strings that eval() to the desired objects.

domNode(base, child1, child2, ...)
                       : Returns the dom node identified by base and the child 
                         offsets. A child offset can be an integer designating 
                         a 'normal' child or can be of the form "a5" to 
                         identify the anonymous child at position 5.

domDumpFull(node)      : Dumps the *complete* DOM starting from node.

rdfDump(url)           : Dumps the rdf datasource at 'url' in the following 
                         form:

                         <Resource> 
                          |
                          |--[Resource]--> <Resource>
                          |
                          |--[Resource]--> {literal}
                        
                         If 'url' points to a remote datasource, it will be 
                         refreshed before dumping.

domDump(node)          : Dumps the DOM around 'node'

reloadXBLDocument(url) : Flushes the xbl file 'url' from the chrome cache. 
                         If defined, reloadXBLDocumentHook(url) is called 
                         before flushing.

reloadXULDocument(url) : Flushes the xul file 'url' from the chrome cache. 
                         If defined, reloadXULDocumentHook(url) is called 
                         before flushing.

XEmacs as a client

Any telnet-like program can be used as an interactive JSSh client (The telnet shipping with MS Windows seems to have some CR/LF problems, though).

XEmacs is particularly suited as a client and the JSSh distribution contains an Emacs-Lisp file, moz-jssh.el with utilities for connecting to a JSSh server. This file can be found in mozilla/extensions/jssh/xemacs/ and can be automatically loaded on XEmacs startup by putting the following code into ~/.xemacs/init.el:

(setq load-path
      (append '("path_to_moz-jssh.el")
              load-path))
(require 'moz-jssh)

moz-jssh.el defines several different functions as well as a mode (moz-jssh mode) for connecting to a JSSh server and executing JavaScript code. To simply create an interactive moz-jssh mode shell, run the command M-x moz-jssh. All other functions defined in moz-jssh.el are prefixed with 'moz-jssh', so their documentation can be obtained by running M-x apropos "moz-jssh".

The host and port of the JSSh server are configured by the variables moz-jssh-host and moz-jssh-port, defaulting to "localhost" and 9997, respectively.

Some of the moz-jssh.el functions, such as e.g. moz-jssh-inspect and moz-jssh-inspect-interface require that the jssh-debug.js script is loaded into the JS context (see above).

Example session

This is an example session with XEmacs connecting to a JSSh server running in a Mozilla browser (Startup script chrome://jssh/content/jssh-debug.js):

Welcome to the Mozilla JavaScript Shell!

> 5+5
10
> help(getWindows)
getWindows() returns a list of all currently open windows

> var w0 = getWindows()[0]

> inspect(w0.getBrowser)
* Object type: function
* Parent chain: [object ChromeWindow @ 0x80e1328] 
* Arity: 0
* Function definition: 
function getBrowser() {
    if (!gBrowser) {
        gBrowser = document.getElementById("content");
    }
    return gBrowser;
}

> var browser = w0.getBrowser()

> domDump(browser)
* Parent: <hbox>
<tabbrowser id='content' flex='1' contenttooltip='aHTMLTooltip' 
 contentcontextmenu='contentAreaContextMenu' onnewtab='BrowserOpenTab();' 
 onbookmarkgroup='addGroupmarkAs();' onclick='return contentAreaClick(event);'>
* Anonymous Children: 
 A[0]: <xul:stringbundle>
 A[1]: <xul:tabbox>

> domDump(domNode(browser, 'a1'))
* Parent: <tabbrowser>
<xul:tabbox flex='1' eventnode='document' inherits='handleCtrlPageUpDown' 
 handleCtrlTab='true'>
* Children: 
 [0]: <xul:hbox>
 [1]: <xul:tabpanels>

> domDump(domNode(browser, 'a1', 1))
* Parent: <xul:tabbox>
<xul:tabpanels flex='1' class='plain' selectedIndex='0'>
* Children: 
 [0]: <xul:browser>
 [1]: <browser>

> domDump(domNode(browser, 'a1', 1, 1))
* Parent: <xul:tabpanels>
<browser type='content' contextmenu='contentAreaContextMenu' 
tooltip='aHTMLTooltip'>

> browser.loadURI("http://www.croczilla.com/svg/")

> domDumpFull(browser.contentDocument)
<HTML><HEAD>
[...]    
  </BODY></HTML>

> instrument("browser", "loadURI")

> dumpInstrumented()
browser.loadURI 

> browser.loadURI("http://www.croczilla.com/svg/samples/")
* Entering browser.loadURI
* Arguments(1): 'http://www.croczilla.com/svg/samples/' 
* Exiting browser.loadURI
* Result: undefined

> exit()
Goodbye!



Process moz-jssh<1> exited abnormally with code 256