Commits

Jan Borsodi committed c0d3190 Merge

Merged in 0.3 release.

Comments (0)

Files changed (106)

 c472d0dd2f0afc2b45421476a7e52d3493c2659c 0.1
 b442c320d4ab69340199b77887d57d73ea8c4fb5 0.2
 cfac3c5a1fbacb5f4fef68fa513a4aae2929e6cc 0.2.1
+ccb42024943b2a679ca969becfda1bd0866c86e3 0.3
+0.3 - 2010-10-27
+----------------
+
+hob
+~~~
+
+* The -v option is no longer used for version display but for enabling verbosity.
+* Positional arguments now display help text if available.
+* Location of template files has been moved (part of package_data) to solve cross-platform issues.
+
+proto
+~~~~~
+
+* Enum fields can now have default values, the enum value object can be fetched
+  from field.defaultObject() and the raw unparsed text from field.defaultText()
+
 0.2.1 - 2010-10-06
 ------------------
 
 
     options:
       -h, --help            show this help message and exit
-      -v, --version         show program's version number and exit
-      --verbose             increase verbosity
+      -v, --verbose         increase verbosity
+      --version             show program's version number and exit
       --quiet               be silent
       -c CONFIG_FILE, --config-file CONFIG_FILE
                             use specific config file instead of system-wide/local
 """A multi-language code generator for the Opera Scope Protocol. Code is generated from Google Protocol Buffer definitions.
 """
 
-__version_num__ = (0, 2, 1)
+__version_num__ = (0, 3)
 __version__ = ".".join(map(str, __version_num__))
 __author__ = "Jan Borsodi, Christian Krebs"
 __author_email__ = "jborsodi@opera.com, chrisk@opera.com"
         ]
     if js_test_framework:
         sources += [
-            ('runtimes.js', 'js-runtimes.mako', ''),
-            ('dom.js', 'js-DOM.mako', ''),
-            ('windows.js', 'js-windows.mako', ''),
+            ('runtimes.js', 'js/js-runtimes.mako', ''),
+            ('dom.js', 'js/js-DOM.mako', ''),
+            ('windows.js', 'js/js-windows.mako', ''),
             ]
 
     for file_name, template, rep in sources:
                 action = posarg[4]
             arg = {"names": [plural],
                    "metavar": singular.lower()}
+            if doc:
+                arg["help"] = doc
             kind = type(default)
             arg["kind"] = kind
             if kind in (tuple, list):
 
     def _resolveSymbols(self):
         for sym in self._symbols:
-            item = self.package.find(sym.name, sym.current)
+            current = sym.current
+            while isinstance(current, Symbol):
+                current = current.resolved
+            item = self.package.find(sym.name, current)
+            sym.resolved = item
             if not isinstance(item, sym.kind):
-                raise TypeError("Expected type %s for symbol %s.%s" % (sym.type.__name__, sym.package, sym.name))
-            setattr(sym.owner, sym.attribute, item)
+                raise TypeError("Expected type %s for symbol %s" % (sym.kind.__name__, ".".join(current.path() + [sym.name])))
+            if sym.attribute != None:
+                setattr(sym.owner, sym.attribute, item)
             if sym.callback:
                 sym.callback(sym, item)
         self._symbols = []
         self._reset_docs()
         if not hasattr(self._item, "setDefault"):
             raise BuildError("Cannot set default value on current item (%r)" % type(self._item))
-        parsed_value = parse_option_value(value)
-        if isinstance(self._item, Field):
-            if self._item.type == Proto.Bool:
-                if type(parsed_value) != bool:
-                    raise BuildError("Default value for bool field must be either true or false")
-            elif self._item.type in (Proto.Int32, Proto.Sint32, Proto.Uint32, Proto.Int64, Proto.Sint64, Proto.Uint64, Proto.Fixed32, Proto.Fixed64):
-                if type(parsed_value) != int:
-                    raise BuildError("Default value for numeric field must be a number")
-            elif self._item.type == Proto.String:
-                if not isinstance(parsed_value, str):
-                    raise BuildError("Default value for string field must be a string or identifier")
-        self._item.setDefault(parsed_value)
+        self._item.setDefaultText(value)
+
+        # If the field is an enum then the value must be looked up later, the name is enum value of the enum
+        if isinstance(self._item, Field) and self._item.message:
+            def set_default_enum(sym, item):
+                if isinstance(sym.owner.message, Enum):
+                    sym.owner.setDefaultObject(item)
+                else:
+                    raise BuildError("Messages can't have default values")
+            enum_sym = Symbol(value, self._item, None, EnumValue, current=self._item.message, callback=set_default_enum)
+            self._symbols.append(enum_sym)
+        else:
+            parsed_value = parse_option_value(value)
+            if isinstance(self._item, Field):
+                if self._item.type == Proto.Bool:
+                    if type(parsed_value) != bool:
+                        raise BuildError("Default value for bool field must be either true or false")
+                elif self._item.type in (Proto.Int32, Proto.Sint32, Proto.Uint32, Proto.Int64, Proto.Sint64, Proto.Uint64, Proto.Fixed32, Proto.Fixed64):
+                    if type(parsed_value) != int:
+                        raise BuildError("Default value for numeric field must be a number")
+                elif self._item.type == Proto.String:
+                    if not isinstance(parsed_value, str):
+                        raise BuildError("Default value for string field must be a string or identifier")
+            self._item.setDefault(parsed_value)
 
     def on_comment(self, comment):
         self._comments.append(comment)
             q = Quantifier.Required
         self.q = q
         self.default = default
+        self.default_text = str(default)
+        self.default_object = default
         self.message = message
         self.comment = None
 
         return ptype.name
 
     def defaultValue(self):
-        if self.type == Proto.Bool:
+        if self.message and isinstance(self.message, Enum):
+            if isinstance(self.default_object, EnumValue):
+                return self.default_object.name
+            raise ProtoError("Invalid default value for Enum object %s" % ".".join(self.message.path()))
+        elif self.type == Proto.Bool:
             if self.default:
                 return "true"
             else:
         else:
             return str(self.default)
 
+    def defaultObject(self):
+        return self.default_object
+
+    def defaultText(self):
+        return self.default_text
+
     def setDefault(self, value):
         self.default = value
+        self.default_object = value
+
+    def setDefaultText(self, text):
+        self.default_text = text
+
+    def setDefaultObject(self, item):
+        self.default_object = item
 
 class Message(Element):
     fields = []
     "pdb": False,
 }
 
+def showVersion(version):
+    "Returns an argparse Action for showing the program version"
+    class ShowVersion(argparse.Action):
+        def __init__(self, **kwargs):
+            kwargs.update(
+                dict(
+                    dest=argparse.SUPPRESS,
+                    default=argparse.SUPPRESS,
+                    nargs=0,
+                )
+            )
+            super(ShowVersion, self).__init__(**kwargs)
+
+        def __call__(self, parser, namespace, values, option_string=None):
+            parser.exit(message=version)
+
+    return ShowVersion
+
 global_options = [
-    ('', 'verbose', False,
+    ('v', 'verbose', False,
      _('increase verbosity')),
+    ('', 'version', False,
+     _("show program's version number and exit"),
+     showVersion(__version__),
+    ),
     ('', 'quiet', False,
      _('be silent')),
     ('c', 'config_file', '',
         ui.config.read(["hob_private.conf"])
     ui.config.reads("[hob]\ntarget=current\n")
     _exts.setup(ui, cmds)
-    parser = argparse.ArgumentParser(version=__version__, prog=__program__)
+    parser = argparse.ArgumentParser(prog=__program__)
     cmds.setup(parser)
     opts = parser.parse_args(args)
     cmds.loadconfig(ui.config, opts)
 class Generator(object):
     def __init__(self, level=4, subdir="proto", lookup_args={}):
         self.level = level
-        self.directories = [os.path.join(root, 'templates'),
+        self.directories = [os.path.join(root, 'hob/templates'),
+                            os.path.join(root, 'templates'),
                             os.path.join(sys.prefix, 'share/hob/templates')]
         self.lookup = TemplateLookup(directories=self.directories,
                                      module_directory=tempdir(["mako", subdir]),

hob/templates/html-doc/coredoc.css

+/* Common style sheet for all core documentation */
+
+body { 
+  margin: 2em;
+  padding: 1em 0;
+  /* font-family: sans-serif; */
+  background: white;
+  color: black;
+}
+
+DIV.logo { /* the Opera logo */
+  float: right;
+}
+
+h1, h2, h3 {
+  font-weight: normal;
+}
+
+h2 {
+	clear: both;
+}
+   
+dt{
+	font-weight: bold;
+}
+h1 {
+   counter-reset: header2;
+   clear: both;
+}  
+ 
+h2:before{
+   counter-increment: header2; 
+   content: counter(header2) "  ";
+   counter-reset:header3 header4;
+}
+
+h2 {
+   background: #EEE;
+}
+
+h3:before {
+   counter-increment: header3; 
+   content: counter(header2) "." counter(header3) "  ";
+   counter-reset:header4;
+}
+
+h4:before {
+   counter-increment: header4; 
+   content: counter(header2) "." counter(header3) "." counter(header4) "  ";
+}
+
+dl.metadata {
+  margin-bottom: 3em;
+  padding: 0.3em;
+  background: #EEE;
+}
+
+.metadata dt { 
+  font-weight: bold;
+
+}
+
+.metadata dt:after { 
+  content: ":";
+}
+
+.metadata .distribution {
+	text-transform: uppercase; 
+	font-weight: bold; 
+	color: red;
+}
+.even{
+	background: #FFC;
+}
+
+.odd{
+	background: #FFE;
+}
+
+th {text-align: left; background: #CC9} 
+th, td {vertical-align: top;  font-size: small;}
+
+.element {
+	font-family: monospace;
+	font-weight: bold;
+	color: #006;
+}
+
+.attribute {
+	font-family: monospace;
+	font-weight: bold;
+	color: #060;
+}
+
+.code { 
+  font-family: monospace;
+  font-size: smaller;
+}
+
+.comment {
+  color: #0B0;
+}
+
+.ok {
+  color: green
+}
+
+.not-started {
+  color: red
+}
+
+.in-progress {
+  color: orange
+}
+
+.active {
+  background-color: yellow
+}
+
+.milestone {
+  background-color: lightblue
+}
+
+.done {
+  background-color: lightgreen;
+}
+.docdata dl dt:after
+{
+  content: ":";
+}
+.docdata dl dd
+{
+  color: #f60;
+  font-weight: bold;
+}

hob/templates/html-doc/html-doc-footer.mako

+<div id="footer">
+Copyright © 2009 Opera Software ASA. All rights reserved.
+</div>
+</body>
+</html>

hob/templates/html-doc/html-doc-header.mako

+<!doctype html>
+<html>
+<head>
+<title>${title}</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<link rel="stylesheet" href="coredoc.css">
+<style>
+h3 { font-weight: bold; font-style: italic }
+h3::before { font-style: normal }
+.string { color: #600; }
+.comment { color: #060; text-decoration: none; }
+.comment a { color: #066; text-decoration: underline; }
+
+
+body > pre
+{
+  border: 1px solid #ccc;
+  background-color: hsl(0, 0%, 98%);
+  white-space: pre-wrap;
+  margin: 1.5em 0;
+  padding: .5em;
+}
+.message-class
+{
+  color: #00c;
+  font-weight: bold;
+}
+
+.int
+{
+  color: #006;
+  font-weight: bold;
+}
+/*
+.string
+{
+  color: green;
+  font-weight: bold;
+}
+.bool
+{
+  color: pink;
+  font-weight: bold;
+}
+.comment
+{
+  color: #999;
+}
+a
+{
+  text-decoration: none;
+  color: #666;
+}
+a:hover
+{
+  color: #000;
+  text-decoration: underline;
+}
+*/
+</style>
+% for script in scripts:
+  % if 'src' in script:
+<script src="${script['src']}"></script>
+  % else:
+<script>${script['text']}</script>
+  % endif
+% endfor
+<body>
+<p class="logo"><img src="http://www.opera.com/media/images/logo/ologo_wback.gif"></p>
+

hob/templates/html-doc/html-doc-index.mako

+<%include file="html-doc-header.mako"/>
+<h1>STP 1 Service Definitions</h1>
+<%include file="html-doc-status.mako"/>
+<ul>
+% for service in services:
+    <li><a href="${service.name}.html">${service.name}</a></li>
+% endfor
+</ul>
+<%include file="html-doc-footer.mako"/>
+
+

hob/templates/html-doc/html-doc-message.mako

+<%
+  def formatDefault(field):
+    if field.default == None:
+      return ""
+    return " [default = %s]" % field.defaultValue()
+  def formatComment(field):
+    if not field.comment:
+      return ""
+    return " // " + field.comment.replace("\n", " ")
+  def formatDoc(field):
+    if not field.doc:
+      return ""
+    return field.doc.toComment() 
+  TYPE_MAP = {
+    'uint32': 'int',
+    'string': 'string',
+    'bool': 'bool'
+    }
+    
+%>\
+<pre id="${message.name}">
+% if message.doc:
+<span class="comment">${message.doc.toComment()}</span>
+% endif
+message <span class="message-class">${message.name}</span>
+{
+% if submessages:
+  % for submessage in submessages:
+	  % for line in submessage.splitlines(False):
+    ${line}
+	  % endfor 
+  % endfor 
+% endif
+<%
+  fields = []
+  lengths = [0]*7
+  for field in message.fields:
+    columns = [formatDoc(field), field.q.name, field.typename(), field.name, 
+                  str(field.number), formatDefault(field), formatComment(field)]
+    for i in range(4):
+      lengths[i] = max(lengths[i], len(columns[i + 1]))
+    fields.append(columns)
+%>\
+% for doc, quantifier, kind, name, number, default, comment in fields:
+  % if doc:
+    <span class="comment">
+    % for line in doc.splitlines():
+    ${line}
+    % endfor
+    </span>
+  % endif
+  % if kind in TYPE_MAP: 
+    ${quantifier.ljust(lengths[0])} \
+<span class="${TYPE_MAP[kind]}">${kind.ljust(lengths[1])}</span> \
+${name.ljust(lengths[2])} = \
+${number.rjust(lengths[3])}${default};<span class="comment">${comment}</span>
+  % else:
+    ${quantifier.ljust(lengths[0])} \
+<a href="#${kind}" class="message-class">${kind.ljust(lengths[1])}</a> \
+${name.ljust(lengths[2])} = \
+${number.rjust(lengths[3])}${default};<span class="comment">${comment}</span>
+  % endif
+% endfor
+}
+</pre>

hob/templates/html-doc/html-doc-service.mako

+<%include file="html-doc-header.mako"/>
+% if service.doc:
+${service.doc.toComment()}
+% endif
+<h1>Service ${service.name}</h1>
+<%include file="html-doc-status.mako"/>
+<h2>Service Definition</h2>
+<pre>
+service ${service.name}
+{
+    option version          = ${strquote(service.version)};
+    option core_release     = ${strquote(service.coreRelease)};
+<% 
+  commands = [(
+      command.name,
+      command.doc,
+      command.messageName(),
+      command.responseName(),
+      command.id,
+      len(command.name) + len(command.messageName()),
+      len(command.responseName()) ) for command in service.itercommands()]
+  events = [(
+      event.name,
+      event.messageName(),
+      event.id,
+      len(event.name),
+      len(event.messageName()) ) for event in service.iterevents()]
+  max_command_length = max(map(lambda x: x[5], commands) + map(lambda x: x[3], events) + [0])
+  max_return_length = max(map(lambda x: x[6], commands) + map(lambda x: x[4], events)+ [0])
+%>
+% for name, doc, messageName, responseName, id, com_length, res_length in commands:
+  % if doc:
+    % for line in doc.toComment().splitlines():
+    ${line}
+    % endfor
+  % endif
+    command ${name}(<a href="#${messageName}" class="message-class">${messageName}</a>) ${' ' * (max_command_length - com_length)}\
+returns <a href="#${responseName}" class="message-class">${responseName}</a> ${' ' * (max_return_length - res_length)}= ${id};
+% endfor
+% for name, messageName, id, com_length, res_length in events:
+    event   ${name}   ${' ' * (max_command_length - com_length)}returns <a href="#${messageName}" class="message-class">${messageName}</a> ${' ' * (max_return_length - res_length)}= ${id};
+% endfor
+}
+</pre>
+<h2>Messages</h2>
+

hob/templates/html-doc/html-doc-status.mako

+<div class="docdata">
+  <dl>
+     <dt>Status</dt><dd>Draft</dd>
+     <dt>Introduced</dt><dd>Core 2.4</dd>
+  </dl>
+</div>

hob/templates/js/clientlib_async.js

+/* Copyright 2006 Opera Software ASA.  */
+
+/**
+ * @fileoverview
+ * 
+ * Convenience library for interacting with the scope proxy.
+ *
+ * The "proxy" object is a singleton that encapsulates logic for
+ * interacting with the debugging proxy.  It is built on top of
+ * XMLHttpRequest.
+ *
+ * Every request to the proxy is synchronous, and the proxy may hold
+ * it for up to 20 seconds before returning a value.  The value is
+ * always XML data.  If no real data is available to return from the
+ * service in 20 seconds, then the proxy returns "<timeout />", and
+ * this code should (optionally) retry the request.
+ *
+ * Every access has a response value, which is normally "<ok />" if
+ * nothing sensible can be returned.
+ *
+ * Properties of this object are read-only except where explicitly 
+ * stated.
+ */
+
+window.cls || ( window.cls = {} );
+
+window.cls.Proxy = function()
+{
+  var self = this;
+
+  /** Configure the proxy connection and lookup the available
+    * services.  This must be called initially.
+    *
+    * @param host (optional) The host name or IP for the proxy
+    * @param port (optional) The port for the proxy
+    * @return     Nothing.
+    * @exceptions Throws if the access to /services failed
+    */
+  this.configure = function ( host, port ) 
+  {
+    if (host) { _host = host; }
+    if (port) { _port = port; }
+    if(_port == window.location.port)
+    {
+      this.GET("/services", parseConfigureResponse);
+    }
+    else
+    {
+      opera.postError("failed to configure the proxy, " +
+        "host and port must be the same as the main document");
+    }
+  }
+
+  var parseConfigureResponse = function (xml, xhr)
+  {
+    var service_elts = xml.getElementsByTagName("service");
+    if (service_elts.length)
+    {
+      var services = new Array();
+      for ( var i=0 ; i < service_elts.length ; i++ )
+      {
+        services.push(service_elts[i].getAttribute("name"));
+      }
+      self.services = services;
+      self.onsetup(xhr);
+    }
+    else
+    {
+      setTimeout(function(){
+        self.GET("/services", parseConfigureResponse);
+      }, 100);
+    }
+  }
+
+  this.onsetup = function(){}
+
+  /** Enable a named service if possible.
+    *
+    * @param service_name  The name of the requested service.
+    * @return      true if enabling succeeded, false otherwise
+    * @exceptions  Throws if the access to the proxy failed
+    */
+  this.enable = function (service_name) 
+  {
+    for (var i=0; i < this.services.length && this.services[i] != service_name ; i++ );
+    if (i == this.services.length)
+    {
+      return false;
+    }
+    self.GET( "/enable/" + service_name, function(){} );  // ignore the response
+    return true;
+  }
+
+  /** Send a GET message to the configured host/port, wait until
+    * there's a response, then return the response data.
+    *
+    * This function will retry the operation if "<timeout />" is
+    * returned from the proxy.
+    *
+    * @param msg  The full message to send, including leading "/"
+    * @return     The responseXML property of the XMLHttpRequest 
+    * @exceptions Throws an exception if the return code is not 200
+    * changed the code to work in an asynchroneous environment
+    */
+  this.GET = function( msg, cb ) 
+  {
+    var x = new XMLHttpRequest;
+    x.onload=function()
+    {
+      if ( this.status != 200) 
+      {
+        throw "Message failed, Status: " + this.status + ", msg: " + msg ;
+      }
+      self.onReceive(x);
+      var xml = this.responseXML;
+      if ( ( !xml || xml.documentElement == null ) && !this.responseText )
+      {
+        if(client)
+        {
+          client.onquit();
+          return;
+        }
+        else
+        {
+          throw "Message failed, GET, empty document: " + this.responseText;
+        }
+      }
+      if(cb) 
+      {
+        cb(xml, x)
+      }
+      else
+      {
+        throw "Loop broken: "+ this.responseText;
+      }
+    }
+    x.open("GET", "http://" + _host + ":" + _port + msg);
+    x.send("");
+  }
+
+
+  /** Send a POST message to the configured host/port, wait until
+    * there's a response, then return the response data.
+    *
+    * This function will *not* retry the operation if "<timeout />" is
+    * returned from the proxy; it should only be used when the service
+    * has been enabled and Opera is known to be listening for data.
+    *
+    * @param msg   The full message to send, including leading "/"
+    * @param data  XML data to post
+    * @return      The responseXML property of the XMLHttpRequest 
+    * @exceptions Throws an exception if the return code is not 200
+    */
+    this.POST = function ( msg, data, cb ) 
+    {
+      var x = new XMLHttpRequest;
+      x.onload=function()
+      {
+        if (this.status != 200) 
+        {
+          throw "Message failed, Status: " + this.status;
+        }
+        //self.onReceive(x);
+        var xml = this.responseXML;
+        if (xml.documentElement == null)
+        {
+          throw "Message failed, POST, empty document: " + this.responseText;
+        }
+        if(cb) cb(xml);
+      }
+      x.open("POST", "http://" + _host + ":" + _port + msg );
+      x.send(data);
+    }
+
+    /** WRITABLE.
+      * Installable handler that will be called every time a GET request 
+      * times out.  By default this does nothing.
+      *
+      * A typical thing for a user handler to do here would be to update 
+      * the UI (eg "waiting..."), or throw an exception to break out of the 
+      * GET call.
+      */
+    this.onTimeout = function () 
+    {
+        // Do nothing by default
+    }
+
+    /** WRITABLE.
+      * Installable handler that will be called with the XMLHttpRequest
+      * object every time a send() completes, before any further processing
+      * is done.
+      *
+      * Useful for logging, debugging, and ad-hoc correction of incoming data.
+      */
+    this.onReceive = function (x) 
+    {
+	    // Do nothing by default
+    }
+
+    /* Proxy host */
+    var _host =  "127.0.0.1";
+
+    /* Proxy port */
+    var _port = "8002"; 
+
+    /* Array of service names */
+    this.services =  []
+};
+    

hob/templates/js/get_message_maps.js

+window.cls || (window.cls = {});
+window.cls.ScopeInterfaceGenerator = function (){};
+
+/* static interface */
+window.cls.ScopeInterfaceGenerator.pretty_print_interface = function(map){};
+
+window.cls.ScopeInterfaceGenerator = function ()
+{
+
+  /* interface */
+  this.get_interface = function(service_descriptions, onsuccess, onerror, should_get_messages){};
+  
+  /* constants */
+  const 
+  INDENT = '  ',
+  NAME = 1,
+  FIELD_LIST = 2,
+  FIELD_NAME = 0,
+  FIELD_TYPE = 1,
+  FIELD_NUMBER = 2,
+  FIELD_Q = 3,
+  FIELD_ID = 4,
+  ENUM_ID = 5,
+  Q_MAP = 
+  {
+    0: "required",
+    1: "optional",
+    2: "repeated"
+  },
+  MSG_TYPE_COMMAND = 1,
+  MSG_TYPE_RESPONSE = 2,
+  MSG_TYPE_EVENT = 3,
+  // Command Info
+  COMMAND_LIST = 0,
+  EVENT_LIST = 1,
+  MSG_NAME = 0,
+  NUMBER = 1,
+  MESSAGE_ID = 2,
+  RESPONSE_ID = 3,
+  // Command MessageInfo
+  MSG_LIST = 0,
+  MSG_ID = 0;
+  
+  /* private */
+  this._service_infos = null;
+  this._map = null;
+  // ===========================
+  // get the messages from scope
+  // ===========================
+  
+  this._request_enums = function()
+  {
+    var service = '', tag = 0;
+    for (service in this._service_infos)
+    {
+      tag = tagManager.set_callback(this, this._handle_enums, [service]);
+      services["scope"].requestEnumInfo(tag, [service, [], 1]);
+    }
+  };
+  
+  this._handle_enums = function(status, msg, service)
+  {
+    if (!status)
+    {
+      this._service_infos[service]['raw_enums'] = msg;
+      if (this._is_map_complete('raw_enums'))
+        this._request_info();
+    }
+    else
+      this._onerror({message: "failed to get enums for " + service + " in _handle_enums"});
+  };
+
+  this._request_info = function()
+  {
+    var service = '', tag = 0;
+    for (service in this._service_infos)
+    {
+      tag = tagManager.set_callback(this, this._handle_infos, [service]);
+      services["scope"].requestInfo(tag, [service]);
+    }
+  };
+
+  this._handle_infos = function(status, msg, service)
+  {
+    if (!status)
+    {
+      this._service_infos[service]['raw_infos'] = msg;
+      if (this._should_get_messages)
+      {
+        var tag = tagManager.set_callback(this, this._handle_messages, [service]);
+        services["scope"].requestMessageInfo(tag, [service, [], 1, 1]);
+      }
+      else
+        this._handle_messages(status, [], service);
+
+    }
+    else
+      this._onerror({message: "failed to get infos for " + service + " in _handle_infos"});
+  };
+
+  this._handle_messages = function(status, msg, service)
+  {
+    if (!status)
+    {
+      this._service_infos[service]['raw_messages'] = msg;
+      try
+      {
+        this._parse_raw_lists(service);
+      }
+      catch(e)
+      {
+        this._service_infos[service]['raw_messages'] = null;
+        this._onerror({message: "failed to build the message maps for " + service});
+      }
+      if (this._is_map_complete('raw_messages'))
+        this._finalize();
+    }
+    else
+      this._onerror({message: "failed to get infos for " + service + " in _handle_infos"});
+  };
+
+  this._is_map_complete = function(prop)
+  {
+    for (var service in this._service_infos)
+      if (!this._service_infos[service][prop])
+        return false;
+    return true;
+  };
+
+  // =======================
+  // create the message maps
+  // =======================
+
+  this._get_msg = function(list, id)
+  {
+    const MSG_ID = 0;
+    for (var i = 0; i < list.length && list[i][MSG_ID] !== id; i++);
+    return list[i];
+  };
+
+  this._get_enum = function(list, id)
+  {
+    var 
+    enums = this._get_msg(list, id),
+    name = enums && enums[1] || '', 
+    dict = {}, 
+    enum = null, 
+    i = 0;
+
+    if (enums && enums.length == 3)
+      for (; enum = enums[2][i]; i++)
+        dict[enum[1]] = enum[0];
+    return [name, dict];
+  };
+
+            
+  this._parse_msg = function(msg, msg_list, parsed_list, raw_enums, ret)
+  {
+    var field = null, i = 0, name = '', field_obj = null, enum = null, sub_msg = null;
+    if (msg)
+    {
+      for (; i < msg[FIELD_LIST].length; i++)
+      {
+        field = msg[FIELD_LIST][i];
+        name = field[FIELD_NAME];
+        field_obj = 
+        {
+          name: name,
+          q: 'required',
+          type: field[FIELD_TYPE],
+        };
+        if (field[FIELD_Q])
+          field_obj.q = Q_MAP[field[FIELD_Q]];
+        if (field[FIELD_ID])
+        {
+          if (name in parsed_list)
+          {
+            field_obj.message = parsed_list[name].message;
+            field_obj.message_name = parsed_list[name].message_name;
+          }
+          else
+          {
+            parsed_list[name] = field_obj;
+            sub_msg = this._get_msg(msg_list, field[FIELD_ID]);
+            field_obj.message_name = sub_msg && sub_msg[1] || 'default';
+            field_obj.message = [];
+            this._parse_msg(sub_msg, msg_list, parsed_list, raw_enums, field_obj.message);
+          }
+        }
+        if (field[ENUM_ID])
+        {
+          enum = this._get_enum(raw_enums, field[ENUM_ID]);
+          field_obj.enum = {name: enum[0], numbers: enum[1]};
+        }
+        ret.push(field_obj);
+      }
+    }
+    return ret
+  };
+
+  this._parse_raw_lists = function(service)
+  {
+    var 
+    map = this._map[service] = {},
+    command_list = this._service_infos[service].raw_infos[COMMAND_LIST],
+    msgs = this._service_infos[service].raw_messages &&
+        this._service_infos[service].raw_messages[MSG_LIST] || [],
+    enums = this._service_infos[service].raw_enums && 
+        this._service_infos[service].raw_enums[MSG_LIST] || [],
+    command = '',
+    command_obj = null, 
+    event_list = this._service_infos[service].raw_infos[EVENT_LIST],
+    event = null,
+    event_obj = null,
+    msg = null,
+    i = 0;
+
+    for (; i < command_list.length; i++)
+    {
+      command = command_list[i];
+      command_obj = map[command[NUMBER]] = {};
+      command_obj.name = command[MSG_NAME];
+      if (this._should_get_messages)
+      {
+        msg = this._get_msg(msgs, command[MESSAGE_ID]);
+        command_obj[MSG_TYPE_COMMAND] = this._parse_msg(msg, msgs, {}, enums, []);
+        msg = this._get_msg(msgs, command[RESPONSE_ID]);
+        command_obj[MSG_TYPE_RESPONSE] = this._parse_msg(msg, msgs, {}, enums, []);
+      }
+    };
+    if (event_list)
+      for (i = 0; i < event_list.length; i++)
+      {
+        event = event_list[i];
+        event_obj = map[event[NUMBER]] = {};
+        event_obj.name = event[MSG_NAME];
+        if (this._should_get_messages)
+        {
+          msg = this._get_msg(msgs, event[MESSAGE_ID]);
+          event_obj[MSG_TYPE_EVENT] = this._parse_msg(msg, msgs, {}, enums, []);
+        }
+      }
+  };
+
+  this._finalize = function()
+  {
+    this._onsuccess(this._map);
+    this._onsuccess = null;
+    this._onerror = null;
+    this._service_infos = null;
+    this._map = null;
+  };
+
+  /* implementation */
+  
+  this.get_interface = function(service_descriptions, onsuccess, onerror, should_get_messages)
+  {
+    /**
+      * service_descriptions must be a dictonary of services. 
+      * Each service must have a name and a version.  
+      * service_descriptions is typically created with the 
+      * respond message of the HostInfo command of the scope service.
+      */
+    if (!service_descriptions || !onsuccess || !onerror)
+      throw new Error("get_maps must be called with a service_descriptions dictionary and " +
+          "an onsuccess and an onerror callback");
+    this._onsuccess = onsuccess;
+    this._onerror = onerror;
+    this._service_infos = {};
+    this._map = {};
+    this._should_get_messages = should_get_messages === false ? false : true;
+    for (var service in service_descriptions)
+    {
+      if (!/^stp-|^core-/.test(service))
+      {
+        this._service_infos[service] =
+        {
+          'raw_enums': null,
+          'raw_infos': null,
+          'raw_messages': null,
+        }
+      }
+    }
+    if (service_descriptions.scope)
+    {
+      var version = service_descriptions.scope.version.split('.').map(Number);
+      if (version[1] >= 1) 
+        this._request_enums();
+      else 
+        this._request_info();
+    }
+    else 
+      this._onerror({message: "failed to get maps, no scope in  service descriptions"});
+  }
+};
+
+// =========================
+// pretty print message maps
+// =========================
+  
+window.cls.ScopeInterfaceGenerator.pretty_print_interface = function(map)
+{
+  var pp_map = this._pretty_print_object('', map, 0, ['message map =']);
+  window.open("data:text/plain," + encodeURIComponent(pp_map.join('\n')));
+};
+
+window.cls.ScopeInterfaceGenerator._pretty_print_object = 
+function(name, obj, level, print_list, circular_check_list)
+{
+  circular_check_list = circular_check_list && circular_check_list.slice(0) || [];
+  const
+  TYPE =
+  {
+     1: 'NUMBER', // Double
+     2: 'NUMBER', // Float
+     3: 'NUMBER', // Int32
+     4: 'NUMBER', // Uint32
+     5: 'NUMBER', // Sint32
+     6: 'NUMBER', // Fixed32
+     7: 'NUMBER', // Sfixed32
+     8: 'BOOLEAN', // Bool
+     9: 'STRING', // String
+    10: 'BYTES', // Bytes
+    11: 'MESSAGE', // Message
+    12: 'NUMBER', // Int64 (not supported yet)
+    13: 'NUMBER', // Uint64 (not supported yet)
+    14: 'NUMBER', // Sint64 (not supported yet)
+    15: 'NUMBER', // Fixed64 (not supported yet)
+    16: 'NUMBER', // Sfixed64 (not supported yet)
+  },
+  MSG_TYPE =
+  {
+    1: "Command",
+    2: "Response",
+    3: "Event",
+  };
+  print_list.push(this._get_indent_string(level) + this._quote(name, ': ') + '{');
+  level++;
+  for (var key in obj)
+  {
+    if (typeof obj[key] == 'string' || typeof obj[key] == 'number')
+    {
+      if (key == 'type' && /^\d+$/.test(obj[key]))
+        print_list.push(this._get_indent_string(level) + 
+            this._quote(key) + ': "' + TYPE[obj[key]] + '", // ' + obj[key]);
+      else
+        print_list.push(this._get_indent_string(level) + 
+            this._quote(key) + ': ' + this._quote(obj[key]) +',');
+    }
+  }
+  for (key in obj)
+  {
+    if (Object.prototype.toString.call(obj[key]) == '[object Object]')
+      this._pretty_print_object(key, obj[key], level, print_list, circular_check_list || []);
+  }
+  for (key in obj)
+  {
+    if (Object.prototype.toString.call(obj[key]) == '[object Array]')
+    {
+      if (key == 'message')
+      {
+        if (circular_check_list.indexOf(obj.message_name) != -1)
+        {
+          print_list.push(this._get_indent_string(level) + '"message": <circular reference>,');
+          continue;
+        }
+        else
+          circular_check_list.push(obj.message_name);
+      }
+      if (level == 3)
+        print_list.push(this._get_indent_string(level) + '// ' + MSG_TYPE[key]);
+      if (obj[key].length)
+      {
+        print_list.push(this._get_indent_string(level) + this._quote(key) + ': [');
+        for (var i = 0; i < obj[key].length; i++)
+          this._pretty_print_object('', obj[key][i], level + 1, print_list, circular_check_list);
+        print_list.push(this._get_indent_string(level) + '],');
+      }
+      else
+        print_list.push(this._get_indent_string(level) + this._quote(key) + ': [],');
+    }
+  }
+  level--;
+  print_list.push(this._get_indent_string(level) + '},');
+  return print_list;
+};
+
+window.cls.ScopeInterfaceGenerator._get_indent_string = function(level, indent)
+{
+  return new Array(level).join(indent || '  ');
+};
+
+window.cls.ScopeInterfaceGenerator._quote = function(value, token)
+{
+  return value ? (/^\d+$/.test(value) ?  value : '"' + value + '"') + (token || '') : '';
+}
+ 

hob/templates/js/js-DOM.mako

+/**
+ * (This file was autogenerated by hob)
+ */
+
+% for service in services:
+  % if service.name == "EcmascriptDebugger" and create_test_framework:
+<%
+  service_name = dashed_name(service.name)
+  commands = [command for command in service.itercommands()]
+  events = [event for event in service.iterevents()]
+  if "version" not in service.options:
+      raise Exception("Option 'version' is not set on service %s" % service.name)
+  version = service.options["version"].value
+%>\
+window.cls || (window.cls = {});
+cls.${service.name} || (cls.${service.name} = {});
+cls.${service.name}["${version}"] || (cls.${service.name}["${version}"] = {});
+
+/**
+  * @constructor 
+  */
+
+cls.${service.name}["${version}"].DOM = function()
+{
+  ## ************************************************************** ##
+  ## interface
+  ## ************************************************************** ##
+  /* interface */
+
+  /* scope interfaces */
+  ## ************************************************************** ##
+  ## commands
+  ## ************************************************************** ##
+    % for command in commands:
+      % if command.name in ["InspectDom"]:
+  this.handle${command.name} = function(status, message){};
+      % endif
+    % endfor
+  /**
+    * an event handler for a loaded top runtime
+    * call this function will trigger the retrieval 
+    * of the whole DOM tree of the top runtime
+    */
+  this.on_top_runtime_loaded = function(top_runtime_id){};
+
+  // privat
+
+  var self = this;
+
+  this._on_root_id = function(status, message)
+  {
+    const
+    /* EvalResult */
+    STATUS = 0, 
+    TYPE = 1, 
+    EVAL_RESULT = 3, 
+    /* ObjectValue */
+    OBJECT_ID = 0;
+
+    if( status == 0 && message[STATUS] == "completed" && message[TYPE] == "object" )
+    {
+      var root_id = message[EVAL_RESULT][OBJECT_ID];
+      services["${service_name}"].requestInspectDom(0, [root_id, "subtree"])
+    }
+    else
+    {
+      // TODO
+    }
+  }
+
+  this._format_processing_instruction_value = function(str)
+  {
+    var 
+      r_attrs = str.split(' '), 
+      r_attr = '', 
+      i = 0, 
+      attrs = '', 
+      attr = null;
+    
+    for( ; i < r_attrs.length; i++)
+    {
+      if(r_attr = r_attrs[i])
+      {
+        attr = r_attr.split('=');
+        attrs += " <key>" + 
+          attr[0].toLowerCase() + 
+          "</key>=<value>" + 
+          attr[1] + 
+          "</value>";
+      }
+    }
+    return attrs;
+  }
+
+  this._create_view = function(data)
+  {
+    const 
+      ID = 0, 
+      TYPE = 1, 
+      NAME = 2, 
+      DEPTH = 3,
+      NAMESPACE = 4, 
+      VALUE = 7, 
+      ATTRS = 5,
+      ATTR_PREFIX = 0,
+      ATTR_KEY = 1, 
+      ATTR_VALUE = 2,
+      CHILDREN_LENGTH = 6, 
+      PUBLIC_ID = 4,
+      SYSTEM_ID = 5,
+      ELEMENT_NODE = 1,
+      TEXT_NODE = 3,
+      CDATA_SECTION_NODE = 4,
+      ENTITY_REFERENCE_NODE = 5,
+      ENTITY_NODE = 6,
+      PROCESSING_INSTRUCTION_NODE = 7,
+      COMMENT_NODE = 8,
+      DOCUMENT_NODE = 9,
+      DOCUMENT_TYPE_NODE = 10,
+      DOCUMENT_FRAGMENT_NODE = 11,
+      NOTATION_NODE = 12;
+
+    var 
+      tree = "<div>", 
+      i = 0, 
+      node = null, 
+      length = data.length,
+      attrs = null, 
+      attr = null, 
+      k = 0, 
+      key = '',
+      is_open = 0,
+      has_only_one_child = 0,
+      one_child_id = "",
+      one_child_value = '',
+      current_depth = 0,
+      child_pointer = 0,
+      child_level = 0,
+      j = 0,
+      children_length = 0,
+      closing_tags = [],
+      node_name = '',
+      tag_head = '',
+      class_name = '',
+      re_formatted = /script|style|#comment/i,
+      container = document.getElementById('dom-tree-container');
+
+    if( data.length )
+    {
+      for( ; node = data[i]; i ++ )
+      {
+        while( current_depth > node[DEPTH] )
+        {
+          tree += closing_tags.pop();
+          current_depth--;
+        }
+        current_depth = node[DEPTH];
+        children_length = node[CHILDREN_LENGTH];
+        child_pointer = 0;
+        node_name =  
+          ( ( node[NAMESPACE] ? node[NAMESPACE] + ':' : '' ) +  node[NAME] ).toLowerCase();
+        switch ( node[TYPE] )
+        {
+          case ELEMENT_NODE:
+          {
+            attrs = '';
+            for( k = 0; attr = node[ATTRS][k]; k++ )
+            {
+              attrs += " <key>" + 
+                ( attr[ATTR_PREFIX] ? attr[ATTR_PREFIX] + ':' : '' ) + 
+                attr[ATTR_KEY].toLowerCase() +
+                "</key>=<value>\"" +  attr[ATTR_VALUE] + "\"</value>";
+            }
+            child_pointer = i + 1;
+            is_open = ( data[child_pointer] && ( node[DEPTH] < data[child_pointer][DEPTH] ) );
+            if( is_open ) 
+            {
+              has_only_one_child = 1;
+              one_child_value = '';
+              child_level = data[child_pointer][DEPTH];
+              for( ; data[child_pointer] &&  
+                      data[child_pointer][DEPTH] == child_level; child_pointer += 1 )
+              {
+                one_child_value += data[child_pointer][VALUE];
+                one_child_id = data[child_pointer][ID];
+                if( data[child_pointer][TYPE] != 3 )
+                {
+                  has_only_one_child = 0;
+                  one_child_value = '';
+                  break;
+                }
+              }
+              if(one_child_value)
+              {
+                one_child_value = one_child_value.replace(/</g, "&lt;");
+              }
+            }
+            if(is_open)
+            {
+              if(has_only_one_child)
+              {
+                class_name = re_formatted.test(node_name) ? " class='pre-wrap'" : '';
+                tree += "<div style='margin-left:" + 16 * node[DEPTH] + 
+                                                  "px;' " + class_name + ">"+
+                        "<node>&lt;" + node_name +  attrs + "&gt;</node>" +
+                        "<text>" + one_child_value + "</text>" +
+                        "<node>&lt;/" + node_name + "&gt;</node>" +
+                        " <span class='object-id'>[" + node[ID] +  "]</span>" +
+                        "</div>";
+                i = child_pointer - 1;
+              }
+              else
+              {
+                tree += "<div style='margin-left:" + 16 * node[DEPTH] + "px;'>"+
+                        "<node>&lt;" + node_name + attrs + "&gt;</node>" +
+                        " <span class='object-id'>[" + node[ID] +  "]</span>" +
+                        "</div>";
+                closing_tags.push(
+                    "<div style='margin-left:" + 16 * node[DEPTH] + "px;'>" +
+                    "<node>&lt;/" + node_name + "&gt;</node></div>"
+                  );
+              }
+            }
+            else
+            {
+              tree += "<div style='margin-left:" + 16 * node[DEPTH] + "px;'>"+
+                      "<node>&lt;" + node_name + attrs + 
+                       ( children_length ? '' : '/' ) + "&gt;</node>" +
+                       " <span class='object-id'>[" + node[ID] +  "]</span>" +
+                       "</div>";
+            }
+            break;
+          }
+          case PROCESSING_INSTRUCTION_NODE: 
+          {
+            tree += "<div style='margin-left:" + 16 * node[DEPTH] + "px;' " +      
+              "class='processing-instruction'>&lt;?" + node[NAME] + ' ' + 
+              this._format_processing_instruction_value(node[VALUE]) + "?&gt;</div>";
+            break;
+          }
+          case COMMENT_NODE:  
+          {
+            tree += "<div style='margin-left:" + 16 * node[DEPTH] + "px;' " +      
+                    "class='comment pre-wrap'>&lt;!--" + 
+                        node[VALUE].replace(/</g, '&lt;') + "--&gt;</div>";
+            break;
+          }
+          case DOCUMENT_NODE: 
+          {
+            break;
+          }
+          case DOCUMENT_TYPE_NODE: 
+          {
+            tree += "<div style='margin-left:" + 16 * node[ DEPTH ] + "px;' class='doctype'>"+
+                    "&lt;!doctype " + this.getDoctypeName(data) +
+                    ( node[PUBLIC_ID] ? 
+                      ( " PUBLIC " + "\"" + node[PUBLIC_ID] + "\"" ) :"" ) +
+                    ( node[SYSTEM_ID] ?  
+                      ( " \"" + node[SYSTEM_ID] + "\"" ) : "" ) +
+                    "&gt;</div>";
+            break;
+          }
+          default:
+          {
+            tree += 
+              "<div style='margin-left:" + ( 16 * node[ DEPTH ] )  + "px;'>" + 
+                "<text ref-id='"+ node[ ID ] + "'>" + node[VALUE].replace(/</g, "&lt;") + "</text>" +
+              "</div>";
+          }
+        }
+      }
+      while( closing_tags.length )
+      {
+        tree += closing_tags.pop();
+      }
+      tree += "</div>";
+      container.innerHTML = tree;
+    }
+  }
+
+  ## ************************************************************** ##
+  ## implementation
+  ## ************************************************************** ##
+  // implementation
+  ## ************************************************************** ##
+  ## commands
+  ## ************************************************************** ##
+    % for command in commands:
+      % if command.name in ["InspectDom"]:
+  this.handle${command.name} = function(status, message)
+  {
+  ## generte all constant identifiers for the message fields
+${generate_field_consts(command.response, lookup, create_test_framework, indent='    ')}\
+      % if command.name == "InspectDom":
+    if( status == 0 )
+    {
+      this._create_view(message[${const_id(command.response.fields[0])}]);
+    }
+    else
+    {
+      // TODO
+    }
+      % endif
+  }
+
+      % endif
+    % endfor
+  this.on_top_runtime_loaded = function(top_runtime_id)
+  {
+    var tag = tagManager.set_callback(this, this._on_root_id);
+    var script = "return document.documentElement";
+    services["${service_name}"].requestEval(tag, [top_runtime_id, 0, 0, script, []]);
+  }
+
+  this.bind = function()
+  {
+    var ecmascript_debugger = window.services['ecmascript-debugger'];
+    ecmascript_debugger.handleInspectDom = function(status, message)
+    {
+      self.handleInspectDom(status, message);
+    }
+
+  }
+
+  this.bind();
+
+}
+  % endif
+% endfor

hob/templates/js/js-build_application.mako

+/**
+ * @fileoverview
+ * (This file was autogenerated by hob)
+ * 
+ * The application is created in two steps:
+ *
+ * Step 1: All objects that do not depend on the services available from the
+ *         debuggee. The only exception is the scope service, as it is needed
+ *         to query the debuggee about what services it provides. All scope
+ *         debuggees run the scope service, and it can not be disabled.
+ *
+ * Step 2: All service objects are created, based on their counterpart on
+ *         the debuggee side. The second step uses
+ *         a pattern where each service has a build function in
+ *         "app.builders.<servuce class name>.<service version>.
+ *         The builders are called as soon as service information have been
+ *         received from the scope service. It's possible to hook up a callback
+ *         after the second step has finished. The callback can either be
+ *         passed as an argument to the build_application call, or by defining
+ *         a function named window.app.on_services_created, which
+ *         will be called automatically
+ *
+ * 
+ * There is an other moment to hook up a callback. 
+ * That is when all services are sucessfully enabled.
+ * The callback can either be passed to the build_application call 
+ * as second argument or by defining a function named 
+ * window.app.on_services_enabled
+ * 
+ */
+if( window.app )
+{
+  throw "window.app does already exist";
+}
+window.app = {};
+window.cls.MessageMixin.apply(window.app); // Give the app object message handling powers
+
+window.app.build_application = function(on_services_created, on_services_enabled)
+{
+
+  var _find_compatible_version = function(version, version_list)
+  {
+    var
+    numbers = version.split(".").map(Number),
+    match = null,
+    ver, nums;
+     // Find the best match for the current version
+    for (ver in version_list)
+    {
+      nums = ver.split(".").map(Number);
+      if (nums[0] != numbers[0])
+        continue;
+      if (!match || nums[1] > match[1][1])
+        match = [ver, nums];
+    }
+    return match && match[0];
+  }
+
+  var on_host_info_callback = function(service_descriptions)
+  {
+    % if create_test_framework:
+    new window.cls.ScopeInterfaceGenerator().get_interface(service_descriptions, 
+      function(map)
+      {
+        window.message_maps = map;
+        window.cls.ServiceBase.populate_map(map);
+        build_and_enable_services(service_descriptions, map);
+        window.test_framework.rebuild_last_state();
+      }, 
+      function(error)
+      {
+        opera.postError(error.message);
+      }
+    );
+    % else:
+    build_and_enable_services(service_descriptions);
+    % endif
+  };
+
+  /**
+   * This callback is invoked when host info is received from the debuggee.
+   *
+   */
+  var build_and_enable_services = function(service_descriptions, map)
+  {
+    var 
+    service_name = '',
+    service = null,
+    class_name = '',
+    re_version = /(^\d+\.\d+)(?:\.\d+)?$/,
+    version = null,
+    i = 0,
+    builder = null,
+    numbers = null;
+
+    for (service_name in service_descriptions)
+    {
+      service = service_descriptions[service_name];
+      version = re_version.exec(service.version);
+      version = version && version[1] || "0";
+      class_name = window.app.helpers.dash_to_class_name(service_name);
+      if (service_name != "scope")
+      {
+        if (window.services[service_name] && 
+          window.services[service_name].create_and_expose_interface(version, map[service_name]))
+        {
+          var
+          match_version = _find_compatible_version(version, window.app.builders[class_name]),
+          builder = window.app.builders[class_name] && window.app.builders[class_name][match_version];
+          if (builder) 
+          {
+            builder(service);
+          }
+        }
+      }
+    }
+    window.app.post('services-created', {'service_description': service_descriptions});
+    if (window.app.on_services_created)
+    {
+      window.app.on_services_created(service_descriptions);
+    }
+    if (on_services_created)
+    {
+      on_services_created(service_descriptions);
+    }
+    for (service_name in service_descriptions)
+    {
+      if(service_name in window.services && 
+            window.services[service_name].is_implemented &&
+            service_name != "scope")
+      {
+        window.services['scope'].requestEnable(0,[service_name]);
+      }
+    }
+  }
+
+  var create_raw_interface = function(service_name)
+  {
+    var ServiceClass = function()
+    {
+      this.name = service_name;
+      this.is_implemented = false;
+    }
+    ServiceClass.prototype = new cls.ServiceBase();
+    ServiceClass.prototype.constructor = ServiceClass;  // this is not really needed
+    window.services.add(new ServiceClass());
+  }
+
+  // ensure that the static methods on cls.ServiceBase exist.
+  new cls.ServiceBase();
+
+  % if create_test_framework:
+  window.test_framework = new cls.TestFramework();
+  window.logger = new cls.Logger();
+  window.document.onclick = window.test_framework.get_bound_click_handler();
+  window.document.onchange = window.test_framework.get_bound_change_handler();
+  % endif
+
+  // global objects
+  window.tagManager = new window.cls.TagManager();
+ 
+  // create window.services namespace and register it.
+  cls.ServiceBase.register_services(new cls.Namespace("services"));
+  [
+  % for service in services:
+    '${dashed_name(service.name)}',
+  % endfor
+  ].forEach(create_raw_interface);
+% for service in services:
+  % if service.name == "Scope":
+  var namespace = cls.${service.name} && cls.${service.name}["${service.options["version"].value}"];
+  namespace.Service.apply(window.services.scope.constructor.prototype);
+  window.services.scope.is_implemented = true;
+  window.services.scope.set_host_info_callback(on_host_info_callback);
+  window.services.scope.set_services_enabled_callback(on_services_enabled);
+  % endif
+% endfor
+
+  // create the client
+  if(window.services.scope)
+  {
+    window.client = new cls.Client();
+    client.setup();
+  }
+  else
+  {
+    throw "scope service couldn't be created, application creation aborted";
+  }
+}
+
+/**
+  * The builders for each service and version.
+  * These calls can also be used to create other parts of the application
+  * which support a given service version.
+  * It is recommended ( but not required ) that classes which support a given
+  * service version are organized in an appropirate namespace, like
+  * ls.<service class name>.<service version>.
+  */
+window.app.builders = {};
+% for service in services:
+  % if not service.name == "Scope":
+window.app.builders.${service.name} || ( window.app.builders.${service.name} = {} );
+/**
+  * @param {Object} service the service description of the according service on the host side
+  */
+<% version = '.'.join(service.options["version"].value.split('.')[:2]) %>\
+window.app.builders.${service.name}["${version}"] = function(service)
+{
+  var namespace = cls.${service.name} && cls.${service.name}["${version}"];
+    % if create_test_framework:
+      % if service.name == "EcmascriptDebugger":
+  window.runtimes = new namespace.Runtimes();
+  window.dom = new namespace.DOM();
+      % elif service.name == "WindowManager":
+  window.windows = new namespace.Windows();
+      % endif
+    % endif
+}
+
+  % endif
+% endfor
+
+window.app.helpers = {};
+
+window.app.helpers.dash_to_class_name = function(name)
+{
+  for ( var cur = '', i = 0, ret = '', do_upper = true; cur = name[i]; i++)
+  {
+    if(cur == '-')
+    {
+      do_upper = true;
+      continue;
+    }
+    ret += do_upper && cur.toUpperCase() || cur;
+    do_upper = false;
+  }
+  return ret;
+}
+
+window.onload = function()
+{
+  window.app.build_application();
+  % if console_logger_tutorial:
+  window.simple_logger = new SimpleLogger();
+  % endif
+}
+

hob/templates/js/js-client-html.mako

+<%
+  def get_submessages(fields, names=[]):
+    ret = []
+    for field in fields:
+      if field.message and isinstance(field.message, proto.Message) and not field.message.name in names:
+        names.append(field.message.name)
+        ret.append(field)
+        ret += get_submessages(field.message.fields, names)
+    return ret
+
+  def array_doc(doc):
+    if not doc:
+      return ""
+    return '"' + '", "'.join(doc.replace('\r', '').replace('"', '\\"').split('\n')) + '"';
+%>\
+<!doctype html>
+<html>
+<head>
+% if create_test_framework:
+  <title>Basic test framework of the Opera scope DOM API</title>
+  <link rel="stylesheet" href="style.css">
+  <script src="${lib_dir}/json.js"></script>
+  <script src="${lib_dir}/namespace.js"></script>
+  <script src="${lib_dir}/messagemixin.js"></script>
+  <script src="${lib_dir}/messagebroker.js"></script>
+  <script src="${lib_dir}/utils.js"></script>
+  <script src="${lib_dir}/clientlib_async.js"></script>
+  <script src="${lib_dir}/http_interface.js"></script>
+  <script src="${lib_dir}/stp_0_wrapper.js"></script>
+  <script src="${lib_dir}/tag_manager.js"></script>
+  <script src="${lib_dir}/message_maps.js"></script>
+  <script src="logger.js"></script>
+  <script src="test_framework.js"></script>
+  <script src="${lib_dir}/service_base.js"></script>
+  <script src="${lib_dir}/get_message_maps.js"></script>
+  <script src="runtimes.js"></script>
+  <script src="dom.js"></script>
+  <script src="windows.js"></script>
+  <script src="client.js"></script>
+  <script src="build_application.js"></script>
+% elif console_logger_tutorial:
+  <title>Simple Console Logger</title>
+  <style> pre { border-bottom: 1px solid #999; padding-bottom: 1em; } </style>
+  <script src="${lib_dir}/json.js"></script>
+  <script src="${lib_dir}/namespace.js"></script>
+  <script src="${lib_dir}/messagemixin.js"></script>
+  <script src="${lib_dir}/messagebroker.js"></script>
+  <script src="${lib_dir}/clientlib_async.js"></script>
+  <script src="${lib_dir}/http_interface.js"></script>
+  <script src="${lib_dir}/stp_0_wrapper.js"></script>
+  <script src="${lib_dir}/tag_manager.js"></script>
+  <script src="${lib_dir}/service_base.js"></script>
+  <script src="client.js"></script>
+  <script src="build_application.js"></script>
+  <script src="simpleconsolelogger.js"></script>
+% else:
+  <title> </title>
+  <script src="${lib_dir}/json.js"></script>
+  <script src="${lib_dir}/namespace.js"></script>
+  <script src="${lib_dir}/messagemixin.js"></script>
+  <script src="${lib_dir}/messagebroker.js"></script>
+  <script src="${lib_dir}/clientlib_async.js"></script>
+  <script src="${lib_dir}/http_interface.js"></script>
+  <script src="${lib_dir}/stp_0_wrapper.js"></script>
+  <script src="${lib_dir}/tag_manager.js"></script>
+  <script src="${lib_dir}/service_base.js"></script>
+  <script src="client.js"></script>
+  <script src="build_application.js"></script>
+% endif
+% for service in services:
+<% version = '_'.join(service.options["version"].value.split('.')[:2]) %>\
+  % if service.name == "Scope":
+  <script src="${lib_dir}/${dashed_name(service.name, dash='_')}_${version}.js"></script>
+  % endif
+% endfor
+</head>
+<body>
+% if create_test_framework:
+  <h1><img src="http://www.opera.com/media/images/logo/ologo_wback.gif">Test Framework for STP 1 </h1>
+  <div class="row">
+    <div id="log">
+      <h2>Log</h2>
+      <div id="log-container">
+        <pre></pre>
+      </div>
+      <p><input type='button' value='clear log' id='clear-log'><p>
+    </div>
+    <div id="windows">
+      <h2>Window List</h2>
+      <ul id="window-list"></ul>
+    </div>
+  </div>
+  <div class="row">
+    <div>
+    <input 
+      type="button" 
+      value="Show message map" 
+      onclick="window.cls.ScopeInterfaceGenerator.pretty_print_interface(window.message_maps)"
+    >
+    </div>
+  </div>
+  <div class="row">
+    <div id="services">
+      <h2>Service List</h2>
+      <ul id="service-list">
+  % for service in services:
+      <li>${service.name}</li>
+  % endfor
+      </ul>
+    </div>
+    <div id="command-list-container">
+      <h2>Command List</h2>
+      <ul id="command-list"></ul>
+    </div>
+    <div id="event-list-container">
+      <h2>Event List</h2>
+      <ul id="event-list"></ul>
+    </div>
+    <div id="message-container"></div>
+  </div>
+  <div id="dom-tree">
+    <h2>DOM Tree</h2>
+    <div id="dom-tree-container" class='dom'></div>
+  </div>
+% endif
+</body>
+</html>

hob/templates/js/js-client.mako

+/**
+ * (This file was autogenerated by hob)
+ *
+ * @fileoverview
+ * fixme: add file overview text
+ * 
+ */
+window.cls || ( window.cls = {} );
+
+window.cls.Client = function()
+{
+  // singleton
+  if(arguments.callee.instance)
+  {
+    return arguments.callee.instance;
+  }
+  arguments.callee.instance = this;
+
+  var self = this;
+
+  var _on_host_connected = function(servicelist)
+  {
+    servicelist = servicelist.split(',');
+    // TODO sort out all protocol version
+    // TODO check proxy version
+    if(servicelist.indexOf('stp-1') != -1)
+    {
+      services.scope.requestHostInfo();
+    }
+  }
+
+  var _on_host_quit = function()
+  {
+
+  }
+
+  var _get_port_number = function()
+  {
+    // TODO
+    // port 0 means debugging to current Opera instance, 
+    // any other port means remote debugging.
+    return 0;
+  }
+
+  this.setup = function()
+  {
+    window.ini || ( window.ini = {debug: false} );
+    % if create_test_framework:
+    window.opera || ( window.opera = {} );
+    % endif
+    if( !opera.scopeAddClient )
+    {
+      // implement the scope DOM API
+      cls.ScopeHTTPInterface.call(opera /*, force_stp_0 */);