Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 03 Mar 2014 17:23:35 -0500
changeset 188838 aa4d76646fd83629431ca960dd5c1a113bf97d6f
parent 188837 49b9cdf425f70201b1d3690e1403879ee42c899a (current diff)
parent 188794 cfc63f40409d0a3f0a58fd5dca11da8848fa0b57 (diff)
child 188839 be92f5071788fd0a53bf8cb3ed4577734cd9ef8d
push id3503
push userraliiev@mozilla.com
push dateMon, 28 Apr 2014 18:51:11 +0000
treeherdermozilla-beta@c95ac01e332e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
configure.in
gfx/2d/moz.build
--- a/addon-sdk/source/app-extension/bootstrap.js
+++ b/addon-sdk/source/app-extension/bootstrap.js
@@ -32,16 +32,22 @@ const REASON = [ 'unknown', 'startup', '
 
 const bind = Function.call.bind(Function.bind);
 
 let loader = null;
 let unload = null;
 let cuddlefishSandbox = null;
 let nukeTimer = null;
 
+let resourceDomains = [];
+function setResourceSubstitution(domain, uri) {
+  resourceDomains.push(domain);
+  resourceHandler.setSubstitution(domain, uri);
+}
+
 // Utility function that synchronously reads local resource from the given
 // `uri` and returns content string.
 function readURI(uri) {
   let ioservice = Cc['@mozilla.org/network/io-service;1'].
     getService(Ci.nsIIOService);
   let channel = ioservice.newChannel(uri, 'UTF-8', null);
   let stream = channel.open();
 
@@ -100,17 +106,17 @@ function startup(data, reasonCode) {
     let domain = id.
       toLowerCase().
       replace(/@/g, '-at-').
       replace(/\./g, '-dot-').
       replace(uuidRe, '$1');
 
     let prefixURI = 'resource://' + domain + '/';
     let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
-    resourceHandler.setSubstitution(domain, resourcesURI);
+    setResourceSubstitution(domain, resourcesURI);
 
     // Create path to URLs mapping supported by loader.
     let paths = {
       // Relative modules resolve to add-on package lib
       './': prefixURI + name + '/lib/',
       './tests/': prefixURI + name + '/tests/',
       '': 'resource://gre/modules/commonjs/'
     };
@@ -164,17 +170,17 @@ function startup(data, reasonCode) {
       // takes the parent folder instead.
       if (fileURI[fileURI.length-1] !== '/')
         fileURI += '/';
 
       // Maps the given file:// URI to a resource:// in order to avoid various
       // failure that happens with file:// URI and be close to production env
       let resourcesURI = ioService.newURI(fileURI, null, null);
       let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
-      resourceHandler.setSubstitution(resName, resourcesURI);
+      setResourceSubstitution(resName, resourcesURI);
 
       result[path] = 'resource://' + resName + '/';
       return result;
     }, paths);
 
     // Make version 2 of the manifest
     let manifest = options.manifest;
 
@@ -300,16 +306,21 @@ function shutdown(data, reasonCode) {
     // Don't waste time cleaning up if the application is shutting down
     if (reason != "shutdown") {
       // Avoid leaking all modules when something goes wrong with one particular
       // module. Do not clean it up immediatly in order to allow executing some
       // actions on addon disabling.
       // We need to keep a reference to the timer, otherwise it is collected
       // and won't ever fire.
       nukeTimer = setTimeout(nukeModules, 1000);
+
+      // Bug 944951 - bootstrap.js must remove the added resource: URIs on unload
+      resourceDomains.forEach(domain => {
+        resourceHandler.setSubstitution(domain, null);
+      })
     }
   }
 };
 
 function nukeModules() {
   nukeTimer = null;
   // module objects store `exports` which comes from sandboxes
   // We should avoid keeping link to these object to avoid leaking sandboxes
--- a/addon-sdk/source/app-extension/install.rdf
+++ b/addon-sdk/source/app-extension/install.rdf
@@ -12,17 +12,17 @@
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
 
     <!-- Firefox -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>26.0</em:minVersion>
-        <em:maxVersion>29.0a1</em:maxVersion>
+        <em:maxVersion>30.0</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- Front End MetaData -->
     <em:name>Test App</em:name>
     <em:description>Harness for tests.</em:description>
     <em:creator>Mozilla Corporation</em:creator>
     <em:homepageURL></em:homepageURL>
--- a/addon-sdk/source/lib/sdk/content/content-worker.js
+++ b/addon-sdk/source/lib/sdk/content/content-worker.js
@@ -65,25 +65,25 @@ const ContentWorker = Object.freeze({
    * events to the chrome module. It returns the EventEmitter as `pipe`
    * attribute, and, `onChromeEvent` a function that allows chrome module
    * to send event into the EventEmitter.
    *
    *                  pipe.emit --> emitToChrome
    *              onChromeEvent --> callback registered through pipe.on
    */
   createPipe: function createPipe(emitToChrome) {
-    function onEvent() {
-      // Convert to real array
-      let args = Array.slice(arguments);
+    function onEvent(type, ...args) {
       // JSON.stringify is buggy with cross-sandbox values,
       // it may return "{}" on functions. Use a replacer to match them correctly.
-      function replacer(k, v) {
-        return typeof v === "function" ? undefined : v;
-      }
-      let str = JSON.stringify(args, replacer);
+      let replacer = (k, v) =>
+        typeof(v) === "function"
+          ? (type === "console" ? Function.toString.call(v) : void(0))
+          : v;
+
+      let str = JSON.stringify([type, ...args], replacer);
       emitToChrome(str);
     }
 
     let { eventEmitter, emit, hasListenerFor } =
       ContentWorker.createEventEmitter(onEvent);
 
     return {
       pipe: eventEmitter,
@@ -158,17 +158,17 @@ const ContentWorker = Object.freeze({
             value: e,
           };
           if (wrapper.instanceOfError) {
             wrapper.value = {
               message: e.message,
               fileName: e.fileName,
               lineNumber: e.lineNumber,
               stack: e.stack,
-              name: e.name, 
+              name: e.name,
             };
           }
           pipe.emit('error', wrapper);
         }
       }
       _timers[id] = timer;
       return id;
     }
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -5,29 +5,23 @@
 
 module.metadata = {
   'stability': 'unstable'
 };
 
 const { Class } = require('../core/heritage');
 const { EventTarget } = require('../event/target');
 const { on, off, emit } = require('../event/core');
-const {
-  requiresAddonGlobal,
-  attach, detach, destroy
-} = require('./utils');
+const { requiresAddonGlobal } = require('./utils');
 const { delay: async } = require('../lang/functional');
 const { Ci, Cu, Cc } = require('chrome');
 const timer = require('../timers');
 const { URL } = require('../url');
 const { sandbox, evaluate, load } = require('../loader/sandbox');
 const { merge } = require('../util/object');
-const xulApp = require('../system/xul-app');
-const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
-                                              '17.0a2', '*');
 const { getTabForContentWindow } = require('../tabs/utils');
 
 // WeakMap of sandboxes so we can access private values
 const sandboxes = new WeakMap();
 
 /* Trick the linker in order to ensure shipping these files in the XPI.
   require('./content-worker.js');
   Then, retrieve URL of these files in the XPI:
@@ -40,30 +34,33 @@ const CONTENT_WORKER_URL = prefix + 'con
 // package.json data. This list is originaly defined by authors in
 // `permissions` attribute of their package.json addon file.
 const permissions = require('@loader/options').metadata['permissions'] || {};
 const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
 
 const JS_VERSION = '1.8';
 
 const WorkerSandbox = Class({
+  implements: [ EventTarget ],
 
-  implements: [
-    EventTarget
-  ],
-  
   /**
    * Emit a message to the worker content sandbox
    */
-  emit: function emit(...args) {
+  emit: function emit(type, ...args) {
+    // JSON.stringify is buggy with cross-sandbox values,
+    // it may return "{}" on functions. Use a replacer to match them correctly.
+    let replacer = (k, v) =>
+      typeof(v) === "function"
+        ? (type === "console" ? Function.toString.call(v) : void(0))
+        : v;
+
     // Ensure having an asynchronous behavior
-    let self = this;
-    async(function () {
-      emitToContent(self, JSON.stringify(args, replacer));
-    });
+    async(() =>
+      emitToContent(this, JSON.stringify([type, ...args], replacer))
+    );
   },
 
   /**
    * Synchronous version of `emit`.
    * /!\ Should only be used when it is strictly mandatory /!\
    *     Doesn't ensure passing only JSON values.
    *     Mainly used by context-menu in order to avoid breaking it.
    */
@@ -129,17 +126,17 @@ const WorkerSandbox = Class({
     let content = sandbox(principals, {
       sandboxPrototype: proto,
       wantXrays: true,
       wantGlobalProperties: wantGlobalProperties,
       sameZoneAs: window,
       metadata: { SDKContentScript: true }
     });
     model.sandbox = content;
-    
+
     // We have to ensure that window.top and window.parent are the exact same
     // object than window object, i.e. the sandbox global object. But not
     // always, in case of iframes, top and parent are another window object.
     let top = window.top === window ? content : content.top;
     let parent = window.parent === window ? content : content.parent;
     merge(content, {
       // We need 'this === window === top' to be true in toplevel scope:
       get window() content,
@@ -244,18 +241,20 @@ const WorkerSandbox = Class({
       importScripts.apply(null, [this].concat(contentScriptFile));
     if (contentScript) {
       evaluateIn(
         this,
         Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
       );
     }
   },
-  destroy: function destroy() {
-    this.emitSync('detach');
+  destroy: function destroy(reason) {
+    if (typeof reason != 'string')
+      reason = '';
+    this.emitSync('event', 'detach', reason);
     let model = modelFor(this);
     model.sandbox = null
     model.worker = null;
   },
 
 });
 
 exports.WorkerSandbox = WorkerSandbox;
@@ -351,24 +350,16 @@ function onContentEvent (workerSandbox, 
   });
 }
 
 
 function modelFor (workerSandbox) {
   return sandboxes.get(workerSandbox);
 }
 
-/**
- * JSON.stringify is buggy with cross-sandbox values,
- * it may return '{}' on functions. Use a replacer to match them correctly.
- */
-function replacer (k, v) {
-  return typeof v === 'function' ? undefined : v;
-}
-
 function getUnsafeWindow (win) {
   return win.wrappedJSObject || win;
 }
 
 function emitToContent (workerSandbox, args) {
   return modelFor(workerSandbox).emitToContent(args);
 }
 
--- a/addon-sdk/source/lib/sdk/content/utils.js
+++ b/addon-sdk/source/lib/sdk/content/utils.js
@@ -30,18 +30,18 @@ function requiresAddonGlobal(model) {
 }
 exports.requiresAddonGlobal = requiresAddonGlobal;
 
 function getAttachEventType(model) {
   if (!model) return null;
   let when = model.contentScriptWhen;
   return requiresAddonGlobal(model) ? 'document-element-inserted' :
          when === 'start' ? 'document-element-inserted' :
+         when === 'ready' ? 'DOMContentLoaded' :
          when === 'end' ? 'load' :
-         when === 'ready' ? 'DOMContentLoaded' :
          null;
 }
 exports.getAttachEventType = getAttachEventType;
 
 let attach = method('worker-attach');
 exports.attach = attach;
 
 let detach = method('worker-detach');
--- a/addon-sdk/source/lib/sdk/content/worker.js
+++ b/addon-sdk/source/lib/sdk/content/worker.js
@@ -29,17 +29,16 @@ let modelFor = (worker) => workers.get(w
 
 const ERR_DESTROYED =
   "Couldn't find the worker to receive this message. " +
   "The script may not be initialized yet, or may already have been unloaded.";
 
 const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
                    "until it is visible again.";
 
-
 /**
  * Message-passing facility for communication between code running
  * in the content and add-on process.
  * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
  */
 const Worker = Class({
   implements: [EventTarget],
   initialize: function WorkerConstructor (options) {
@@ -157,21 +156,24 @@ attach.define(Worker, function (worker, 
   // worker was initialized.
   model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
 });
 
 /**
  * Remove all internal references to the attached document
  * Tells _port to unload itself and removes all the references from itself.
  */
-detach.define(Worker, function (worker) {
+detach.define(Worker, function (worker, reason) {
   let model = modelFor(worker);
+
   // maybe unloaded before content side is created
-  if (model.contentWorker)
-    model.contentWorker.destroy();
+  if (model.contentWorker) {
+    model.contentWorker.destroy(reason);
+  }
+
   model.contentWorker = null;
   if (model.window) {
     model.window.removeEventListener("pageshow", model.pageShow, true);
     model.window.removeEventListener("pagehide", model.pageHide, true);
   }
   model.window = null;
   // This method may be called multiple times,
   // avoid dispatching `detach` event more than once
@@ -183,18 +185,18 @@ detach.define(Worker, function (worker) 
   }
   model.inited = false;
 });
 
 /**
  * Tells content worker to unload itself and
  * removes all the references from itself.
  */
-destroy.define(Worker, function (worker) {
-  detach(worker);
+destroy.define(Worker, function (worker, reason) {
+  detach(worker, reason);
   modelFor(worker).inited = true;
   // Specifying no type or listener removes all listeners
   // from target
   off(worker);
 });
 
 /**
  * Events fired by workers
@@ -228,17 +230,16 @@ function pageHide () {
  */
 
 function processMessage (worker, ...args) {
   let model = modelFor(worker) || {};
   if (!model.contentWorker)
     throw new Error(ERR_DESTROYED);
   if (model.frozen)
     throw new Error(ERR_FROZEN);
-
   model.contentWorker.emit.apply(null, args);
 }
 
 function createModel () {
   return {
     // List of messages fired before worker is initialized
     earlyEvents: [],
     // Is worker connected to the content worker sandbox ?
@@ -274,9 +275,8 @@ function emitEventToContent (worker, ...
   let model = modelFor(worker);
   let args = ['event'].concat(eventArgs);
   if (!model.inited) {
     model.earlyEvents.push(args);
     return;
   }
   processMessage.apply(null, [worker].concat(args));
 }
-
--- a/addon-sdk/source/lib/sdk/deprecated/api-utils.js
+++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js
@@ -6,28 +6,29 @@
 module.metadata = {
   "stability": "deprecated"
 };
 
 const memory = require("./memory");
 
 const { merge } = require("../util/object");
 const { union } = require("../util/array");
-const { isNil } = require("../lang/type");
+const { isNil, isRegExp } = require("../lang/type");
 
 // The possible return values of getTypeOf.
 const VALID_TYPES = [
   "array",
   "boolean",
   "function",
   "null",
   "number",
   "object",
   "string",
   "undefined",
+  "regexp"
 ];
 
 const { isArray } = Array;
 
 /**
  * Returns a validated options dictionary given some requirements.  If any of
  * the requirements are not met, an exception is thrown.
  *
@@ -41,18 +42,18 @@ const { isArray } = Array;
  *         its key.  There are four optional keys in this object:
  *           map: A function that's passed the value of the key in options.
  *                map's return value is taken as the key's value in the final
  *                validated options, is, and ok.  If map throws an exception
  *                it's caught and discarded, and the key's value is its value in
  *                options.
  *           is:  An array containing any number of the typeof type names.  If
  *                the key's value is none of these types, it fails validation.
- *                Arrays and null are identified by the special type names
- *                "array" and "null"; "object" will not match either.  No type
+ *                Arrays, null and regexps are identified by the special type names
+ *                "array", "null", "regexp"; "object" will not match either.  No type
  *                coercion is done.
  *           ok:  A function that's passed the key's value.  If it returns
  *                false, the value fails validation.
  *           msg: If the key's value fails validation, an exception is thrown.
  *                This string will be used as its message.  If undefined, a
  *                generic message is used, unless is is defined, in which case
  *                the message will state that the value needs to be one of the
  *                given types.
@@ -122,25 +123,27 @@ exports.addIterator = function addIterat
     // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values,
     // and "for (.. in Iterator(..))" gets [key, value] pairs.
     let index = keysOnly ? 0 : 1;
     while (true)
       yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index];
   };
 };
 
-// Similar to typeof, except arrays and null are identified by "array" and
-// "null", not "object".
+// Similar to typeof, except arrays, null and regexps are identified by "array" and
+// "null" and "regexp", not "object".
 let getTypeOf = exports.getTypeOf = function getTypeOf(val) {
   let typ = typeof(val);
   if (typ === "object") {
     if (!val)
       return "null";
     if (isArray(val))
       return "array";
+    if (isRegExp(val))
+      return "regexp";
   }
   return typ;
 }
 
 function RequirementError(key, requirement) {
   Error.call(this);
 
   this.name = "RequirementError";
--- a/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
+++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
@@ -21,20 +21,17 @@ const { EventEmitter, EventEmitterTrait 
 const { Ci, Cu, Cc } = require('chrome');
 const timer = require('../timers');
 const { URL } = require('../url');
 const unload = require('../system/unload');
 const observers = require('../system/events');
 const { Cortex } = require('./cortex');
 const { sandbox, evaluate, load } = require("../loader/sandbox");
 const { merge } = require('../util/object');
-const xulApp = require("../system/xul-app");
-const { getInnerId } = require("../window/utils")
-const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
-                                              "17.0a2", "*");
+const { getInnerId } = require("../window/utils");
 const { getTabForWindow } = require('../tabs/helpers');
 const { getTabForContentWindow } = require('../tabs/utils');
 
 /* Trick the linker in order to ensure shipping these files in the XPI.
   require('../content/content-worker.js');
   Then, retrieve URL of these files in the XPI:
 */
 let prefix = module.uri.split('deprecated/traits-worker.js')[0];
--- a/addon-sdk/source/lib/sdk/event/dom.js
+++ b/addon-sdk/source/lib/sdk/event/dom.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-let { emit, on, off } = require("./core");
+let { emit } = require("./core");
 
 // Simple utility function takes event target, event type and optional
 // `options.capture` and returns node style event stream that emits "data"
 // events every time event of that type occurs on the given `target`.
 function open(target, type, options) {
   let output = {};
   let capture = options && options.capture ? true : false;
 
--- a/addon-sdk/source/lib/sdk/l10n.js
+++ b/addon-sdk/source/lib/sdk/l10n.js
@@ -2,31 +2,34 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const core = require("./l10n/core");
+const json = require("./l10n/json/core");
+const { get: getKey } = require("./l10n/core");
+const properties = require("./l10n/properties/core");
 const { getRulesForLocale } = require("./l10n/plural-rules");
 
 // Retrieve the plural mapping function
-let pluralMappingFunction = getRulesForLocale(core.language()) ||
+let pluralMappingFunction = getRulesForLocale(json.language()) ||
                             getRulesForLocale("en");
 
 exports.get = function get(k) {
   // For now, we only accept a "string" as first argument
   // TODO: handle plural forms in gettext pattern
   if (typeof k !== "string")
     throw new Error("First argument of localization method should be a string");
+  let n = arguments[1];
 
   // Get translation from big hashmap or default to hard coded string:
-  let localized = core.get(k) || k;
+  let localized = getKey(k, n) || k;
 
   // # Simplest usecase:
   //   // String hard coded in source code:
   //   _("Hello world")
   //   // Identifier of a key stored in properties file
   //   _("helloString")
   if (arguments.length <= 1)
     return localized;
--- a/addon-sdk/source/lib/sdk/l10n/core.js
+++ b/addon-sdk/source/lib/sdk/l10n/core.js
@@ -1,35 +1,9 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// Following pseudo module is set by `api-utils/addon/runner` and its load
-// method needs to be called before loading `core` module. But it may have
-// failed, so that this pseudo won't be available
-
-module.metadata = {
-  "stability": "unstable"
-};
-
+"use strict";
 
-let hash = {}, bestMatchingLocale = null;
-try {
-  let data = require("@l10n/data");
-  hash = data.hash;
-  bestMatchingLocale = data.bestMatchingLocale;
-}
-catch(e) {}
+const json = require("./json/core");
+const properties = require("./properties/core");
 
-// Returns the translation for a given key, if available.
-exports.get = function get(k) {
-  return k in hash ? hash[k] : null;
-}
-
-// Returns the full length locale code: ja-JP-mac, en-US or fr
-exports.locale = function locale() {
-  return bestMatchingLocale;
-}
-// Returns the short locale code: ja, en, fr
-exports.language = function language() {
-  return bestMatchingLocale ? bestMatchingLocale.split("-")[0].toLowerCase()
-                            : null;
-}
+exports.get = json.usingJSON ? json.get : properties.get;
--- a/addon-sdk/source/lib/sdk/l10n/html.js
+++ b/addon-sdk/source/lib/sdk/l10n/html.js
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Ci, Cu } = require("chrome");
 const events = require("../system/events");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/json/core.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ "use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+let usingJSON = false;
+let hash = {}, bestMatchingLocale = null;
+try {
+  let data = require("@l10n/data");
+  hash = data.hash;
+  bestMatchingLocale = data.bestMatchingLocale;
+  usingJSON = true;
+}
+catch(e) {}
+
+exports.usingJSON = usingJSON;
+
+// Returns the translation for a given key, if available.
+exports.get = function get(k) {
+  return k in hash ? hash[k] : null;
+}
+
+// Returns the full length locale code: ja-JP-mac, en-US or fr
+exports.locale = function locale() {
+  return bestMatchingLocale;
+}
+// Returns the short locale code: ja, en, fr
+exports.language = function language() {
+  return bestMatchingLocale ? bestMatchingLocale.split("-")[0].toLowerCase()
+                            : null;
+}
--- a/addon-sdk/source/lib/sdk/l10n/loader.js
+++ b/addon-sdk/source/lib/sdk/l10n/loader.js
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Cc, Ci } = require("chrome");
 const { getPreferedLocales, findClosestLocale } = require("./locale");
@@ -41,17 +40,17 @@ function getBestLocale(rootURI) {
 
     // Compute the most preferable locale to use by using these two lists
     return findClosestLocale(availableLocales, preferedLocales);
   });
 }
 
 /**
  * Read localization files and returns a promise of data to put in `@l10n/data`
- * pseudo module, in order to allow l10n/core to fetch it.
+ * pseudo module, in order to allow l10n/json/core to fetch it.
  */
 exports.load = function load(rootURI) {
   // First, search for a locale file:
   return getBestLocale(rootURI).then(function (bestMatchingLocale) {
     // It may be null if the addon doesn't have any locale file
     if (!bestMatchingLocale)
       return resolve(null);
 
--- a/addon-sdk/source/lib/sdk/l10n/locale.js
+++ b/addon-sdk/source/lib/sdk/l10n/locale.js
@@ -6,30 +6,31 @@
 module.metadata = {
   "stability": "unstable"
 };
 
 const prefs = require("../preferences/service");
 const { Cu, Cc, Ci } = require("chrome");
 const { Services } = Cu.import("resource://gre/modules/Services.jsm");
 
-
 /**
  * Gets the currently selected locale for display.
  * Gets all usable locale that we can use sorted by priority of relevance
  * @return  Array of locales, begins with highest priority
  */
 const PREF_MATCH_OS_LOCALE  = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE  = "general.useragent.locale";
 const PREF_ACCEPT_LANGUAGES = "intl.accept_languages";
-exports.getPreferedLocales = function getPreferedLocales() {
+
+function getPreferedLocales(caseSensitve) {
   let locales = [];
-
   function addLocale(locale) {
-    locale = locale.toLowerCase();
+    locale = locale.trim();
+    if (!caseSensitve)
+      locale = locale.toLowerCase();
     if (locales.indexOf(locale) === -1)
       locales.push(locale);
   }
 
   // Most important locale is OS one. But we use it, only if
   // "intl.locale.matchOS" pref is set to `true`.
   // Currently only used for multi-locales mobile builds.
   // http://mxr.mozilla.org/mozilla-central/source/mobile/android/installer/Makefile.in#46
@@ -43,48 +44,47 @@ exports.getPreferedLocales = function ge
   // In some cases, mainly on Fennec and on Linux version,
   // `general.useragent.locale` is a special 'localized' value, like:
   // "chrome://global/locale/intl.properties"
   let browserUiLocale = prefs.getLocalized(PREF_SELECTED_LOCALE, "") ||
                         prefs.get(PREF_SELECTED_LOCALE, "");
   if (browserUiLocale)
     addLocale(browserUiLocale);
 
-
   // Third priority is the list of locales used for web content
   let contentLocales = prefs.get(PREF_ACCEPT_LANGUAGES, "");
   if (contentLocales) {
     // This list is a string of locales seperated by commas.
     // There is spaces after commas, so strip each item
     for each(let locale in contentLocales.split(","))
       addLocale(locale.replace(/(^\s+)|(\s+$)/g, ""));
   }
 
   // Finally, we ensure that en-US is the final fallback if it wasn't added
   addLocale("en-US");
 
   return locales;
 }
+exports.getPreferedLocales = getPreferedLocales;
 
 /**
  * Selects the closest matching locale from a list of locales.
  *
  * @param  aLocales
  *         An array of available locales
  * @param  aMatchLocales
  *         An array of prefered locales, ordered by priority. Most wanted first.
  *         Locales have to be in lowercase.
  *         If null, uses getPreferedLocales() results
  * @return the best match for the currently selected locale
  *
  * Stolen from http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm
  */
 exports.findClosestLocale = function findClosestLocale(aLocales, aMatchLocales) {
-
-  aMatchLocales = aMatchLocales || exports.getPreferedLocales();
+  aMatchLocales = aMatchLocales || getPreferedLocales();
 
   // Holds the best matching localized resource
   let bestmatch = null;
   // The number of locale parts it matched with
   let bestmatchcount = 0;
   // The number of locale parts in the match
   let bestpartcount = 0;
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/l10n/properties/core.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Cu } = require("chrome");
+const { newURI } = require('../../url/utils')
+const { getRulesForLocale } = require("../plural-rules");
+const { getPreferedLocales } = require('../locale');
+const { rootURI } = require("@loader/options");
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const baseURI = rootURI + "locale/";
+const preferedLocales = getPreferedLocales(true);
+
+function getLocaleURL(locale) {
+  // if the locale is a valid chrome URI, return it
+  try {
+    let uri = newURI(locale);
+    if (uri.scheme == 'chrome')
+      return uri.spec;
+  }
+  catch(_) {}
+  // otherwise try to construct the url
+  return baseURI + locale + ".properties";
+}
+
+function getKey(locale, key) {
+  let bundle = Services.strings.createBundle(getLocaleURL(locale));
+  try {
+    return bundle.GetStringFromName(key) + "";
+  }
+  catch (_) {}
+  return undefined;
+}
+
+function get(key, n, locales) {
+  // try this locale
+  let locale = locales.shift();
+  let localized;
+
+  if (typeof n == 'number') {
+    if (n == 0) {
+      localized = getKey(locale, key + '[zero]');
+    }
+    else if (n == 1) {
+      localized = getKey(locale, key + '[one]');
+    }
+    else if (n == 2) {
+      localized = getKey(locale, key + '[two]');
+    }
+
+    if (!localized) {
+      // Retrieve the plural mapping function
+      let pluralForm = (getRulesForLocale(locale.split("-")[0].toLowerCase()) ||
+                        getRulesForLocale("en"))(n);
+      localized = getKey(locale, key + '[' + pluralForm + ']');
+    }
+
+    if (!localized) {
+      localized = getKey(locale, key + '[other]');
+    }
+  }
+
+  if (!localized) {
+    localized = getKey(locale, key);
+  }
+
+  if (localized) {
+    return localized;
+  }
+
+  // try next locale
+  if (locales.length)
+    return get(key, n, locales);
+
+  return undefined;
+}
+exports.get = function(k, n) get(k, n, Array.slice(preferedLocales));
--- a/addon-sdk/source/lib/sdk/lang/functional.js
+++ b/addon-sdk/source/lib/sdk/lang/functional.js
@@ -323,8 +323,41 @@ exports.chain =
 // two arguments.
 //
 // [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ]
 const is = curry((expected, actual) => actual === expected);
 exports.is = is;
 
 const isnt = complement(is);
 exports.isnt = isnt;
+
+/**
+ * From underscore's `_.debounce`
+ * http://underscorejs.org
+ * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Underscore may be freely distributed under the MIT license.
+ */
+const debounce = function debounce (fn, wait) {
+  let timeout, args, context, timestamp, result;
+
+  let later = function () {
+    let last = Date.now() - timestamp;
+    if (last < wait) {
+      timeout = setTimeout(later, wait - last);
+    } else {
+      timeout = null;
+      result = fn.apply(context, args);
+      context = args = null;
+    }
+  };
+
+  return function (...aArgs) {
+    context = this;
+    args = aArgs;
+    timestamp  = Date.now();
+    if (!timeout) {
+      timeout = setTimeout(later, wait);
+    }
+
+    return result;
+  };
+};
+exports.debounce = debounce;
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -3,271 +3,249 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
 const observers = require('./system/events');
-const { Loader, validationAttributes } = require('./content/loader');
+const { contract: loaderContract } = require('./content/loader');
+const { contract } = require('./util/contract');
+const { getAttachEventType, WorkerHost } = require('./content/utils');
+const { Class } = require('./core/heritage');
+const { Disposable } = require('./core/disposable');
 const { Worker } = require('./content/worker');
-const { Registry } = require('./util/registry');
-const { EventEmitter } = require('./deprecated/events');
-const { on, emit } = require('./event/core');
-const { validateOptions : validate } = require('./deprecated/api-utils');
-const { Cc, Ci } = require('chrome');
+const { EventTarget } = require('./event/target');
+const { on, emit, once, setListeners } = require('./event/core');
+const { on: domOn, removeListener: domOff } = require('./dom/events');
+const { pipe } = require('./event/utils');
+const { isRegExp } = require('./lang/type');
 const { merge } = require('./util/object');
-const { readURISync } = require('./net/url');
 const { windowIterator } = require('./deprecated/window-utils');
 const { isBrowser, getFrames } = require('./window/utils');
 const { getTabs, getTabContentWindow, getTabForContentWindow,
         getURI: getTabURI } = require('./tabs/utils');
 const { ignoreWindow } = require('sdk/private-browsing/utils');
 const { Style } = require("./stylesheet/style");
 const { attach, detach } = require("./content/mod");
 const { has, hasAny } = require("./util/array");
 const { Rules } = require("./util/rules");
+const { List, addListItem, removeListItem } = require('./util/list');
+const { when: unload } = require("./system/unload");
 
 // Valid values for `attachTo` option
 const VALID_ATTACHTO_OPTIONS = ['existing', 'top', 'frame'];
 
-const mods = new WeakMap();
+const pagemods = new Set();
+const workers = new WeakMap();
+const styles = new WeakMap();
+const models = new WeakMap();
+let modelFor = (mod) => models.get(mod);
+let workerFor = (mod) => workers.get(mod);
+let styleFor = (mod) => styles.get(mod);
 
-// contentStyle* / contentScript* are sharing the same validation constraints,
-// so they can be mostly reused, except for the messages.
-const validStyleOptions = {
-  contentStyle: merge(Object.create(validationAttributes.contentScript), {
+// Bind observer
+observers.on('document-element-inserted', onContentWindow);
+unload(() => observers.off('document-element-inserted', onContentWindow));
+
+let isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
+
+// Validation Contracts
+const modOptions = {
+  // contentStyle* / contentScript* are sharing the same validation constraints,
+  // so they can be mostly reused, except for the messages.
+  contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
     msg: 'The `contentStyle` option must be a string or an array of strings.'
   }),
-  contentStyleFile: merge(Object.create(validationAttributes.contentScriptFile), {
+  contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
     msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
-  })
+  }),
+  include: {
+    is: ['string', 'array', 'regexp'],
+    ok: (rule) => {
+      if (isRegExpOrString(rule))
+        return true;
+      if (Array.isArray(rule) && rule.length > 0)
+        return rule.every(isRegExpOrString);
+      return false;
+    },
+    msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.'
+  },
+  attachTo: {
+    is: ['string', 'array', 'undefined'],
+    map: function (attachTo) {
+      if (!attachTo) return ['top', 'frame'];
+      if (typeof attachTo === 'string') return [attachTo];
+      return attachTo;
+    },
+    ok: function (attachTo) {
+      return hasAny(attachTo, ['top', 'frame']) &&
+        attachTo.every(has.bind(null, ['top', 'frame', 'existing']));
+    },
+    msg: 'The `attachTo` option must be a string or an array of strings. ' +
+      'The only valid options are "existing", "top" and "frame", and must ' +
+      'contain at least "top" or "frame" values.'
+  },
 };
 
+const modContract = contract(merge({}, loaderContract.rules, modOptions));
+
 /**
  * PageMod constructor (exported below).
  * @constructor
  */
-const PageMod = Loader.compose(EventEmitter, {
-  on: EventEmitter.required,
-  _listeners: EventEmitter.required,
-  attachTo: [],
-  contentScript: Loader.required,
-  contentScriptFile: Loader.required,
-  contentScriptWhen: Loader.required,
-  contentScriptOptions: Loader.required,
-  include: null,
-  constructor: function PageMod(options) {
-    this._onContent = this._onContent.bind(this);
-    options = options || {};
-
-    let { contentStyle, contentStyleFile } = validate(options, validStyleOptions);
+const PageMod = Class({
+  implements: [
+    modContract.properties(modelFor),
+    EventTarget,
+    Disposable
+  ],
+  extends: WorkerHost(workerFor),
+  setup: function PageMod(options) {
+    let mod = this;
+    let model = modContract(options);
+    models.set(this, model);
 
-    if ('contentScript' in options)
-      this.contentScript = options.contentScript;
-    if ('contentScriptFile' in options)
-      this.contentScriptFile = options.contentScriptFile;
-    if ('contentScriptOptions' in options)
-      this.contentScriptOptions = options.contentScriptOptions;
-    if ('contentScriptWhen' in options)
-      this.contentScriptWhen = options.contentScriptWhen;
-    if ('onAttach' in options)
-      this.on('attach', options.onAttach);
-    if ('onError' in options)
-      this.on('error', options.onError);
-    if ('attachTo' in options) {
-      if (typeof options.attachTo == 'string')
-        this.attachTo = [options.attachTo];
-      else if (Array.isArray(options.attachTo))
-        this.attachTo = options.attachTo;
-      else
-        throw new Error('The `attachTo` option must be a string or an array ' +
-                        'of strings.');
+    // Set listeners on {PageMod} itself, not the underlying worker,
+    // like `onMessage`, as it'll get piped.
+    setListeners(this, options);
 
-      let isValidAttachToItem = function isValidAttachToItem(item) {
-        return typeof item === 'string' &&
-               VALID_ATTACHTO_OPTIONS.indexOf(item) !== -1;
-      }
-      if (!this.attachTo.every(isValidAttachToItem))
-        throw new Error('The `attachTo` option valid accept only following ' +
-                        'values: '+ VALID_ATTACHTO_OPTIONS.join(', '));
-      if (!hasAny(this.attachTo, ["top", "frame"]))
-        throw new Error('The `attachTo` option must always contain at least' +
-                        ' `top` or `frame` value');
-    }
-    else {
-      this.attachTo = ["top", "frame"];
+    let include = model.include;
+    model.include = Rules();
+    model.include.add.apply(model.include, [].concat(include));
+
+    if (model.contentStyle || model.contentStyleFile) {
+      styles.set(mod, Style({
+        uri: model.contentStyleFile,
+        source: model.contentStyle
+      }));
     }
 
-    let include = options.include;
-    let rules = this.include = Rules();
-
-    if (!include)
-      throw new Error('The `include` option must always contain atleast one rule');
-
-    rules.add.apply(rules, [].concat(include));
+    pagemods.add(this);
 
-    if (contentStyle || contentStyleFile) {
-      this._style = Style({
-        uri: contentStyleFile,
-        source: contentStyle
-      });
-    }
-
-    this.on('error', this._onUncaughtError = this._onUncaughtError.bind(this));
-    pageModManager.add(this._public);
-    mods.set(this._public, this);
-
-    // `_applyOnExistingDocuments` has to be called after `pageModManager.add()`
-    // otherwise its calls to `_onContent` method won't do anything.
-    if ('attachTo' in options && has(options.attachTo, 'existing'))
-      this._applyOnExistingDocuments();
+    // `applyOnExistingDocuments` has to be called after `pagemods.add()`
+    // otherwise its calls to `onContent` method won't do anything.
+    if (has(model.attachTo, 'existing'))
+      applyOnExistingDocuments(mod);
   },
 
   destroy: function destroy() {
-    if (this._style)
-      detach(this._style);
+    let style = styleFor(this);
+    if (style)
+      detach(style);
 
     for (let i in this.include)
       this.include.remove(this.include[i]);
 
-    mods.delete(this._public);
-    pageModManager.remove(this._public);
-  },
-
-  _applyOnExistingDocuments: function _applyOnExistingDocuments() {
-    let mod = this;
-    let tabs = getAllTabs();
-
-    tabs.forEach(function (tab) {
-      // Fake a newly created document
-      let window = getTabContentWindow(tab);
-      if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab)))
-        mod._onContent(window);
-      if (has(mod.attachTo, "frame")) {
-        getFrames(window).
-            filter((iframe) => mod.include.matchesAny(iframe.location.href)).
-            forEach(mod._onContent);
-      }
-    });
-  },
-
-  _onContent: function _onContent(window) {
-    // not registered yet
-    if (!pageModManager.has(this))
-      return;
-
-    let isTopDocument = window.top === window;
-    // Is a top level document and `top` is not set, ignore
-    if (isTopDocument && !has(this.attachTo, "top"))
-      return;
-    // Is a frame document and `frame` is not set, ignore
-    if (!isTopDocument && !has(this.attachTo, "frame"))
-      return;
-
-    if (this._style)
-      attach(this._style, window);
-
-    // Immediatly evaluate content script if the document state is already
-    // matching contentScriptWhen expectations
-    let state = window.document.readyState;
-    if ('start' === this.contentScriptWhen ||
-        // Is `load` event already dispatched?
-        'complete' === state ||
-        // Is DOMContentLoaded already dispatched and waiting for it?
-        ('ready' === this.contentScriptWhen && state === 'interactive') ) {
-      this._createWorker(window);
-      return;
-    }
-
-    let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
-    let self = this;
-    window.addEventListener(eventName, function onReady(event) {
-      if (event.target.defaultView != window)
-        return;
-      window.removeEventListener(eventName, onReady, true);
-
-      self._createWorker(window);
-    }, true);
-  },
-  _createWorker: function _createWorker(window) {
-    let worker = Worker({
-      window: window,
-      contentScript: this.contentScript,
-      contentScriptFile: this.contentScriptFile,
-      contentScriptOptions: this.contentScriptOptions,
-      onError: this._onUncaughtError
-    });
-    this._emit('attach', worker);
-    let self = this;
-    worker.once('detach', function detach() {
-      worker.destroy();
-    });
-  },
-  _onUncaughtError: function _onUncaughtError(e) {
-    if (this._listeners('error').length == 1)
-      console.exception(e);
+    pagemods.delete(this);
   }
 });
-exports.PageMod = function(options) PageMod(options)
-exports.PageMod.prototype = PageMod.prototype;
+exports.PageMod = PageMod;
 
-const PageModManager = Registry.resolve({
-  constructor: '_init',
-  _destructor: '_registryDestructor'
-}).compose({
-  constructor: function PageModRegistry(constructor) {
-    this._init(PageMod);
-    observers.on(
-      'document-element-inserted',
-      this._onContentWindow = this._onContentWindow.bind(this)
-    );
-  },
-  _destructor: function _destructor() {
-    observers.off('document-element-inserted', this._onContentWindow);
-    this._removeAllListeners();
-
-    // We need to do some cleaning er PageMods, like unregistering any
-    // `contentStyle*`
-    this._registry.forEach(function(pageMod) {
-      pageMod.destroy();
-    });
+function onContentWindow({ subject: document }) {
+  // Return if we have no pagemods
+  if (pagemods.size === 0)
+    return;
 
-    this._registryDestructor();
-  },
-  _onContentWindow: function _onContentWindow({ subject: document }) {
-    let window = document.defaultView;
-    // XML documents don't have windows, and we don't yet support them.
-    if (!window)
-      return;
-    // We apply only on documents in tabs of Firefox
-    if (!getTabForContentWindow(window))
-      return;
+  let window = document.defaultView;
+  // XML documents don't have windows, and we don't yet support them.
+  if (!window)
+    return;
+  // We apply only on documents in tabs of Firefox
+  if (!getTabForContentWindow(window))
+    return;
 
-    // When the tab is private, only addons with 'private-browsing' flag in
-    // their package.json can apply content script to private documents
-    if (ignoreWindow(window)) {
-      return;
-    }
+  // When the tab is private, only addons with 'private-browsing' flag in
+  // their package.json can apply content script to private documents
+  if (ignoreWindow(window))
+    return;
 
-    this._registry.forEach(function(mod) {
-      if (mod.include.matchesAny(document.URL))
-        mods.get(mod)._onContent(window);
-    });
-  },
-  off: function off(topic, listener) {
-    this.removeListener(topic, listener);
+  for (let pagemod of pagemods) {
+    if (pagemod.include.matchesAny(document.URL))
+      onContent(pagemod, window);
   }
-});
-const pageModManager = PageModManager();
+}
 
 // Returns all tabs on all currently opened windows
 function getAllTabs() {
   let tabs = [];
   // Iterate over all chrome windows
   for (let window in windowIterator()) {
     if (!isBrowser(window))
       continue;
     tabs = tabs.concat(getTabs(window));
   }
   return tabs;
 }
+
+function applyOnExistingDocuments (mod) {
+  let tabs = getAllTabs();
+
+  tabs.forEach(function (tab) {
+    // Fake a newly created document
+    let window = getTabContentWindow(tab);
+    if (has(mod.attachTo, "top") && mod.include.matchesAny(getTabURI(tab)))
+      onContent(mod, window);
+    if (has(mod.attachTo, "frame")) {
+      getFrames(window).
+        filter((iframe) => mod.include.matchesAny(iframe.location.href)).
+        forEach((frame) => onContent(mod, frame));
+    }
+  });
+}
+
+function createWorker (mod, window) {
+  let worker = Worker({
+    window: window,
+    contentScript: mod.contentScript,
+    contentScriptFile: mod.contentScriptFile,
+    contentScriptOptions: mod.contentScriptOptions,
+  });
+  workers.set(mod, worker);
+  pipe(worker, mod);
+  emit(mod, 'attach', worker);
+  once(worker, 'detach', function detach() {
+    worker.destroy();
+  });
+}
+
+function onContent (mod, window) {
+  // not registered yet
+  if (!pagemods.has(mod))
+    return;
+
+  let isTopDocument = window.top === window;
+  // Is a top level document and `top` is not set, ignore
+  if (isTopDocument && !has(mod.attachTo, "top"))
+    return;
+  // Is a frame document and `frame` is not set, ignore
+  if (!isTopDocument && !has(mod.attachTo, "frame"))
+    return;
+
+  let style = styleFor(mod);
+  if (style)
+    attach(style, window);
+
+  // Immediatly evaluate content script if the document state is already
+  // matching contentScriptWhen expectations
+  if (isMatchingAttachState(mod, window)) {
+    createWorker(mod, window);
+    return;
+  }
+
+  let eventName = getAttachEventType(mod) || 'load';
+  domOn(window, eventName, function onReady (e) {
+    if (e.target.defaultView !== window)
+      return;
+    domOff(window, eventName, onReady, true);
+    createWorker(mod, window);
+  }, true);
+}
+
+function isMatchingAttachState (mod, window) {
+  let state = window.document.readyState;
+  return 'start' === mod.contentScriptWhen ||
+      // Is `load` event already dispatched?
+      'complete' === state ||
+      // Is DOMContentLoaded already dispatched and waiting for it?
+      ('ready' === mod.contentScriptWhen && state === 'interactive')
+}
--- a/addon-sdk/source/lib/sdk/private-browsing.js
+++ b/addon-sdk/source/lib/sdk/private-browsing.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 module.metadata = {
   "stability": "stable"
 };
 
+const { Ci } = require('chrome');
 const { setMode, getMode, on: onStateChange, isPermanentPrivateBrowsing } = require('./private-browsing/utils');
 const { isWindowPrivate } = require('./window/utils');
 const { emit, on, once, off } = require('./event/core');
 const { when: unload } = require('./system/unload');
 const { deprecateUsage, deprecateFunction, deprecateEvent } = require('./util/deprecate');
 const { getOwnerWindow } = require('./private-browsing/window/utils');
 
 onStateChange('start', function onStart() {
@@ -58,16 +59,22 @@ exports.isPrivate = function(thing) {
           return isThingPrivate;
       }
     }
 
     // can we find an associated window?
     let window = getOwnerWindow(thing);
     if (window)
       return isWindowPrivate(window);
+
+    try {
+      let { isChannelPrivate } = thing.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+      if (isChannelPrivate)
+        return true;
+    } catch(e) {}
   }
 
   // check if the post pwpb, global pb service is enabled.
   if (isPermanentPrivateBrowsing())
     return true;
 
   // if we get here, and global private browsing
   // is available, and it is true, then return
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process.js
@@ -0,0 +1,329 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+  'stability': 'experimental'
+};
+
+let { Ci } = require('chrome');
+let subprocess = require('./child_process/subprocess');
+let { EventTarget } = require('../event/target');
+let { Stream } = require('../io/stream');
+let { on, emit, off } = require('../event/core');
+let { Class } = require('../core/heritage');
+let { platform } = require('../system');
+let { isFunction, isArray } = require('../lang/type');
+let { delay } = require('../lang/functional');
+let { merge } = require('../util/object');
+let { setTimeout, clearTimeout } = require('../timers');
+let isWindows = platform.indexOf('win') === 0;
+
+let processes = WeakMap();
+
+
+/**
+ * The `Child` class wraps a subprocess command, exposes
+ * the stdio streams, and methods to manipulate the subprocess
+ */
+let Child = Class({
+  implements: [EventTarget],
+  initialize: function initialize (options) {
+    let child = this;
+    let proc;
+
+    this.killed = false;
+    this.exitCode = undefined;
+    this.signalCode = undefined;
+
+    this.stdin = Stream();
+    this.stdout = Stream();
+    this.stderr = Stream();
+
+    try {
+      proc = subprocess.call({
+        command: options.file,
+        arguments: options.cmdArgs,
+        environment: serializeEnv(options.env),
+        workdir: options.cwd,
+        charset: options.encoding,
+        stdout: data => emit(child.stdout, 'data', data),
+        stderr: data => emit(child.stderr, 'data', data),
+        stdin: stream => {
+          child.stdin.on('data', pumpStdin);
+          child.stdin.on('end', function closeStdin () {
+            child.stdin.off('data', pumpStdin);
+            child.stdin.off('end', closeStdin);
+            stream.close();
+          });
+          function pumpStdin (data) {
+            stream.write(data);
+          }
+        },
+        done: function (result) {
+          // Only emit if child is not killed; otherwise,
+          // the `kill` method will handle this
+          if (!child.killed) {
+            child.exitCode = result.exitCode;
+            child.signalCode = null;
+
+            // If process exits with < 0, there was an error
+            if (child.exitCode < 0) {
+              handleError(new Error('Process exited with exit code ' + child.exitCode));
+            }
+            else {
+              // Also do 'exit' event as there's not much of
+              // a difference in our implementation as we're not using
+              // node streams
+              emit(child, 'exit', child.exitCode, child.signalCode);
+            }
+
+            // Emit 'close' event with exit code and signal,
+            // which is `null`, as it was not a killed process
+            emit(child, 'close', child.exitCode, child.signalCode);
+          }
+        }
+      });
+      processes.set(child, proc);
+    } catch (e) {
+      // Delay the error handling so an error handler can be set
+      // during the same tick that the Child was created
+      delay(() => handleError(e));
+    }
+
+    // `handleError` is called when process could not even
+    // be spawned
+    function handleError (e) {
+      // If error is an nsIObject, make a fresh error object
+      // so we're not exposing nsIObjects, and we can modify it
+      // with additional process information, like node
+      let error = e;
+      if (e instanceof Ci.nsISupports) {
+        error = new Error(e.message, e.filename, e.lineNumber);
+      }
+      emit(child, 'error', error);
+      child.exitCode = -1;
+      child.signalCode = null;
+      emit(child, 'close', child.exitCode, child.signalCode);
+    }
+  },
+  kill: function kill (signal) {
+    let proc = processes.get(this);
+    proc.kill(signal);
+    this.killed = true;
+    this.exitCode = null;
+    this.signalCode = signal;
+    emit(this, 'exit', this.exitCode, this.signalCode);
+    emit(this, 'close', this.exitCode, this.signalCode);
+  },
+  get pid() { return processes.get(this, {}).pid || -1; }
+});
+
+function spawn (file, ...args) {
+  let cmdArgs = [];
+  // Default options
+  let options = {
+    cwd: null,
+    env: null,
+    encoding: 'UTF-8'
+  };
+
+  if (args[1]) {
+    merge(options, args[1]);
+    cmdArgs = args[0];
+  }
+  else {
+    if (isArray(args[0]))
+      cmdArgs = args[0];
+    else
+      merge(options, args[0]);
+  }
+
+  if ('gid' in options)
+    console.warn('`gid` option is not yet supported for `child_process`');
+  if ('uid' in options)
+    console.warn('`uid` option is not yet supported for `child_process`');
+  if ('detached' in options)
+    console.warn('`detached` option is not yet supported for `child_process`');
+
+  options.file = file;
+  options.cmdArgs = cmdArgs;
+
+  return Child(options);
+}
+
+exports.spawn = spawn;
+
+/**
+ * exec(command, options, callback)
+ */
+function exec (cmd, ...args) {
+  let file, cmdArgs, callback, options = {};
+
+  if (isFunction(args[0]))
+    callback = args[0];
+  else {
+    merge(options, args[0]);
+    callback = args[1];
+  }
+
+  if (isWindows) {
+    file = 'C:\\Windows\\System32\\cmd.exe';
+    cmdArgs = ['/s', '/c', (cmd || '').split(' ')];
+  }
+  else {
+    file = '/bin/sh';
+    cmdArgs = ['-c', cmd];
+  }
+
+  // Undocumented option from node being able to specify shell
+  if (options && options.shell)
+    file = options.shell;
+
+  return execFile(file, cmdArgs, options, callback);
+}
+exports.exec = exec;
+/**
+ * execFile (file, args, options, callback)
+ */
+function execFile (file, ...args) {
+  let cmdArgs = [], callback;
+  // Default options
+  let options = {
+    cwd: null,
+    env: null,
+    encoding: 'utf8',
+    timeout: 0,
+    maxBuffer: 200 * 1024,
+    killSignal: 'SIGTERM'
+  };
+
+  if (isFunction(args[args.length - 1]))
+    callback = args[args.length - 1];
+
+  if (isArray(args[0])) {
+    cmdArgs = args[0];
+    merge(options, args[1]);
+  } else if (!isFunction(args[0]))
+    merge(options, args[0]);
+
+  let child = spawn(file, cmdArgs, options);
+  let exited = false;
+  let stdout = '';
+  let stderr = '';
+  let error = null;
+  let timeoutId = null;
+
+  child.stdout.setEncoding(options.encoding);
+  child.stderr.setEncoding(options.encoding);
+
+  on(child.stdout, 'data', pumpStdout);
+  on(child.stderr, 'data', pumpStderr);
+  on(child, 'close', exitHandler);
+  on(child, 'error', errorHandler);
+
+  if (options.timeout > 0) {
+    setTimeout(() => {
+      kill();
+      timeoutId = null;
+    }, options.timeout);
+  }
+
+  function exitHandler (code, signal) {
+
+    // Return if exitHandler called previously, occurs
+    // when multiple maxBuffer errors thrown and attempt to kill multiple
+    // times
+    if (exited) return;
+    exited = true;
+
+    if (!isFunction(callback)) return;
+
+    if (timeoutId) {
+      clearTimeout(timeoutId);
+      timeoutId = null;
+    }
+
+    if (!error && (code !== 0 || signal !== null))
+      error = createProcessError(new Error('Command failed: ' + stderr), {
+        code: code,
+        signal: signal,
+        killed: !!child.killed
+      });
+
+    callback(error, stdout, stderr);
+
+    off(child.stdout, 'data', pumpStdout);
+    off(child.stderr, 'data', pumpStderr);
+    off(child, 'close', exitHandler);
+    off(child, 'error', errorHandler);
+  }
+
+  function errorHandler (e) {
+    error = e;
+    exitHandler();
+  }
+
+  function kill () {
+    try {
+      child.kill(options.killSignal);
+    } catch (e) {
+      // In the scenario where the kill signal happens when
+      // the process is already closing, just abort the kill fail
+      if (/library is not open/.test(e))
+        return;
+      error = e;
+      exitHandler(-1, options.killSignal);
+    }
+  }
+
+  function pumpStdout (data) {
+    stdout += data;
+    if (stdout.length > options.maxBuffer) {
+      error = new Error('stdout maxBuffer exceeded');
+      kill();
+    }
+  }
+
+  function pumpStderr (data) {
+    stderr += data;
+    if (stderr.length > options.maxBuffer) {
+      error = new Error('stderr maxBuffer exceeded');
+      kill();
+    }
+  }
+
+  return child;
+}
+exports.execFile = execFile;
+
+exports.fork = function fork () {
+  throw new Error("child_process#fork is not currently supported");
+};
+
+function serializeEnv (obj) {
+  return Object.keys(obj || {}).map(prop => prop + '=' + obj[prop]);
+}
+
+function createProcessError (err, options = {}) {
+  // If code and signal look OK, this was probably a failure
+  // attempting to spawn the process (like ENOENT in node) -- use
+  // the code from the error message
+  if (!options.code && !options.signal) {
+    let match = err.message.match(/(NS_ERROR_\w*)/);
+    if (match && match.length > 1)
+      err.code = match[1];
+    else {
+      // If no good error message found, use the passed in exit code;
+      // this occurs when killing a process that's already closing,
+      // where we want both a valid exit code (0) and the error
+      err.code = options.code != null ? options.code : null;
+    }
+  }
+  else
+    err.code = options.code != null ? options.code : null;
+  err.signal = options.signal || null;
+  err.killed = options.killed || false;
+  return err;
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process/LICENSE
@@ -0,0 +1,36 @@
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (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.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is subprocess.jsm.
+   -
+   - The Initial Developer of this code is Jan Gerber.
+   - Portions created by Jan Gerber <j@mailb.org>
+   - are Copyright (C) 2011 Jan Gerber.
+   - All Rights Reserved.
+   -
+   -
+   - Contributor(s):
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the LGPL or the GPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process/README.md
@@ -0,0 +1,129 @@
+# subprocess
+
+
+Created by [Jan Gerber](j@mailb.org), with contributions from [Patrick Brunschwig](patrick@enigmail.net), subprocess.jsm is used as the underlying library, supporting the Addon-SDK's implementation of Node's `child-process` API.
+
+`subprocess.jsm` is originally from [http://hg.mozilla.org/ipccode/](http://hg.mozilla.org/ipccode/).
+
+How to Use subprocess.jsm in your Add-on
+----------------------------------------
+
+1. copy subprocess.jsm and subprocess_worker_*.js into the modules/ directory
+   of your add-on.
+
+2. add this line to chrome.manifest:
+ resource EXTENSION modules/
+
+3. import it where needed:
+ Components.utils.import("resource://EXTENSION/subprocess.jsm");
+
+This object allows to start a process, and read/write data to/from it
+using stdin/stdout/stderr streams.
+
+Usage example:
+
+  var p = subprocess.call({
+    command: '/bin/foo',
+    arguments: ['-v', 'foo'],
+    environment: [ "XYZ=abc", "MYVAR=def" ],
+    charset: 'UTF-8',
+    workdir: '/home/foo',
+    //stdin: "some value to write to stdin\nfoobar",
+    stdin: function(stdin) {
+      stdin.write("some value to write to stdin\nfoobar");
+      stdin.close();
+    },
+    stdout: function(data) {
+      dump("got data on stdout:" + data + "\n");
+    },
+    stderr: function(data) {
+      dump("got data on stderr:" + data + "\n");
+    },
+    done: function(result) {
+      dump("process terminated with " + result.exitCode + "\n");
+    },
+    mergeStderr: false
+  });
+
+  p.wait(); // wait for the subprocess to terminate,
+            // this will block the main thread,
+            // only do if you can wait that long
+
+
+Description of subprocess.call(...) Parameters
+----------------------------------------------
+Apart from <command>, all arguments are optional.
+
+ command: either a |nsIFile| object pointing to an executable file or a
+              String containing the platform-dependent path to an executable
+              file.
+
+ arguments: optional string array containing the arguments to the command.
+
+ environment: optional string array containing environment variables to pass
+              to the command. The array elements must have the form
+              "VAR=data". Please note that if environment is defined, it
+              replaces any existing environment variables for the subprocess.
+
+ charset: Output is decoded with given charset and a string is returned.
+              If charset is undefined, "UTF-8" is used as default.
+              To get binary data, set this to null and the returned string
+              is not decoded in any way.
+
+ workdir: Optional; either a |nsIFile| object or string containing the
+              platform-dependent path to a directory to become the current
+              working directory of the subprocess.
+
+ stdin: Optional input data for the process to be passed on standard
+              input. stdin can either be a string or a function.
+              A |string| gets written to stdin and stdin gets closed;
+              A |function| gets passed an object with write and close function.
+              Please note that the write() function will return almost immediately;
+              data is always written asynchronously on a separate thread.
+
+ stdout: An optional function that can receive output data from the
+              process. The stdout-function is called asynchronously; it can be
+              called mutliple times during the execution of a process.
+              At a minimum at each occurance of \n or \r.
+              Please note that null-characters might need to be escaped
+              with something like 'data.replace(/\0/g, "\\0");'.
+
+ stderr: An optional function that can receive stderr data from the
+              process. The stderr-function is called asynchronously; it can be
+              called mutliple times during the execution of a process. Please
+              note that null-characters might need to be escaped with
+              something like 'data.replace(/\0/g, "\\0");'.
+              (on windows it only gets called once right now)
+
+ done: Optional function that is called when the process has terminated.
+              The exit code from the process available via result.exitCode. If
+              stdout is not defined, then the output from stdout is available
+              via result.stdout. stderr data is in result.stderr
+
+ mergeStderr: Optional boolean value. If true, stderr is merged with stdout;
+              no data will be provided to stderr.
+
+
+Description of object returned by subprocess.call(...)
+------------------------------------------------------
+The object returned by subprocess.call offers a few methods that can be
+executed:
+
+ wait(): waits for the subprocess to terminate. It is not required to use
+              wait; done will be called in any case when the subprocess terminated.
+
+ kill(): kill the subprocess. Any open pipes will be closed and
+              done will be called.
+
+
+Other methods exported by subprocess
+------------------------------------
+The following functions help debugging and provide logging facilities.
+
+ registerDebugHandler(functionRef): register a handler that is called to get
+                                      debugging information
+ registerLogHandler(functionRef): register a handler that is called to get error
+                                      messages
+
+ example:
+    subprocess.registerLogHandler( function(s) { dump(s); } );
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process/subprocess.js
@@ -0,0 +1,1687 @@
+// -*- coding: utf-8 -*-
+// vim: et:ts=4:sw=4:sts=4:ft=javascript
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "MPL"); you may not use this file
+ * except in compliance with the MPL. You may obtain a copy of
+ * the MPL at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the MPL is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the MPL for the specific language governing
+ * rights and limitations under the MPL.
+ *
+ * The Original Code is subprocess.jsm.
+ *
+ * The Initial Developer of this code is Jan Gerber.
+ * Portions created by Jan Gerber <j@mailb.org>
+ * are Copyright (C) 2011 Jan Gerber.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ * Patrick Brunschwig <patrick@enigmail.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * This object allows to start a process, and read/write data to/from it
+ * using stdin/stdout/stderr streams.
+ * Usage example:
+ *
+ *  var p = subprocess.call({
+ *    command:     '/bin/foo',
+ *    arguments:   ['-v', 'foo'],
+ *    environment: [ "XYZ=abc", "MYVAR=def" ],
+ *    charset: 'UTF-8',
+ *    workdir: '/home/foo',
+ *    //stdin: "some value to write to stdin\nfoobar",
+ *    stdin: function(stdin) {
+ *      stdin.write("some value to write to stdin\nfoobar");
+ *      stdin.close();
+ *    },
+ *    stdout: function(data) {
+ *      dump("got data on stdout:" + data + "\n");
+ *    },
+ *    stderr: function(data) {
+ *      dump("got data on stderr:" + data + "\n");
+ *    },
+ *    done: function(result) {
+ *      dump("process terminated with " + result.exitCode + "\n");
+ *    },
+ *    mergeStderr: false
+ *  });
+ *  p.wait(); // wait for the subprocess to terminate
+ *            // this will block the main thread,
+ *            // only do if you can wait that long
+ *
+ *
+ * Description of parameters:
+ * --------------------------
+ * Apart from <command>, all arguments are optional.
+ *
+ * command:     either a |nsIFile| object pointing to an executable file or a
+ *              String containing the platform-dependent path to an executable
+ *              file.
+ *
+ * arguments:   optional string array containing the arguments to the command.
+ *
+ * environment: optional string array containing environment variables to pass
+ *              to the command. The array elements must have the form
+ *              "VAR=data". Please note that if environment is defined, it
+ *              replaces any existing environment variables for the subprocess.
+ *
+ * charset:     Output is decoded with given charset and a string is returned.
+ *              If charset is undefined, "UTF-8" is used as default.
+ *              To get binary data, set this to null and the returned string
+ *              is not decoded in any way.
+ *
+ * workdir:     optional; String containing the platform-dependent path to a
+ *              directory to become the current working directory of the subprocess.
+ *
+ * stdin:       optional input data for the process to be passed on standard
+ *              input. stdin can either be a string or a function.
+ *              A |string| gets written to stdin and stdin gets closed;
+ *              A |function| gets passed an object with write and close function.
+ *              Please note that the write() function will return almost immediately;
+ *              data is always written asynchronously on a separate thread.
+ *
+ * stdout:      an optional function that can receive output data from the
+ *              process. The stdout-function is called asynchronously; it can be
+ *              called mutliple times during the execution of a process.
+ *              At a minimum at each occurance of \n or \r.
+ *              Please note that null-characters might need to be escaped
+ *              with something like 'data.replace(/\0/g, "\\0");'.
+ *
+ * stderr:      an optional function that can receive stderr data from the
+ *              process. The stderr-function is called asynchronously; it can be
+ *              called mutliple times during the execution of a process. Please
+ *              note that null-characters might need to be escaped with
+ *              something like 'data.replace(/\0/g, "\\0");'.
+ *              (on windows it only gets called once right now)
+ *
+ * done:        optional function that is called when the process has terminated.
+ *              The exit code from the process available via result.exitCode. If
+ *              stdout is not defined, then the output from stdout is available
+ *              via result.stdout. stderr data is in result.stderr
+ *
+ * mergeStderr: optional boolean value. If true, stderr is merged with stdout;
+ *              no data will be provided to stderr.
+ *
+ *
+ * Description of object returned by subprocess.call(...)
+ * ------------------------------------------------------
+ * The object returned by subprocess.call offers a few methods that can be
+ * executed:
+ *
+ * wait():         waits for the subprocess to terminate. It is not required to use
+ *                 wait; done will be called in any case when the subprocess terminated.
+ *
+ * kill(hardKill): kill the subprocess. Any open pipes will be closed and
+ *                 done will be called.
+ *                 hardKill [ignored on Windows]:
+ *                  - false: signal the process terminate (SIGTERM)
+ *                  - true:  kill the process (SIGKILL)
+ *
+ *
+ * Other methods in subprocess
+ * ---------------------------
+ *
+ * registerDebugHandler(functionRef):   register a handler that is called to get
+ *                                      debugging information
+ * registerLogHandler(functionRef):     register a handler that is called to get error
+ *                                      messages
+ *
+ * example:
+ *    subprocess.registerLogHandler( function(s) { dump(s); } );
+ */
+
+'use strict';
+
+const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
+
+Cu.import("resource://gre/modules/ctypes.jsm");
+
+const NS_LOCAL_FILE = "@mozilla.org/file/local;1";
+
+const Runtime = require("sdk/system/runtime");
+const Environment = require("sdk/system/environment").env;
+const DEFAULT_ENVIRONMENT = [];
+if (Runtime.OS == "Linux" && "DISPLAY" in Environment) {
+  DEFAULT_ENVIRONMENT.push("DISPLAY=" + Environment.DISPLAY);
+}
+
+/*
+Fake require statements to ensure worker scripts are packaged:
+require("./subprocess_worker_win.js");
+require("./subprocess_worker_unix.js");
+*/
+const URL_PREFIX = module.uri.replace(/subprocess\.js/, "");
+const WORKER_URL_WIN = URL_PREFIX + "subprocess_worker_win.js";
+const WORKER_URL_UNIX = URL_PREFIX + "subprocess_worker_unix.js";
+
+//Windows API definitions
+if (ctypes.size_t.size == 8) {
+    var WinABI = ctypes.default_abi;
+} else {
+    var WinABI = ctypes.winapi_abi;
+}
+const WORD = ctypes.uint16_t;
+const DWORD = ctypes.uint32_t;
+const LPDWORD = DWORD.ptr;
+
+const UINT = ctypes.unsigned_int;
+const BOOL = ctypes.bool;
+const HANDLE = ctypes.size_t;
+const HWND = HANDLE;
+const HMODULE = HANDLE;
+const WPARAM = ctypes.size_t;
+const LPARAM = ctypes.size_t;
+const LRESULT = ctypes.size_t;
+const ULONG_PTR = ctypes.uintptr_t;
+const PVOID = ctypes.voidptr_t;
+const LPVOID = PVOID;
+const LPCTSTR = ctypes.jschar.ptr;
+const LPCWSTR = ctypes.jschar.ptr;
+const LPTSTR = ctypes.jschar.ptr;
+const LPSTR = ctypes.char.ptr;
+const LPCSTR = ctypes.char.ptr;
+const LPBYTE = ctypes.char.ptr;
+
+const CREATE_NEW_CONSOLE = 0x00000010;
+const CREATE_NO_WINDOW = 0x08000000;
+const CREATE_UNICODE_ENVIRONMENT = 0x00000400;
+const STARTF_USESHOWWINDOW = 0x00000001;
+const STARTF_USESTDHANDLES = 0x00000100;
+const SW_HIDE = 0;
+const DUPLICATE_SAME_ACCESS = 0x00000002;
+const STILL_ACTIVE = 259;
+const INFINITE = DWORD(0xFFFFFFFF);
+const WAIT_TIMEOUT = 0x00000102;
+
+/*
+typedef struct _SECURITY_ATTRIBUTES {
+ DWORD  nLength;
+ LPVOID lpSecurityDescriptor;
+ BOOL   bInheritHandle;
+} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
+*/
+const SECURITY_ATTRIBUTES = new ctypes.StructType("SECURITY_ATTRIBUTES", [
+    {"nLength": DWORD},
+    {"lpSecurityDescriptor": LPVOID},
+    {"bInheritHandle": BOOL},
+]);
+
+/*
+typedef struct _STARTUPINFO {
+  DWORD  cb;
+  LPTSTR lpReserved;
+  LPTSTR lpDesktop;
+  LPTSTR lpTitle;
+  DWORD  dwX;
+  DWORD  dwY;
+  DWORD  dwXSize;
+  DWORD  dwYSize;
+  DWORD  dwXCountChars;
+  DWORD  dwYCountChars;
+  DWORD  dwFillAttribute;
+  DWORD  dwFlags;
+  WORD   wShowWindow;
+  WORD   cbReserved2;
+  LPBYTE lpReserved2;
+  HANDLE hStdInput;
+  HANDLE hStdOutput;
+  HANDLE hStdError;
+} STARTUPINFO, *LPSTARTUPINFO;
+*/
+const STARTUPINFO = new ctypes.StructType("STARTUPINFO", [
+    {"cb": DWORD},
+    {"lpReserved": LPTSTR},
+    {"lpDesktop": LPTSTR},
+    {"lpTitle": LPTSTR},
+    {"dwX": DWORD},
+    {"dwY": DWORD},
+    {"dwXSize": DWORD},
+    {"dwYSize": DWORD},
+    {"dwXCountChars": DWORD},
+    {"dwYCountChars": DWORD},
+    {"dwFillAttribute": DWORD},
+    {"dwFlags": DWORD},
+    {"wShowWindow": WORD},
+    {"cbReserved2": WORD},
+    {"lpReserved2": LPBYTE},
+    {"hStdInput": HANDLE},
+    {"hStdOutput": HANDLE},
+    {"hStdError": HANDLE},
+]);
+
+/*
+typedef struct _PROCESS_INFORMATION {
+  HANDLE hProcess;
+  HANDLE hThread;
+  DWORD  dwProcessId;
+  DWORD  dwThreadId;
+} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
+*/
+const PROCESS_INFORMATION = new ctypes.StructType("PROCESS_INFORMATION", [
+    {"hProcess": HANDLE},
+    {"hThread": HANDLE},
+    {"dwProcessId": DWORD},
+    {"dwThreadId": DWORD},
+]);
+
+/*
+typedef struct _OVERLAPPED {
+  ULONG_PTR Internal;
+  ULONG_PTR InternalHigh;
+  union {
+    struct {
+      DWORD Offset;
+      DWORD OffsetHigh;
+    };
+    PVOID  Pointer;
+  };
+  HANDLE    hEvent;
+} OVERLAPPED, *LPOVERLAPPED;
+*/
+const OVERLAPPED = new ctypes.StructType("OVERLAPPED");
+
+//UNIX definitions
+const pid_t = ctypes.int32_t;
+const WNOHANG = 1;
+const F_GETFD = 1;
+const F_SETFL = 4;
+
+const LIBNAME       = 0;
+const O_NONBLOCK    = 1;
+const RLIM_T        = 2;
+const RLIMIT_NOFILE = 3;
+
+function getPlatformValue(valueType) {
+
+    if (! gXulRuntime)
+        gXulRuntime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+
+    const platformDefaults = {
+        // Windows API:
+        'winnt':   [ 'kernel32.dll' ],
+
+        // Unix API:
+        //            library name   O_NONBLOCK RLIM_T                RLIMIT_NOFILE
+        'darwin':  [ 'libc.dylib',   0x04     , ctypes.uint64_t     , 8 ],
+        'linux':   [ 'libc.so.6',    2024     , ctypes.unsigned_long, 7 ],
+        'freebsd': [ 'libc.so.7',    0x04     , ctypes.int64_t      , 8 ],
+        'openbsd': [ 'libc.so.61.0', 0x04     , ctypes.int64_t      , 8 ],
+        'sunos':   [ 'libc.so',      0x80     , ctypes.unsigned_long, 5 ]
+    }
+
+    return platformDefaults[gXulRuntime.OS.toLowerCase()][valueType];
+}
+
+
+var gDebugFunc = null,
+    gLogFunc = null,
+    gXulRuntime = null;
+
+function LogError(s) {
+    if (gLogFunc)
+        gLogFunc(s);
+    else
+        dump(s);
+}
+
+function debugLog(s) {
+    if (gDebugFunc)
+        gDebugFunc(s);
+}
+
+function setTimeout(callback, timeout) {
+    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    timer.initWithCallback(callback, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+};
+
+function getBytes(data) {
+  var string = '';
+  data.forEach(function(x) { string += String.fromCharCode(x < 0 ? x + 256 : x) });
+  return string;
+}
+
+function readString(data, length, charset) {
+    var string = '', bytes = [];
+    for(var i = 0;i < length; i++) {
+        if(data[i] == 0 && charset !== null) // stop on NULL character for non-binary data
+           break
+        bytes.push(data[i]);
+    }
+    if (!bytes || bytes.length == 0)
+        return string;
+    if(charset === null) {
+        return bytes;
+    }
+    return convertBytes(bytes, charset);
+}
+
+function convertBytes(bytes, charset) {
+    var string = '';
+    charset = charset || 'UTF-8';
+    var unicodeConv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                        .getService(Ci.nsIScriptableUnicodeConverter);
+    try {
+        unicodeConv.charset = charset;
+        string = unicodeConv.convertFromByteArray(bytes, bytes.length);
+    } catch (ex) {
+        LogError("String conversion failed: "+ex.toString()+"\n")
+        string = '';
+    }
+    string += unicodeConv.Finish();
+    return string;
+}
+
+
+// temporary solution for removal of nsILocalFile
+function getLocalFileApi() {
+  if ("nsILocalFile" in Ci) {
+    return Ci.nsILocalFile;
+  }
+  else
+    return Ci.nsIFile;
+}
+
+function getCommandStr(command) {
+    let commandStr = null;
+    if (typeof(command) == "string") {
+        let file = Cc[NS_LOCAL_FILE].createInstance(getLocalFileApi());
+        file.initWithPath(command);
+        if (! (file.isExecutable() && file.isFile()))
+            throw("File '"+command+"' is not an executable file");
+        commandStr = command;
+    }
+    else {
+        if (! (command.isExecutable() && command.isFile()))
+            throw("File '"+command.path+"' is not an executable file");
+        commandStr = command.path;
+    }
+
+    return commandStr;
+}
+
+function getWorkDir(workdir) {
+    let workdirStr = null;
+    if (typeof(workdir) == "string") {
+        let file = Cc[NS_LOCAL_FILE].createInstance(getLocalFileApi());
+        file.initWithPath(workdir);
+        if (! (file.isDirectory()))
+            throw("Directory '"+workdir+"' does not exist");
+        workdirStr = workdir;
+    }
+    else if (workdir) {
+        if (! workdir.isDirectory())
+            throw("Directory '"+workdir.path+"' does not exist");
+        workdirStr = workdir.path;
+    }
+    return workdirStr;
+}
+
+
+var subprocess = {
+    call: function(options) {
+        options.mergeStderr = options.mergeStderr || false;
+        options.workdir = options.workdir ||  null;
+        options.environment = options.environment || DEFAULT_ENVIRONMENT;
+        if (options.arguments) {
+            var args = options.arguments;
+            options.arguments = [];
+            args.forEach(function(argument) {
+                options.arguments.push(argument);
+            });
+        } else {
+            options.arguments = [];
+        }
+
+        options.libc = getPlatformValue(LIBNAME);
+
+        if (gXulRuntime.OS.substring(0, 3) == "WIN") {
+            return subprocess_win32(options);
+        } else {
+            return subprocess_unix(options);
+        }
+
+    },
+    registerDebugHandler: function(func) {
+        gDebugFunc = func;
+    },
+    registerLogHandler: function(func) {
+        gLogFunc = func;
+    }
+};
+
+
+
+function subprocess_win32(options) {
+    var kernel32dll = ctypes.open(options.libc),
+        hChildProcess,
+        active = true,
+        done = false,
+        exitCode = -1,
+        child = {},
+        stdinWorker = null,
+        stdoutWorker = null,
+        stderrWorker = null,
+        pendingWriteCount = 0,
+        readers = options.mergeStderr ? 1 : 2,
+        stdinOpenState = 2,
+        error = '',
+        output = '';
+
+    // stdin pipe states
+    const OPEN = 2;
+    const CLOSEABLE = 1;
+    const CLOSED = 0;
+
+    //api declarations
+    /*
+    BOOL WINAPI CloseHandle(
+      __in  HANDLE hObject
+    );
+    */
+    var CloseHandle = kernel32dll.declare("CloseHandle",
+                                            WinABI,
+                                            BOOL,
+                                            HANDLE
+    );
+
+    /*
+    BOOL WINAPI CreateProcess(
+      __in_opt     LPCTSTR lpApplicationName,
+      __inout_opt  LPTSTR lpCommandLine,
+      __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
+      __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
+      __in         BOOL bInheritHandles,
+      __in         DWORD dwCreationFlags,
+      __in_opt     LPVOID lpEnvironment,
+      __in_opt     LPCTSTR lpCurrentDirectory,
+      __in         LPSTARTUPINFO lpStartupInfo,
+      __out        LPPROCESS_INFORMATION lpProcessInformation
+    );
+     */
+    var CreateProcessW = kernel32dll.declare("CreateProcessW",
+                                            WinABI,
+                                            BOOL,
+                                            LPCTSTR,
+                                            LPTSTR,
+                                            SECURITY_ATTRIBUTES.ptr,
+                                            SECURITY_ATTRIBUTES.ptr,
+                                            BOOL,
+                                            DWORD,
+                                            LPVOID,
+                                            LPCTSTR,
+                                            STARTUPINFO.ptr,
+                                            PROCESS_INFORMATION.ptr
+                                         );
+
+//     /*
+//     BOOL WINAPI ReadFile(
+//       __in         HANDLE hFile,
+//       __out        LPVOID ReadFileBuffer,
+//       __in         DWORD nNumberOfBytesToRead,
+//       __out_opt    LPDWORD lpNumberOfBytesRead,
+//       __inout_opt  LPOVERLAPPED lpOverlapped
+//     );
+//     */
+//     var ReadFileBufferSize = 1024,
+//         ReadFileBuffer = ctypes.char.array(ReadFileBufferSize),
+//         ReadFile = kernel32dll.declare("ReadFile",
+//                                         WinABI,
+//                                         BOOL,
+//                                         HANDLE,
+//                                         ReadFileBuffer,
+//                                         DWORD,
+//                                         LPDWORD,
+//                                         OVERLAPPED.ptr
+//     );
+//
+//     /*
+//     BOOL WINAPI PeekNamedPipe(
+//       __in       HANDLE hNamedPipe,
+//       __out_opt  LPVOID lpBuffer,
+//       __in       DWORD nBufferSize,
+//       __out_opt  LPDWORD lpBytesRead,
+//       __out_opt  LPDWORD lpTotalBytesAvail,
+//       __out_opt  LPDWORD lpBytesLeftThisMessage
+//     );
+//     */
+//     var PeekNamedPipe = kernel32dll.declare("PeekNamedPipe",
+//                                         WinABI,
+//                                         BOOL,
+//                                         HANDLE,
+//                                         ReadFileBuffer,
+//                                         DWORD,
+//                                         LPDWORD,
+//                                         LPDWORD,
+//                                         LPDWORD
+//     );
+//
+//     /*
+//     BOOL WINAPI WriteFile(
+//       __in         HANDLE hFile,
+//       __in         LPCVOID lpBuffer,
+//       __in         DWORD nNumberOfBytesToWrite,
+//       __out_opt    LPDWORD lpNumberOfBytesWritten,
+//       __inout_opt  LPOVERLAPPED lpOverlapped
+//     );
+//     */
+//     var WriteFile = kernel32dll.declare("WriteFile",
+//                                         WinABI,
+//                                         BOOL,
+//                                         HANDLE,
+//                                         ctypes.char.ptr,
+//                                         DWORD,
+//                                         LPDWORD,
+//                                         OVERLAPPED.ptr
+//     );
+
+    /*
+    BOOL WINAPI CreatePipe(
+      __out     PHANDLE hReadPipe,
+      __out     PHANDLE hWritePipe,
+      __in_opt  LPSECURITY_ATTRIBUTES lpPipeAttributes,
+      __in      DWORD nSize
+    );
+    */
+    var CreatePipe = kernel32dll.declare("CreatePipe",
+                                        WinABI,
+                                        BOOL,
+                                        HANDLE.ptr,
+                                        HANDLE.ptr,
+                                        SECURITY_ATTRIBUTES.ptr,
+                                        DWORD
+    );
+
+    /*
+    HANDLE WINAPI GetCurrentProcess(void);
+    */
+    var GetCurrentProcess = kernel32dll.declare("GetCurrentProcess",
+                                        WinABI,
+                                        HANDLE
+    );
+
+    /*
+    DWORD WINAPI GetLastError(void);
+    */
+    var GetLastError = kernel32dll.declare("GetLastError",
+                                        WinABI,
+                                        DWORD
+    );
+
+    /*
+    BOOL WINAPI DuplicateHandle(
+      __in   HANDLE hSourceProcessHandle,
+      __in   HANDLE hSourceHandle,
+      __in   HANDLE hTargetProcessHandle,
+      __out  LPHANDLE lpTargetHandle,
+      __in   DWORD dwDesiredAccess,
+      __in   BOOL bInheritHandle,
+      __in   DWORD dwOptions
+    );
+    */
+    var DuplicateHandle = kernel32dll.declare("DuplicateHandle",
+                                        WinABI,
+                                        BOOL,
+                                        HANDLE,
+                                        HANDLE,
+                                        HANDLE,
+                                        HANDLE.ptr,
+                                        DWORD,
+                                        BOOL,
+                                        DWORD
+    );
+
+
+    /*
+    BOOL WINAPI GetExitCodeProcess(
+      __in   HANDLE hProcess,
+      __out  LPDWORD lpExitCode
+    );
+    */
+    var GetExitCodeProcess = kernel32dll.declare("GetExitCodeProcess",
+                                        WinABI,
+                                        BOOL,
+                                        HANDLE,
+                                        LPDWORD
+    );
+
+    /*
+    DWORD WINAPI WaitForSingleObject(
+      __in  HANDLE hHandle,
+      __in  DWORD dwMilliseconds
+    );
+    */
+    var WaitForSingleObject = kernel32dll.declare("WaitForSingleObject",
+                                        WinABI,
+                                        DWORD,
+                                        HANDLE,
+                                        DWORD
+    );
+
+    /*
+    BOOL WINAPI TerminateProcess(
+      __in  HANDLE hProcess,
+      __in  UINT uExitCode
+    );
+    */
+    var TerminateProcess = kernel32dll.declare("TerminateProcess",
+                                        WinABI,
+                                        BOOL,
+                                        HANDLE,
+                                        UINT
+    );
+
+    //functions
+    function popen(command, workdir, args, environment, child) {
+        //escape arguments
+        args.unshift(command);
+        for (var i = 0; i < args.length; i++) {
+          if (typeof args[i] != "string") { args[i] = args[i].toString(); }
+          /* quote arguments with spaces */
+          if (args[i].match(/\s/)) {
+            args[i] = "\"" + args[i] + "\"";
+          }
+          /* If backslash is followed by a quote, double it */
+          args[i] = args[i].replace(/\\\"/g, "\\\\\"");
+        }
+        command = args.join(' ');
+
+        environment = environment || [];
+        if(environment.length) {
+            //An environment block consists of
+            //a null-terminated block of null-terminated strings.
+            //Using CREATE_UNICODE_ENVIRONMENT so needs to be jschar
+            environment = ctypes.jschar.array()(environment.join('\0') + '\0');
+        } else {
+            environment = null;
+        }
+
+        var hOutputReadTmp = new HANDLE(),
+            hOutputRead = new HANDLE(),
+            hOutputWrite = new HANDLE();
+
+        var hErrorRead = new HANDLE(),
+            hErrorReadTmp = new HANDLE(),
+            hErrorWrite = new HANDLE();
+
+        var hInputRead = new HANDLE(),
+            hInputWriteTmp = new HANDLE(),
+            hInputWrite = new HANDLE();
+
+        // Set up the security attributes struct.
+        var sa = new SECURITY_ATTRIBUTES();
+        sa.nLength = SECURITY_ATTRIBUTES.size;
+        sa.lpSecurityDescriptor = null;
+        sa.bInheritHandle = true;
+
+        // Create output pipe.
+
+        if(!CreatePipe(hOutputReadTmp.address(), hOutputWrite.address(), sa.address(), 0))
+            LogError('CreatePipe hOutputReadTmp failed');
+
+        if(options.mergeStderr) {
+          // Create a duplicate of the output write handle for the std error
+          // write handle. This is necessary in case the child application
+          // closes one of its std output handles.
+          if (!DuplicateHandle(GetCurrentProcess(), hOutputWrite,
+                               GetCurrentProcess(), hErrorWrite.address(), 0,
+                               true, DUPLICATE_SAME_ACCESS))
+             LogError("DuplicateHandle hOutputWrite failed");
+        } else {
+            // Create error pipe.
+            if(!CreatePipe(hErrorReadTmp.address(), hErrorWrite.address(), sa.address(), 0))
+                LogError('CreatePipe hErrorReadTmp failed');
+        }
+
+        // Create input pipe.
+        if (!CreatePipe(hInputRead.address(),hInputWriteTmp.address(),sa.address(), 0))
+            LogError("CreatePipe hInputRead failed");
+
+        // Create new output/error read handle and the input write handles. Set
+        // the Properties to FALSE. Otherwise, the child inherits the
+        // properties and, as a result, non-closeable handles to the pipes
+        // are created.
+        if (!DuplicateHandle(GetCurrentProcess(), hOutputReadTmp,
+                             GetCurrentProcess(),
+                             hOutputRead.address(), // Address of new handle.
+                             0, false, // Make it uninheritable.
+                             DUPLICATE_SAME_ACCESS))
+             LogError("DupliateHandle hOutputReadTmp failed");
+
+        if(!options.mergeStderr) {
+            if (!DuplicateHandle(GetCurrentProcess(), hErrorReadTmp,
+                             GetCurrentProcess(),
+                             hErrorRead.address(), // Address of new handle.
+                             0, false, // Make it uninheritable.
+                             DUPLICATE_SAME_ACCESS))
+             LogError("DupliateHandle hErrorReadTmp failed");
+        }
+        if (!DuplicateHandle(GetCurrentProcess(), hInputWriteTmp,
+                             GetCurrentProcess(),
+                             hInputWrite.address(), // Address of new handle.
+                             0, false, // Make it uninheritable.
+                             DUPLICATE_SAME_ACCESS))
+          LogError("DupliateHandle hInputWriteTmp failed");
+
+        // Close inheritable copies of the handles.
+        if (!CloseHandle(hOutputReadTmp)) LogError("CloseHandle hOutputReadTmp failed");
+        if(!options.mergeStderr)
+            if (!CloseHandle(hErrorReadTmp)) LogError("CloseHandle hErrorReadTmp failed");
+        if (!CloseHandle(hInputWriteTmp)) LogError("CloseHandle failed");
+
+        var pi = new PROCESS_INFORMATION();
+        var si = new STARTUPINFO();
+
+        si.cb = STARTUPINFO.size;
+        si.dwFlags = STARTF_USESTDHANDLES;
+        si.hStdInput  = hInputRead;
+        si.hStdOutput = hOutputWrite;
+        si.hStdError  = hErrorWrite;
+
+        // Launch the process
+        if(!CreateProcessW(null,            // executable name
+                           command,         // command buffer
+                           null,            // process security attribute
+                           null,            // thread security attribute
+                           true,            // inherits system handles
+                           CREATE_UNICODE_ENVIRONMENT|CREATE_NO_WINDOW, // process flags
+                           environment,     // envrionment block
+                           workdir,          // set as current directory
+                           si.address(),    // (in) startup information
+                           pi.address()     // (out) process information
+        ))
+            throw("Fatal - Could not launch subprocess '"+command+"'");
+
+        // Close any unnecessary handles.
+        if (!CloseHandle(pi.hThread))
+            LogError("CloseHandle pi.hThread failed");
+
+        // Close pipe handles (do not continue to modify the parent).
+        // You need to make sure that no handles to the write end of the
+        // output pipe are maintained in this process or else the pipe will
+        // not close when the child process exits and the ReadFile will hang.
+        if (!CloseHandle(hInputRead)) LogError("CloseHandle hInputRead failed");
+        if (!CloseHandle(hOutputWrite)) LogError("CloseHandle hOutputWrite failed");
+        if (!CloseHandle(hErrorWrite)) LogError("CloseHandle hErrorWrite failed");
+
+        //return values
+        child.stdin = hInputWrite;
+        child.stdout = hOutputRead;
+        child.stderr = options.mergeStderr ? undefined : hErrorRead;
+        child.process = pi.hProcess;
+        return pi.hProcess;
+    }
+
+    /*
+     * createStdinWriter ()
+     *
+     * Create a ChromeWorker object for writing data to the subprocess' stdin
+     * pipe. The ChromeWorker object lives on a separate thread; this avoids
+     * internal deadlocks.
+     */
+    function createStdinWriter() {
+        debugLog("Creating new stdin worker\n");
+        stdinWorker = new ChromeWorker(WORKER_URL_WIN);
+        stdinWorker.onmessage = function(event) {
+            switch(event.data) {
+            case "WriteOK":
+                pendingWriteCount--;
+                debugLog("got OK from stdinWorker - remaining count: "+pendingWriteCount+"\n");
+                break;
+            case "ClosedOK":
+                stdinOpenState = CLOSED;
+                debugLog("Stdin pipe closed\n");
+                break;
+            default:
+                debugLog("got msg from stdinWorker: "+event.data+"\n");
+            }
+        }
+        stdinWorker.onerror = function(error) {
+            pendingWriteCount--;
+            LogError("got error from stdinWorker: "+error.message+"\n");
+        }
+
+        stdinWorker.postMessage({msg: "init", libc: options.libc});
+    }
+
+    /*
+     * writeStdin()
+     * @data: String containing the data to write
+     *
+     * Write data to the subprocess' stdin (equals to sending a request to the
+     * ChromeWorker object to write the data).
+     */
+    function writeStdin(data) {
+        ++pendingWriteCount;
+        debugLog("sending "+data.length+" bytes to stdinWorker\n");
+        var pipePtr = parseInt(ctypes.cast(child.stdin.address(), ctypes.uintptr_t).value);
+
+        stdinWorker.postMessage({
+                msg: 'write',
+                pipe: pipePtr,
+                data: data
+            });
+    }
+
+    /*
+     * closeStdinHandle()
+     *
+     * Close the stdin pipe, either directly or by requesting the ChromeWorker to
+     * close the pipe. The ChromeWorker will only close the pipe after the last write
+     * request process is done.
+     */
+
+    function closeStdinHandle() {
+        debugLog("trying to close stdin\n");
+        if (stdinOpenState != OPEN) return;
+        stdinOpenState = CLOSEABLE;
+
+        if (stdinWorker) {
+            debugLog("sending close stdin to worker\n");
+            var pipePtr = parseInt(ctypes.cast(child.stdin.address(), ctypes.uintptr_t).value);
+            stdinWorker.postMessage({
+                msg: 'close',
+                pipe: pipePtr
+            });
+        }
+        else {
+            stdinOpenState = CLOSED;
+            debugLog("Closing Stdin\n");
+            CloseHandle(child.stdin) || LogError("CloseHandle hInputWrite failed");
+        }
+    }
+
+
+    /*
+     * createReader(pipe, name)
+     *
+     * @pipe: handle to the pipe
+     * @name: String containing the pipe name (stdout or stderr)
+     *
+     * Create a ChromeWorker object for reading data asynchronously from
+     * the pipe (i.e. on a separate thread), and passing the result back to
+     * the caller.
+     */
+    function createReader(pipe, name, callbackFunc) {
+        var worker = new ChromeWorker(WORKER_URL_WIN);
+        worker.onmessage = function(event) {
+            switch(event.data.msg) {
+            case "data":
+                debugLog("got "+event.data.count+" bytes from "+name+"\n");
+                var data = '';
+                if (options.charset === null) {
+                    data = getBytes(event.data.data);
+                }
+                else {
+                    try {
+                        data = convertBytes(event.data.data, options.charset);
+                    }
+                    catch(ex) {
+                        console.warn("error decoding output: " + ex);
+                        data = getBytes(event.data.data);
+                    }
+                }
+
+                callbackFunc(data);
+                break;
+            case "done":
+                debugLog("Pipe "+name+" closed\n");
+                --readers;
+                if (readers == 0) cleanup();
+                break;
+            default:
+                debugLog("Got msg from "+name+": "+event.data.data+"\n");
+            }
+        }
+
+        worker.onerror = function(errorMsg) {
+            LogError("Got error from "+name+": "+errorMsg.message);
+        }
+
+        var pipePtr = parseInt(ctypes.cast(pipe.address(), ctypes.uintptr_t).value);
+
+        worker.postMessage({
+                msg: 'read',
+                pipe: pipePtr,
+                libc: options.libc,
+                charset: options.charset === null ? "null" : options.charset,
+                name: name
+            });
+
+        return worker;
+    }
+
+    /*
+     * readPipes()
+     *
+     * Open the pipes for reading from stdout and stderr
+     */
+    function readPipes() {
+        stdoutWorker = createReader(child.stdout, "stdout", function (data) {
+            if(options.stdout) {
+                setTimeout(function() {
+                    options.stdout(data);
+                }, 0);
+            } else {
+                output += data;
+            }
+        });
+
+
+        if (!options.mergeStderr) stderrWorker = createReader(child.stderr, "stderr", function (data) {
+            if(options.stderr) {
+                setTimeout(function() {
+                    options.stderr(data);
+                }, 0);
+            } else {
+                error += data;
+            }
+        });
+    }
+
+    /*
+     * cleanup()
+     *
+     * close stdin if needed, get the exit code from the subprocess and invoke
+     * the caller's done() function.
+     *
+     * Note: because stdout() and stderr() are called using setTimeout, we need to
+     * do the same here in order to guarantee the message sequence.
+     */
+    function cleanup() {
+        debugLog("Cleanup called\n");
+        if(active) {
+            active = false;
+
+            closeStdinHandle(); // should only be required in case of errors
+
+            var exit = new DWORD();
+            GetExitCodeProcess(child.process, exit.address());
+            exitCode = exit.value;
+
+            if (stdinWorker)
+                stdinWorker.postMessage({msg: 'stop'})
+
+            setTimeout(function _done() {
+                if (options.done) {
+                    try {
+                        options.done({
+                            exitCode: exitCode,
+                            stdout: output,
+                            stderr: error,
+                        });
+                    }
+                    catch (ex) {
+                        // prevent from blocking if options.done() throws an error
+                        done = true;
+                        throw ex;
+                    }
+                }
+                done = true;
+            }, 0);
+            kernel32dll.close();
+        }
+    }
+
+    var cmdStr = getCommandStr(options.command);
+    var workDir = getWorkDir(options.workdir);
+
+    //main
+    hChildProcess = popen(cmdStr, workDir, options.arguments, options.environment, child);
+
+    readPipes();
+
+    if (options.stdin) {
+       createStdinWriter();
+
+        if(typeof(options.stdin) == 'function') {
+            try {
+                options.stdin({
+                    write: function(data) {
+                        writeStdin(data);
+                    },
+                    close: function() {
+                        closeStdinHandle();
+                    }
+                });
+            }
+            catch (ex) {
+                // prevent from failing if options.stdin() throws an exception
+                closeStdinHandle();
+                throw ex;
+            }
+        } else {
+            writeStdin(options.stdin);
+            closeStdinHandle();
+        }
+    }
+    else
+        closeStdinHandle();
+
+    return {
+        kill: function(hardKill) {
+            // hardKill is currently ignored on Windows
+            var r = !!TerminateProcess(child.process, 255);
+            cleanup(-1);
+            return r;
+        },
+        wait: function() {
+            // wait for async operations to complete
+            var thread = Cc['@mozilla.org/thread-manager;1'].getService(Ci.nsIThreadManager).currentThread;
+            while (!done) thread.processNextEvent(true);
+
+            return exitCode;
+        }
+    }
+}
+
+
+function subprocess_unix(options) {
+    // stdin pipe states
+    const OPEN = 2;
+    const CLOSEABLE = 1;
+    const CLOSED = 0;
+
+    var libc = ctypes.open(options.libc),
+        active = true,
+        done = false,
+        exitCode = -1,
+        workerExitCode = 0,
+        child = {},
+        pid = -1,
+        stdinWorker = null,
+        stdoutWorker = null,
+        stderrWorker = null,
+        pendingWriteCount = 0,
+        readers = options.mergeStderr ? 1 : 2,
+        stdinOpenState = OPEN,
+        error = '',
+        output = '';
+
+    //api declarations
+
+    //pid_t fork(void);
+    var fork = libc.declare("fork",
+                         ctypes.default_abi,
+                         pid_t
+    );
+
+    //NULL terminated array of strings, argv[0] will be command >> + 2
+    var argv = ctypes.char.ptr.array(options.arguments.length + 2);
+    var envp = ctypes.char.ptr.array(options.environment.length + 1);
+
+    // posix_spawn_file_actions_t is a complex struct that may be different on
+    // each platform. We do not care about its attributes, we don't need to
+    // get access to them, but we do need to allocate the right amount
+    // of memory for it.
+    // At 2013/10/28, its size was 80 on linux, but better be safe (and larger),
+    // than crash when posix_spawn_file_actions_init fill `action` with zeros.
+    // Use `gcc sizeof_fileaction.c && ./a.out` to check that size.
+    var posix_spawn_file_actions_t = ctypes.uint8_t.array(100);
+
+    //int posix_spawn(pid_t *restrict pid, const char *restrict path,
+    //   const posix_spawn_file_actions_t *file_actions,
+    //   const posix_spawnattr_t *restrict attrp,
+    //   char *const argv[restrict], char *const envp[restrict]);
+    var posix_spawn = libc.declare("posix_spawn",
+                         ctypes.default_abi,
+                         ctypes.int,
+                         pid_t.ptr,
+                         ctypes.char.ptr,
+                         posix_spawn_file_actions_t.ptr,
+                         ctypes.voidptr_t,
+                         argv,
+                         envp
+    );
+
+    //int posix_spawn_file_actions_init(posix_spawn_file_actions_t *file_actions);
+    var posix_spawn_file_actions_init = libc.declare("posix_spawn_file_actions_init",
+                         ctypes.default_abi,
+                         ctypes.int,
+                         posix_spawn_file_actions_t.ptr
+    );
+
+    //int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *file_actions);
+    var posix_spawn_file_actions_destroy = libc.declare("posix_spawn_file_actions_destroy",
+                         ctypes.default_abi,
+                         ctypes.int,
+                         posix_spawn_file_actions_t.ptr
+    );
+
+    // int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *
+    //                                      file_actions, int fildes, int newfildes);
+    var posix_spawn_file_actions_adddup2 = libc.declare("posix_spawn_file_actions_adddup2",
+                         ctypes.default_abi,
+                         ctypes.int,
+                         posix_spawn_file_actions_t.ptr,
+                         ctypes.int,
+                         ctypes.int
+    );
+
+    // int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *
+    //                                       file_actions, int fildes);
+    var posix_spawn_file_actions_addclose = libc.declare("posix_spawn_file_actions_addclose",
+                         ctypes.default_abi,
+                         ctypes.int,
+                         posix_spawn_file_actions_t.ptr,
+                         ctypes.int
+    );
+
+    //int pipe(int pipefd[2]);
+    var pipefd = ctypes.int.array(2);
+    var pipe = libc.declare("pipe",
+                         ctypes.default_abi,
+                         ctypes.int,
+                         pipefd
+    );
+
+    //int close(int fd);
+    var close = libc.declare("close",
+                          ctypes.default_abi,
+                          ctypes.int,
+                          ctypes.int
+    );
+
+    //pid_t waitpid(pid_t pid, int *status, int options);
+    var waitpid = libc.declare("waitpid",
+                          ctypes.default_abi,
+                          pid_t,
+                          pid_t,
+                          ctypes.int.ptr,
+                          ctypes.int
+    );
+
+    //int kill(pid_t pid, int sig);
+    var kill = libc.declare("kill",
+                          ctypes.default_abi,
+                          ctypes.int,
+                          pid_t,
+                          ctypes.int
+    );
+
+    //int read(int fd, void *buf, size_t count);
+    var bufferSize = 1024;
+    var buffer = ctypes.char.array(bufferSize);
+    var read = libc.declare("read",
+                          ctypes.default_abi,
+                          ctypes.int,
+                          ctypes.int,
+                          buffer,
+                          ctypes.int
+    );
+
+    //ssize_t write(int fd, const void *buf, size_t count);
+    var write = libc.declare("write",
+                          ctypes.default_abi,
+                          ctypes.int,
+                          ctypes.int,
+                          ctypes.char.ptr,
+                          ctypes.int
+    );
+
+    //int chdir(const char *path);
+    var chdir = libc.declare("chdir",
+                          ctypes.default_abi,
+                          ctypes.int,
+                          ctypes.char.ptr
+    );
+
+    //int fcntl(int fd, int cmd, ... /* arg */ );
+    var fcntl = libc.declare("fcntl",
+                          ctypes.default_abi,
+                          ctypes.int,
+                          ctypes.int,
+                          ctypes.int,
+                          ctypes.int
+    );
+
+    function popen(command, workdir, args, environment, child) {
+        var _in,
+            _out,
+            _err,
+            pid,
+            rc;
+        _in = new pipefd();
+        _out = new pipefd();
+        if(!options.mergeStderr)
+            _err = new pipefd();
+
+        var _args = argv();
+        args.unshift(command);
+        for(var i=0;i<args.length;i++) {
+            _args[i] = ctypes.char.array()(args[i]);
+        }
+        var _envp = envp();
+        for(var i=0;i<environment.length;i++) {
+            _envp[i] = ctypes.char.array()(environment[i]);
+            // LogError(_envp);
+        }
+
+        rc = pipe(_in);
+        if (rc < 0) {
+            return -1;
+        }
+        rc = pipe(_out);
+        fcntl(_out[0], F_SETFL, getPlatformValue(O_NONBLOCK));
+        if (rc < 0) {
+            close(_in[0]);
+            close(_in[1]);
+            return -1
+        }
+        if(!options.mergeStderr) {
+            rc = pipe(_err);
+            fcntl(_err[0], F_SETFL, getPlatformValue(O_NONBLOCK));
+            if (rc < 0) {
+                close(_in[0]);
+                close(_in[1]);
+                close(_out[0]);
+                close(_out[1]);
+                return -1
+            }
+        }
+
+        let STDIN_FILENO = 0;
+        let STDOUT_FILENO = 1;
+        let STDERR_FILENO = 2;
+
+        let action = posix_spawn_file_actions_t();
+        posix_spawn_file_actions_init(action.address());
+
+        posix_spawn_file_actions_adddup2(action.address(), _in[0], STDIN_FILENO);
+        posix_spawn_file_actions_addclose(action.address(), _in[1]);
+        posix_spawn_file_actions_addclose(action.address(), _in[0]);
+
+        posix_spawn_file_actions_adddup2(action.address(), _out[1], STDOUT_FILENO);
+        posix_spawn_file_actions_addclose(action.address(), _out[1]);
+        posix_spawn_file_actions_addclose(action.address(), _out[0]);
+
+        if (!options.mergeStderr) {
+          posix_spawn_file_actions_adddup2(action.address(), _err[1], STDERR_FILENO);
+          posix_spawn_file_actions_addclose(action.address(), _err[1]);
+          posix_spawn_file_actions_addclose(action.address(), _err[0]);
+        }
+
+        // posix_spawn doesn't support setting a custom workdir for the child,
+        // so change the cwd in the parent process before launching the child process.
+        if (workdir) {
+          if (chdir(workdir) < 0) {
+            throw new Error("Unable to change workdir before launching child process");
+          }
+        }
+
+        closeOtherFds(action, _in[1], _out[0], options.mergeStderr ? undefined : _err[0]);
+
+        let id = pid_t(0);
+        let rv = posix_spawn(id.address(), command, action.address(), null, _args, _envp);
+        posix_spawn_file_actions_destroy(action.address());
+        if (rv != 0) {
+          // we should not really end up here
+          if(!options.mergeStderr) {
+            close(_err[0]);
+            close(_err[1]);
+          }
+          close(_out[0]);
+          close(_out[1]);
+          close(_in[0]);
+          close(_in[1]);
+          throw new Error("Fatal - failed to create subprocess '"+command+"'");
+        }
+        pid = id.value;
+
+        close(_in[0]);
+        close(_out[1]);
+        if (!options.mergeStderr)
+          close(_err[1]);
+        child.stdin  = _in[1];
+        child.stdout = _out[0];
+        child.stderr = options.mergeStderr ? undefined : _err[0];
+        child.pid = pid;
+
+        return pid;
+    }
+
+
+    // close any file descriptors that are not required for the process
+    function closeOtherFds(action, fdIn, fdOut, fdErr) {
+        // Unfortunately on mac, any fd registered in posix_spawn_file_actions_addclose
+        // that can't be closed correctly will make posix_spawn fail...
+        // Even if we ensure registering only still opened fds.
+        if (gXulRuntime.OS == "Darwin")
+            return;
+
+        var maxFD = 256; // arbitrary max
+
+
+        var rlim_t = getPlatformValue(RLIM_T);
+
+        const RLIMITS = new ctypes.StructType("RLIMITS", [
+            {"rlim_cur": rlim_t},
+            {"rlim_max": rlim_t}
+        ]);
+
+        try {
+            var getrlimit = libc.declare("getrlimit",
+                                  ctypes.default_abi,
+                                  ctypes.int,
+                                  ctypes.int,
+                                  RLIMITS.ptr
+            );
+
+            var rl = new RLIMITS();
+            if (getrlimit(getPlatformValue(RLIMIT_NOFILE), rl.address()) == 0) {
+                maxFD = rl.rlim_cur;
+            }
+            debugLog("getlimit: maxFD="+maxFD+"\n");
+
+        }
+        catch(ex) {
+            debugLog("getrlimit: no such function on this OS\n");
+            debugLog(ex.toString());
+        }
+
+        // close any file descriptors
+        // fd's 0-2 are already closed
+        for (var i = 3; i < maxFD; i++) {
+            if (i != fdIn && i != fdOut && i != fdErr && fcntl(i, F_GETFD, -1) >= 0) {
+                posix_spawn_file_actions_addclose(action.address(), i);
+            }
+        }
+    }
+
+    /*
+     * createStdinWriter ()
+     *
+     * Create a ChromeWorker object for writing data to the subprocess' stdin
+     * pipe. The ChromeWorker object lives on a separate thread; this avoids
+     * internal deadlocks.
+     */
+    function createStdinWriter() {
+        debugLog("Creating new stdin worker\n");
+        stdinWorker = new ChromeWorker(WORKER_URL_UNIX);
+        stdinWorker.onmessage = function(event) {
+            switch (event.data.msg) {
+            case "info":
+                switch(event.data.data) {
+                case "WriteOK":
+                    pendingWriteCount--;
+                    debugLog("got OK from stdinWorker - remaining count: "+pendingWriteCount+"\n");
+                    break;
+                case "ClosedOK":
+                    stdinOpenState = CLOSED;
+                    debugLog("Stdin pipe closed\n");
+                    break;
+                default:
+                    debugLog("got msg from stdinWorker: "+event.data.data+"\n");
+                }
+                break;
+            case "debug":
+                debugLog("stdinWorker: "+event.data.data+"\n");
+                break;
+            case "error":
+                LogError("got error from stdinWorker: "+event.data.data+"\n");
+                pendingWriteCount = 0;
+                stdinOpenState = CLOSED;
+            }
+        }
+        stdinWorker.onerror = function(error) {
+            pendingWriteCount = 0;
+            closeStdinHandle();
+            LogError("got error from stdinWorker: "+error.message+"\n");
+        }
+        stdinWorker.postMessage({msg: "init", libc: options.libc});
+    }
+
+    /*
+     * writeStdin()
+     * @data: String containing the data to write
+     *
+     * Write data to the subprocess' stdin (equals to sending a request to the
+     * ChromeWorker object to write the data).
+     */
+    function writeStdin(data) {
+        if (stdinOpenState == CLOSED) return; // do not write to closed pipes
+
+        ++pendingWriteCount;
+        debugLog("sending "+data.length+" bytes to stdinWorker\n");
+        var pipe = parseInt(child.stdin);
+
+        stdinWorker.postMessage({
+            msg: 'write',
+            pipe: pipe,
+            data: data
+        });
+    }
+
+
+    /*
+     * closeStdinHandle()
+     *
+     * Close the stdin pipe, either directly or by requesting the ChromeWorker to
+     * close the pipe. The ChromeWorker will only close the pipe after the last write
+     * request process is done.
+     */
+
+    function closeStdinHandle() {
+        debugLog("trying to close stdin\n");
+        if (stdinOpenState != OPEN) return;
+        stdinOpenState = CLOSEABLE;
+
+        if (stdinWorker) {
+            debugLog("sending close stdin to worker\n");
+            var pipePtr = parseInt(child.stdin);
+
+            stdinWorker.postMessage({
+                msg: 'close',
+                pipe: pipePtr
+            });
+        }
+        else {
+            stdinOpenState = CLOSED;
+            debugLog("Closing Stdin\n");
+            close(child.stdin) && LogError("CloseHandle stdin failed");
+        }
+    }
+
+
+    /*
+     * createReader(pipe, name)
+     *
+     * @pipe: handle to the pipe
+     * @name: String containing the pipe name (stdout or stderr)
+     * @callbackFunc: function to be called with the read data
+     *
+     * Create a ChromeWorker object for reading data asynchronously from
+     * the pipe (i.e. on a separate thread), and passing the result back to
+     * the caller.
+     *
+     */
+    function createReader(pipe, name, callbackFunc) {
+        var worker = new ChromeWorker(WORKER_URL_UNIX);
+        worker.onmessage = function(event) {
+            switch(event.data.msg) {
+            case "data":
+                debugLog("got "+event.data.count+" bytes from "+name+"\n");
+                var data = '';
+                if (options.charset === null) {
+                    data = getBytes(event.data.data);
+                }
+                else {
+                    try {
+                        data = convertBytes(event.data.data, options.charset);
+                    }
+                    catch(ex) {
+                        console.warn("error decoding output: " + ex);
+                        data = getBytes(event.data.data);
+                    }
+                }
+
+                callbackFunc(data);
+                break;
+            case "done":
+                debugLog("Pipe "+name+" closed\n");
+                if (event.data.data > 0) workerExitCode = event.data.data;
+                --readers;
+                if (readers == 0) cleanup();
+                break;
+            default:
+                debugLog("Got msg from "+name+": "+event.data.data+"\n");
+            }
+        }
+        worker.onerror = function(error) {
+            LogError("Got error from "+name+": "+error.message);
+        }
+
+        worker.postMessage({
+                msg: 'read',
+                pipe: pipe,
+                pid: pid,
+                libc: options.libc,
+                charset: options.charset === null ? "null" : options.charset,
+                name: name
+            });
+
+        return worker;
+    }
+
+    /*
+     * readPipes()
+     *
+     * Open the pipes for reading from stdout and stderr
+     */
+    function readPipes() {
+
+        stdoutWorker = createReader(child.stdout, "stdout", function (data) {
+            if(options.stdout) {
+                setTimeout(function() {
+                    options.stdout(data);
+                }, 0);
+            } else {
+                output += data;
+            }
+        });
+
+        if (!options.mergeStderr) stderrWorker = createReader(child.stderr, "stderr", function (data) {
+            if(options.stderr) {
+                setTimeout(function() {
+                    options.stderr(data);
+                 }, 0);
+            } else {
+                error += data;
+            }
+        });
+    }
+
+    function cleanup() {
+        debugLog("Cleanup called\n");
+        if(active) {
+            active = false;
+
+            closeStdinHandle(); // should only be required in case of errors
+
+            var result, status = ctypes.int();
+            result = waitpid(child.pid, status.address(), 0);
+            if (result > 0)
+                exitCode = status.value
+            else
+                if (workerExitCode >= 0)
+                    exitCode = workerExitCode
+                else
+                    exitCode = status.value;
+
+            if (stdinWorker)
+                stdinWorker.postMessage({msg: 'stop'})
+
+            setTimeout(function _done() {
+                if (options.done) {
+                    try {
+                        options.done({
+                            exitCode: exitCode,
+                            stdout: output,
+                            stderr: error,
+                        });
+                    }
+                    catch(ex) {
+                        // prevent from blocking if options.done() throws an error
+                        done = true;
+                        throw ex;
+                    }
+
+                }
+                done = true;
+            }, 0);
+
+            libc.close();
+        }
+    }
+
+    //main
+
+    var cmdStr = getCommandStr(options.command);
+    var workDir = getWorkDir(options.workdir);
+
+    child = {};
+    pid = popen(cmdStr, workDir, options.arguments, options.environment, child);
+
+    debugLog("subprocess started; got PID "+pid+"\n");
+
+    readPipes();
+
+    if (options.stdin) {
+        createStdinWriter();
+        if(typeof(options.stdin) == 'function') {
+            try {
+                options.stdin({
+                    write: function(data) {
+                        writeStdin(data);
+                    },
+                    close: function() {
+                        closeStdinHandle();
+                    }
+                });
+            }
+            catch(ex) {
+                // prevent from failing if options.stdin() throws an exception
+                closeStdinHandle();
+                throw ex;
+            }
+        } else {
+            writeStdin(options.stdin);
+            closeStdinHandle();
+        }
+    }
+
+    return {
+        wait: function() {
+            // wait for async operations to complete
+            var thread = Cc['@mozilla.org/thread-manager;1'].getService(Ci.nsIThreadManager).currentThread;
+            while (! done) thread.processNextEvent(true)
+            return exitCode;
+        },
+        kill: function(hardKill) {
+            var rv = kill(pid, (hardKill ? 9: 15));
+            cleanup(-1);
+            return rv;
+        },
+        pid: pid
+    }
+}
+
+
+module.exports = subprocess;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process/subprocess_worker_unix.js
@@ -0,0 +1,278 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "MPL"); you may not use this file
+ * except in compliance with the MPL. You may obtain a copy of
+ * the MPL at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the MPL is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the MPL for the specific language governing
+ * rights and limitations under the MPL.
+ *
+ * The Original Code is subprocess.jsm.
+ *
+ * The Initial Developer of this code is Patrick Brunschwig.
+ * Portions created by Patrick Brunschwig <patrick@enigmail.net>
+ * are Copyright (C) 2011 Patrick Brunschwig.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jan Gerber <j@mailb.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * ChromeWorker Object subprocess.jsm on Unix-like systems (Linux, Mac OS X, ...)
+ * to process stdin/stdout/stderr on separate threads.
+ *
+ */
+
+// Being a ChromeWorker object, implicitly uses the following:
+// Cu.import("resource://gre/modules/ctypes.jsm");
+
+'use strict';
+
+const BufferSize = 1024;
+
+var libc = null;
+var libcFunc = {};
+
+
+/*
+    struct pollfd {
+         int    fd;       // file descriptor
+         short  events;   // events to look for
+         short  revents;  // events returned
+     };
+*/
+
+var pollfd = new ctypes.StructType("pollfd",
+                        [   {'fd': ctypes.int},
+                            {'events': ctypes.short},
+                            {'revents': ctypes.short}
+                        ]);
+
+var WriteBuffer = ctypes.uint8_t.array(BufferSize);
+var ReadBuffer = ctypes.char.array(BufferSize);
+
+
+const POLLIN     = 0x0001;
+const POLLOUT    = 0x0004;
+
+const POLLERR    = 0x0008;         // some poll error occurred
+const POLLHUP    = 0x0010;         // file descriptor was "hung up"
+const POLLNVAL   = 0x0020;         // requested events "invalid"
+
+const WNOHANG    = 0x01;
+
+const pid_t = ctypes.int32_t;
+
+const INDEFINITE = -1;
+const NOWAIT     = 0;
+const WAITTIME   = 200  // wait time for poll() in ms
+
+function initLibc(libName) {
+    postMessage({msg: "debug", data: "initialising library with "+ libName});
+
+    libc = ctypes.open(libName);
+
+    libcFunc.pollFds = pollfd.array(1);
+
+    // int poll(struct pollfd fds[], nfds_t nfds, int timeout);
+    libcFunc.poll = libc.declare("poll",
+                                  ctypes.default_abi,
+                                  ctypes.int,
+                                  libcFunc.pollFds,
+                                  ctypes.unsigned_int,
+                                  ctypes.int);
+
+    //ssize_t write(int fd, const void *buf, size_t count);
+    // NOTE: buf is declared as array of unsigned int8 instead of char to avoid
+    // implicit charset conversion
+    libcFunc.write = libc.declare("write",
+                                  ctypes.default_abi,
+                                  ctypes.int,
+                                  ctypes.int,
+                                  WriteBuffer,
+                                  ctypes.int);
+
+    //int read(int fd, void *buf, size_t count);
+    libcFunc.read = libc.declare("read",
+                                  ctypes.default_abi,
+                                  ctypes.int,
+                                  ctypes.int,
+                                  ReadBuffer,
+                                  ctypes.int);
+
+    //int pipe(int pipefd[2]);
+    libcFunc.pipefd = ctypes.int.array(2);
+
+    //int close(int fd);
+    libcFunc.close = libc.declare("close",
+                                  ctypes.default_abi,
+                                  ctypes.int,
+                                  ctypes.int);
+
+    //pid_t waitpid(pid_t pid, int *status, int options);
+    libcFunc.waitpid = libc.declare("waitpid",
+                                  ctypes.default_abi,
+                                  pid_t,
+                                  pid_t,
+                                  ctypes.int.ptr,
+                                  ctypes.int);
+}
+
+function closePipe(pipe) {
+    libcFunc.close(pipe);
+}
+
+function writePipe(pipe, data) {
+
+    postMessage({msg: "debug", data: "trying to write to "+pipe});
+
+    let numChunks = Math.floor(data.length / BufferSize);
+    let pData = new WriteBuffer();
+
+    for (var chunk = 0; chunk <= numChunks; chunk ++) {
+        let numBytes = chunk < numChunks ? BufferSize : data.length - chunk * BufferSize;
+        for (var i=0; i < numBytes; i++) {
+            pData[i] = data.charCodeAt(chunk * BufferSize + i) % 256;
+        }
+
+        let bytesWritten = libcFunc.write(pipe, pData, numBytes);
+        if (bytesWritten != numBytes) {
+            closePipe(pipe);
+            postMessage({ msg: "error", data: "error: wrote "+bytesWritten+" instead of "+numBytes+" bytes"});
+            close();
+        }
+    }
+    postMessage({msg: "info", data: "wrote "+data.length+" bytes of data"});
+}
+
+
+function readString(data, length, charset) {
+    var string = '', bytes = [];
+    for(var i = 0;i < length; i++) {
+        if(data[i] == 0 && charset != "null") // stop on NULL character for non-binary data
+           break;
+
+        bytes.push(data[i]);
+    }
+
+    return bytes;
+}
+
+function readPipe(pipe, charset, pid) {
+    var p = new libcFunc.pollFds;
+    p[0].fd = pipe;
+    p[0].events = POLLIN | POLLERR | POLLHUP;
+    p[0].revents = 0;
+    var pollTimeout = WAITTIME;
+    var exitCode = -1;
+    var readCount = 0;
+    var result, status = ctypes.int();
+    result = 0;
+
+
+    const i=0;
+    while (true) {
+        if (result == 0) {
+            result = libcFunc.waitpid(pid, status.address(), WNOHANG);
+            if (result > 0) {
+                pollTimeout = NOWAIT;
+                exitCode = parseInt(status.value);
+                postMessage({msg: "debug", data: "waitpid signaled subprocess stop, exitcode="+status.value });
+            }
+        }
+        var r = libcFunc.poll(p, 1, pollTimeout);
+        if (r > 0) {
+            if (p[i].revents & POLLIN) {
+                postMessage({msg: "debug", data: "reading next chunk"});
+                readCount = readPolledFd(p[i].fd, charset);
+                if (readCount == 0) break;
+            }
+
+            if (p[i].revents & POLLHUP) {
+                postMessage({msg: "debug", data: "poll returned HUP"});
+                break;
+            }
+            else if (p[i].revents & POLLERR) {
+                postMessage({msg: "error", data: "poll returned error"});
+                break;
+            }
+            else if (p[i].revents != POLLIN) {
+                postMessage({msg: "error", data: "poll returned "+p[i]});
+                break;
+            }
+        }
+        else
+            if (pollTimeout == 0 || r < 0) break;
+    }
+
+    // continue reading until the buffer is empty
+    while (readCount > 0) {
+      readCount = readPolledFd(pipe, charset);
+    }
+
+    libcFunc.close(pipe);
+    postMessage({msg: "done", data: exitCode });
+    libc.close();
+    close();
+}
+
+function readPolledFd(pipe, charset) {
+    var line = new ReadBuffer();
+    var r = libcFunc.read(pipe, line, BufferSize);
+
+    if (r > 0) {
+        var c = readString(line, r, charset);
+        postMessage({msg: "data", data: c, count: c.length});
+    }
+    return r;
+}
+
+onmessage = function (event) {
+    switch (event.data.msg) {
+    case "init":
+        initLibc(event.data.libc);
+        break;
+    case "read":
+        initLibc(event.data.libc);
+        readPipe(event.data.pipe, event.data.charset, event.data.pid);
+        break;
+    case "write":
+        // data contents:
+        //   msg: 'write'
+        //   data: the data (string) to write
+        //   pipe: ptr to pipe
+        writePipe(event.data.pipe, event.data.data);
+        postMessage({msg: "info", data: "WriteOK"});
+        break;
+    case "close":
+        postMessage({msg: "debug", data: "closing stdin\n"});
+
+        closePipe(event.data.pipe);
+        postMessage({msg: "info", data: "ClosedOK"});
+        break;
+    case "stop":
+        libc.close(); // do not use libc after this point
+        close();
+        break;
+    default:
+        throw("error: Unknown command"+event.data.msg+"\n");
+    }
+    return;
+};
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/child_process/subprocess_worker_win.js
@@ -0,0 +1,237 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "MPL"); you may not use this file
+ * except in compliance with the MPL. You may obtain a copy of
+ * the MPL at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the MPL is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the MPL for the specific language governing
+ * rights and limitations under the MPL.
+ *
+ * The Original Code is subprocess.jsm.
+ *
+ * The Initial Developer of this code is Patrick Brunschwig.
+ * Portions created by Patrick Brunschwig <patrick@enigmail.net>
+ * are Copyright (C) 2011 Patrick Brunschwig.
+ * All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jan Gerber <j@mailb.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * ChromeWorker Object subprocess.jsm on Windows to process stdin/stdout/stderr
+ * on separate threads.
+ *
+ */
+
+// Being a ChromeWorker object, implicitly uses the following:
+// Cu.import("resource://gre/modules/ctypes.jsm");
+
+'use strict';
+
+const BufferSize = 1024;
+
+const BOOL = ctypes.bool;
+const HANDLE = ctypes.size_t;
+const DWORD = ctypes.uint32_t;
+const LPDWORD = DWORD.ptr;
+const PVOID = ctypes.voidptr_t;
+const LPVOID = PVOID;
+
+/*
+typedef struct _OVERLAPPED {
+  ULONG_PTR Internal;
+  ULONG_PTR InternalHigh;
+  union {
+    struct {
+      DWORD Offset;
+      DWORD OffsetHigh;
+    };
+    PVOID  Pointer;
+  };
+  HANDLE    hEvent;
+} OVERLAPPED, *LPOVERLAPPED;
+*/
+const OVERLAPPED = new ctypes.StructType("OVERLAPPED");
+
+var ReadFileBuffer = ctypes.char.array(BufferSize);
+var WriteFileBuffer = ctypes.uint8_t.array(BufferSize);
+
+var kernel32dll = null;
+var libFunc = {};
+
+function initLib(libName) {
+    if (ctypes.size_t.size == 8) {
+        var WinABI = ctypes.default_abi;
+    } else {
+        var WinABI = ctypes.winapi_abi;
+    }
+
+    kernel32dll = ctypes.open(libName);
+
+    /*
+    BOOL WINAPI WriteFile(
+      __in         HANDLE hFile,
+      __in         LPCVOID lpBuffer,
+      __in         DWORD nNumberOfBytesToWrite,
+      __out_opt    LPDWORD lpNumberOfBytesWritten,
+      __inout_opt  LPOVERLAPPED lpOverlapped
+    );
+
+    NOTE: lpBuffer is declared as array of unsigned int8 instead of char to avoid
+           implicit charset conversion
+    */
+    libFunc.WriteFile = kernel32dll.declare("WriteFile",
+                                        WinABI,
+                                        BOOL,
+                                        HANDLE,
+                                        WriteFileBuffer,
+                                        DWORD,
+                                        LPDWORD,
+                                        OVERLAPPED.ptr
+    );
+
+    /*
+    BOOL WINAPI ReadFile(
+      __in         HANDLE hFile,
+      __out        LPVOID ReadFileBuffer,
+      __in         DWORD nNumberOfBytesToRead,
+      __out_opt    LPDWORD lpNumberOfBytesRead,
+      __inout_opt  LPOVERLAPPED lpOverlapped
+    );
+    */
+    libFunc.ReadFile = kernel32dll.declare("ReadFile",
+                                        WinABI,
+                                        BOOL,
+                                        HANDLE,
+                                        ReadFileBuffer,
+                                        DWORD,
+                                        LPDWORD,
+                                        OVERLAPPED.ptr
+    );
+
+    /*
+    BOOL WINAPI CloseHandle(
+      __in  HANDLE hObject
+    );
+    */
+    libFunc.CloseHandle = kernel32dll.declare("CloseHandle",
+                                            WinABI,
+                                            BOOL,
+                                            HANDLE
+    );
+}
+
+
+function writePipe(pipe, data) {
+    var bytesWritten = DWORD(0);
+
+    var pData = new WriteFileBuffer();
+
+    var numChunks = Math.floor(data.length / BufferSize);
+    for (var chunk = 0; chunk <= numChunks; chunk ++) {
+        var numBytes = chunk < numChunks ? BufferSize : data.length - chunk * BufferSize;
+        for (var i=0; i < numBytes; i++) {
+            pData[i] = data.charCodeAt(chunk * BufferSize + i) % 256;
+        }
+
+      var r = libFunc.WriteFile(pipe, pData, numBytes, bytesWritten.address(), null);
+      if (bytesWritten.value != numBytes)
+          throw("error: wrote "+bytesWritten.value+" instead of "+numBytes+" bytes");
+    }
+    postMessage("wrote "+data.length+" bytes of data");
+}
+
+function readString(data, length, charset) {
+    var string = '', bytes = [];
+    for(var i = 0;i < length; i++) {
+        if(data[i] == 0 && charset != "null") // stop on NULL character for non-binary data
+           break;
+
+        bytes.push(data[i]);
+    }
+
+    return bytes;
+}
+
+function readPipe(pipe, charset) {
+    while (true) {
+        var bytesRead = DWORD(0);
+        var line = new ReadFileBuffer();
+        var r = libFunc.ReadFile(pipe, line, BufferSize, bytesRead.address(), null);
+
+        if (!r) {
+            // stop if we get an error (such as EOF reached)
+            postMessage({msg: "info", data: "ReadFile failed"});
+            break;
+        }
+
+        if (bytesRead.value > 0) {
+            var c = readString(line, bytesRead.value, charset);
+            postMessage({msg: "data", data: c, count: c.length});
+        }
+        else {
+            break;
+        }
+    }
+    libFunc.CloseHandle(pipe);
+    postMessage({msg: "done"});
+    kernel32dll.close();
+    close();
+}
+
+onmessage = function (event) {
+    let pipePtr;
+    switch (event.data.msg) {
+    case "init":
+        initLib(event.data.libc);
+        break;
+    case "write":
+        // data contents:
+        //   msg: 'write'
+        //   data: the data (string) to write
+        //   pipe: ptr to pipe
+        pipePtr = HANDLE.ptr(event.data.pipe);
+        writePipe(pipePtr.contents, event.data.data);
+        postMessage("WriteOK");
+        break;
+    case "read":
+        initLib(event.data.libc);
+        pipePtr = HANDLE.ptr(event.data.pipe);
+        readPipe(pipePtr.contents, event.data.charset);
+        break;
+    case "close":
+        pipePtr = HANDLE.ptr(event.data.pipe);
+        postMessage("closing stdin\n");
+
+        if (libFunc.CloseHandle(pipePtr.contents)) {
+            postMessage("ClosedOK");
+        }
+        else
+            postMessage("Could not close stdin handle");
+        break;
+    case "stop":
+        kernel32dll.close();
+        close();
+        break;
+    default:
+        throw("error: Unknown command"+event.data.msg+"\n");
+    }
+    return;
+};
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/system/process.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+  "stability": "unstable"
+};
+
+const {
+  exit, version, stdout, stderr, platform, architecture
+} = require("../system");
+
+/**
+ * Supported
+ */
+
+exports.stdout = stdout;
+exports.stderr = stderr;
+exports.version = version;
+exports.versions = {};
+exports.config = {};
+exports.arch = architecture;
+exports.platform = platform;
+exports.exit = exit;
+
+/**
+ * Partial support
+ */
+
+// An alias to `setTimeout(fn, 0)`, which isn't the same as node's `nextTick`,
+// but atleast ensures it'll occur asynchronously
+exports.nextTick = (callback) => setTimeout(callback, 0);
+
+/**
+ * Unsupported
+ */
+
+exports.maxTickDepth = 1000;
+exports.pid = 0;
+exports.title = "";
+exports.stdin = {};
+exports.argv = [];
+exports.execPath = "";
+exports.execArgv = [];
+exports.abort = function () {};
+exports.chdir = function () {};
+exports.cwd = function () {};
+exports.env = {};
+exports.getgid = function () {};
+exports.setgid = function () {};
+exports.getuid = function () {};
+exports.setuid = function () {};
+exports.getgroups = function () {};
+exports.setgroups = function () {};
+exports.initgroups = function () {};
+exports.kill = function () {};
+exports.memoryUsage = function () {};
+exports.umask = function () {};
+exports.uptime = function () {};
+exports.hrtime = function () {};
--- a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
+++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js
@@ -30,19 +30,24 @@ const Tab = Class({
 
     let window = tabInternals.window = options.window || getOwnerWindow(tab);
     tabInternals.tab = tab;
 
     // TabReady
     let onReady = tabInternals.onReady = onTabReady.bind(this);
     tab.browser.addEventListener(EVENTS.ready.dom, onReady, false);
 
+    // TabPageShow
     let onPageShow = tabInternals.onPageShow = onTabPageShow.bind(this);
     tab.browser.addEventListener(EVENTS.pageshow.dom, onPageShow, false);
 
+    // TabLoad
+    let onLoad = tabInternals.onLoad = onTabLoad.bind(this);
+    tab.browser.addEventListener(EVENTS.load.dom, onLoad, true);
+
     // TabClose
     let onClose = tabInternals.onClose = onTabClose.bind(this);
     window.BrowserApp.deck.addEventListener(EVENTS.close.dom, onClose, false);
 
     unload(cleanupTab.bind(null, this));
   },
 
   /**
@@ -184,35 +189,46 @@ viewFor.define(Tab, x => tabNS(x).tab);
 function cleanupTab(tab) {
   let tabInternals = tabNS(tab);
   if (!tabInternals.tab)
     return;
 
   if (tabInternals.tab.browser) {
     tabInternals.tab.browser.removeEventListener(EVENTS.ready.dom, tabInternals.onReady, false);
     tabInternals.tab.browser.removeEventListener(EVENTS.pageshow.dom, tabInternals.onPageShow, false);
+    tabInternals.tab.browser.removeEventListener(EVENTS.load.dom, tabInternals.onLoad, true);
   }
   tabInternals.onReady = null;
   tabInternals.onPageShow = null;
+  tabInternals.onLoad = null;
   tabInternals.window.BrowserApp.deck.removeEventListener(EVENTS.close.dom, tabInternals.onClose, false);
   tabInternals.onClose = null;
   rawTabNS(tabInternals.tab).tab = null;
   tabInternals.tab = null;
   tabInternals.window = null;
 }
 
 function onTabReady(event) {
   let win = event.target.defaultView;
 
   // ignore frames
   if (win === win.top) {
     emit(this, 'ready', this);
   }
 }
 
+function onTabLoad (event) {
+  let win = event.target.defaultView;
+
+  // ignore frames
+  if (win === win.top) {
+    emit(this, 'load', this);
+  }
+}
+
 function onTabPageShow(event) {
   let win = event.target.defaultView;
   if (win === win.top)
     emit(this, 'pageshow', this, event.persisted);
 }
 
 // TabClose
 function onTabClose(event) {
--- a/addon-sdk/source/lib/sdk/tabs/utils.js
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -177,16 +177,21 @@ exports.getBrowserForTab = getBrowserFor
 function getTabId(tab) {
   if (tab.browser) // fennec
     return tab.id
 
   return String.split(tab.linkedPanel, 'panel').pop();
 }
 exports.getTabId = getTabId;
 
+function getTabForId(id) {
+  return getTabs().find(tab => getTabId(tab) === id) || null;
+}
+exports.getTabForId = getTabForId;
+
 function getTabTitle(tab) {
   return getBrowserForTab(tab).contentDocument.title || tab.label || "";
 }
 exports.getTabTitle = getTabTitle;
 
 function setTabTitle(tab, title) {
   title = String(title);
   if (tab.browser)
@@ -259,17 +264,17 @@ function getTabForWindow(window) {
     if (!BrowserApp)
       continue;
 
     for each (let tab in BrowserApp.tabs) {
       if (tab.browser.contentWindow == window.top)
         return tab;
     }
   }
-  return null; 
+  return null;
 }
 
 function getTabURL(tab) {
   if (tab.browser) // fennec
     return String(tab.browser.currentURI.spec);
   return String(getBrowserForTab(tab).currentURI.spec);
 }
 exports.getTabURL = getTabURL;
--- a/addon-sdk/source/lib/sdk/ui/button/action.js
+++ b/addon-sdk/source/lib/sdk/ui/button/action.js
@@ -24,21 +24,24 @@ catch (e) {
 const { Class } = require('../../core/heritage');
 const { merge } = require('../../util/object');
 const { Disposable } = require('../../core/disposable');
 const { on, off, emit, setListeners } = require('../../event/core');
 const { EventTarget } = require('../../event/target');
 
 const view = require('./view');
 const { buttonContract, stateContract } = require('./contract');
-const { properties, render, state, register, unregister } = require('../state');
+const { properties, render, state, register, unregister,
+        getDerivedStateFor } = require('../state');
 const { events: stateEvents } = require('../state/events');
 const { events: viewEvents } = require('./view/events');
 const events = require('../../event/utils');
 
+const { getActiveTab } = require('../../tabs/utils');
+
 const { id: addonID } = require('../../self');
 const { identify } = require('../id');
 
 const buttons = new Map();
 
 const toWidgetId = id =>
   ('action-button--' + addonID.toLowerCase()+ '-' + id).
     replace(/[^a-z0-9_-]/g, '');
@@ -92,17 +95,17 @@ let actionButtonStateEvents = events.fil
 let actionButtonViewEvents = events.filter(viewEvents,
   e => buttons.has(e.target));
 
 let clickEvents = events.filter(actionButtonViewEvents, e => e.type === 'click');
 let updateEvents = events.filter(actionButtonViewEvents, e => e.type === 'update');
 
 on(clickEvents, 'data', ({target: id, window}) => {
   let button = buttons.get(id);
-  let state = button.state('tab');
+  let state = getDerivedStateFor(button, getActiveTab(window));
 
   emit(button, 'click', state);
 });
 
 on(updateEvents, 'data', ({target: id, window}) => {
   render(buttons.get(id), window);
 });
 
--- a/addon-sdk/source/lib/sdk/ui/button/toggle.js
+++ b/addon-sdk/source/lib/sdk/ui/button/toggle.js
@@ -24,21 +24,24 @@ catch (e) {
 const { Class } = require('../../core/heritage');
 const { merge } = require('../../util/object');
 const { Disposable } = require('../../core/disposable');
 const { on, off, emit, setListeners } = require('../../event/core');
 const { EventTarget } = require('../../event/target');
 
 const view = require('./view');
 const { toggleButtonContract, toggleStateContract } = require('./contract');
-const { properties, render, state, register, unregister } = require('../state');
+const { properties, render, state, register, unregister,
+  setStateFor, getStateFor, getDerivedStateFor } = require('../state');
 const { events: stateEvents } = require('../state/events');
 const { events: viewEvents } = require('./view/events');
 const events = require('../../event/utils');
 
+const { getActiveTab } = require('../../tabs/utils');
+
 const { id: addonID } = require('../../self');
 const { identify } = require('../id');
 
 const buttons = new Map();
 
 const toWidgetId = id =>
   ('toggle-button--' + addonID.toLowerCase()+ '-' + id).
     replace(/[^a-z0-9_-]/g, '');
@@ -100,23 +103,25 @@ on(toggleButtonStateEvents, 'data', ({ta
   let id = toWidgetId(target.id);
 
   view.setIcon(id, window, state.icon);
   view.setLabel(id, window, state.label);
   view.setDisabled(id, window, state.disabled);
   view.setChecked(id, window, state.checked);
 });
 
-on(clickEvents, 'data', ({target: id, window}) => {
+on(clickEvents, 'data', ({target: id, window, checked }) => {
   let button = buttons.get(id);
-  let state = button.state('tab');
+  let windowState = getStateFor(button, window);
 
-  state = merge({}, state, { checked: !state.checked });
+  let newWindowState = merge({}, windowState, { checked: checked });
 
-  button.state('tab', state);
+  setStateFor(button, window, newWindowState);
+
+  let state = getDerivedStateFor(button, getActiveTab(window));
 
   emit(button, 'click', state);
 
   emit(button, 'change', state);
 });
 
 on(updateEvents, 'data', ({target: id, window}) => {
   render(buttons.get(id), window);
--- a/addon-sdk/source/lib/sdk/ui/button/view.js
+++ b/addon-sdk/source/lib/sdk/ui/button/view.js
@@ -145,17 +145,18 @@ function create(options) {
         label: label
       });
 
       node.addEventListener('command', function(event) {
         if (views.has(id)) {
           emit(viewEvents, 'data', {
             type: 'click',
             target: id,
-            window: event.view
+            window: event.view,
+            checked: node.checked
           });
         }
       });
 
       return node;
     }
   });
 };
--- a/addon-sdk/source/lib/sdk/ui/frame/view.html
+++ b/addon-sdk/source/lib/sdk/ui/frame/view.html
@@ -8,10 +8,11 @@ window.onmessage = function(event) {
   // If message is posted from chrome it has no `event.source`.
   if (event.source === null)
     content.postMessage(event.data, "*");
 };
 // Hack: Ideally we would have used srcdoc on iframe, but in
 // that case origin of document is either content which is unable
 // to load add-on resources or a chrome to which add-on resource
 // can not send messages back.
+document.documentElement.style.overflow = "hidden";
 document.documentElement.innerHTML = atob(location.hash.substr(1));
 </script>
--- a/addon-sdk/source/lib/sdk/ui/frame/view.js
+++ b/addon-sdk/source/lib/sdk/ui/frame/view.js
@@ -63,29 +63,32 @@ const registerFrame = ({id, url}) => {
       view.setAttribute("id", id);
       view.setAttribute("flex", 2);
 
       let innerFrame = document.createElementNS(HTML_NS, "iframe");
       innerFrame.setAttribute("id", id);
       innerFrame.setAttribute("src", url);
       innerFrame.setAttribute("seamless", "seamless");
       innerFrame.setAttribute("sandbox", "allow-scripts");
+      innerFrame.setAttribute("scrolling", "no");
       innerFrame.setAttribute("data-is-sdk-inner-frame", true);
       innerFrame.setAttribute("style", [ "border:none",
-        "position:absolute", "width:100%", "top: 0", "left: 0"].join(";"));
+        "position:absolute", "width:100%", "top: 0",
+        "left: 0", "overflow: hidden"].join(";"));
 
       let outerFrame = document.createElementNS(XUL_NS, "iframe");
       outerFrame.setAttribute("src", OUTER_FRAME_URI + "#" +
                                      encode(innerFrame.outerHTML));
       outerFrame.setAttribute("id", "outer-" + id);
       outerFrame.setAttribute("data-is-sdk-outer-frame", true);
       outerFrame.setAttribute("type", "content");
       outerFrame.setAttribute("transparent", true);
       outerFrame.setAttribute("flex", 2);
       outerFrame.setAttribute("style", "overflow: hidden;");
+      outerFrame.setAttribute("scrolling", "no");
       outerFrame.setAttribute("disablehistory", true);
       outerFrame.setAttribute("seamless", "seamless");
 
       view.appendChild(outerFrame);
 
       return view;
     }
   });
--- a/addon-sdk/source/lib/sdk/ui/state.js
+++ b/addon-sdk/source/lib/sdk/ui/state.js
@@ -33,16 +33,19 @@ const { isNil } = require('../lang/type'
 
 const { viewFor } = require('../view/core');
 
 const components = new WeakMap();
 
 const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
   'The object may be not be registered, or may already have been unloaded.';
 
+const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' +
+  'Only window, tab and registered component are valid targets.';
+
 const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
 const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab';
 const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
 const isEnumerable = window => !ignoreWindow(window);
 const browsers = _ =>
   windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
 const getMostRecentTab = _ => getActiveTab(getMostRecentBrowserWindow());
 
@@ -50,16 +53,36 @@ function getStateFor(component, target) 
   if (!isRegistered(component))
     throw new Error(ERR_UNREGISTERED);
 
   if (!components.has(component))
     return null;
 
   let states = components.get(component);
 
+  if (target) {
+    if (isTab(target) || isWindow(target) || target === component)
+      return states.get(target) || null;
+    else
+      throw new Error(ERR_INVALID_TARGET);
+  }
+
+  return null;
+}
+exports.getStateFor = getStateFor;
+
+function getDerivedStateFor(component, target) {
+  if (!isRegistered(component))
+    throw new Error(ERR_UNREGISTERED);
+
+  if (!components.has(component))
+    return null;
+
+  let states = components.get(component);
+
   let componentState = states.get(component);
   let windowState = null;
   let tabState = null;
 
   if (target) {
     // has a target
     if (isTab(target)) {
       windowState = states.get(getOwnerWindow(target), null);
@@ -72,70 +95,71 @@ function getStateFor(component, target) 
     else if (isWindow(target) && states.has(target)) {
       // we have a window state
       windowState = states.get(target);
     }
   }
 
   return freeze(merge({}, componentState, windowState, tabState));
 }
-exports.getStateFor = getStateFor;
+exports.getDerivedStateFor = getDerivedStateFor;
 
 function setStateFor(component, target, state) {
   if (!isRegistered(component))
     throw new Error(ERR_UNREGISTERED);
 
   let isComponentState = target === component;
   let targetWindows = isWindow(target) ? [target] :
                       isActiveTab(target) ? [getOwnerWindow(target)] :
                       isComponentState ? browsers() :
                       isTab(target) ? [] :
                       null;
 
   if (!targetWindows)
-    throw new Error('target not allowed.');
+    throw new Error(ERR_INVALID_TARGET);
 
   // initialize the state's map
   if (!components.has(component))
     components.set(component, new WeakMap());
 
   let states = components.get(component);
 
   if (state === null && !isComponentState) // component state can't be deleted
     states.delete(target);
   else {
     let base = isComponentState ? states.get(target) : null;
     states.set(target, freeze(merge({}, base, state)));
   }
 
   render(component, targetWindows);
 }
+exports.setStateFor = setStateFor;
 
 function render(component, targetWindows) {
   targetWindows = targetWindows ? [].concat(targetWindows) : browsers();
 
   for (let window of targetWindows.filter(isEnumerable)) {
-    let tabState = getStateFor(component, getActiveTab(window));
+    let tabState = getDerivedStateFor(component, getActiveTab(window));
 
     emit(stateEvents, 'data', {
       type: 'render',
       target: component,
       window: window,
       state: tabState
     });
 
   }
 }
 exports.render = render;
 
 function properties(contract) {
   let { rules } = contract;
   let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
     descriptor[name] = {
-      get: function() { return getStateFor(this)[name] },
+      get: function() { return getDerivedStateFor(this)[name] },
       set: function(value) {
         let changed = {};
         changed[name] = value;
 
         setStateFor(this, this, contract(changed));
       }
     }
     return descriptor;
@@ -154,17 +178,17 @@ function state(contract) {
 
       if (!nativeTarget && target !== this && !isNil(target))
         throw new Error('target not allowed.');
 
       target = nativeTarget || target;
 
       // jquery style
       return arguments.length < 2
-        ? getStateFor(this, target)
+        ? getDerivedStateFor(this, target)
         : setStateFor(this, target, contract(state))
     }
   }
 }
 exports.state = state;
 
 const register = (component, state) => {
   add(components, component);
@@ -195,17 +219,17 @@ on(activate, 'data', ({target}) => {
 
   if (ignoreWindow(window)) return;
 
   for (let component of iterator(components)) {
     emit(stateEvents, 'data', {
       type: 'render',
       target: component,
       window: window,
-      state: getStateFor(component, tab)
+      state: getDerivedStateFor(component, tab)
     });
   }
 });
 
 on(close, 'data', function({target}) {
   for (let component of iterator(components)) {
     components.get(component).delete(target);
   }
--- a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js
+++ b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js
@@ -72,16 +72,19 @@ const Tabs = Class({
         options.onOpen(tab);
 
       tab.on('open', options.onOpen);
     }
 
     if (options.onReady)
       tab.on('ready', options.onReady);
 
+    if (options.onLoad)
+      tab.on('load', options.onLoad);
+
     if (options.onPageShow)
       tab.on('pageshow', options.onPageShow);
 
     if (options.onActivate)
       tab.on('activate', options.onActivate);
 
     return tab;
   }
@@ -131,17 +134,17 @@ function onTabOpen(event) {
 
   tabNS(tab).opened = true;
 
   tab.on('ready', function() emit(gTabs, 'ready', tab));
   tab.once('close', onTabClose);
 
   tab.on('pageshow', function(_tab, persisted)
     emit(gTabs, 'pageshow', tab, persisted));
-  
+
   emit(tab, 'open', tab);
   emit(gTabs, 'open', tab);
 }
 
 // TabSelect
 function onTabSelect(event) {
   let browser = event.target;
 
--- a/addon-sdk/source/lib/sdk/windows/tabs-firefox.js
+++ b/addon-sdk/source/lib/sdk/windows/tabs-firefox.js
@@ -12,19 +12,23 @@ const { List } = require("../deprecated/
 const { Tab } = require("../tabs/tab");
 const { EventEmitter } = require("../deprecated/events");
 const { EVENTS } = require("../tabs/events");
 const { getOwnerWindow, getActiveTab, getTabs,
         openTab } = require("../tabs/utils");
 const { Options } = require("../tabs/common");
 const { observer: tabsObserver } = require("../tabs/observer");
 const { ignoreWindow } = require("../private-browsing/utils");
+const { when: unload } = require('../system/unload');
 
 const TAB_BROWSER = "tabbrowser";
 
+let unloaded = false;
+unload(_ => unloaded = true);
+
 /**
  * This is a trait that is used in composition of window wrapper. Trait tracks
  * tab related events of the wrapped window in order to keep track of open
  * tabs and maintain their wrappers. Every new tab gets wrapped and jetpack
  * type event is emitted.
  */
 const WindowTabTracker = Trait.compose({
   /**
@@ -116,16 +120,19 @@ const WindowTabTracker = Trait.compose({
         wrappedTab.on("load", this._onTabLoad);
         wrappedTab.on("pageshow", this._onTabPageShow);
       }
 
       this._emitEvent(type, wrappedTab);
     }
   },
   _emitEvent: function _emitEvent(type, tag) {
+    if (unloaded)
+      return;
+
     // Slices additional arguments and passes them into exposed
     // listener like other events (for pageshow)
     let args = Array.slice(arguments);
     // Notifies combined tab list that tab was added / removed.
     tabs._emit.apply(tabs, args);
     // Notifies contained tab list that window was added / removed.
     this._tabs._emit.apply(this._tabs, args);
   }
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -102,17 +102,19 @@ freeze(Array.prototype);
 freeze(String);
 freeze(String.prototype);
 
 // This function takes `f` function sets it's `prototype` to undefined and
 // freezes it. We need to do this kind of deep freeze with all the exposed
 // functions so that untrusted code won't be able to use them a message
 // passing channel.
 function iced(f) {
-  f.prototype = undefined;
+  if (!Object.isFrozen(f)) {
+    f.prototype = undefined;
+  }
   return freeze(f);
 }
 
 // Defines own properties of given `properties` object on the given
 // target object overriding any existing property with a conflicting name.
 // Returns `target` object. Note we only export this function because it's
 // useful during loader bootstrap when other util modules can't be used &
 // thats only case where this export should be used.
@@ -126,49 +128,52 @@ const override = iced(function override(
 });
 exports.override = override;
 
 function sourceURI(uri) { return String(uri).split(" -> ").pop(); }
 exports.sourceURI = iced(sourceURI);
 
 function isntLoaderFrame(frame) { return frame.fileName !== module.uri }
 
-var parseStack = iced(function parseStack(stack) {
+function parseURI(uri) { return String(uri).split(" -> ").pop(); }
+exports.parseURI = parseURI;
+
+function parseStack(stack) {
   let lines = String(stack).split("\n");
   return lines.reduce(function(frames, line) {
     if (line) {
       let atIndex = line.indexOf("@");
       let columnIndex = line.lastIndexOf(":");
       let lineIndex = line.lastIndexOf(":", columnIndex - 1);
-      let fileName = sourceURI(line.slice(atIndex + 1, lineIndex));
+      let fileName = parseURI(line.slice(atIndex + 1, lineIndex));
       let lineNumber = parseInt(line.slice(lineIndex + 1, columnIndex));
       let columnNumber = parseInt(line.slice(columnIndex + 1));
       let name = line.slice(0, atIndex).split("(").shift();
       frames.unshift({
         fileName: fileName,
         name: name,
         lineNumber: lineNumber,
         columnNumber: columnNumber
       });
     }
     return frames;
   }, []);
-})
-exports.parseStack = parseStack
+}
+exports.parseStack = parseStack;
 
-var serializeStack = iced(function serializeStack(frames) {
+function serializeStack(frames) {
   return frames.reduce(function(stack, frame) {
     return frame.name + "@" +
            frame.fileName + ":" +
            frame.lineNumber + ":" +
            frame.columnNumber + "\n" +
            stack;
   }, "");
-})
-exports.serializeStack = serializeStack
+}
+exports.serializeStack = serializeStack;
 
 function readURI(uri) {
   let stream = NetUtil.newChannel(uri, 'UTF-8', null).open();
   let count = stream.available();
   let data = NetUtil.readInputStreamToString(stream, count, {
     charset: 'UTF-8'
   });
 
@@ -613,17 +618,17 @@ const Require = iced(function Require(lo
         uri = uri + '.js';
       }
     }
     // If not yet cached, load and cache it.
     // We also freeze module to prevent it from further changes
     // at runtime.
     if (!(uri in modules)) {
       // Many of the loader's functionalities are dependent
-      // on modules[uri] being set before loading, so we set it and 
+      // on modules[uri] being set before loading, so we set it and
       // remove it if we have any errors.
       module = modules[uri] = Module(requirement, uri);
       try {
         freeze(load(loader, module));
       }
       catch (e) {
         // Clear out modules cache so we can throw on a second invalid require
         delete modules[uri];
--- a/addon-sdk/source/mapping.json
+++ b/addon-sdk/source/mapping.json
@@ -1,17 +1,17 @@
 {
   "api-utils": "sdk/deprecated/api-utils",
   "base64": "sdk/base64",
   "content": "sdk/content/content",
   "deprecate": "sdk/util/deprecate",
   "event/core": "sdk/event/core",
   "events": "sdk/deprecated/events",
   "functional": "sdk/core/functional",
-  "l10n/core": "sdk/l10n/core",
+  "l10n/core": "sdk/l10n/json/core",
   "l10n/html": "sdk/l10n/html",
   "l10n/loader": "sdk/l10n/loader",
   "l10n/locale": "sdk/l10n/locale",
   "l10n/prefs": "sdk/l10n/prefs",
   "list": "sdk/util/list",
   "loader": "sdk/loader/loader",
   "memory": "sdk/deprecated/memory",
   "namespace": "sdk/core/namespace",
--- a/addon-sdk/source/python-lib/cuddlefish/rdf.py
+++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py
@@ -160,21 +160,21 @@ def gen_manifest(template_root_dir, targ
         ta_desc = dom.createElement("Description")
         target_app.appendChild(ta_desc)
 
         elem = dom.createElement("em:id")
         elem.appendChild(dom.createTextNode("{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
         ta_desc.appendChild(elem)
 
         elem = dom.createElement("em:minVersion")
-        elem.appendChild(dom.createTextNode("19.0"))
+        elem.appendChild(dom.createTextNode("26.0"))
         ta_desc.appendChild(elem)
 
         elem = dom.createElement("em:maxVersion")
-        elem.appendChild(dom.createTextNode("22.0a1"))
+        elem.appendChild(dom.createTextNode("30.0a1"))
         ta_desc.appendChild(elem)
 
     if target_cfg.get("homepage"):
         manifest.set("em:homepageURL", target_cfg.get("homepage"))
     else:
         manifest.remove("em:homepageURL")
 
     return manifest
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/child_process/index.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { exec } = require("sdk/system/child_process");
+const { platform, pathFor } = require("sdk/system");
+const PROFILE_DIR = pathFor("ProfD");
+const isWindows = platform.toLowerCase().indexOf("win") === 0;
+
+/**
+ * Ensures using child_process and underlying subprocess.jsm
+ * works within an addon
+ */
+exports["test child_process in an addon"] = (assert, done) => {
+  exec(isWindows ? "DIR /A-D" : "ls -al", {
+    cwd: PROFILE_DIR
+  }, (err, stdout, stderr) => {
+    assert.ok(!err, "no errors");
+    assert.equal(stderr, "", "stderr is empty");
+    assert.ok(/extensions\.ini/.test(stdout), "stdout output of `ls -al` finds files");
+
+    if (isWindows)
+      assert.ok(!/<DIR>/.test(stdout), "passing args works");
+    else
+      assert.ok(/d(r[-|w][-|x]){3}/.test(stdout), "passing args works");
+    done();
+  });
+};
+
+require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/child_process/package.json
@@ -0,0 +1,4 @@
+{
+  "id": "test-child-process",
+  "main": "index.js"
+}
--- a/addon-sdk/source/test/addons/chrome/main.js
+++ b/addon-sdk/source/test/addons/chrome/main.js
@@ -15,17 +15,17 @@ const { NetUtil } = Cu.import('resource:
 
 exports.testChromeSkin = function(assert, done) {
   let skinURL = 'chrome://test/skin/style.css';
 
   Request({
     url: skinURL,
     overrideMimeType: 'text/plain',
     onComplete: function (response) {
-      assert.equal(response.text, 'test{}\n', 'chrome.manifest skin folder was registered!');
+      assert.equal(response.text.trim(), 'test{}', 'chrome.manifest skin folder was registered!');
       done();
     }
   }).get();
 
   assert.pass('requesting ' + skinURL);
 }
 
 exports.testChromeContent = function(assert, done) {
--- a/addon-sdk/source/test/addons/content-permissions/main.js
+++ b/addon-sdk/source/test/addons/content-permissions/main.js
@@ -3,25 +3,26 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { PageMod } = require("sdk/page-mod");
 const tabs = require("sdk/tabs");
 const { startServerAsync } = require("sdk/test/httpd");
 
 const serverPort = 8099;
+const TEST_TAB_URL = "about:mozilla";
 
 exports.testCrossDomainIframe = function(assert, done) {
   let server = startServerAsync(serverPort);
   server.registerPathHandler("/iframe", function handle(request, response) {
     response.write("<html><body>foo</body></html>");
   });
 
   let pageMod = PageMod({
-    include: "about:*",
+    include: TEST_TAB_URL,
     contentScript: "new " + function ContentScriptScope() {
       self.on("message", function (url) {
         let iframe = document.createElement("iframe");
         iframe.addEventListener("load", function onload() {
           iframe.removeEventListener("load", onload, false);
           self.postMessage(iframe.contentWindow.document.body.innerHTML);
         }, false);
         iframe.setAttribute("src", url);
@@ -32,34 +33,34 @@ exports.testCrossDomainIframe = function
       w.on("message", function (body) {
         assert.equal(body, "foo", "received iframe html content");
         pageMod.destroy();
         w.tab.close(function() {
           server.stop(done);
         });
       });
 
-      w.postMessage("http://localhost:8099/iframe");
+      w.postMessage("http://localhost:" + serverPort + "/iframe");
     }
   });
 
   tabs.open({
-    url: "about:home",
+    url: TEST_TAB_URL,
     inBackground: true
   });
 };
 
 exports.testCrossDomainXHR = function(assert, done) {
   let server = startServerAsync(serverPort);
   server.registerPathHandler("/xhr", function handle(request, response) {
     response.write("foo");
   });
 
   let pageMod = PageMod({
-    include: "about:*",
+    include: TEST_TAB_URL,
     contentScript: "new " + function ContentScriptScope() {
       self.on("message", function (url) {
         let request = new XMLHttpRequest();
         request.overrideMimeType("text/plain");
         request.open("GET", url, true);
         request.onload = function () {
           self.postMessage(request.responseText);
         };
@@ -70,19 +71,19 @@ exports.testCrossDomainXHR = function(as
       w.on("message", function (body) {
         assert.equal(body, "foo", "received XHR content");
         pageMod.destroy();
         w.tab.close(function() {
           server.stop(done);
         });
       });
 
-      w.postMessage("http://localhost:8099/xhr");
+      w.postMessage("http://localhost:" + serverPort + "/xhr");
     }
   });
 
   tabs.open({
-    url: "about:home",
+    url: TEST_TAB_URL,
     inBackground: true
   });
 };
 
 require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/contributors/main.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { Cu } = require('chrome');
+const self = require('sdk/self');
+const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {});
+
+exports.testContributors = function(assert, done) {
+  AddonManager.getAddonByID(self.id, function(addon) {
+    let count = 0;
+    addon.contributors.forEach(function({ name }) {
+      count++;
+      assert.equal(name, count == 1 ? 'A' : 'B', 'The contributors keys are correct');
+    });
+    assert.equal(count, 2, 'The key count is correct');
+    assert.equal(addon.contributors.length, 2, 'The key length is correct');
+    done();
+  });
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/contributors/package.json
@@ -0,0 +1,4 @@
+{
+  "id": "test-contributors",
+  "contributors": [ "A", "B" ]
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/application.ini
@@ -0,0 +1,11 @@
+[App]
+Vendor=Varma
+Name=Test App
+Version=1.0
+BuildID=20060101
+Copyright=Copyright (c) 2009 Atul Varma
+ID=xulapp@toolness.com
+
+[Gecko]
+MinVersion=1.9.2.0
+MaxVersion=2.0.*
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/bootstrap.js
@@ -0,0 +1,337 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp
+
+'use strict';
+
+// IMPORTANT: Avoid adding any initialization tasks here, if you need to do
+// something before add-on is loaded consider addon/runner module instead!
+
+const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
+        results: Cr, manager: Cm } = Components;
+const ioService = Cc['@mozilla.org/network/io-service;1'].
+                  getService(Ci.nsIIOService);
+const resourceHandler = ioService.getProtocolHandler('resource').
+                        QueryInterface(Ci.nsIResProtocolHandler);
+const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
+const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
+                     getService(Ci.mozIJSSubScriptLoader);
+const prefService = Cc['@mozilla.org/preferences-service;1'].
+                    getService(Ci.nsIPrefService).
+                    QueryInterface(Ci.nsIPrefBranch);
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+                getService(Ci.nsIXULAppInfo);
+const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
+           getService(Ci.nsIVersionComparator);
+
+
+const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
+                 'install', 'uninstall', 'upgrade', 'downgrade' ];
+
+const bind = Function.call.bind(Function.bind);
+
+let loader = null;
+let unload = null;
+let cuddlefishSandbox = null;
+let nukeTimer = null;
+
+// Utility function that synchronously reads local resource from the given
+// `uri` and returns content string.
+function readURI(uri) {
+  let ioservice = Cc['@mozilla.org/network/io-service;1'].
+    getService(Ci.nsIIOService);
+  let channel = ioservice.newChannel(uri, 'UTF-8', null);
+  let stream = channel.open();
+
+  let cstream = Cc['@mozilla.org/intl/converter-input-stream;1'].
+    createInstance(Ci.nsIConverterInputStream);
+  cstream.init(stream, 'UTF-8', 0, 0);
+
+  let str = {};
+  let data = '';
+  let read = 0;
+  do {
+    read = cstream.readString(0xffffffff, str);
+    data += str.value;
+  } while (read != 0);
+
+  cstream.close();
+
+  return data;
+}
+
+// We don't do anything on install & uninstall yet, but in a future
+// we should allow add-ons to cleanup after uninstall.
+function install(data, reason) {}
+function uninstall(data, reason) {}
+
+function startup(data, reasonCode) {
+  try {
+    let reason = REASON[reasonCode];
+    // URI for the root of the XPI file.
+    // 'jar:' URI if the addon is packed, 'file:' URI otherwise.
+    // (Used by l10n module in order to fetch `locale` folder)
+    let rootURI = data.resourceURI.spec;
+
+    // TODO: Maybe we should perform read harness-options.json asynchronously,
+    // since we can't do anything until 'sessionstore-windows-restored' anyway.
+    let options = JSON.parse(readURI(rootURI + './harness-options.json'));
+
+    let id = options.jetpackID;
+    let name = options.name;
+
+    // Clean the metadata
+    options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {};
+
+    // freeze the permissionss
+    Object.freeze(options.metadata[name]['permissions']);
+    // freeze the metadata
+    Object.freeze(options.metadata[name]);
+
+    // Register a new resource 'domain' for this addon which is mapping to
+    // XPI's `resources` folder.
+    // Generate the domain name by using jetpack ID, which is the extension ID
+    // by stripping common characters that doesn't work as a domain name:
+    let uuidRe =
+      /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
+
+    let domain = id.
+      toLowerCase().
+      replace(/@/g, '-at-').
+      replace(/\./g, '-dot-').
+      replace(uuidRe, '$1');
+
+    let prefixURI = 'resource://' + domain + '/';
+    let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null);
+    resourceHandler.setSubstitution(domain, resourcesURI);
+
+    // Create path to URLs mapping supported by loader.
+    let paths = {
+      // Relative modules resolve to add-on package lib
+      './': prefixURI + name + '/lib/',
+      './tests/': prefixURI + name + '/tests/',
+      '': 'resource://gre/modules/commonjs/'
+    };
+
+    // Maps addon lib and tests ressource folders for each package
+    paths = Object.keys(options.metadata).reduce(function(result, name) {
+      result[name + '/'] = prefixURI + name + '/lib/'
+      result[name + '/tests/'] = prefixURI + name + '/tests/'
+      return result;
+    }, paths);
+
+    // We need to map tests folder when we run sdk tests whose package name
+    // is stripped
+    if (name == 'addon-sdk')
+      paths['tests/'] = prefixURI + name + '/tests/';
+
+    let useBundledSDK = options['force-use-bundled-sdk'];
+    if (!useBundledSDK) {
+      try {
+        useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK");
+      }
+      catch (e) {
+        // Pref doesn't exist, allow using Firefox shipped SDK
+      }
+    }
+
+    // Starting with Firefox 21.0a1, we start using modules shipped into firefox
+    // Still allow using modules from the xpi if the manifest tell us to do so.
+    // And only try to look for sdk modules in xpi if the xpi actually ship them
+    if (options['is-sdk-bundled'] &&
+        (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) {
+      // Maps sdk module folders to their resource folder
+      paths[''] = prefixURI + 'addon-sdk/lib/';
+      // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder,
+      // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder
+      // until we no longer support SDK modules in XPI:
+      paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js';
+    }
+
+    // Retrieve list of module folder overloads based on preferences in order to
+    // eventually used a local modules instead of files shipped into Firefox.
+    let branch = prefService.getBranch('extensions.modules.' + id + '.path');
+    paths = branch.getChildList('', {}).reduce(function (result, name) {
+      // Allows overloading of any sub folder by replacing . by / in pref name
+      let path = name.substr(1).split('.').join('/');
+      // Only accept overloading folder by ensuring always ending with `/`
+      if (path) path += '/';
+      let fileURI = branch.getCharPref(name);
+
+      // On mobile, file URI has to end with a `/` otherwise, setSubstitution
+      // takes the parent folder instead.
+      if (fileURI[fileURI.length-1] !== '/')
+        fileURI += '/';
+
+      // Maps the given file:// URI to a resource:// in order to avoid various
+      // failure that happens with file:// URI and be close to production env
+      let resourcesURI = ioService.newURI(fileURI, null, null);
+      let resName = 'extensions.modules.' + domain + '.commonjs.path' + name;
+      resourceHandler.setSubstitution(resName, resourcesURI);
+
+      result[path] = 'resource://' + resName + '/';
+      return result;
+    }, paths);
+
+    // Make version 2 of the manifest
+    let manifest = options.manifest;
+
+    // Import `cuddlefish.js` module using a Sandbox and bootstrap loader.
+    let cuddlefishPath = 'loader/cuddlefish.js';
+    let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath;
+    if (paths['sdk/']) { // sdk folder has been overloaded
+                         // (from pref, or cuddlefish is still in the xpi)
+      cuddlefishURI = paths['sdk/'] + cuddlefishPath;
+    }
+    else if (paths['']) { // root modules folder has been overloaded
+      cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath;
+    }
+
+    cuddlefishSandbox = loadSandbox(cuddlefishURI);
+    let cuddlefish = cuddlefishSandbox.exports;
+
+    // Normalize `options.mainPath` so that it looks like one that will come
+    // in a new version of linker.
+    let main = options.mainPath;
+
+    unload = cuddlefish.unload;
+    loader = cuddlefish.Loader({
+      paths: paths,
+      // modules manifest.
+      manifest: manifest,
+
+      // Add-on ID used by different APIs as a unique identifier.
+      id: id,
+      // Add-on name.
+      name: name,
+      // Add-on version.
+      version: options.metadata[name].version,
+      // Add-on package descriptor.
+      metadata: options.metadata[name],
+      // Add-on load reason.
+      loadReason: reason,
+
+      prefixURI: prefixURI,
+      // Add-on URI.
+      rootURI: rootURI,
+      // options used by system module.
+      // File to write 'OK' or 'FAIL' (exit code emulation).
+      resultFile: options.resultFile,
+      // Arguments passed as --static-args
+      staticArgs: options.staticArgs,
+
+      // Arguments related to test runner.
+      modules: {
+        '@test/options': {
+          allTestModules: options.allTestModules,
+          iterations: options.iterations,
+          filter: options.filter,
+          profileMemory: options.profileMemory,
+          stopOnError: options.stopOnError,
+          verbose: options.verbose,
+          parseable: options.parseable,
+          checkMemory: options.check_memory,
+        }
+      }
+    });
+
+    let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI);
+    let require = cuddlefish.Require(loader, module);
+
+    require('sdk/addon/runner').startup(reason, {
+      loader: loader,
+      main: main,
+      prefsURI: rootURI + 'defaults/preferences/prefs.js'
+    });
+  } catch (error) {
+    dump('Bootstrap error: ' +
+         (error.message ? error.message : String(error)) + '\n' +
+         (error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
+    throw error;
+  }
+};
+
+function loadSandbox(uri) {
+  let proto = {
+    sandboxPrototype: {
+      loadSandbox: loadSandbox,
+      ChromeWorker: ChromeWorker
+    }
+  };
+  let sandbox = Cu.Sandbox(systemPrincipal, proto);
+  // Create a fake commonjs environnement just to enable loading loader.js
+  // correctly
+  sandbox.exports = {};
+  sandbox.module = { uri: uri, exports: sandbox.exports };
+  sandbox.require = function (id) {
+    if (id !== "chrome")
+      throw new Error("Bootstrap sandbox `require` method isn't implemented.");
+
+    return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
+      CC: bind(CC, Components), components: Components,
+      ChromeWorker: ChromeWorker });
+  };
+  scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
+  return sandbox;
+}
+
+function unloadSandbox(sandbox) {
+  if ("nukeSandbox" in Cu)
+    Cu.nukeSandbox(sandbox);
+}
+
+function setTimeout(callback, delay) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({ notify: callback }, delay,
+                         Ci.nsITimer.TYPE_ONE_SHOT);
+  return timer;
+}
+
+function shutdown(data, reasonCode) {
+  let reason = REASON[reasonCode];
+  if (loader) {
+    unload(loader, reason);
+    unload = null;
+
+    // Don't waste time cleaning up if the application is shutting down
+    if (reason != "shutdown") {
+      // Avoid leaking all modules when something goes wrong with one particular
+      // module. Do not clean it up immediatly in order to allow executing some
+      // actions on addon disabling.
+      // We need to keep a reference to the timer, otherwise it is collected
+      // and won't ever fire.
+      nukeTimer = setTimeout(nukeModules, 1000);
+    }
+  }
+};
+
+function nukeModules() {
+  nukeTimer = null;
+  // module objects store `exports` which comes from sandboxes
+  // We should avoid keeping link to these object to avoid leaking sandboxes
+  for (let key in loader.modules) {
+    delete loader.modules[key];
+  }
+  // Direct links to sandboxes should be removed too
+  for (let key in loader.sandboxes) {
+    let sandbox = loader.sandboxes[key];
+    delete loader.sandboxes[key];
+    // Bug 775067: From FF17 we can kill all CCW from a given sandbox
+    unloadSandbox(sandbox);
+  }
+  loader = null;
+
+  // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via
+  // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when
+  // the addon is unload.
+
+  unloadSandbox(cuddlefishSandbox.loaderSandbox);
+  unloadSandbox(cuddlefishSandbox.xulappSandbox);
+
+  // Bug 764840: We need to unload cuddlefish otherwise it will stay alive
+  // and keep a reference to this compartment.
+  unloadSandbox(cuddlefishSandbox);
+  cuddlefishSandbox = null;
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/install.rdf
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>xulapp@toolness.com</em:id>
+    <em:version>1.0</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:unpack>false</em:unpack>
+
+    <!-- Firefox -->
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>21.0</em:minVersion>
+        <em:maxVersion>25.0a1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Test App</em:name>
+    <em:description>Harness for tests.</em:description>
+    <em:creator>Mozilla Corporation</em:creator>
+    <em:homepageURL></em:homepageURL>
+    <em:optionsType></em:optionsType>
+    <em:updateURL></em:updateURL>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/locale/en-GB.properties
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Translated= Yes
+
+text-content=no <b>HTML</b> injection
+
+downloadsCount=%d downloads
+downloadsCount[one]=one download
+
+pluralTest=fallback to other
+pluralTest[zero]=optional zero form
+
+explicitPlural[one]=one
+explicitPlural[other]=other
+
+# You can use unicode char escaping in order to inject space at the beginning/
+# end of your string. (Regular spaces are automatically ignore by .properties
+# file parser)
+unicodeEscape = \u0020\u0040\u0020
+# this string equals to " @ "
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/locale/en-US.properties
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Translated= Yes
+
+text-content=no <b>HTML</b> injection
+
+downloadsCount=%d downloads
+downloadsCount[one]=one download
+
+pluralTest=fallback to other
+pluralTest[zero]=optional zero form
+
+explicitPlural[one]=one
+explicitPlural[other]=other
+
+# You can use unicode char escaping in order to inject space at the beginning/
+# end of your string. (Regular spaces are automatically ignore by .properties
+# file parser)
+unicodeEscape = \u0020\u0040\u0020
+# this string equals to " @ "
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/locale/eo.properties
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Translated= jes
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/app-extension/locale/fr-FR.properties
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Translated= Oui
+
+placeholderString= Placeholder %s
+
+# Plural forms
+%d downloads=%d téléchargements
+%d downloads[one]=%d téléchargement
+
+downloadsCount=%d téléchargements
+downloadsCount[one]=%d téléchargement
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/data/test-localization.html
@@ -0,0 +1,24 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>HTML Localization</title>
+  </head>
+  <body>
+    <div data-l10n-id="Not translated">Kept as-is</div>
+    <ul data-l10n-id="Translated">
+      <li>Inner html content is replaced,</li>
+      <li data-l10n-id="text-content">
+        Elements with data-l10n-id attribute whose parent element is translated
+        will be replaced by the content of the translation.
+      </li>
+    </ul>
+    <div data-l10n-id="text-content">No</div>
+    <div data-l10n-id="Translated">
+      A data-l10n-id value can be used in multiple elements
+    </div>
+  </body>
+</html
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/main.js
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const prefs = require("sdk/preferences/service");
+const { Loader } = require('sdk/test/loader');
+const { resolveURI } = require('toolkit/loader');
+const { rootURI } = require("@loader/options");
+const { usingJSON } = require('sdk/l10n/json/core');
+
+const PREF_MATCH_OS_LOCALE  = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE  = "general.useragent.locale";
+
+function setLocale(locale) {
+  prefs.set(PREF_MATCH_OS_LOCALE, false);
+  prefs.set(PREF_SELECTED_LOCALE, locale);
+}
+
+function resetLocale() {
+  prefs.reset(PREF_MATCH_OS_LOCALE);
+  prefs.reset(PREF_SELECTED_LOCALE);
+}
+
+function definePseudo(loader, id, exports) {
+  let uri = resolveURI(id, loader.mapping);
+  loader.modules[uri] = { exports: exports };
+}
+
+function createTest(locale, testFunction) {
+  return function (assert, done) {
+    let loader = Loader(module);
+    // Change the locale before loading new l10n modules in order to load
+    // the right .json file
+    setLocale(locale);
+    // Initialize main l10n module in order to load new locale files
+    loader.require("sdk/l10n/loader").
+      load(rootURI).
+      then(function success(data) {
+             definePseudo(loader, '@l10n/data', data);
+             // Execute the given test function
+             try {
+               testFunction(assert, loader, function onDone() {
+                 loader.unload();
+                 resetLocale();
+                 done();
+               });
+             }
+             catch(e) {
+              console.exception(e);
+             }
+           },
+           function failure(error) {
+             assert.fail("Unable to load locales: " + error);
+           });
+  };
+}
+
+exports.testExactMatching = createTest("fr-FR", function(assert, loader, done) {
+  let _ = loader.require("sdk/l10n").get;
+  assert.equal(_("Not translated"), "Not translated",
+                   "Key not translated");
+  assert.equal(_("Translated"), "Oui",
+                   "Simple key translated");
+
+  // Placeholders
+  assert.equal(_("placeholderString", "works"), "Placeholder works",
+                   "Value with placeholder");
+  assert.equal(_("Placeholder %s", "works"), "Placeholder works",
+                   "Key without value but with placeholder");
+  assert.equal(_("Placeholders %2s %1s %s.", "working", "are", "correctly"),
+                   "Placeholders are working correctly.",
+                   "Multiple placeholders");
+
+  // Plurals
+   assert.equal(_("downloadsCount", 0),
+                   "0 téléchargement",
+                   "PluralForm form 'one' for 0 in french");
+  assert.equal(_("downloadsCount", 1),
+                   "1 téléchargement",
+                   "PluralForm form 'one' for 1 in french");
+  assert.equal(_("downloadsCount", 2),
+                   "2 téléchargements",
+                   "PluralForm form 'other' for n > 1 in french");
+
+  done();
+});
+
+exports.testHtmlLocalization = createTest("en-GB", function(assert, loader, done) {
+  // Ensure initing html component that watch document creations
+  // Note that this module is automatically initialized in
+  // cuddlefish.js:Loader.main in regular addons. But it isn't for unit tests.
+  let loaderHtmlL10n = loader.require("sdk/l10n/html");
+  loaderHtmlL10n.enable();
+
+  let uri = require("sdk/self").data.url("test-localization.html");
+  let worker = loader.require("sdk/page-worker").Page({
+    contentURL: uri,
+    contentScript: "new " + function ContentScriptScope() {
+      let nodes = document.body.querySelectorAll("*[data-l10n-id]");
+      self.postMessage([nodes[0].innerHTML,
+                        nodes[1].innerHTML,
+                        nodes[2].innerHTML,
+                        nodes[3].innerHTML]);
+    },
+    onMessage: function (data) {
+      assert.equal(
+        data[0],
+        "Kept as-is",
+        "Nodes with unknown id in .properties are kept 'as-is'"
+      );
+      assert.equal(data[1], "Yes", "HTML is translated");
+      assert.equal(
+        data[2],
+        "no &lt;b&gt;HTML&lt;/b&gt; injection",
+        "Content from .properties is text content; HTML can't be injected."
+      );
+      assert.equal(data[3], "Yes", "Multiple elements with same data-l10n-id are accepted.");
+
+      done();
+    }
+  });
+});
+
+exports.testEnUsLocaleName = createTest("en-GB", function(assert, loader, done) {
+  let _ = loader.require("sdk/l10n").get;
+
+  assert.equal(_("Not translated"), "Not translated",
+               "String w/o translation is kept as-is");
+  assert.equal(_("Translated"), "Yes",
+               "String with translation is correctly translated");
+
+  // Check Unicode char escaping sequences
+  assert.equal(_("unicodeEscape"), " @ ",
+               "Unicode escaped sequances are correctly converted");
+
+  // Check plural forms regular matching
+  assert.equal(_("downloadsCount", 0),
+                   "0 downloads",
+                   "PluralForm form 'other' for 0 in english");
+  assert.equal(_("downloadsCount", 1),
+                   "one download",
+                   "PluralForm form 'one' for 1 in english");
+  assert.equal(_("downloadsCount", 2),
+                   "2 downloads",
+                   "PluralForm form 'other' for n != 1 in english");
+
+  // Check optional plural forms
+  assert.equal(_("pluralTest", 0),
+                   "optional zero form",
+                   "PluralForm form 'zero' can be optionaly specified. (Isn't mandatory in english)");
+  assert.equal(_("pluralTest", 1),
+                   "fallback to other",
+                   "If the specific plural form is missing, we fallback to 'other'");
+
+  // Ensure that we can omit specifying the generic key without [other]
+  // key[one] = ...
+  // key[other] = ...  # Instead of `key = ...`
+  assert.equal(_("explicitPlural", 1),
+                   "one",
+                   "PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
+  assert.equal(_("explicitPlural", 10),
+                   "other",
+                   "PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
+
+  done();
+});
+
+exports.testUsingJSON = function(assert) {
+  assert.equal(usingJSON, false, 'not using json');
+}
+
+exports.testShortLocaleName = createTest("eo", function(assert, loader, done) {
+  let _ = loader.require("sdk/l10n").get;
+  assert.equal(_("Not translated"), "Not translated",
+               "String w/o translation is kept as-is");
+  assert.equal(_("Translated"), "jes",
+               "String with translation is correctly translated");
+
+  done();
+});
+
+
+// Before running tests, disable HTML service which is automatially enabled
+// in api-utils/addon/runner.js
+require('sdk/l10n/html').disable();
+
+require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/l10n-properties/package.json
@@ -0,0 +1,3 @@
+{
+  "id": "test-l10n"
+}
--- a/addon-sdk/source/test/addons/l10n/main.js
+++ b/addon-sdk/source/test/addons/l10n/main.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const prefs = require("sdk/preferences/service");
 const { Loader } = require('sdk/test/loader');
 const { resolveURI } = require('toolkit/loader');
 const { rootURI } = require("@loader/options");
+const { usingJSON } = require('sdk/l10n/json/core');
 
 const PREF_MATCH_OS_LOCALE  = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE  = "general.useragent.locale";
 
 function setLocale(locale) {
   prefs.set(PREF_MATCH_OS_LOCALE, false);
   prefs.set(PREF_SELECTED_LOCALE, locale);
 }
@@ -62,17 +63,17 @@ exports.testExactMatching = createTest("
   assert.equal(_("Translated"), "Oui",
                    "Simple key translated");
 
   // Placeholders
   assert.equal(_("placeholderString", "works"), "Placeholder works",
                    "Value with placeholder");
   assert.equal(_("Placeholder %s", "works"), "Placeholder works",
                    "Key without value but with placeholder");
-  assert.equal(_("Placeholders %2s %1s %s.", "working", "are", "correctly"), 
+  assert.equal(_("Placeholders %2s %1s %s.", "working", "are", "correctly"),
                    "Placeholders are working correctly.",
                    "Multiple placeholders");
 
   // Plurals
    assert.equal(_("downloadsCount", 0),
                    "0 téléchargement",
                    "PluralForm form 'one' for 0 in french");
   assert.equal(_("downloadsCount", 1),
@@ -162,16 +163,20 @@ exports.testEnUsLocaleName = createTest(
                    "PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
   assert.equal(_("explicitPlural", 10),
                    "other",
                    "PluralForm form can be omitting generic key [i.e. without ...[other] at end of key)");
 
   done();
 });
 
+exports.testUsingJSON = function(assert) {
+  assert.equal(usingJSON, true, 'using json');
+}
+
 exports.testShortLocaleName = createTest("eo", function(assert, loader, done) {
   let _ = loader.require("sdk/l10n").get;
   assert.equal(_("Not translated"), "Not translated",
                "String w/o translation is kept as-is");
   assert.equal(_("Translated"), "jes",
                "String with translation is correctly translated");
 
   done();
--- a/addon-sdk/source/test/addons/l10n/package.json
+++ b/addon-sdk/source/test/addons/l10n/package.json
@@ -1,3 +1,3 @@
 {
   "id": "test-l10n"
-}
\ No newline at end of file
+}
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-global-private-browsing.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-global-private-browsing.js
@@ -133,18 +133,18 @@ exports.testWindowIteratorDoesNotIgnoreP
     makeEmptyBrowserWindow().then(function(window) {
       assert.ok(isWindowPrivate(window), "window is private");
       assert.equal(isPrivate(window), true, 'the opened window is private');
       assert.ok(toArray(windowUtils.windowIterator()).indexOf(window) > -1,
                 "window is in windowIterator()");
       assert.ok(windows(null, { includePrivate: true }).indexOf(window) > -1,
                 "window is in windows()");
 
-      close(window).then(function() {
+      return close(window).then(function() {
         pb.once('stop', function() {
           done();
         });
         pb.deactivate();
       });
-    });
+    }).then(null, assert.fail);
   });
   pb.activate();
 };
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-panel.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-panel.js
@@ -47,17 +47,18 @@ exports.testShowPanelInPrivateWindow = f
         panel.hide();
       });
 
       panel.show();
 
       return promise;
     }).
     then(close).
-    then(done, assert.fail.bind(assert));
+    then(done).
+    then(null, assert.fail);
 };
 
 
 function makeEmptyPrivateBrowserWindow(options) {
   options = options || {};
   return open(BROWSER, {
     features: {
       chrome: true,
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-selection.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-selection.js
@@ -246,17 +246,18 @@ exports["test PWPB Selection Listener"] 
           // check state of window
           assert.ok(isFocused(window), "the window is focused");
           assert.ok(isPrivate(window), "the window should be a private window");
 
           assert.equal(selection.text, "fo");
 
           close(window).
             then(loader.unload).
-            then(done, assert.fail);
+            then(done).
+            then(null, assert.fail);
         });
       });
       return window;
     }).
     then(selectContentFirstDiv).
     then(dispatchSelectionEvent).
     then(null, assert.fail);
 };
@@ -275,17 +276,18 @@ exports["test PWPB Textarea OnSelect Lis
         );
 
         // window should be focused, but force the focus anyhow.. see bug 841823
         focus(window).then(function() {
           assert.equal(selection.text, "noodles");
 
           close(window).
             then(loader.unload).
-            then(done, assert.fail);
+            then(done).
+            then(null, assert.fail);
         });
       });
       return window;
     }).
     then(selectTextarea).
     then(dispatchOnSelectEvent).
     then(null, assert.fail);
 };
@@ -314,17 +316,17 @@ exports["test PWPB Single DOM Selection"
           "iterable selection.text with single DOM Selection works.");
 
         assert.equal(sel.html, "<div>foo</div>",
           "iterable selection.html with single DOM Selection works.");
       }
 
       assert.equal(selectionCount, 1,
         "One iterable selection");
-    }).then(close).then(loader.unload).then(done, assert.fail);
+    }).then(close).then(loader.unload).then(done).then(null, assert.fail);
 }
 
 exports["test PWPB Textarea Selection"] = function(assert, done) {
   let loader = Loader(module);
   let selection = loader.require("sdk/selection");
 
   open(URL, {private: true, title: "PWPB Textarea Listener"}).
     then(selectTextarea).
@@ -348,18 +350,17 @@ exports["test PWPB Textarea Selection"] 
           "iterable selection.text with Textarea Selection works.");
 
         assert.strictEqual(sel.html, null,
           "iterable selection.html with Textarea Selection works.");
       }
 
       assert.equal(selectionCount, 1,
         "One iterable selection");
-
-    }).then(close).then(loader.unload).then(done, assert.fail);
+    }).then(close).then(loader.unload).then(done).then(null, assert.fail);
 };
 
 exports["test PWPB Set HTML in Multiple DOM Selection"] = function(assert, done) {
   let loader = Loader(module);
   let selection = loader.require("sdk/selection");
 
   open(URL, {private: true, title: "PWPB Set HTML in Multiple DOM Selection"}).
     then(selectAllDivs).
@@ -387,17 +388,17 @@ exports["test PWPB Set HTML in Multiple 
         assert.equal(sel.html, expectedHTML[selectionCount],
           "iterable selection.html with multiple DOM Selection works.");
 
         selectionCount++;
       }
 
       assert.equal(selectionCount, 2,
         "Two iterable selections");
-    }).then(close).then(loader.unload).then(done, assert.fail);
+    }).then(close).then(loader.unload).then(done).then(null, assert.fail);
 };
 
 exports["test PWPB Set Text in Textarea Selection"] = function(assert, done) {
   let loader = Loader(module);
   let selection = loader.require("sdk/selection");
 
   open(URL, {private: true, title: "test PWPB Set Text in Textarea Selection"}).
     then(selectTextarea).
@@ -423,17 +424,17 @@ exports["test PWPB Set Text in Textarea 
 
         assert.strictEqual(sel.html, null,
           "iterable selection.html with Textarea Selection works.");
       }
 
       assert.equal(selectionCount, 1,
         "One iterable selection");
 
-    }).then(close).then(loader.unload).then(done, assert.fail);
+    }).then(close).then(loader.unload).then(done).then(null, assert.fail);
 };
 
 // If the platform doesn't support the PBPW, we're replacing PBPW tests
 if (!require("sdk/private-browsing/utils").isWindowPBSupported) {
   module.exports = {
     "test PBPW Unsupported": function Unsupported (assert) {
       assert.pass("Private Window Per Browsing is not supported on this platform.");
     }
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js
@@ -34,18 +34,18 @@ exports.testSideBarIsInNewPrivateWindows
       let ele = window.document.getElementById(makeID(testName));
       assert.ok(isPrivate(window), 'the new window is private');
       assert.ok(!!ele, 'sidebar element was added');
 
       sidebar.destroy();
       assert.ok(!window.document.getElementById(makeID(testName)), 'sidebar id DNE');
       assert.ok(!startWindow.document.getElementById(makeID(testName)), 'sidebar id DNE');
 
-      close(window).then(done, assert.fail);
-  })
+      return close(window);
+  }).then(done).then(null, assert.fail);
 }
 
 // Disabled in order to land other fixes, see bug 910647 for further details.
 /*
 exports.testSidebarIsOpenInNewPrivateWindow = function(assert, done) {
   const { Sidebar } = require('sdk/ui/sidebar');
   let testName = 'testSidebarIsOpenInNewPrivateWindow';
   let window = getMostRecentBrowserWindow();
@@ -138,19 +138,18 @@ exports.testDestroyEdgeCaseBugWithPrivat
           assert.equal(isSidebarShowing(window), false, 'the sidebar is not showing');
 
           done();
         }
       })
 
       sidebar.show();
       assert.pass('showing the sidebar');
-
-    });
-  });
+    }).then(null, assert.fail);
+  }).then(null, assert.fail);
 }
 
 exports.testShowInPrivateWindow = function(assert, done) {
   const { Sidebar } = require('sdk/ui/sidebar');
   let testName = 'testShowInPrivateWindow';
   let window1 = getMostRecentBrowserWindow();
   let url = 'data:text/html;charset=utf-8,'+testName;
 
@@ -185,22 +184,22 @@ exports.testShowInPrivateWindow = functi
                   'the menuitem on the new window dne');
 
         // test old window state
         assert.equal(isSidebarShowing(window1), false, 'the old window sidebar is not showing');
         assert.equal(window1.document.getElementById(menuitemID),
                      null,
                      'the menuitem on the old window dne');
 
-        close(window).then(done);
+        close(window).then(done).then(null, assert.fail);
       },
       function bad() {
         assert.fail('a successful show should not happen here..');
       });
-  }, assert.fail);
+  }).then(null, assert.fail);
 }
 
 // If the module doesn't support the app we're being run in, require() will
 // throw.  In that case, remove all tests above from exports, and add one dummy
 // test that passes.
 try {
   require('sdk/ui/sidebar');
 }
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-window-tabs.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-window-tabs.js
@@ -14,72 +14,72 @@ exports.testOpenTabWithPrivateActiveWind
 
     tabs.open({
       url: 'about:blank',
       onOpen: function(tab) {
         assert.ok(isPrivate(tab), 'new tab is private');
         assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
         assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
 
-        close(window).then(done, assert.fail);
+        close(window).then(done).then(null, assert.fail);
       }
     })
-  }, assert.fail).then(null, assert.fail);
+  }).then(null, assert.fail);
 }
 
 exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) {
   let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
 
   windowPromise(window, 'load').then(focus).then(function (window) {
     assert.equal(isPrivate(window), false, 'new window is not private');
 
     tabs.open({
       url: 'about:blank',
       onOpen: function(tab) {
         assert.equal(isPrivate(tab), false, 'new tab is not private');
         assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
         assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
 
-        close(window).then(done, assert.fail);
+        close(window).then(done).then(null, assert.fail);
       }
     })
-  }, assert.fail).then(null, assert.fail);
+  }).then(null, assert.fail);
 }
 
 exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) {
   let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true });
 
   windowPromise(window, 'load').then(focus).then(function (window) {
     assert.ok(isPrivate(window), 'new window is private');
 
     tabs.open({
       url: 'about:blank',
       isPrivate: true,
       onOpen: function(tab) {
         assert.ok(isPrivate(tab), 'new tab is private');
         assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private');
         assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same');
 
-        close(window).then(done, assert.fail);
+        close(window).then(done).then(null, assert.fail);
       }
     })
-  }, assert.fail).then(null, assert.fail);
+  }).then(null, assert.fail);
 }
 
 exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) {
   let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false });
 
   windowPromise(window, 'load').then(focus).then(function (window) {
     assert.equal(isPrivate(window), false, 'new window is not private');
 
     tabs.open({
       url: 'about:blank',
       isPrivate: false,
       onOpen: function(tab) {
         assert.equal(isPrivate(tab), false, 'new tab is not private');
         assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private');
         assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same');
 
-        close(window).then(done, assert.fail);
+        close(window).then(done).then(null, assert.fail);
       }
     })
-  }, assert.fail).then(null, assert.fail);
+  }).then(null, assert.fail);
 }
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-windows.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/test-windows.js
@@ -59,26 +59,26 @@ exports.testWindowTrackerIgnoresPrivateW
 
     assert.ok(trackedWindowIds.indexOf(myPrivateWindowId) >= 0, 'private window was tracked');
     assert.equal(isPrivate(window), isWindowPBSupported, 'private window isPrivate');
     assert.equal(isWindowPrivate(window), isWindowPBSupported);
     assert.ok(getFrames(window).length > 1, 'there are frames for private window');
     assert.equal(getWindowTitle(window), window.document.title,
                  'getWindowTitle works');
 
-    close(window).then(function() {
+    return close(window).then(function() {
       assert.pass('private window was closed');
 
-      makeEmptyBrowserWindow().then(function(window) {
+      return makeEmptyBrowserWindow().then(function(window) {
         myNonPrivateWindowId = getInnerId(window);
         assert.notEqual(myPrivateWindowId, myNonPrivateWindowId, 'non private window was opened');
-        close(window);
+        return close(window);
       });
     });
-  });
+  }).then(null, assert.fail);
 };
 
 // Test setting activeWIndow and onFocus for private windows
 exports.testSettingActiveWindowDoesNotIgnorePrivateWindow = function(assert, done) {
   let browserWindow = WM.getMostRecentWindow("navigator:browser");
   let testSteps;
 
   assert.equal(winUtils.activeBrowserWindow, browserWindow,
@@ -140,17 +140,17 @@ exports.testSettingActiveWindowDoesNotIg
         continueAfterFocus(winUtils.activeWindow = browserWindow);
       },
       function() {
         assert.deepEqual(winUtils.activeBrowserWindow, browserWindow,
                          "Correct active browser window when pb mode is supported [4]");
         assert.deepEqual(winUtils.activeWindow, browserWindow,
                          "Correct active window when pb mode is supported [4]");
 
-        close(window).then(done);
+        close(window).then(done).then(null, assert.fail);
       }
     ];
 
     function nextTest() {
       let args = arguments;
       if (testSteps.length) {
         require('sdk/timers').setTimeout(function() {
           (testSteps.shift()).apply(null, args);
@@ -192,31 +192,31 @@ exports.testActiveWindowDoesNotIgnorePri
       assert.equal(isPrivate(winUtils.activeWindow), false,
                    "active window is not private");
       assert.equal(isPrivate(winUtils.activeBrowserWindow), false,
                    "active browser window is not private");
       assert.equal(isWindowPrivate(window), false, "window is not private");
       assert.equal(isPrivate(window), false, "window is not private");
     }
 
-    close(window).then(done);
-  });
+    return close(window);
+  }).then(done).then(null, assert.fail);
 }
 
 exports.testWindowIteratorIgnoresPrivateWindows = function(assert, done) {
   // make a new private window
   makeEmptyBrowserWindow({
     private: true
   }).then(function(window) {
     assert.equal(isWindowPrivate(window), isWindowPBSupported);
     assert.ok(toArray(winUtils.windowIterator()).indexOf(window) > -1,
               "window is in windowIterator()");
 
-    close(window).then(done);
-  });
+    return close(window);
+  }).then(done).then(null, assert.fail);
 };
 
 // test that it is not possible to find a private window in
 // windows module's iterator
 exports.testWindowIteratorPrivateDefault = function(assert, done) {
   // there should only be one window open here, if not give us the
   // the urls
   if (browserWindows.length > 1) {
@@ -238,11 +238,11 @@ exports.testWindowIteratorPrivateDefault
     assert.equal(isPrivate(window), isWindowPBSupported, 'there is a private window open');
     assert.equal(isPrivate(winUtils.activeWindow), isWindowPBSupported);
     assert.equal(isPrivate(getMostRecentWindow()), isWindowPBSupported);
     assert.equal(isPrivate(browserWindows.activeWindow), isWindowPBSupported);
 
     assert.equal(browserWindows.length, 2, '2 windows open');
     assert.equal(windows(null, { includePrivate: true }).length, 2);
 
-    close(window).then(done);
-  });
+    return close(window);
+  }).then(done).then(null, assert.fail);
 };
--- a/addon-sdk/source/test/addons/self/main.js
+++ b/addon-sdk/source/test/addons/self/main.js
@@ -3,18 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const self = require("sdk/self");
 
 exports["test self.data.load"] = assert => {
 
-  assert.equal(self.data.load("data.md"),
-               "# hello world\n",
+  assert.equal(self.data.load("data.md").trim(),
+               "# hello world",
                "paths work");
 
-  assert.equal(self.data.load("./data.md"),
-               "# hello world\n",
+  assert.equal(self.data.load("./data.md").trim(),
+               "# hello world",
                "relative paths work");
 };
 
 require("sdk/test/runner").runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/tab-close-on-startup/main.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const { setTimeout } = require('sdk/timers');
+const tabs = require('sdk/tabs');
+
+let closeEvents = 0;
+const closeEventDetector = _ => closeEvents++;
+
+exports.testNoTabCloseOnStartup = function(assert, done) {
+  setTimeout(_ => {
+    assert.equal(closeEvents, 0, 'there were no tab close events detected');
+    tabs.open({
+      url: 'about:mozilla',
+      inBackground: true,
+      onReady: tab => tab.close(),
+      onClose: _ => {
+        assert.equal(closeEvents, 1, 'there was one tab close event detected');
+        done();
+      }
+    })
+  });
+}
+
+
+exports.main = function() {
+  tabs.on('close', closeEventDetector);
+
+  require("sdk/test/runner").runTestsFromModule(module);
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/tab-close-on-startup/package.json
@@ -0,0 +1,3 @@
+{
+  "id": "test-tabs@jetpack"
+}
--- a/addon-sdk/source/test/addons/translators/main.js
+++ b/addon-sdk/source/test/addons/translators/main.js
@@ -2,23 +2,22 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { Cc, Ci, Cu, Cm, components } = require('chrome');
 const self = require('sdk/self');
 const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {});
 
-
 exports.testTranslators = function(assert, done) {
   AddonManager.getAddonByID(self.id, function(addon) {
     let count = 0;
     addon.translators.forEach(function({ name }) {
       count++;
       assert.equal(name, 'Erik Vold', 'The translator keys are correct');
     });
-      assert.equal(count, 1, 'The translator key count is correct');
-      assert.equal(addon.translators.length, 1, 'The translator key length is correct');
+    assert.equal(count, 1, 'The translator key count is correct');
+    assert.equal(addon.translators.length, 1, 'The translator key length is correct');
     done();
   });
 }
 
 require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6a00b485c6cf414cb0a9641870349988ac4370a9
GIT binary patch
literal 9775
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
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/child-process-scripts.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const { platform, pathFor } = require('sdk/system');
+const { defer } = require('sdk/core/promise');
+const { emit } = require('sdk/event/core');
+const { join } = require('sdk/fs/path');
+const { writeFile, unlinkSync, existsSync } = require('sdk/io/fs');
+const PROFILE_DIR= pathFor('ProfD');
+const isWindows = platform.toLowerCase().indexOf('win') === 0;
+const isOSX = platform.toLowerCase().indexOf('darwin') === 0;
+
+let scripts = {
+  'args.sh': 'echo $1 $2 $3 $4',
+  'args.bat': 'echo %1 %2 %3 %4',
+  'check-env.sh': 'echo $CHILD_PROCESS_ENV_TEST',
+  'check-env.bat': 'echo %CHILD_PROCESS_ENV_TEST%',
+  'check-pwd.sh': 'echo $PWD',
+  'check-pwd.bat': 'cd',
+  'large-err.sh': 'for n in `seq 0 $1` ; do echo "E" 1>&2; done',
+  'large-err-mac.sh': 'for ((i=0; i<$1; i=i+1)); do echo "E" 1>&2; done',
+  'large-err.bat': 'FOR /l %%i in (0,1,%1) DO echo "E" 1>&2',
+  'large-out.sh': 'for n in `seq 0 $1` ; do echo "O"; done',
+  'large-out-mac.sh': 'for ((i=0; i<$1; i=i+1)); do echo "O"; done',
+  'large-out.bat': 'FOR /l %%i in (0,1,%1) DO echo "O"',
+  'wait.sh': 'sleep 2',
+  // Use `ping` to an invalid IP address because `timeout` isn't
+  // on all environments? http://stackoverflow.com/a/1672349/1785755
+  'wait.bat': 'ping 1.1.1.1 -n 1 -w 2000 > nul'
+};
+
+Object.keys(scripts).forEach(filename => {
+  if (/\.sh$/.test(filename))
+    scripts[filename] = '#!/bin/sh\n' + scripts[filename];
+  else if (/\.bat$/.test(filename))
+    scripts[filename] = '@echo off\n' + scripts[filename];
+});
+
+function getScript (name) {
+  // Use specific OSX script if exists
+  if (isOSX && scripts[name + '-mac.sh'])
+    name = name + '-mac';
+  let fileName = name + (isWindows ? '.bat' : '.sh');
+  return createFile(fileName, scripts[fileName]);
+}
+exports.getScript = getScript;
+
+function createFile (name, data) {
+  let { promise, resolve, reject } = defer();
+  let fileName = join(PROFILE_DIR, name);
+  writeFile(fileName, data, function (err) {
+    if (err) reject();
+    else {
+      makeExecutable(fileName);
+      resolve(fileName);
+    }
+  });
+  return promise;
+}
+
+// TODO Use fs.chmod once implemented, bug 914606
+function makeExecutable (name) {
+  let { CC } = require('chrome');
+  let nsILocalFile = CC('@mozilla.org/file/local;1', 'nsILocalFile', 'initWithPath');
+  let file = nsILocalFile(name);
+  file.permissions = parseInt('0777', 8);
+}
+
+function deleteFile (name) {
+  let file = join(PROFILE_DIR, name);
+  if (existsSync(file))
+    unlinkSync(file);
+}
+
+function cleanUp () {
+  Object.keys(scripts).forEach(deleteFile);
+}
+exports.cleanUp = cleanUp;
--- a/addon-sdk/source/test/pagemod-test-helpers.js
+++ b/addon-sdk/source/test/pagemod-test-helpers.js
@@ -57,8 +57,34 @@ exports.testPageMod = function testPageM
         done();
       }
     );
   }
   b.addEventListener("load", onPageLoad, true);
 
   return pageMods;
 }
+
+/**
+ * helper function that creates a PageMod and calls back the appropriate handler
+ * based on the value of document.readyState at the time contentScript is attached
+ */
+exports.handleReadyState = function(url, contentScriptWhen, callbacks) {
+  const { PageMod } = Loader(module).require('sdk/page-mod');
+
+  let pagemod = PageMod({
+    include: url,
+    attachTo: ['existing', 'top'],
+    contentScriptWhen: contentScriptWhen,
+    contentScript: "self.postMessage(document.readyState)",
+    onAttach: worker => {
+      let { tab } = worker;
+      worker.on('message', readyState => {
+        pagemod.destroy();
+        // generate event name from `readyState`, e.g. `"loading"` becomes `onLoading`.
+        let type = 'on' + readyState[0].toUpperCase() + readyState.substr(1);
+
+        if (type in callbacks)
+          callbacks[type](tab); 
+      })
+    }
+  });
+}
--- a/addon-sdk/source/test/tabs/test-fennec-tabs.js
+++ b/addon-sdk/source/test/tabs/test-fennec-tabs.js
@@ -576,9 +576,31 @@ exports.testUniqueTabIds = function(asse
     let fn = steps[index];
     index++;
     fn(index);
   }
 
   next(0);
 }
 
+exports.testOnLoadEventWithDOM = function(assert, done) {
+  let count = 0;
+  let title = 'testOnLoadEventWithDOM';
+
+  tabs.open({
+    url: 'data:text/html;charset=utf-8,<title>' + title + '</title>',
+    inBackground: true,
+    onLoad: function(tab) {
+      assert.equal(tab.title, title, 'tab passed in as arg, load called');
+
+      if (++count > 1) {
+        assert.pass('onLoad event called on reload');
+        tab.close(done);
+      }
+      else {
+        assert.pass('first onLoad event occured');
+        tab.reload();
+      }
+    }
+  });
+};
+
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -1,25 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { Cc, Ci } = require('chrome');
 const { Loader } = require('sdk/test/loader');
-const timer = require('sdk/timers');
+const { setTimeout } = require('sdk/timers');
 const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
 const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { open, focus, close } = require('sdk/window/helpers');
 const tabs = require('sdk/tabs');
 const { browserWindows } = require('sdk/windows');
 const { set: setPref } = require("sdk/preferences/service");
 const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
-
-const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAA";
+const fixtures = require("../fixtures");
 
 // Bug 682681 - tab.title should never be empty
 exports.testBug682681_aboutURI = function(assert, done) {
   let url = 'chrome://browser/locale/tabbrowser.properties';
   let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
                         getService(Ci.nsIStringBundleService).
                         createBundle(url);
   let emptyTabTitle = stringBundle.GetStringFromName('tabs.emptyTabTitle');
@@ -60,42 +59,105 @@ exports.testBrowserWindowCreationOnActiv
   let gotActivate = false;
 
   tabs.once('activate', function onActivate(eventTab) {
     assert.ok(windows.activeWindow, "Is able to fetch activeWindow");
     gotActivate = true;
   });
 
   open().then(function(window) {
-    assert.ok(gotActivate, "Received activate event before openBrowserWindow's callback is called");
-    close(window).then(done);
-  });
+    assert.ok(gotActivate, "Received activate event");
+    return close(window);
+  }).then(done).then(null, assert.fail);
 }
 
 // TEST: tab unloader
-exports.testAutomaticDestroy = function(assert, done) {
-  // Create a second tab instance that we will destroy
+exports.testAutomaticDestroyEventOpen = function(assert, done) {
   let called = false;
-
   let loader = Loader(module);
   let tabs2 = loader.require("sdk/tabs");
-  tabs2.on('open', function onOpen(tab) {
-    called = true;
+  tabs2.on('open', _ => called = true);
+
+  // Fire a tab event and ensure that the destroyed tab is inactive
+  tabs.once('open', tab => {
+    setTimeout(_ => {
+      assert.ok(!called, "Unloaded tab module is destroyed and inactive");
+      tab.close(done);
+    });
   });
 
   loader.unload();
+  tabs.open("data:text/html;charset=utf-8,testAutomaticDestroyEventOpen");
+};
+
+exports.testAutomaticDestroyEventActivate = function(assert, done) {
+  let called = false;
+  let loader = Loader(module);
+  let tabs2 = loader.require("sdk/tabs");
+  tabs2.on('activate', _ => called = true);
 
   // Fire a tab event and ensure that the destroyed tab is inactive
-  tabs.once('open', function (tab) {
-    timer.setTimeout(function () {
+  tabs.once('activate', tab => {
+    setTimeout(_ => {
       assert.ok(!called, "Unloaded tab module is destroyed and inactive");
       tab.close(done);
     });
   });
-  tabs.open("data:text/html;charset=utf-8,foo");
+
+  loader.unload();
+  tabs.open("data:text/html;charset=utf-8,testAutomaticDestroyEventActivate");
+};
+
+exports.testAutomaticDestroyEventDeactivate = function(assert, done) {
+  let called = false;
+  let currentTab = tabs.activeTab;
+  let loader = Loader(module);
+  let tabs2 = loader.require("sdk/tabs");
+
+  tabs.open({
+    url: "data:text/html;charset=utf-8,testAutomaticDestroyEventDeactivate",
+    onActivate: _ => setTimeout(_ => {
+      tabs2.on('deactivate', _ => called = true);
+
+      // Fire a tab event and ensure that the destroyed tab is inactive
+      tabs.once('deactivate', tab => {
+        setTimeout(_ => {
+          assert.ok(!called, "Unloaded tab module is destroyed and inactive");
+          tab.close(done);
+        });
+      });
+
+      loader.unload();
+      currentTab.activate();
+    })
+  });
+};
+
+exports.testAutomaticDestroyEventClose = function(assert, done) {
+  let called = false;
+  let loader = Loader(module);
+  let tabs2 = loader.require("sdk/tabs");
+
+  tabs.open({
+    url: "data:text/html;charset=utf-8,testAutomaticDestroyEventClose",
+    onReady: tab => {
+      tabs2.on('close', _ => called = true);
+
+      // Fire a tab event and ensure that the destroyed tab is inactive
+      tabs.once('close', tab => {
+        setTimeout(_ => {
+          assert.ok(!called, "Unloaded tab module is destroyed and inactive");
+          done();
+        });
+      });
+
+      loader.unload();
+      tab.close();
+    }
+  });
 };
 
 exports.testTabPropertiesInNewWindow = function(assert, done) {
   let warning = "DEPRECATED: tab.favicon is deprecated, please use require(\"sdk/places/favicon\").getFavicon instead.\n"
   const { LoaderWithFilteredConsole } = require("sdk/test/loader");
   let loader = LoaderWithFilteredConsole(module, function(type, message) {
     if (type == "error" && message.substring(0, warning.length) == warning)
       return false;
@@ -103,17 +165,17 @@ exports.testTabPropertiesInNewWindow = f
   });
 
   let tabs = loader.require('sdk/tabs');
   let { getOwnerWindow } = loader.require('sdk/private-browsing/window/utils');
 
   let count = 0;
   function onReadyOrLoad (tab) {
     if (count++) {
-      close(getOwnerWindow(tab)).then(done);
+      close(getOwnerWindow(tab)).then(done).then(null, assert.fail);
     }
   }
 
   let url = "data:text/html;charset=utf-8,<html><head><title>foo</title></head><body>foo</body></html>";
   tabs.open({
     inNewWindow: true,
     url: url,
     onReady: function(tab) {
@@ -199,17 +261,17 @@ exports.testTabContentTypeAndReload = fu
       url: url,
       onReady: function(tab) {
         if (tab.url === url) {
           assert.equal(tab.contentType, "text/html");
           tab.url = urlXML;
         }
         else {
           assert.equal(tab.contentType, "text/xml");
-          close(window).then(done);
+          close(window).then(done).then(null, assert.fail);
         }
       }
     });
   });
 };
 
 // TEST: tabs iterator and length property
 exports.testTabsIteratorAndLength = function(assert, done) {
@@ -224,34 +286,34 @@ exports.testTabsIteratorAndLength = func
     tabs.open({
       url: url,
       onOpen: function(tab) {
         let count = 0;
         for each (let t in tabs) count++;
         assert.equal(count, startCount + 3, "iterated tab count matches");
         assert.equal(startCount + 3, tabs.length, "iterated tab count matches length property");
 
-        close(window).then(done);
+        close(window).then(done).then(null, assert.fail);
       }
     });
   });
 };
 
 // TEST: tab.url setter
 exports.testTabLocation = function(assert, done) {
   open().then(focus).then(function(window) {
     let url1 = "data:text/html;charset=utf-8,foo";
     let url2 = "data:text/html;charset=utf-8,bar";
 
     tabs.on('ready', function onReady(tab) {
       if (tab.url != url2)
         return;
       tabs.removeListener('ready', onReady);
       assert.pass("tab.load() loaded the correct url");
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     });
 
     tabs.open({
       url: url1,
       onOpen: function(tab) {
         tab.url = url2
       }
     });
@@ -295,20 +357,20 @@ exports.testTabMove = function(assert, d
     let url = "data:text/html;charset=utf-8,foo";
 
     tabs.open({
       url: url,
       onOpen: function(tab) {
         assert.equal(tab.index, 1, "tab index before move matches");
         tab.index = 0;
         assert.equal(tab.index, 0, "tab index after move matches");
-        close(window).then(done);
+        close(window).then(done).then(null, assert.fail);
       }
     });
-  });
+  }).then(null, assert.fail);
 };
 
 // TEST: open tab with default options
 exports.testOpen = function(assert, done) {
   let url = "data:text/html;charset=utf-8,default";
   tabs.open({
     url: url,
     onReady: function(tab) {
@@ -349,22 +411,22 @@ exports.testPinUnpin = function(assert, 
   });
 }
 
 // TEST: open tab in background
 exports.testInBackground = function(assert, done) {
   let window = getMostRecentBrowserWindow();
   let activeUrl = tabs.activeTab.url;
   let url = "data:text/html;charset=utf-8,background";
-  assert.equal(activeWindow, window, "activeWindow matches this window");
+  assert.equal(getMostRecentBrowserWindow(), window, "getMostRecentBrowserWindow() matches this window");
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
     assert.equal(tabs.activeTab.url, activeUrl, "URL of active tab has not changed");
     assert.equal(tab.url, url, "URL of the new background tab matches");
-    assert.equal(activeWindow, window, "a new window was not opened");
+    assert.equal(getMostRecentBrowserWindow(), window, "a new window was not opened");
     assert.notEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL");
     tab.close(done);
   });
 
   tabs.open({
     url: url,
     inBackground: true
   });
@@ -378,23 +440,23 @@ exports.testOpenInNewWindow = function(a
   tabs.open({
     url: url,
     inNewWindow: true,
     onReady: function(tab) {
       let newWindow = getOwnerWindow(tab);
       assert.equal(windows().length, startWindowCount + 1, "a new window was opened");
 
       onFocus(newWindow).then(function() {
-        assert.equal(activeWindow, newWindow, "new window is active");
+        assert.equal(getMostRecentBrowserWindow(), newWindow, "new window is active");
         assert.equal(tab.url, url, "URL of the new tab matches");
         assert.equal(newWindow.content.location, url, "URL of new tab in new window matches");
         assert.equal(tabs.activeTab.url, url, "URL of activeTab matches");
 
-        close(newWindow).then(done);
-      }, assert.fail).then(null, assert.fail);
+        return close(newWindow).then(done);
+      }).then(null, assert.fail);
     }
   });
 
 }
 
 // Test tab.open inNewWindow + onOpen combination
 exports.testOpenInNewWindowOnOpen = function(assert, done) {
   let startWindowCount = windows().length;
@@ -403,91 +465,94 @@ exports.testOpenInNewWindowOnOpen = func
   tabs.open({
     url: url,
     inNewWindow: true,
     onOpen: function(tab) {
       let newWindow = getOwnerWindow(tab);
 
       onFocus(newWindow).then(function() {
         assert.equal(windows().length, startWindowCount + 1, "a new window was opened");
-        assert.equal(activeWindow, newWindow, "new window is active");
+        assert.equal(getMostRecentBrowserWindow(), newWindow, "new window is active");
 
-        close(newWindow).then(done);
+        close(newWindow).then(done).then(null, assert.fail);
       });
     }
   });
 };
 
 // TEST: onOpen event handler
 exports.testTabsEvent_onOpen = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let url = "data:text/html;charset=utf-8,1";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('open', listener1);
 
     // add listener via collection add
     tabs.on('open', function listener2(tab) {
       assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('open', listener1);
       tabs.removeListener('open', listener2);
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     });
 
     tabs.open(url);
-  });
+  }).then(null, assert.fail);
 };
 
 // TEST: onClose event handler
 exports.testTabsEvent_onClose = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let url = "data:text/html;charset=utf-8,onclose";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     }
     tabs.on('close', listener1);
 
     // add listener via collection add
     tabs.on('close', function listener2(tab) {
       assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('close', listener1);
       tabs.removeListener('close', listener2);
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     });
 
     tabs.on('ready', function onReady(tab) {
       tabs.removeListener('ready', onReady);
       tab.close();
     });
 
     tabs.open(url);
-  });
+  }).then(null, assert.fail);
 };
 
 // TEST: onClose event handler when a window is closed
 exports.testTabsEvent_onCloseWindow = function(assert, done) {
   let closeCount = 0;
   let individualCloseCount = 0;
 
-  openBrowserWindow(function(window) {
+  open().then(focus).then(window => {
+    assert.pass('opened a new window');
+
     tabs.on("close", function listener() {
       if (++closeCount == 4) {
         tabs.removeListener("close", listener);
       }
     });
 
     function endTest() {
       if (++individualCloseCount < 3) {
+        assert.pass('tab closed ' + individualCloseCount);
         return;
       }
 
       assert.equal(closeCount, 4, "Correct number of close events received");
       assert.equal(individualCloseCount, 3,
                    "Each tab with an attached onClose listener received a close " +
                    "event when the window was closed");
 
@@ -495,16 +560,17 @@ exports.testTabsEvent_onCloseWindow = fu
     }
 
     // One tab is already open with the window
     let openTabs = 1;
     function testCasePossiblyLoaded() {
       if (++openTabs == 4) {
         window.close();
       }
+      assert.pass('tab opened ' + openTabs);
     }
 
     tabs.open({
       url: "data:text/html;charset=utf-8,tab2",
       onOpen: testCasePossiblyLoaded,
       onClose: endTest
     });
 
@@ -514,22 +580,22 @@ exports.testTabsEvent_onCloseWindow = fu
       onClose: endTest
     });
 
     tabs.open({
       url: "data:text/html;charset=utf-8,tab4",
       onOpen: testCasePossiblyLoaded,
       onClose: endTest
     });
-  });
+  }).then(null, assert.fail);
 }
 
 // TEST: onReady event handler
 exports.testTabsEvent_onReady = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let url = "data:text/html;charset=utf-8,onready";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('ready', listener1);
@@ -538,148 +604,147 @@ exports.testTabsEvent_onReady = function
     tabs.on('ready', function listener2(tab) {
       assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('ready', listener1);
       tabs.removeListener('ready', listener2);
       close(window).then(done);
     });
 
     tabs.open(url);
-  });
+  }).then(null, assert.fail);
 };
 
 // TEST: onActivate event handler
 exports.testTabsEvent_onActivate = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let url = "data:text/html;charset=utf-8,onactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('activate', listener1);
 
     // add listener via collection add
     tabs.on('activate', function listener2(tab) {
       assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('activate', listener1);
       tabs.removeListener('activate', listener2);
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     });
 
     tabs.open(url);
-  });
+  }).then(null, assert.fail);
 };
 
 // onDeactivate event handler
 exports.testTabsEvent_onDeactivate = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let url = "data:text/html;charset=utf-8,ondeactivate";
     let eventCount = 0;
 
     // add listener via property assignment
     function listener1(tab) {
       eventCount++;
     };
     tabs.on('deactivate', listener1);
 
     // add listener via collection add
     tabs.on('deactivate', function listener2(tab) {
       assert.equal(++eventCount, 2, "both listeners notified");
       tabs.removeListener('deactivate', listener1);
       tabs.removeListener('deactivate', listener2);
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     });
 
     tabs.on('open', function onOpen(tab) {
       tabs.removeListener('open', onOpen);
       tabs.open("data:text/html;charset=utf-8,foo");
     });
 
     tabs.open(url);
-  });
+  }).then(null, assert.fail);
 };
 
 // pinning
 exports.testTabsEvent_pinning = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let url = "data:text/html;charset=utf-8,1";
 
     tabs.on('open', function onOpen(tab) {
       tabs.removeListener('open', onOpen);
       tab.pin();
     });
 
     tabs.on('pinned', function onPinned(tab) {
       tabs.removeListener('pinned', onPinned);
       assert.ok(tab.isPinned, "notified tab is pinned");
       tab.unpin();
     });
 
     tabs.on('unpinned', function onUnpinned(tab) {
       tabs.removeListener('unpinned', onUnpinned);
       assert.ok(!tab.isPinned, "notified tab is not pinned");
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     });
 
     tabs.open(url);
-  });
+  }).then(null, assert.fail);
 };
 
 // TEST: per-tab event handlers
 exports.testPerTabEvents = function(assert, done) {
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let eventCount = 0;
 
     tabs.open({
       url: "data:text/html;charset=utf-8,foo",
       onOpen: function(tab) {
         // add listener via property assignment
         function listener1() {
           eventCount++;
         };
         tab.on('ready', listener1);
 
         // add listener via collection add
         tab.on('ready', function listener2() {
           assert.equal(eventCount, 1, "both listeners notified");
           tab.removeListener('ready', listener1);
           tab.removeListener('ready', listener2);
-          close(window).then(done);
+          close(window).then(done).then(null, assert.fail);
         });
       }
     });
-  });
+  }).then(null, assert.fail);
 };
 
 exports.testAttachOnOpen = function (assert, done) {
   // Take care that attach has to be called on tab ready and not on tab open.
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     tabs.open({
       url: "data:text/html;charset=utf-8,foobar",
       onOpen: function (tab) {
         let worker = tab.attach({
           contentScript: 'self.postMessage(document.location.href); ',
           onMessage: function (msg) {
             assert.equal(msg, "about:blank",
               "Worker document url is about:blank on open");
             worker.destroy();
-            close(window).then(done);
+            close(window).then(done).then(null, assert.fail);
           }
         });
       }
     });
-
-  });
+  }).then(null, assert.fail);
 }
 
 exports.testAttachOnMultipleDocuments = function (assert, done) {
   // Example of attach that process multiple tab documents
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let firstLocation = "data:text/html;charset=utf-8,foobar";
     let secondLocation = "data:text/html;charset=utf-8,bar";
     let thirdLocation = "data:text/html;charset=utf-8,fox";
     let onReadyCount = 0;
     let worker1 = null;
     let worker2 = null;
     let detachEventCount = 0;
 
@@ -741,26 +806,25 @@ exports.testAttachOnMultipleDocuments = 
     });
 
     function checkEnd() {
       if (detachEventCount != 2)
         return;
 
       assert.pass("Got all detach events");
 
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     }
-
-  });
+  }).then(null, assert.fail);
 }
 
 
 exports.testAttachWrappers = function (assert, done) {
   // Check that content script has access to wrapped values by default
-  openBrowserWindow(function(window, browser) {
+  open().then(focus).then(window => {
     let document = "data:text/html;charset=utf-8,<script>var globalJSVar = true; " +
                    "                       document.getElementById = 3;</script>";
     let count = 0;
 
     tabs.open({
       url: document,
       onReady: function (tab) {
         let worker = tab.attach({
@@ -768,23 +832,22 @@ exports.testAttachWrappers = function (a
                          '  self.postMessage(!("globalJSVar" in window));' +
                          '  self.postMessage(typeof window.globalJSVar == "undefined");' +
                          '} catch(e) {' +
                          '  self.postMessage(e.message);' +
                          '}',
           onMessage: function (msg) {
             assert.equal(msg, true, "Worker has wrapped objects ("+count+")");
             if (count++ == 1)
-              close(window).then(done);
+              close(window).then(done).then(null, assert.fail);
           }
         });
       }
     });
-
-  });
+  }).then(null, assert.fail);
 }
 
 /*
 // We do not offer unwrapped access to DOM since bug 601295 landed
 // See 660780 to track progress of unwrap feature
 exports.testAttachUnwrapped = function (assert, done) {
   // Check that content script has access to unwrapped values through unsafeWindow
   openBrowserWindow(function(window, browser) {
@@ -822,36 +885,36 @@ exports['test window focus changes activ
       assert.pass("window 2 is open");
 
       focus(win2).then(function() {
         tabs.on("activate", function onActivate(tab) {
           tabs.removeListener("activate", onActivate);
           assert.pass("activate was called on windows focus change.");
           assert.equal(tab.url, url1, 'the activated tab url is correct');
 
-          close(win2).then(function() {
+          return close(win2).then(function() {
             assert.pass('window 2 was closed');
             return close(win1);
-          }).then(done);
+          }).then(done).then(null, assert.fail);
         });
 
         win1.focus();
       });
     }, "data:text/html;charset=utf-8,test window focus changes active tab</br><h1>Window #2");
   }, url1);
 };
 
 exports['test ready event on new window tab'] = function(assert, done) {
   let uri = encodeURI("data:text/html;charset=utf-8,Waiting for ready event!");
 
   require("sdk/tabs").on("ready", function onReady(tab) {
     if (tab.url === uri) {
       require("sdk/tabs").removeListener("ready", onReady);
       assert.pass("ready event was emitted");
-      close(window).then(done);
+      close(window).then(done).then(null, assert.fail);
     }
   });
 
   let window = openBrowserWindow(function(){}, uri);
 };
 
 exports['test unique tab ids'] = function(assert, done) {
   var windows = require('sdk/windows').browserWindows;
@@ -910,17 +973,17 @@ exports.testOnLoadEventWithDOM = functio
   });
 };
 
 // related to Bug 671305
 exports.testOnLoadEventWithImage = function(assert, done) {
   let count = 0;
 
   tabs.open({
-    url: base64png,
+    url: fixtures.url('Firefox.jpg'),
     inBackground: true,
     onLoad: function(tab) {
       if (++count > 1) {
         assert.pass('onLoad event called on reload with image');
         tab.close(done);
       }
       else {
         assert.pass('first onLoad event occured');
@@ -949,40 +1012,33 @@ exports.testFaviconGetterDeprecation = f
       tab.close(done);
       loader.unload();
     }
   });
 }
 
 /******************* helpers *********************/
 
-// Helper for getting the active window
-this.__defineGetter__("activeWindow", function activeWindow() {
-  return Cc["@mozilla.org/appshell/window-mediator;1"].
-         getService(Ci.nsIWindowMediator).
-         getMostRecentWindow("navigator:browser");
-});
-
 // Utility function to open a new browser window.
 function openBrowserWindow(callback, url) {
   let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
            getService(Ci.nsIWindowWatcher);
   let urlString = Cc["@mozilla.org/supports-string;1"].
                   createInstance(Ci.nsISupportsString);
   urlString.data = url;
   let window = ww.openWindow(null, "chrome://browser/content/browser.xul",
                              "_blank", "chrome,all,dialog=no", urlString);
 
   if (callback) {
     window.addEventListener("load", function onLoad(event) {
       if (event.target && event.target.defaultView == window) {
         window.removeEventListener("load", onLoad, true);
         let browsers = window.document.getElementsByTagName("tabbrowser");
         try {
-          timer.setTimeout(function () {
+          setTimeout(function () {
             callback(window, browsers[0]);
           }, 10);
         }
         catch (e) {
           console.exception(e);
         }
       }
     }, true);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-child_process.js
@@ -0,0 +1,543 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const { spawn, exec, execFile, fork } = require('sdk/system/child_process');
+const { env, platform, pathFor } = require('sdk/system');
+const { isNumber } = require('sdk/lang/type');
+const { after } = require('sdk/test/utils');
+const { emit } = require('sdk/event/core');
+const PROFILE_DIR= pathFor('ProfD');
+const isWindows = platform.toLowerCase().indexOf('win') === 0;
+const { getScript, cleanUp } = require('./fixtures/child-process-scripts');
+
+// We use direct paths to these utilities as we currently cannot
+// call non-absolute paths to utilities in subprocess.jsm
+const CAT_PATH = isWindows ? 'C:\\Windows\\System32\\more.com' : '/bin/cat';
+
+exports.testExecCallbackSuccess = function (assert, done) {
+  exec(isWindows ? 'DIR /A-D' : 'ls -al', {
+    cwd: PROFILE_DIR
+  }, function (err, stdout, stderr) {
+    assert.ok(!err, 'no errors found');
+    assert.equal(stderr, '', 'stderr is empty');
+    assert.ok(/extensions\.ini/.test(stdout), 'stdout output of `ls -al` finds files');
+
+    if (isWindows) {
+      // `DIR /A-D` does not display directories on WIN
+      assert.ok(!/<DIR>/.test(stdout),
+        'passing arguments in `exec` works');
+    }
+    else {
+      // `ls -al` should list all the priviledge information on Unix
+      assert.ok(/d(r[-|w][-|x]){3}/.test(stdout),
+        'passing arguments in `exec` works');
+    }
+    done();
+  });
+};
+
+exports.testExecCallbackError = function (assert, done) {
+  exec('not-real-command', { cwd: PROFILE_DIR }, function (err, stdout, stderr) {
+    assert.ok(/not-real-command/.test(err.toString()),
+      'error contains error message');
+    assert.ok(err.lineNumber >= 0, 'error contains lineNumber');
+    assert.ok(/resource:\/\//.test(err.fileName), 'error contains fileName');
+    assert.ok(err.code && isNumber(err.code), 'non-zero error code property on error');
+    assert.equal(err.signal, null,
+      'null signal property when not manually terminated');
+    assert.equal(stdout, '', 'stdout is empty');
+    assert.ok(/not-real-command/.test(stderr), 'stderr contains error message');
+    done();
+  });
+};
+
+exports.testExecOptionsEnvironment = function (assert, done) {
+  getScript('check-env').then(envScript => {
+    exec(envScript, {
+      cwd: PROFILE_DIR,
+      env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' }
+    }, function (err, stdout, stderr) {
+      assert.equal(stderr, '', 'stderr is empty');
+      assert.ok(!err, 'received `cwd` option');
+      assert.ok(/my-value-test/.test(stdout),
+        'receives environment option');
+      done();
+    });
+  });
+};
+
+exports.testExecOptionsTimeout = function (assert, done) {
+  let count = 0;
+  getScript('wait').then(script => {
+    let child = exec(script, { timeout: 100 }, (err, stdout, stderr) => {
+      assert.equal(err.killed, true, 'error has `killed` property as true');
+      assert.equal(err.code, null, 'error has `code` as null');
+      assert.equal(err.signal, 'SIGTERM',
+        'error has `signal` as SIGTERM by default');
+      assert.equal(stdout, '', 'stdout is empty');
+      assert.equal(stderr, '', 'stderr is empty');
+      if (++count === 3) complete();
+    });
+
+    function exitHandler (code, signal) {
+      assert.equal(code, null, 'error has `code` as null');
+      assert.equal(signal, 'SIGTERM',
+        'error has `signal` as SIGTERM by default');
+      if (++count === 3) complete();
+    }
+
+    function closeHandler (code, signal) {
+      assert.equal(code, null, 'error has `code` as null');
+      assert.equal(signal, 'SIGTERM',
+        'error has `signal` as SIGTERM by default');
+      if (++count === 3) complete();
+    }
+
+    child.on('exit', exitHandler);
+    child.on('close', closeHandler);
+
+    function complete () {
+      child.off('exit', exitHandler);
+      child.off('close', closeHandler);
+      done();
+    }
+  });
+};
+
+exports.testExecFileCallbackSuccess = function (assert, done) {
+  getScript('args').then(script => {
+    execFile(script, ['--myargs', '-j', '-s'], { cwd: PROFILE_DIR }, function (err, stdout, stderr) {
+      assert.ok(!err, 'no errors found');
+      assert.equal(stderr, '', 'stderr is empty');
+      // Trim output since different systems have different new line output
+      assert.equal(stdout.trim(), '--myargs -j -s'.trim(), 'passes in correct arguments');
+      done();
+    });
+  });
+};
+
+exports.testExecFileCallbackError = function (assert, done) {
+  execFile('not-real-command', { cwd: PROFILE_DIR }, function (err, stdout, stderr) {
+    assert.ok(/NS_ERROR_FILE_UNRECOGNIZED_PATH/.test(err.message),
+      'error contains error message');
+    assert.ok(err.lineNumber >= 0, 'error contains lineNumber');
+    assert.ok(/resource:\/\//.test(err.fileName), 'error contains fileName');
+    assert.equal(stdout, '', 'stdout is empty');
+    assert.equal(stderr, '', 'stdout is empty');
+    done();
+  });
+};
+
+exports.testExecFileOptionsEnvironment = function (assert, done) {
+  getScript('check-env').then(script => {
+    execFile(script, {
+      cwd: PROFILE_DIR,
+      env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' }
+    }, function (err, stdout, stderr) {
+      assert.equal(stderr, '', 'stderr is empty');
+      assert.ok(!err, 'received `cwd` option');
+      assert.ok(/my-value-test/.test(stdout),
+        'receives environment option');
+      done();
+    });
+  });
+};
+
+exports.testExecFileOptionsTimeout = function (assert, done) {
+  let count = 0;
+  getScript('wait').then(script => {
+    let child = execFile(script, { timeout: 100 }, (err, stdout, stderr) => {
+      assert.equal(err.killed, true, 'error has `killed` property as true');
+      assert.equal(err.code, null, 'error has `code` as null');
+      assert.equal(err.signal, 'SIGTERM',
+        'error has `signal` as SIGTERM by default');
+      assert.equal(stdout, '', 'stdout is empty');
+      assert.equal(stderr, '', 'stderr is empty');
+      if (++count === 3) complete();
+    });
+
+    function exitHandler (code, signal) {
+      assert.equal(code, null, 'error has `code` as null');
+      assert.equal(signal, 'SIGTERM',
+        'error has `signal` as SIGTERM by default');
+      if (++count === 3) complete();
+    }
+
+    function closeHandler (code, signal) {
+      assert.equal(code, null, 'error has `code` as null');
+      assert.equal(signal, 'SIGTERM',
+        'error has `signal` as SIGTERM by default');
+      if (++count === 3) complete();
+    }
+
+    child.on('exit', exitHandler);
+    child.on('close', closeHandler);
+
+    function complete () {
+      child.off('exit', exitHandler);
+      child.off('close', closeHandler);
+      done();
+    }
+  });
+};
+
+/**
+ * Not necessary to test for both `exec` and `execFile`, but
+ * it is necessary to test both when the buffer is larger
+ * and smaller than buffer size used by the subprocess library (1024)
+ */
+exports.testExecFileOptionsMaxBufferLargeStdOut = function (assert, done) {
+  let count = 0;
+  let stdoutChild;
+
+  // Creates a buffer of 2000 to stdout, greater than 1024
+  getScript('large-out').then(script => {
+    stdoutChild = execFile(script, ['10000'], { maxBuffer: 50 }, (err, stdout, stderr) => {
+      assert.ok(/stdout maxBuffer exceeded/.test(err.toString()),
+        'error contains stdout maxBuffer exceeded message');
+      assert.ok(stdout.length >= 50, 'stdout has full buffer');
+      assert.equal(stderr, '', 'stderr is empty');
+      if (++count === 3) complete();
+    });
+    stdoutChild.on('exit', exitHandler);
+    stdoutChild.on('close', closeHandler);
+  });
+
+  function exitHandler (code, signal) {
+    assert.equal(code, null, 'Exit code is null in exit handler');
+    assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler');
+    if (++count === 3) complete();
+  }
+
+  function closeHandler (code, signal) {
+    assert.equal(code, null, 'Exit code is null in close handler');
+    assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler');
+    if (++count === 3) complete();
+  }
+
+  function complete () {
+    stdoutChild.off('exit', exitHandler);
+    stdoutChild.off('close', closeHandler);
+    done();
+  }
+};
+
+exports.testExecFileOptionsMaxBufferLargeStdOErr = function (assert, done) {
+  let count = 0;
+  let stderrChild;
+  // Creates a buffer of 2000 to stderr, greater than 1024
+  getScript('large-err').then(script => {
+    stderrChild = execFile(script, ['10000'], { maxBuffer: 50 }, (err, stdout, stderr) => {
+      assert.ok(/stderr maxBuffer exceeded/.test(err.toString()),
+        'error contains stderr maxBuffer exceeded message');
+      assert.ok(stderr.length >= 50, 'stderr has full buffer');
+      assert.equal(stdout, '', 'stdout is empty');
+      if (++count === 3) complete();
+    });
+    stderrChild.on('exit', exitHandler);
+    stderrChild.on('close', closeHandler);
+  });
+
+  function exitHandler (code, signal) {
+    assert.equal(code, null, 'Exit code is null in exit handler');
+    assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler');
+    if (++count === 3) complete();
+  }
+
+  function closeHandler (code, signal) {
+    assert.equal(code, null, 'Exit code is null in close handler');
+    assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler');
+    if (++count === 3) complete();
+  }
+
+  function complete () {
+    stderrChild.off('exit', exitHandler);
+    stderrChild.off('close', closeHandler);
+    done();
+  }
+};
+
+/**
+ * When total buffer is < process buffer (1024), the process will exit
+ * and not get a chance to be killed for violating the maxBuffer,
+ * although the error will still be sent through (node behaviour)
+ */
+exports.testExecFileOptionsMaxBufferSmallStdOut = function (assert, done) {
+  let count = 0;
+  let stdoutChild;
+
+  // Creates a buffer of 60 to stdout, less than 1024
+  getScript('large-out').then(script => {
+    stdoutChild = execFile(script, ['60'], { maxBuffer: 50 }, (err, stdout, stderr) => {
+      assert.ok(/stdout maxBuffer exceeded/.test(err.toString()),
+        'error contains stdout maxBuffer exceeded message');
+      assert.ok(stdout.length >= 50, 'stdout has full buffer');
+      assert.equal(stderr, '', 'stderr is empty');
+      if (++count === 3) complete();
+    });
+    stdoutChild.on('exit', exitHandler);
+    stdoutChild.on('close', closeHandler);
+  });
+
+  function exitHandler (code, signal) {
+    // Sometimes the buffer limit is hit before the process closes successfully
+    // on both OSX/Windows
+    if (code === null) {
+      assert.equal(code, null, 'Exit code is null in exit handler');
+      assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler');
+    }
+    else {
+      assert.equal(code, 0, 'Exit code is 0 in exit handler');
+      assert.equal(signal, null, 'Signal is null in exit handler');
+    }
+    if (++count === 3) complete();
+  }
+
+  function closeHandler (code, signal) {
+    // Sometimes the buffer limit is hit before the process closes successfully
+    // on both OSX/Windows
+    if (code === null) {
+      assert.equal(code, null, 'Exit code is null in close handler');
+      assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler');
+    }
+    else {
+      assert.equal(code, 0, 'Exit code is 0 in close handler');
+      assert.equal(signal, null, 'Signal is null in close handler');
+    }
+    if (++count === 3) complete();
+  }
+
+  function complete () {
+    stdoutChild.off('exit', exitHandler);
+    stdoutChild.off('close', closeHandler);
+    done();
+  }
+};
+
+exports.testExecFileOptionsMaxBufferSmallStdErr = function (assert, done) {
+  let count = 0;
+  let stderrChild;
+  // Creates a buffer of 60 to stderr, less than 1024
+  getScript('large-err').then(script => {
+    stderrChild = execFile(script, ['60'], { maxBuffer: 50 }, (err, stdout, stderr) => {
+      assert.ok(/stderr maxBuffer exceeded/.test(err.toString()),
+        'error contains stderr maxBuffer exceeded message');
+      assert.ok(stderr.length >= 50, 'stderr has full buffer');
+      assert.equal(stdout, '', 'stdout is empty');
+      if (++count === 3) complete();
+    });
+    stderrChild.on('exit', exitHandler);
+    stderrChild.on('close', closeHandler);
+  });
+
+  function exitHandler (code, signal) {
+    // Sometimes the buffer limit is hit before the process closes successfully
+    // on both OSX/Windows
+    if (code === null) {
+      assert.equal(code, null, 'Exit code is null in exit handler');
+      assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler');
+    }
+    else {
+      assert.equal(code, 0, 'Exit code is 0 in exit handler');
+      assert.equal(signal, null, 'Signal is null in exit handler');
+    }
+    if (++count === 3) complete();
+  }
+
+  function closeHandler (code, signal) {
+    // Sometimes the buffer limit is hit before the process closes successfully
+    // on both OSX/Windows
+    if (code === null) {
+      assert.equal(code, null, 'Exit code is null in close handler');
+      assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler');
+    }
+    else {
+      assert.equal(code, 0, 'Exit code is 0 in close handler');
+      assert.equal(signal, null, 'Signal is null in close handler');
+    }
+    if (++count === 3) complete();
+  }
+
+  function complete () {
+    stderrChild.off('exit', exitHandler);
+    stderrChild.off('close', closeHandler);
+    done();
+  }
+};
+
+exports.testChildExecFileKillSignal = function (assert, done) {
+  getScript('wait').then(script => {
+    execFile(script, {
+      killSignal: 'beepbeep',
+      timeout: 10
+    }, function (err, stdout, stderr) {
+      assert.equal(err.signal, 'beepbeep', 'correctly used custom killSignal');
+      done();
+    });
+  });
+};
+
+exports.testChildProperties = function (assert, done) {
+  getScript('check-env').then(script => {
+    let child = spawn(script, {
+      env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' }
+    });
+
+    if (isWindows)
+      assert.ok(true, 'Windows environment does not have `pid`');
+    else
+      assert.ok(child.pid > 0, 'Child has a pid');
+    done();
+  });
+};
+
+exports.testChildStdinStreamLarge = function (assert, done) {
+  let REPEAT = 2000;
+  let allData = '';
+  // Use direct paths to more/cat, as we do not currently support calling non-files
+  // from subprocess.jsm
+  let child = spawn(CAT_PATH);
+
+  child.stdout.on('data', onData);
+  child.on('close', onClose);
+
+  for (let i = 0; i < REPEAT; i++)
+    emit(child.stdin, 'data', '12345\n');
+
+  emit(child.stdin, 'end');
+
+  function onData (data) {
+    allData += data;
+  }
+
+  function onClose (code, signal) {
+    child.stdout.off('data', onData);
+    child.off('close', onClose);
+    assert.equal(code, 0, 'exited succesfully');
+    assert.equal(signal, null, 'no kill signal given');
+    assert.equal(allData.replace(/\W/g, '').length, '12345'.length * REPEAT,
+      'all data processed from stdin');
+    done();
+  }
+};
+
+exports.testChildStdinStreamSmall = function (assert, done) {
+  let allData = '';
+  let child = spawn(CAT_PATH);
+  child.stdout.on('data', onData);
+  child.on('close', onClose);
+
+  emit(child.stdin, 'data', '12345');
+  emit(child.stdin, 'end');
+
+  function onData (data) {
+    allData += data;
+  }
+
+  function onClose (code, signal) {
+    child.stdout.off('data', onData);
+    child.off('close', onClose);
+    assert.equal(code, 0, 'exited succesfully');
+    assert.equal(signal, null, 'no kill signal given');
+    assert.equal(allData.trim(), '12345', 'all data processed from stdin');
+    done();
+  }
+};
+/*
+ * This tests failures when an error is thrown attempting to
+ * spawn the process, like an invalid command
+ */
+exports.testChildEventsSpawningError= function (assert, done) {
+  let handlersCalled = 0;
+  let child = execFile('i-do-not-exist', (err, stdout, stderr) => {
+    assert.ok(err, 'error was passed into callback');
+    assert.equal(stdout, '', 'stdout is empty')
+    assert.equal(stderr, '', 'stderr is empty');
+    if (++handlersCalled === 3) complete();
+  });
+
+  child.on('error', handleError);
+  child.on('exit', handleExit);
+  child.on('close', handleClose);
+
+  function handleError (e) {
+    assert.ok(e, 'error passed into error handler');
+    if (++handlersCalled === 3) complete();
+  }
+
+  function handleClose (code, signal) {
+    assert.equal(code, -1,
+      'process was never spawned, therefore exit code is -1');
+    assert.equal(signal, null, 'signal should be null');
+    if (++handlersCalled === 3) complete();
+  }
+
+  function handleExit (code, signal) {
+    assert.fail('Close event should not be called on init failure');
+  }
+
+  function complete () {
+    child.off('error', handleError);
+    child.off('exit', handleExit);
+    child.off('close', handleClose);
+    done();
+  }
+};
+
+exports.testSpawnOptions = function (assert, done) {
+  let count = 0;
+  let envStdout = '';
+  let cwdStdout = '';
+  let checkEnv, checkPwd, envChild, cwdChild;
+  getScript('check-env').then(script => {
+    checkEnv = script;
+    return getScript('check-pwd');
+  }).then(script => {
+    checkPwd = script;
+
+    envChild = spawn(checkEnv, {
+      env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' }
+    });
+    cwdChild = spawn(checkPwd, { cwd: PROFILE_DIR });
+
+    // Do these need to be unbound?
+    envChild.stdout.on('data', data => envStdout += data);
+    cwdChild.stdout.on('data', data => cwdStdout += data);
+
+    envChild.on('close', envClose);
+    cwdChild.on('close', cwdClose);
+  });
+
+  function envClose () {
+    assert.equal(envStdout.trim(), 'my-value-test', 'spawn correctly passed in ENV');
+    if (++count === 2) complete();
+  }
+
+  function cwdClose () {
+    // Check for PROFILE_DIR in the output because
+    // some systems resolve symbolic links, and on OSX
+    // /var -> /private/var
+    let isCorrectPath = ~cwdStdout.trim().indexOf(PROFILE_DIR);
+    assert.ok(isCorrectPath, 'spawn correctly passed in cwd');
+    if (++count === 2) complete();
+  }
+
+  function complete () {
+    envChild.off('close', envClose);
+    cwdChild.off('close', cwdClose);
+    done();
+  }
+};
+
+exports.testFork = function (assert) {
+  assert.throws(function () {
+    fork();
+  }, /not currently supported/, 'fork() correctly throws an unsupported error');
+};
+
+after(exports, cleanUp);
+
+require("test").run(exports);
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -245,17 +245,17 @@ exports["test:post-json-values-only"] = 
       assert.equal(message[3], DEFAULT_CONTENT_URL,
                        "jsonable attributes are accessible");
       // See bug 714891, Arrays may be broken over compartements:
       assert.ok(message[4], "Array keeps being an array");
       assert.equal(message[5], JSON.stringify(array),
                        "Array is correctly serialized");
       done();
     });
-    // Add a new url property sa the Class function used by 
+    // Add a new url property sa the Class function used by
     // Worker doesn't set enumerables to true for non-functions
     worker._url = DEFAULT_CONTENT_URL;
 
     worker.postMessage({ fun: function () {}, w: worker, array: array });
   }
 );
 
 exports["test:emit-json-values-only"] = WorkerTest(
@@ -297,17 +297,17 @@ exports["test:emit-json-values-only"] = 
                        "Array is correctly serialized");
       done();
     });
 
     let obj = {
       fun: function () {},
       dom: browser.contentWindow.document.createElement("div")
     };
-    // Add a new url property sa the Class function used by 
+    // Add a new url property sa the Class function used by
     // Worker doesn't set enumerables to true for non-functions
     worker._url = DEFAULT_CONTENT_URL;
     worker.port.emit("addon-to-content", function () {}, worker, obj, array);
   }
 );
 
 exports["test:content is wrapped"] = WorkerTest(
   "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
@@ -415,22 +415,22 @@ exports["test:setTimeout works with stri
   function(assert, browser, done) {
     let worker = Worker({
       window: browser.contentWindow,
       contentScript: "new " + function ContentScriptScope() {
         // must use "window.scVal" instead of "var csVal"
         // since we are inside ContentScriptScope function.
         // i'm NOT putting code-in-string inside code-in-string </YO DAWG>
         window.csVal = 13;
-        setTimeout("self.postMessage([" + 
-                      "csVal, " + 
-                      "window.docVal, " + 
-                      "'ContentWorker' in window, " + 
-                      "'UNWRAP_ACCESS_KEY' in window, " + 
-                      "'getProxyForObject' in window, " + 
+        setTimeout("self.postMessage([" +
+                      "csVal, " +
+                      "window.docVal, " +
+                      "'ContentWorker' in window, " +
+                      "'UNWRAP_ACCESS_KEY' in window, " +
+                      "'getProxyForObject' in window, " +
                     "])", 1);
       },
       contentScriptWhen: "ready",
       onMessage: function([csVal, docVal, chrome1, chrome2, chrome3]) {
         // test timer code is executed in the correct context
         assert.equal(csVal, 13, "accessing content-script values");
         assert.notEqual(docVal, 5, "can't access document values (directly)");
         assert.ok(!chrome1 && !chrome2 && !chrome3, "nothing is leaked from chrome");
@@ -462,61 +462,61 @@ exports["test:setInterval async Errors p
   function(assert, browser, done) {
     let count = 0;
     let worker = Worker({
       window: browser.contentWindow,
       contentScript: "setInterval(() => { throw Error('ubik') }, 50)",
       contentScriptWhen: "ready",
       onError: function(err) {
         count++;
-        assert.equal(err.message, "ubik", 
+        assert.equal(err.message, "ubik",
             "error (corectly) propagated  " + count + " time(s)");
         if (count >= 3) done();
       }
     });
   }
 );
 
 exports["test:setTimeout throws array, passed to .onError"] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
     let worker = Worker({
       window: browser.contentWindow,
       contentScript: "setTimeout(function() { throw ['array', 42] }, 1)",
       contentScriptWhen: "ready",
       onError: function(arr) {
-        assert.ok(isArray(arr), 
+        assert.ok(isArray(arr),
             "the type of thrown/propagated object is array");
-        assert.ok(arr.length==2, 
+        assert.ok(arr.length==2,
             "the propagated thrown array is the right length");
-        assert.equal(arr[1], 42, 
+        assert.equal(arr[1], 42,
             "element inside the thrown array correctly propagated");
         done();
       }
     });
   }
 );
 
 exports["test:setTimeout string arg with SyntaxError to .onError"] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
     let worker = Worker({
       window: browser.contentWindow,
       contentScript: "setTimeout('syntax 123 error', 1)",
       contentScriptWhen: "ready",
       onError: function(err) {
-        assert.equal(err.name, "SyntaxError", 
+        assert.equal(err.name, "SyntaxError",
             "received SyntaxError thrown from bad code in string argument to setTimeout");
-        assert.ok('fileName' in err, 
+        assert.ok('fileName' in err,
             "propagated SyntaxError contains a fileName property");
-        assert.ok('stack' in err, 
+        assert.ok('stack' in err,
             "propagated SyntaxError contains a stack property");
-        assert.equal(err.message, "missing ; before statement", 
+        assert.equal(err.message, "missing ; before statement",
             "propagated SyntaxError has the correct (helpful) message");
-        assert.equal(err.lineNumber, 1, 
+        assert.equal(err.lineNumber, 1,
             "propagated SyntaxError was thrown on the right lineNumber");
         done();
       }
     });
   }
 );
 
 exports["test:setTimeout can't be cancelled by content"] = WorkerTest(
@@ -826,50 +826,87 @@ exports['test:conentScriptFile as URL in
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
 
     let url = new URL(fixtures.url("test-contentScriptFile.js"));
     let worker =  Worker({
       window: browser.contentWindow,
       contentScriptFile: url,
       onMessage: function(msg) {
-        assert.equal(msg, "msg from contentScriptFile", 
+        assert.equal(msg, "msg from contentScriptFile",
             "received a wrong message from contentScriptFile");
         done();
       }
     });
   }
 );
 
-exports.testWorkerEvents = WorkerTest(DEFAULT_CONTENT_URL, function (assert, browser, done) {
-  let window = browser.contentWindow;
-  let events = [];
-  let worker = Worker({
-    window: window,
-    contentScript: 'new ' + function WorkerScope() {
-      self.postMessage('start');
-    },
-    onAttach: win => {
-      events.push('attach');
-      assert.pass('attach event called when attached');
-      assert.equal(window, win, 'attach event passes in attached window');
-    },
-    onError: err => {
-      assert.equal(err.message, 'Custom',
-        'Error passed into error event');
-      worker.detach();
-    },
-    onMessage: msg => {
-      assert.pass('`onMessage` handles postMessage')
-      throw new Error('Custom');
-    },
-    onDetach: _ => {
-      assert.pass('`onDetach` called when worker detached');
-      done();
-    }
-  });
-  // `attach` event is called synchronously during instantiation,
-  // so we can't listen to that, TODO FIX?
-  //  worker.on('attach', obj => console.log('attach', obj));
-});
+exports["test:worker events"] = WorkerTest(
+  DEFAULT_CONTENT_URL,
+  function (assert, browser, done) {
+    let window = browser.contentWindow;
+    let events = [];
+    let worker = Worker({
+      window: window,
+      contentScript: 'new ' + function WorkerScope() {
+        self.postMessage('start');
+      },
+      onAttach: win => {
+        events.push('attach');
+        assert.pass('attach event called when attached');
+        assert.equal(window, win, 'attach event passes in attached window');
+      },
+      onError: err => {
+        assert.equal(err.message, 'Custom',
+          'Error passed into error event');
+        worker.detach();
+      },
+      onMessage: msg => {
+        assert.pass('`onMessage` handles postMessage')
+        throw new Error('Custom');
+      },
+      onDetach: _ => {
+        assert.pass('`onDetach` called when worker detached');
+        done();
+      }
+    });
+    // `attach` event is called synchronously during instantiation,
+    // so we can't listen to that, TODO FIX?
+    //  worker.on('attach', obj => console.log('attach', obj));
+  }
+);
 
+exports["test:console method log functions properly"] = WorkerTest(
+  DEFAULT_CONTENT_URL,
+  function(assert, browser, done) {
+    let logs = [];
+
+    let clean = message =>
+          message.trim().
+          replace(/[\r\n]/g, " ").
+          replace(/ +/g, " ");
+
+    let onMessage = (type, message) => logs.push(clean(message));
+    let { loader } = LoaderWithHookedConsole(module, onMessage);
+
+    let worker =  loader.require("sdk/content/worker").Worker({
+      window: browser.contentWindow,
+      contentScript: "new " + function WorkerScope() {
+        console.log(Function);
+        console.log((foo) => foo * foo);
+        console.log(function foo(bar) { return bar + bar });
+
+        self.postMessage();
+      },
+      onMessage: () => {
+        assert.deepEqual(logs, [
+          "function Function() { [native code] }",
+          "(foo) => foo * foo",
+          "function foo(bar) { \"use strict\"; return bar + bar }"
+        ]);
+
+        done();
+      }
+    });
+  }
+);
 
 require("test").run(exports);
--- a/addon-sdk/source/test/test-functional.js
+++ b/addon-sdk/source/test/test-functional.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { setTimeout } = require('sdk/timers');
 const utils = require('sdk/lang/functional');
 const { invoke, defer, partial, compose, memoize, once, is, isnt,
-        delay, wrap, curry, chainable, field, query, isInstance } = utils;
+        delay, wrap, curry, chainable, field, query, isInstance, debounce } = utils;
 const { LoaderWithHookedConsole } = require('sdk/test/loader');
 
 exports['test forwardApply'] = function(assert) {
   function sum(b, c) { return this.a + b + c; }
   assert.equal(invoke(sum, [2, 3], { a: 1 }), 6,
                'passed arguments and pseoude-variable are used');
 
   assert.equal(invoke(sum.bind({ a: 2 }), [2, 3], { a: 1 }), 7,
@@ -414,9 +414,25 @@ exports["test isnt"] = assert => {
                    "is can be partially applied");
 
   assert.ok(!isnt(1, 1));
   assert.ok(isnt({}, {}));
   assert.ok(!isnt()(1)()(1));
   assert.ok(isnt()(1)()(2));
 };
 
+exports["test debounce"] = (assert, done) => {
+  let counter = 0;
+  let fn = debounce(() => counter++, 100);
+
+  new Array(10).join(0).split("").forEach(fn);
+
+  assert.equal(counter, 0, "debounce does not fire immediately");
+  setTimeout(() => {
+    assert.equal(counter, 1, "function called after wait time");
+    fn();
+    setTimeout(() => {
+      assert.equal(counter, 2, "function able to be called again after wait");
+      done();
+    }, 150);
+  }, 200);
+};
 require('test').run(exports);
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -1,18 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { PageMod } = require("sdk/page-mod");
-const testPageMod = require("./pagemod-test-helpers").testPageMod;
+const { testPageMod, handleReadyState } = require("./pagemod-test-helpers");
 const { Loader } = require('sdk/test/loader');
 const tabs = require("sdk/tabs");
-const timer = require("sdk/timers");
+const { setTimeout } = require("sdk/timers");
 const { Cc, Ci, Cu } = require("chrome");
 const { open, getFrames, getMostRecentBrowserWindow } = require('sdk/window/utils');
 const windowUtils = require('sdk/deprecated/window-utils');
 const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab } = require('sdk/tabs/utils');
 const xulApp = require("sdk/system/xul-app");
 const { isPrivateBrowsingSupported } = require('sdk/self');
 const { isPrivate } = require('sdk/private-browsing');
 const { openWebpage } = require('./private-browsing/helper');
@@ -132,22 +132,57 @@ exports.testPageModIncludes = function(a
               fn(assert, win);
             });
             done();
           });
     }
     );
 };
 
-exports.testPageModErrorHandling = function(assert) {
-  assert.throws(function() {
-      new PageMod();
-    },
-    /The `include` option must always contain atleast one rule/,
-    "PageMod() throws when 'include' option is not specified.");
+exports.testPageModValidationAttachTo = function(assert) {
+  [{ val: 'top', type: 'string "top"' },
+   { val: 'frame', type: 'string "frame"' },
+   { val: ['top', 'existing'], type: 'array with "top" and "existing"' },
+   { val: ['frame', 'existing'], type: 'array with "frame" and "existing"' },
+   { val: ['top'], type: 'array with "top"' },
+   { val: ['frame'], type: 'array with "frame"' },
+   { val: undefined, type: 'undefined' }].forEach((attachTo) => {
+    new PageMod({ attachTo: attachTo.val, include: '*.validation111' });
+    assert.pass("PageMod() does not throw when attachTo is " + attachTo.type);
+  });
+
+  [{ val: 'existing', type: 'string "existing"' },
+   { val: ['existing'], type: 'array with "existing"' },
+   { val: 'not-legit', type: 'string with "not-legit"' },
+   { val: ['not-legit'], type: 'array with "not-legit"' },
+   { val: {}, type: 'object' }].forEach((attachTo) => {
+    assert.throws(() =>
+      new PageMod({ attachTo: attachTo.val, include: '*.validation111' }),
+      /The `attachTo` option/,
+      "PageMod() throws when 'attachTo' option is " + attachTo.type + ".");
+  });
+};
+
+exports.testPageModValidationInclude = function(assert) {
+  [{ val: undefined, type: 'undefined' },
+   { val: {}, type: 'object' },
+   { val: [], type: 'empty array'},
+   { val: [/regexp/, 1], type: 'array with non string/regexp' },
+   { val: 1, type: 'number' }].forEach((include) => {
+    assert.throws(() => new PageMod({ include: include.val }),
+      /The `include` option must always contain atleast one rule/,
+      "PageMod() throws when 'include' option is " + include.type + ".");
+  });
+
+  [{ val: '*.validation111', type: 'string' },
+   { val: /validation111/, type: 'regexp' },
+   { val: ['*.validation111'], type: 'array with length > 0'}].forEach((include) => {
+    new PageMod({ include: include.val });
+    assert.pass("PageMod() does not throw when include option is " + include.type);
+  });
 };
 
 /* Tests for internal functions. */
 exports.testCommunication1 = function(assert, done) {
   let workerDone = false,
       callbackDone = null;
 
   testPageMod(assert, done, "about:", [{
@@ -407,17 +442,17 @@ exports.testWorksWithExistingTabs = func
     onReady: function onReady(tab) {
       let pageModOnExisting = new PageMod({
         include: url,
         attachTo: ["existing", "top", "frame"],
         onAttach: function(worker) {
           assert.ok(!!worker.tab, "Worker.tab exists");
           assert.equal(tab, worker.tab, "A worker has been created on this existing tab");
 
-          timer.setTimeout(function() {
+          setTimeout(function() {
             pageModOnExisting.destroy();
             pageModOffExisting.destroy();
             tab.close(done);
           }, 0);
         }
       });
 
       let pageModOffExisting = new PageMod({
@@ -439,17 +474,17 @@ exports.testExistingFrameDoesntMatchIncl
     onReady: function onReady(tab) {
       let pagemod = new PageMod({
         include: url,
         attachTo: ['existing', 'frame'],
         onAttach: function() {
           assert.fail("Existing iframe URL doesn't match include, must not attach to anything");
         }
       });
-      timer.setTimeout(function() {
+      setTimeout(function() {
         assert.pass("PageMod didn't attach to anything")
         pagemod.destroy();
         tab.close(done);
       }, 250);
     }
   });
 };
 
@@ -459,26 +494,205 @@ exports.testExistingOnlyFrameMatchesIncl
   let url = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframe);
   tabs.open({
     url: url,
     onReady: function onReady(tab) {
       let pagemod = new PageMod({
         include: iframeURL,
         attachTo: ['existing', 'frame'],
         onAttach: function(worker) {
-          assert.equal(iframeURL, worker.url, 
+          assert.equal(iframeURL, worker.url,
               "PageMod attached to existing iframe when only it matches include rules");
           pagemod.destroy();
           tab.close(done);
         }
       });
     }
   });
 };
 
+exports.testContentScriptWhenDefault = function(assert) {
+  let pagemod = PageMod({include: '*'});
+
+  assert.equal(pagemod.contentScriptWhen, 'end', "Default contentScriptWhen is 'end'");
+  pagemod.destroy();
+}
+
+// test timing for all 3 contentScriptWhen options (start, ready, end)
+// for new pages, or tabs opened after PageMod is created
+exports.testContentScriptWhenForNewTabs = function(assert, done) {
+  const url = "data:text/html;charset=utf-8,testContentScriptWhenForNewTabs";
+
+  let count = 0;
+
+  handleReadyState(url, 'start', {
+    onLoading: (tab) => {
+      assert.pass("PageMod is attached while document is loading");
+      if (++count === 3) 
+        tab.close(done);
+    },
+    onInteractive: () => assert.fail("onInteractive should not be called with 'start'."),
+    onComplete: () => assert.fail("onComplete should not be called with 'start'."),
+  });
+
+  handleReadyState(url, 'ready', {
+    onInteractive: (tab) => {
+      assert.pass("PageMod is attached while document is interactive");
+      if (++count === 3) 
+        tab.close(done);
+    },
+    onLoading: () => assert.fail("onLoading should not be called with 'ready'."),
+    onComplete: () => assert.fail("onComplete should not be called with 'ready'."),
+  });
+
+  handleReadyState(url, 'end', {
+    onComplete: (tab) => {
+      assert.pass("PageMod is attached when document is complete");
+      if (++count === 3) 
+        tab.close(done);
+    },
+    onLoading: () => assert.fail("onLoading should not be called with 'end'."),
+    onInteractive: () => assert.fail("onInteractive should not be called with 'end'."),
+  });
+
+  tabs.open(url);
+}
+
+// test timing for all 3 contentScriptWhen options (start, ready, end)
+// for PageMods created right as the tab is created (in tab.onOpen)
+exports.testContentScriptWhenOnTabOpen = function(assert, done) {
+  const url = "data:text/html;charset=utf-8,testContentScriptWhenOnTabOpen";
+
+  tabs.open({
+    url: url,
+    onOpen: function(tab) {
+      let count = 0;
+
+      handleReadyState(url, 'start', {
+        onLoading: () => {
+          assert.pass("PageMod is attached while document is loading");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onInteractive: () => assert.fail("onInteractive should not be called with 'start'."),
+        onComplete: () => assert.fail("onComplete should not be called with 'start'."),
+      });
+
+      handleReadyState(url, 'ready', {
+        onInteractive: () => {
+          assert.pass("PageMod is attached while document is interactive");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'ready'."),
+        onComplete: () => assert.fail("onComplete should not be called with 'ready'."),
+      });
+
+      handleReadyState(url, 'end', {
+        onComplete: () => {
+          assert.pass("PageMod is attached when document is complete");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'end'."),
+        onInteractive: () => assert.fail("onInteractive should not be called with 'end'."),
+      });
+
+    }
+  });
+}
+
+// test timing for all 3 contentScriptWhen options (start, ready, end)
+// for PageMods created while the tab is interactive (in tab.onReady)
+exports.testContentScriptWhenOnTabReady = function(assert, done) {
+  const url = "data:text/html;charset=utf-8,testContentScriptWhenOnTabReady";
+
+  tabs.open({
+    url: url,
+    onReady: function(tab) {
+      let count = 0;
+
+      handleReadyState(url, 'start', {
+        onInteractive: () => {
+          assert.pass("PageMod is attached while document is interactive");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'start'."),
+        onComplete: () => assert.fail("onComplete should not be called with 'start'."),
+      });
+
+      handleReadyState(url, 'ready', {
+        onInteractive: () => {
+          assert.pass("PageMod is attached while document is interactive");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'ready'."),
+        onComplete: () => assert.fail("onComplete should not be called with 'ready'."),
+      });
+
+      handleReadyState(url, 'end', {
+        onComplete: () => {
+          assert.pass("PageMod is attached when document is complete");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'end'."),
+        onInteractive: () => assert.fail("onInteractive should not be called with 'end'."),
+      });
+
+    }
+  });
+}
+
+// test timing for all 3 contentScriptWhen options (start, ready, end)
+// for PageMods created after a tab has completed loading (in tab.onLoad)
+exports.testContentScriptWhenOnTabLoad = function(assert, done) {
+  const url = "data:text/html;charset=utf-8,testContentScriptWhenOnTabLoad";
+
+  tabs.open({
+    url: url,
+    onLoad: function(tab) {
+      let count = 0;
+
+      handleReadyState(url, 'start', {
+        onComplete: () => {
+          assert.pass("PageMod is attached when document is complete");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'start'."),
+        onInteractive: () => assert.fail("onInteractive should not be called with 'start'."),
+      });
+
+      handleReadyState(url, 'ready', {
+        onComplete: () => {
+          assert.pass("PageMod is attached when document is complete");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'ready'."),
+        onInteractive: () => assert.fail("onInteractive should not be called with 'ready'."),
+      });
+
+      handleReadyState(url, 'end', {
+        onComplete: () => {
+          assert.pass("PageMod is attached when document is complete");
+          if (++count === 3) 
+            tab.close(done);
+        },
+        onLoading: () => assert.fail("onLoading should not be called with 'end'."),
+        onInteractive: () => assert.fail("onInteractive should not be called with 'end'."),
+      });
+
+    }
+  });
+}
+
 exports.testTabWorkerOnMessage = function(assert, done) {
   let { browserWindows } = require("sdk/windows");
   let tabs = require("sdk/tabs");
   let { PageMod } = require("sdk/page-mod");
 
   let url1 = "data:text/html;charset=utf-8,<title>tab1</title><h1>worker1.tab</h1>";
   let url2 = "data:text/html;charset=utf-8,<title>tab2</title><h1>worker2.tab</h1>";
   let worker1 = null;
@@ -564,17 +778,16 @@ exports.testAttachToTabsOnly = function(
       }
       else {
         assert.fail('page-mod attached to a non-tab document');
       }
     }
   });
 
   function openHiddenFrame() {
-    console.info('Open iframe in hidden window');
     let hiddenFrames = require('sdk/frame/hidden-frame');
     let hiddenFrame = hiddenFrames.add(hiddenFrames.HiddenFrame({
       onReady: function () {
         let element = this.element;
         element.addEventListener('DOMContentLoaded', function onload() {
           element.removeEventListener('DOMContentLoaded', onload, false);
           hiddenFrames.remove(hiddenFrame);
 
@@ -586,43 +799,40 @@ exports.testAttachToTabsOnly = function(
           }
         }, false);
         element.setAttribute('src', 'data:text/html;charset=utf-8,foo');
       }
     }));
   }
 
   function openToplevelWindow() {
-    console.info('Open toplevel window');
     let win = open('data:text/html;charset=utf-8,bar');
     win.addEventListener('DOMContentLoaded', function onload() {
       win.removeEventListener('DOMContentLoaded', onload, false);
       win.close();
       openBrowserIframe();
     }, false);
   }
 
   function openBrowserIframe() {
-    console.info('Open iframe in browser window');
     let window = require('sdk/deprecated/window-utils').activeBrowserWindow;
     let document = window.document;
     let iframe = document.createElement('iframe');
     iframe.setAttribute('type', 'content');
     iframe.setAttribute('src', 'data:text/html;charset=utf-8,foobar');
     iframe.addEventListener('DOMContentLoaded', function onload() {
       iframe.removeEventListener('DOMContentLoaded', onload, false);
       iframe.parentNode.removeChild(iframe);
       openTabWithIframes();
     }, false);
     document.documentElement.appendChild(iframe);
   }
 
   // Only these three documents will be accepted by the page-mod
   function openTabWithIframes() {
-    console.info('Open iframes in a tab');
     let subContent = '<iframe src="data:text/html;charset=utf-8,sub frame" />'
     let content = '<iframe src="data:text/html;charset=utf-8,' +
                   encodeURIComponent(subContent) + '" />';
     require('sdk/tabs').open({
       url: 'data:text/html;charset=utf-8,' + encodeURIComponent(content),
       onOpen: function onOpen(tab) {
         openedTab = tab;
       }
@@ -990,17 +1200,17 @@ exports.testExistingOnFrames = function(
           assert.equal(iFrameURL, worker.url, '1st attach is for top frame');
         }
         else if (counter > 2) {
           assert.fail('applied page mod too many times');
         }
         else {
           assert.equal(subFrameURL, worker.url, '2nd attach is for sub frame');
           // need timeout because onAttach is called before the constructor returns
-          timer.setTimeout(function() {
+          setTimeout(function() {
             pagemodOnExisting.destroy();
             pagemodOffExisting.destroy();
             closeTab(tab);
             done();
           }, 0);
         }
       }
     });
@@ -1147,22 +1357,21 @@ exports["test page-mod on private tab in
 
 // Bug 699450: Calling worker.tab.close() should not lead to exception
 exports.testWorkerTabClose = function(assert, done) {
   let callbackDone;
   testPageMod(assert, done, "about:", [{
       include: "about:",
       contentScript: '',
       onAttach: function(worker) {
-        console.log("call close");
         worker.tab.close(function () {
           // On Fennec, tab is completely destroyed right after close event is
           // dispatch, so we need to wait for the next event loop cycle to
           // check for tab nulliness.
-          timer.setTimeout(function () {
+          setTimeout(function () {
             assert.ok(!worker.tab,
                         "worker.tab should be null right after tab.close()");
             callbackDone();
           }, 0);
         });
       }
     }],
     function(win, done) {
@@ -1194,9 +1403,109 @@ exports.testDebugMetadata = function(ass
           return false;
         }
       }), "one of the globals is a content script");
       done();
     }
   );
 };
 
+exports.testDetachOnDestroy = function(assert, done) {
+  let tab;
+  const TEST_URL = 'data:text/html;charset=utf-8,detach';
+  const loader = Loader(module);
+  const { PageMod } = loader.require('sdk/page-mod');
+
+  let mod1 = PageMod({
+    include: TEST_URL,
+    contentScript: Isolate(function() {
+      self.port.on('detach', function(reason) {
+        window.document.body.innerHTML += '!' + reason;
+      });
+    }),
+    onAttach: worker => {
+      assert.pass('attach[1] happened');
+
+      worker.on('detach', _ => setTimeout(_ => {
+        assert.pass('detach happened');
+
+        let mod2 = PageMod({
+          attachTo: [ 'existing', 'top' ],
+          include: TEST_URL,
+          contentScript: Isolate(function() {
+            self.port.on('test', _ => {
+              self.port.emit('result', { result: window.document.body.innerHTML});
+            });
+          }),
+          onAttach: worker => {
+            assert.pass('attach[2] happened');
+            worker.port.once('result', ({ result }) => {
+              assert.equal(result, 'detach!', 'the body.innerHTML is as expected');
+              mod1.destroy();
+              mod2.destroy();
+              loader.unload();
+              tab.close(done);
+            });
+            worker.port.emit('test');
+          }
+        });
+      }));
+
+      worker.destroy();
+    }
+  });
+
+  tabs.open({
+    url: TEST_URL,
+    onOpen: t => tab = t
+  })
+}
+
+exports.testDetachOnUnload = function(assert, done) {
+  let tab;
+  const TEST_URL = 'data:text/html;charset=utf-8,detach';
+  const loader = Loader(module);
+  const { PageMod } = loader.require('sdk/page-mod');
+
+  let mod1 = PageMod({
+    include: TEST_URL,
+    contentScript: Isolate(function() {
+      self.port.on('detach', function(reason) {
+        window.document.body.innerHTML += '!' + reason;
+      });
+    }),
+    onAttach: worker => {
+      assert.pass('attach[1] happened');
+
+      worker.on('detach', _ => setTimeout(_ => {
+        assert.pass('detach happened');
+
+        let mod2 = require('sdk/page-mod').PageMod({
+          attachTo: [ 'existing', 'top' ],
+          include: TEST_URL,
+          contentScript: Isolate(function() {
+            self.port.on('test', _ => {
+              self.port.emit('result', { result: window.document.body.innerHTML});
+            });
+          }),
+          onAttach: worker => {
+            assert.pass('attach[2] happened');
+            worker.port.once('result', ({ result }) => {
+              assert.equal(result, 'detach!shutdown', 'the body.innerHTML is as expected');
+              mod2.destroy();
+              tab.close(done);
+            });
+            worker.port.emit('test');
+          }
+        });
+      }));
+
+      loader.unload('shutdown');
+    }
+  });
+
+  tabs.open({
+    url: TEST_URL,
+    onOpen: t => tab = t
+  })
+}
+
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -342,21 +342,21 @@ exports.testRedirectIncludeArrays = func
       '})();',
     include: ['about:blank', 'data:*']
   });
 
   page.port.on('load', function (url) {
     if (url === firstURL) {
       page.port.emit('redirect', 'about:blank');
     } else if (url === 'about:blank') {
-      page.port.emit('redirect', 'about:home');
+      page.port.emit('redirect', 'about:mozilla');
       assert.ok('`include` property handles arrays');
       assert.equal(url, 'about:blank', 'Redirects work with accepted domains');
       done();
-    } else if (url === 'about:home') {
+    } else if (url === 'about:mozilla') {
       assert.fail('Should not redirect to restricted domain');
     }
   });
 };
 
 exports.testRedirectFromWorker = function (assert, done) {
   let firstURL = 'data:text/html;charset=utf-8,first-page';
   let secondURL = 'data:text/html;charset=utf-8,second-page';
@@ -373,17 +373,17 @@ exports.testRedirectFromWorker = functio
   });
 
   page.port.on('load', function (url) {
     if (url === firstURL) {
       page.port.emit('redirect', secondURL);
     } else if (url === secondURL) {
       page.port.emit('redirect', thirdURL);
     } else if (url === thirdURL) {
-      page.port.emit('redirect', 'about:home');
+      page.port.emit('redirect', 'about:mozilla');
       assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
       done();
     } else {
       assert.fail('Should not redirect to unauthorized domains');
     }
   });
 };
 
@@ -400,17 +400,17 @@ exports.testRedirectWithContentURL = fun
   });
 
   page.port.on('load', function (url) {
     if (url === firstURL) {
       page.contentURL = secondURL;
     } else if (url === secondURL) {
       page.contentURL = thirdURL;
     } else if (url === thirdURL) {
-      page.contentURL = 'about:home';
+      page.contentURL = 'about:mozilla';
       assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
       done();
     } else {
       assert.fail('Should not redirect to unauthorized domains');
     }
   });
 };
 
--- a/addon-sdk/source/test/test-private-browsing.js
+++ b/addon-sdk/source/test/test-private-browsing.js
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
-const { Ci } = require('chrome');
+const { Ci, Cu } = require('chrome');
 const { safeMerge } = require('sdk/util/object');
 const windows = require('sdk/windows').browserWindows;
 const tabs = require('sdk/tabs');
 const winUtils = require('sdk/window/utils');
 const { isWindowPrivate } = winUtils;
 const { isPrivateBrowsingSupported } = require('sdk/self');
 const { is } = require('sdk/system/xul-app');
 const { isPrivate } = require('sdk/private-browsing');
@@ -16,16 +16,18 @@ const { getOwnerWindow } = require('sdk/
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 const { getMode, isGlobalPBSupported,
         isWindowPBSupported, isTabPBSupported } = require('sdk/private-browsing/utils');
 const { pb } = require('./private-browsing/helper');
 const prefs = require('sdk/preferences/service');
 const { set: setPref } = require("sdk/preferences/service");
 const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
 
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
 const kAutoStartPref = "browser.privatebrowsing.autostart";
 
 // is global pb is enabled?
 if (isGlobalPBSupported) {
   safeMerge(module.exports, require('./private-browsing/global'));
 
   exports.testGlobalOnlyOnFirefox = function(assert) {
     assert.ok(is("Firefox"), "isGlobalPBSupported is only true on Firefox");
@@ -88,33 +90,41 @@ exports.testGetOwnerWindow = function(as
   assert.ok(chromeWindow instanceof Ci.nsIDOMWindow, 'associated window is found');
 
   tabs.open({
     url: 'about:blank',
     isPrivate: true,
     onOpen: function(tab) {
       // test that getOwnerWindow works as expected
       if (is('Fennec')) {
-        assert.notStrictEqual(chromeWindow, getOwnerWindow(tab)); 
-        assert.ok(getOwnerWindow(tab) instanceof Ci.nsIDOMWindow); 
+        assert.notStrictEqual(chromeWindow, getOwnerWindow(tab));
+        assert.ok(getOwnerWindow(tab) instanceof Ci.nsIDOMWindow);
       }
       else {
         assert.strictEqual(chromeWindow, getOwnerWindow(tab), 'associated window is the same for window and window\'s tab');
       }
 
       // test that the tab is not private
       // private flag should be ignored by default
       assert.ok(!isPrivate(tab));
       assert.ok(!isPrivate(getOwnerWindow(tab)));
 
       tab.close(done);
     }
   });
 };
 
+exports.testNSIPrivateBrowsingChannel = function(assert) {
+  let channel = Services.io.newChannel("about:blank", null, null);
+  channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+  assert.equal(isPrivate(channel), false, 'isPrivate detects non-private channels');
+  channel.setPrivate(true);
+  assert.ok(isPrivate(channel), 'isPrivate detects private channels');
+}
+
 exports.testNewGlobalPBService = function(assert) {
   assert.equal(isPrivate(), false, 'isPrivate() is false by default');
   prefs.set(kAutoStartPref, true);
   assert.equal(prefs.get(kAutoStartPref, false), true, kAutoStartPref + ' is true now');
   assert.equal(isPrivate(), true, 'isPrivate() is true now');
   prefs.set(kAutoStartPref, false);
   assert.equal(isPrivate(), false, 'isPrivate() is false again');
 };
--- a/addon-sdk/source/test/test-ui-action-button.js
+++ b/addon-sdk/source/test/test-ui-action-button.js
@@ -10,16 +10,21 @@ module.metadata = {
 };
 
 const { Cu } = require('chrome');
 const { Loader } = require('sdk/test/loader');
 const { data } = require('sdk/self');
 const { open, focus, close } = require('sdk/window/helpers');
 const { setTimeout } = require('sdk/timers');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { partial } = require('sdk/lang/functional');
+
+const openBrowserWindow = partial(open, null, {features: {toolbar: true}});
+const openPrivateBrowserWindow = partial(open, null,
+  {features: {toolbar: true, private: true}});
 
 function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
   const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
   const { AREA_NAVBAR } = CustomizableUI;
 
   let widgets = CustomizableUI.getWidgetIdsInArea(AREA_NAVBAR).
     filter((id) => id.startsWith('action-button--') && id.endsWith(buttonId));
 
@@ -290,17 +295,17 @@ exports['test button global state update
   let button = ActionButton({
     id: 'my-button-5',
     label: 'my button',
     icon: './icon.png'
   });
 
   let nodes = [getWidget(button.id).node];
 
-  open(null, { features: { toolbar: true }}).then(window => {
+  openBrowserWindow().then(window => {
     nodes.push(getWidget(button.id, window).node);
 
     button.label = 'New label';
     button.icon = './new-icon.png';
     button.disabled = true;
 
     for (let node of nodes) {
       assert.equal(node.getAttribute('label'), 'New label',
@@ -335,17 +340,17 @@ exports['test button window state'] = fu
     id: 'my-button-6',
     label: 'my button',
     icon: './icon.png'
   });
 
   let mainWindow = browserWindows.activeWindow;
   let nodes = [getWidget(button.id).node];
 
-  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+  openBrowserWindow().then(focus).then(window => {
     nodes.push(getWidget(button.id, window).node);
 
     let { activeWindow } = browserWindows;
 
     button.state(activeWindow, {
       label: 'New label',
       icon: './new-icon.png',
       disabled: true
@@ -571,17 +576,17 @@ exports['test button click'] = function(
     label: 'my button',
     icon: './icon.png',
     onClick: ({label}) => labels.push(label)
   });
 
   let mainWindow = browserWindows.activeWindow;
   let chromeWindow = getMostRecentBrowserWindow();
 
-  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+  openBrowserWindow().then(focus).then(window => {
     button.state(mainWindow, { label: 'nothing' });
     button.state(mainWindow.tabs.activeTab, { label: 'foo'})
     button.state(browserWindows.activeWindow, { label: 'bar' });
 
     button.click();
 
     focus(chromeWindow).then(() => {
       button.click();
@@ -723,17 +728,17 @@ exports['test button are not in private 
   let { browserWindows } = loader.require('sdk/windows');
 
   let button = ActionButton({
     id: 'my-button-13',
     label: 'my button',
     icon: './icon.png'
   });
 
-  open(null, { features: { toolbar: true, private: true }}).then(window => {
+  openPrivateBrowserWindow().then(window => {
     assert.ok(isPrivate(window),
       'the new window is private');
 
     let { node } = getWidget(button.id, window);
 
     assert.ok(!node || node.style.display === 'none',
       'the button is not added / is not visible on private window');
 
--- a/addon-sdk/source/test/test-ui-toggle-button.js
+++ b/addon-sdk/source/test/test-ui-toggle-button.js
@@ -10,16 +10,21 @@ module.metadata = {
 };
 
 const { Cu } = require('chrome');
 const { Loader } = require('sdk/test/loader');
 const { data } = require('sdk/self');
 const { open, focus, close } = require('sdk/window/helpers');
 const { setTimeout } = require('sdk/timers');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { partial } = require('sdk/lang/functional');
+
+const openBrowserWindow = partial(open, null, {features: {toolbar: true}});
+const openPrivateBrowserWindow = partial(open, null,
+  {features: {toolbar: true, private: true}});
 
 function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
   const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
   const { AREA_NAVBAR } = CustomizableUI;
 
   let widgets = CustomizableUI.getWidgetIdsInArea(AREA_NAVBAR).
     filter((id) => id.startsWith('toggle-button--') && id.endsWith(buttonId));
 
@@ -300,17 +305,17 @@ exports['test button global state update
   let button = ToggleButton({
     id: 'my-button-5',
     label: 'my button',
     icon: './icon.png'
   });
 
   let nodes = [getWidget(button.id).node];
 
-  open(null, { features: { toolbar: true }}).then(window => {
+  openBrowserWindow().then(window => {
     nodes.push(getWidget(button.id, window).node);
 
     button.label = 'New label';
     button.icon = './new-icon.png';
     button.disabled = true;
 
     for (let node of nodes) {
       assert.equal(node.getAttribute('label'), 'New label',
@@ -345,17 +350,17 @@ exports['test button window state'] = fu
     id: 'my-button-6',
     label: 'my button',
     icon: './icon.png'
   });
 
   let mainWindow = browserWindows.activeWindow;
   let nodes = [getWidget(button.id).node];
 
-  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+  openBrowserWindow().then(focus).then(window => {
     nodes.push(getWidget(button.id, window).node);
 
     let { activeWindow } = browserWindows;
 
     button.state(activeWindow, {
       label: 'New label',
       icon: './new-icon.png',
       disabled: true
@@ -581,17 +586,17 @@ exports['test button click'] = function(
     label: 'my button',
     icon: './icon.png',
     onClick: ({label}) => labels.push(label)
   });
 
   let mainWindow = browserWindows.activeWindow;
   let chromeWindow = getMostRecentBrowserWindow();
 
-  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+  openBrowserWindow().then(focus).then(window => {
     button.state(mainWindow, { label: 'nothing' });
     button.state(mainWindow.tabs.activeTab, { label: 'foo'})
     button.state(browserWindows.activeWindow, { label: 'bar' });
 
     button.click();
 
     focus(chromeWindow).then(() => {
       button.click();
@@ -732,17 +737,17 @@ exports['test button are not in private 
   let { browserWindows } = loader.require('sdk/windows');
 
   let button = ToggleButton({
     id: 'my-button-13',
     label: 'my button',
     icon: './icon.png'
   });
 
-  open(null, { features: { toolbar: true, private: true }}).then(window => {
+  openPrivateBrowserWindow().then(window => {
     assert.ok(isPrivate(window),
       'the new window is private');
 
     let { node } = getWidget(button.id, window);
 
     assert.ok(!node || node.style.display === 'none',
       'the button is not added / is not visible on private window');
 
@@ -882,17 +887,17 @@ exports['test button checked'] = functio
   let { node } = getWidget(button.id);
 
   assert.equal(node.getAttribute('type'), 'checkbox',
     'node type is properly set');
 
   let mainWindow = browserWindows.activeWindow;
   let chromeWindow = getMostRecentBrowserWindow();
 
-  open(null, { features: { toolbar: true }}).then(focus).then(window => {
+  openBrowserWindow().then(focus).then(window => {
     button.state(mainWindow, { label: 'nothing' });
     button.state(mainWindow.tabs.activeTab, { label: 'foo'})
     button.state(browserWindows.activeWindow, { label: 'bar' });
 
     button.click();
     button.click();
 
     focus(chromeWindow).then(() => {
@@ -907,16 +912,119 @@ exports['test button checked'] = functio
 
       close(window).
         then(loader.unload).
         then(done, assert.fail);
     })
   }).then(null, assert.fail);
 }
 
+exports['test button is checked on window level'] = function(assert, done) {
+  let loader = Loader(module);
+  let { ToggleButton } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+  let tabs = loader.require('sdk/tabs');
+
+  let button = ToggleButton({
+    id: 'my-button-20',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  let mainWindow = browserWindows.activeWindow;
+  let mainTab = tabs.activeTab;
+
+  assert.equal(button.checked, false,
+    'global state, checked is `false`.');
+  assert.equal(button.state(mainTab).checked, false,
+    'tab state, checked is `false`.');
+  assert.equal(button.state(mainWindow).checked, false,
+    'window state, checked is `false`.');
+
+  button.click();
+
+  tabs.open({
+    url: 'about:blank',
+    onActivate: function onActivate(tab) {
+      tab.removeListener('activate', onActivate);
+
+      assert.notEqual(mainTab, tab,
+        'the current tab is not the same.');
+
+      assert.equal(button.checked, false,
+        'global state, checked is `false`.');
+      assert.equal(button.state(mainTab).checked, true,
+        'previous tab state, checked is `true`.');
+      assert.equal(button.state(tab).checked, true,
+        'current tab state, checked is `true`.');
+      assert.equal(button.state(mainWindow).checked, true,
+        'window state, checked is `true`.');
+
+      openBrowserWindow().then(focus).then(window => {
+        let { activeWindow } = browserWindows;
+        let { activeTab } = activeWindow.tabs;
+
+        assert.equal(button.checked, false,
+          'global state, checked is `false`.');
+        assert.equal(button.state(activeTab).checked, false,
+          'tab state, checked is `false`.');
+
+        assert.equal(button.state(activeWindow).checked, false,
+          'window state, checked is `false`.');
+
+        tab.close(()=> {
+          close(window).
+            then(loader.unload).
+            then(done, assert.fail);
+        })
+      }).
+      then(null, assert.fail);
+    }
+  });
+
+};
+
+exports['test button click do not messing up states'] = function(assert) {
+  let loader = Loader(module);
+  let { ToggleButton } = loader.require('sdk/ui');
+  let { browserWindows } = loader.require('sdk/windows');
+
+  let button = ToggleButton({
+    id: 'my-button-21',
+    label: 'my button',
+    icon: './icon.png'
+  });
+
+  let mainWindow = browserWindows.activeWindow;
+  let { activeTab } = mainWindow.tabs;
+
+  button.state(mainWindow, { icon: './new-icon.png' });
+  button.state(activeTab, { label: 'foo'})
+
+  assert.equal(button.state(mainWindow).label, 'my button',
+    'label property for window state, properly derived from global state');
+
+  assert.equal(button.state(activeTab).icon, './new-icon.png',
+    'icon property for tab state, properly derived from window state');
+
+  button.click();
+
+  button.label = 'bar';
+
+  assert.equal(button.state(mainWindow).label, 'bar',
+    'label property for window state, properly derived from global state');
+
+  button.state(mainWindow, null);
+
+  assert.equal(button.state(activeTab).icon, './icon.png',
+    'icon property for tab state, properly derived from window state');
+
+  loader.unload();
+}
+
 // If the module doesn't support the app we're being run in, require() will
 // throw.  In that case, remove all tests above from exports, and add one dummy
 // test that passes.
 try {
   require('sdk/ui/button/toggle');
 }
 catch (err) {
   if (!/^Unsupported Application/.test(err.message))
--- a/b2g/build.mk
+++ b/b2g/build.mk
@@ -1,17 +1,22 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk
+
 installer: 
 	@$(MAKE) -C b2g/installer installer
 
 package:
 	@$(MAKE) -C b2g/installer
+ifdef FXOS_SIMULATOR
+	$(PYTHON) $(srcdir)/b2g/simulator/build_xpi.py $(MOZ_PKG_PLATFORM)
+endif
 
 install::
 	@echo 'B2G can't be installed directly.'
 	@exit 1
 
 upload::
 	@$(MAKE) -C b2g/installer upload
 
--- a/b2g/chrome/content/devtools.js
+++ b/b2g/chrome/content/devtools.js
@@ -30,18 +30,18 @@ XPCOMUtils.defineLazyGetter(this, 'Memor
 
 /**
  * The Developer HUD is an on-device developer tool that displays widgets,
  * showing visual debug information about apps. Each widget corresponds to a
  * metric as tracked by a metric watcher (e.g. consoleWatcher).
  */
 let developerHUD = {
 
-  _apps: new Map(),
-  _urls: new Map(),
+  _targets: new Map(),
+  _frames: new Map(),
   _client: null,
   _webappsActor: null,
   _watchers: [],
 
   /**
    * This method registers a metric watcher that will watch one or more metrics
    * of apps that are being tracked. A watcher must implement the trackApp(app)
    * and untrackApp(app) methods, add entries to the app.metrics map, keep them
@@ -67,174 +67,173 @@ let developerHUD = {
         this._webappsActor = res.webappsActor;
 
         for (let w of this._watchers) {
           if (w.init) {
             w.init(this._client);
           }
         }
 
-        Services.obs.addObserver(this, 'remote-browser-pending', false);
+        Services.obs.addObserver(this, 'remote-browser-shown', false);
         Services.obs.addObserver(this, 'inprocess-browser-shown', false);
         Services.obs.addObserver(this, 'message-manager-disconnect', false);
 
         let systemapp = document.querySelector('#systemapp');
-        let manifestURL = systemapp.getAttribute("mozapp");
-        this.trackApp(manifestURL);
+        this.trackFrame(systemapp);
 
-        let frames =
-          systemapp.contentWindow.document.querySelectorAll('iframe[mozapp]');
+        let frames = systemapp.contentWindow.document.querySelectorAll('iframe[mozapp]');
         for (let frame of frames) {
-          let manifestURL = frame.getAttribute("mozapp");
-          this.trackApp(manifestURL);
+          this.trackFrame(frame);
         }
       });
     });
   },
 
   uninit: function dwp_uninit() {
     if (!this._client)
       return;
 
-    for (let manifest of this._apps.keys()) {
-      this.untrackApp(manifest);
+    for (let frame of this._targets.keys()) {
+      this.untrackFrame(frame);
     }
 
-    Services.obs.removeObserver(this, 'remote-browser-pending');
+    Services.obs.removeObserver(this, 'remote-browser-shown');
     Services.obs.removeObserver(this, 'inprocess-browser-shown');
     Services.obs.removeObserver(this, 'message-manager-disconnect');
 
     this._client.close();
     delete this._client;
   },
 
   /**
    * This method will ask all registered watchers to track and update metrics
-   * on an app.
+   * on an app frame.
    */
-  trackApp: function dwp_trackApp(manifestURL) {
-    if (this._apps.has(manifestURL))
+  trackFrame: function dwp_trackFrame(frame) {
+    if (this._targets.has(frame))
       return;
 
     // FIXME(Bug 962577) Factor getAppActor out of webappsActor.
     this._client.request({
       to: this._webappsActor,
       type: 'getAppActor',
-      manifestURL: manifestURL
+      manifestURL: frame.appManifestURL
     }, (res) => {
       if (res.error) {
         return;
       }
 
-      let app = new App(manifestURL, res.actor);
-      this._apps.set(manifestURL, app);
+      let target = new Target(frame, res.actor);
+      this._targets.set(frame, target);
 
       for (let w of this._watchers) {
-        w.trackApp(app);
+        w.trackTarget(target);
       }
     });
   },
 
-  untrackApp: function dwp_untrackApp(manifestURL) {
-    let app = this._apps.get(manifestURL);
-    if (app) {
+  untrackFrame: function dwp_untrackFrame(frame) {
+    let target = this._targets.get(frame);
+    if (target) {
       for (let w of this._watchers) {
-        w.untrackApp(app);
+        w.untrackTarget(target);
       }
 
       // Delete the metrics and call display() to clean up the front-end.
-      delete app.metrics;
-      app.display();
+      delete target.metrics;
+      target.display();
 
-      this._apps.delete(manifestURL);
+      this._target.delete(frame);
     }
   },
 
   observe: function dwp_observe(subject, topic, data) {
     if (!this._client)
       return;
 
-    let manifestURL;
+    let frame;
 
     switch(topic) {
 
       // listen for frame creation in OOP (device) as well as in parent process (b2g desktop)
-      case 'remote-browser-pending':
+      case 'remote-browser-shown':
       case 'inprocess-browser-shown':
         let frameLoader = subject;
         // get a ref to the app <iframe>
         frameLoader.QueryInterface(Ci.nsIFrameLoader);
         // Ignore notifications that aren't from a BrowserOrApp
         if (!frameLoader.ownerIsBrowserOrAppFrame) {
           return;
         }
-        manifestURL = frameLoader.ownerElement.appManifestURL;
-        if (!manifestURL) // Ignore all frames but apps
+        frame = frameLoader.ownerElement;
+        if (!frame.appManifestURL) // Ignore all frames but app frames
           return;
-        this.trackApp(manifestURL);
-        this._urls.set(frameLoader.messageManager, manifestURL);
+        this.trackFrame(frame);
+        this._frames.set(frameLoader.messageManager, frame);
         break;
 
       // Every time an iframe is destroyed, its message manager also is
       case 'message-manager-disconnect':
         let mm = subject;
-        manifestURL = this._urls.get(mm);
-        if (!manifestURL)
+        frame = this._frames.get(mm);
+        if (!frame)
           return;
-        this.untrackApp(manifestURL);
-        this._urls.delete(mm);
+        this.untrackFrame(frame);
+        this._frames.delete(mm);
         break;
     }
   },
 
   log: function dwp_log(message) {
     dump(DEVELOPER_HUD_LOG_PREFIX + ': ' + message + '\n');
   }
 
 };
 
 
 /**
  * An App object represents all there is to know about a Firefox OS app that is
  * being tracked, e.g. its manifest information, current values of watched
  * metrics, and how to update these values on the front-end.
  */
-function App(manifest, actor) {
-  this.manifest = manifest;
+function Target(frame, actor) {
+  this.frame = frame;
   this.actor = actor;
   this.metrics = new Map();
 }
 
-App.prototype = {
+Target.prototype = {
+  display: function target_display() {
+    let data = {
+      metrics: []
+    };
 
-  display: function app_display() {
-    let data = {manifestURL: this.manifest, metrics: []};
     let metrics = this.metrics;
-
     if (metrics && metrics.size > 0) {
       for (let name of metrics.keys()) {
         data.metrics.push({name: name, value: metrics.get(name)});
       }
     }
 
-    shell.sendCustomEvent('developer-hud-update', data);
+    shell.sendEvent(this.frame, 'developer-hud-update', Cu.cloneInto(data, this.frame));
+
     // FIXME(after bug 963239 lands) return event.isDefaultPrevented();
     return false;
   }
 
 };
 
 
 /**
  * The Console Watcher tracks the following metrics in apps: reflows, warnings,
  * and errors.
  */
 let consoleWatcher = {
 
-  _apps: new Map(),
+  _targets: new Map(),
   _watching: {
     reflows: false,
     warnings: false,
     errors: false
   },
   _client: null,
 
   init: function cw_init(client) {
@@ -247,126 +246,126 @@ let consoleWatcher = {
       let metric = key;
       SettingsListener.observe('hud.' + metric, false, watch => {
         // Watch or unwatch the metric.
         if (watching[metric] = watch) {
           return;
         }
 
         // If unwatched, remove any existing widgets for that metric.
-        for (let app of this._apps.values()) {
-          app.metrics.set(metric, 0);
-          app.display();
+        for (let target of this._targets.values()) {
+          target.metrics.set(metric, 0);
+          target.display();
         }
       });
     }
 
     client.addListener('logMessage', this.consoleListener);
     client.addListener('pageError', this.consoleListener);
     client.addListener('consoleAPICall', this.consoleListener);
     client.addListener('reflowActivity', this.consoleListener);
   },
 
-  trackApp: function cw_trackApp(app) {
-    app.metrics.set('reflows', 0);
-    app.metrics.set('warnings', 0);
-    app.metrics.set('errors', 0);
+  trackTarget: function cw_trackTarget(target) {
+    target.metrics.set('reflows', 0);
+    target.metrics.set('warnings', 0);
+    target.metrics.set('errors', 0);
 
     this._client.request({
-      to: app.actor.consoleActor,
+      to: target.actor.consoleActor,
       type: 'startListeners',
       listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
     }, (res) => {
-      this._apps.set(app.actor.consoleActor, app);
+      this._targets.set(target.actor.consoleActor, target);
     });
   },
 
-  untrackApp: function cw_untrackApp(app) {
+  untrackTarget: function cw_untrackTarget(target) {
     this._client.request({
-      to: app.actor.consoleActor,
+      to: target.actor.consoleActor,
       type: 'stopListeners',
       listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
     }, (res) => { });
 
-    this._apps.delete(app.actor.consoleActor);
+    this._targets.delete(target.actor.consoleActor);
   },
 
-  bump: function cw_bump(app, metric) {
+  bump: function cw_bump(target, metric) {
     if (!this._watching[metric]) {
       return false;
     }
 
-    let metrics = app.metrics;
+    let metrics = target.metrics;
     metrics.set(metric, metrics.get(metric) + 1);
     return true;
   },
 
   consoleListener: function cw_consoleListener(type, packet) {
-    let app = this._apps.get(packet.from);
+    let target = this._targets.get(packet.from);
     let output = '';
 
     switch (packet.type) {
 
       case 'pageError':
         let pageError = packet.pageError;
 
         if (pageError.warning || pageError.strict) {
-          if (!this.bump(app, 'warnings')) {
+          if (!this.bump(target, 'warnings')) {
             return;
           }
           output = 'warning (';
         } else {
-          if (!this.bump(app, 'errors')) {
+          if (!this.bump(target, 'errors')) {
             return;
           }
           output += 'error (';
         }
 
         let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError;
         output += category + '): "' + (errorMessage.initial || errorMessage) +
           '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber;
         break;
 
       case 'consoleAPICall':
         switch (packet.message.level) {
 
           case 'error':
-            if (!this.bump(app, 'errors')) {
+            if (!this.bump(target, 'errors')) {
               return;
             }
             output = 'error (console)';
             break;
 
           case 'warn':
-            if (!this.bump(app, 'warnings')) {
+            if (!this.bump(target, 'warnings')) {
               return;
             }
             output = 'warning (console)';
             break;
 
           default:
             return;
         }
         break;
 
       case 'reflowActivity':
-        if (!this.bump(app, 'reflows')) {
+        if (!this.bump(target, 'reflows')) {
           return;
         }
 
         let {start, end, sourceURL} = packet;
         let duration = Math.round((end - start) * 100) / 100;
         output = 'reflow: ' + duration + 'ms';
         if (sourceURL) {
           output += ' ' + this.formatSourceURL(packet);
         }
         break;
     }
 
-    if (!app.display()) {
+    if (!target.display()) {
       // If the information was not displayed, log it.
       developerHUD.log(output);
     }
   },
 
   formatSourceURL: function cw_formatSourceURL(packet) {
     // Abbreviate source URL
     let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL);
@@ -396,51 +395,51 @@ let eventLoopLagWatcher = {
   settingsListener: function(value) {
     if (this._active == value) {
       return;
     }
     this._active = value;
 
     // Toggle the state of existing fronts.
     let fronts = this._fronts;
-    for (let app of fronts.keys()) {
+    for (let target of fronts.keys()) {
       if (value) {
-        fronts.get(app).start();
+        fronts.get(target).start();
       } else {
-        fronts.get(app).stop();
-        app.metrics.set('jank', 0);
-        app.display();
+        fronts.get(target).stop();
+        target.metrics.set('jank', 0);
+        target.display();
       }
     }
   },
 
-  trackApp: function(app) {
-    app.metrics.set('jank', 0);
+  trackTarget: function(target) {
+    target.metrics.set('jank', 0);
 
-    let front = new EventLoopLagFront(this._client, app.actor);
-    this._fronts.set(app, front);
+    let front = new EventLoopLagFront(this._client, target.actor);
+    this._fronts.set(target, front);
 
     front.on('event-loop-lag', time => {
-      app.metrics.set('jank', time);
+      target.metrics.set('jank', time);
 
-      if (!app.display()) {
+      if (!target.display()) {
         developerHUD.log('jank: ' + time + 'ms');
       }
     });
 
     if (this._active) {
       front.start();
     }
   },
 
-  untrackApp: function(app) {
+  untrackTarget: function(target) {
     let fronts = this._fronts;
-    if (fronts.has(app)) {
-      fronts.get(app).destroy();
-      fronts.delete(app);
+    if (fronts.has(target)) {
+      fronts.get(target).destroy();
+      fronts.delete(target);
     }
   }
 };
 developerHUD.registerWatcher(eventLoopLagWatcher);
 
 
 /**
  * The Memory Watcher uses devtools actors to track memory usage.
@@ -468,33 +467,35 @@ let memoryWatcher = {
       let category = key;
       SettingsListener.observe('hud.' + category, false, watch => {
         watching[category] = watch;
       });
     }
 
     SettingsListener.observe('hud.appmemory', false, enabled => {
       if (this._active = enabled) {
-        for (let app of this._fronts.keys()) {
-          this.measure(app);
+        for (let target of this._fronts.keys()) {
+          this.measure(target);
         }
       } else {
-        for (let timer of this._timers.values()) {
-          clearTimeout(this._timers.get(app));
+        for (let target of this._fronts.keys()) {
+          clearTimeout(this._timers.get(target));
+          target.metrics.set('memory', 0);
+          target.display();
         }
       }
     });
   },
 
-  measure: function mw_measure(app) {
+  measure: function mw_measure(target) {
 
     // TODO Also track USS (bug #976024).
 
     let watch = this._watching;
-    let front = this._fronts.get(app);
+    let front = this._fronts.get(target);
 
     front.measure().then((data) => {
 
       let total = 0;
       if (watch.jsobjects) {
         total += parseInt(data.jsObjectsSize);
       }
       if (watch.jsstrings) {
@@ -509,38 +510,38 @@ let memoryWatcher = {
       if (watch.style) {
         total += parseInt(data.styleSize);
       }
       if (watch.other) {
         total += parseInt(data.otherSize);
       }
       // TODO Also count images size (bug #976007).
 
-      app.metrics.set('memory', total);
-      app.display();
+      target.metrics.set('memory', total);
+      target.display();
       let duration = parseInt(data.jsMilliseconds) + parseInt(data.nonJSMilliseconds);
-      let timer = setTimeout(() => this.measure(app), 100 * duration);
-      this._timers.set(app, timer);
+      let timer = setTimeout(() => this.measure(target), 100 * duration);
+      this._timers.set(target, timer);
     }, (err) => {
       console.error(err);
     });
   },
 
-  trackApp: function mw_trackApp(app) {
-    app.metrics.set('uss', 0);
-    app.metrics.set('memory', 0);
-    this._fronts.set(app, MemoryFront(this._client, app.actor));
+  trackTarget: function mw_trackTarget(target) {
+    target.metrics.set('uss', 0);
+    target.metrics.set('memory', 0);
+    this._fronts.set(target, MemoryFront(this._client, target.actor));
     if (this._active) {
-      this.measure(app);
+      this.measure(target);
     }
   },
 
-  untrackApp: function mw_untrackApp(app) {
-    let front = this._fronts.get(app);
+  untrackTarget: function mw_untrackTarget(target) {
+    let front = this._fronts.get(target);
     if (front) {
       front.destroy();
-      clearTimeout(this._timers.get(app));
-      this._fronts.delete(app);
-      this._timers.delete(app);
+      clearTimeout(this._timers.get(target));
+      this._fronts.delete(target);
+      this._timers.delete(target);
     }
   }
 };
 developerHUD.registerWatcher(memoryWatcher);
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -555,28 +555,28 @@ var shell = {
         });
         break;
       case 'unload':
         this.stop();
         break;
     }
   },
 
-  sendEvent: function shell_sendEvent(content, type, details) {
-    let event = content.document.createEvent('CustomEvent');
+  // Send an event to a specific window, document or element.
+  sendEvent: function shell_sendEvent(target, type, details) {
+    let doc = target.document || target.ownerDocument || target;
+    let event = doc.createEvent('CustomEvent');
     event.initCustomEvent(type, true, true, details ? details : {});
-    content.dispatchEvent(event);
+    target.dispatchEvent(event);
   },
 
   sendCustomEvent: function shell_sendCustomEvent(type, details) {
-    let content = getContentWindow();
-    let event = content.document.createEvent('CustomEvent');
-    let payload = details ? Cu.cloneInto(details, content) : {};
-    event.initCustomEvent(type, true, true, payload);
-    content.dispatchEvent(event);
+    let target = getContentWindow();
+    let payload = details ? Cu.cloneInto(details, target) : {};
+    this.sendEvent(target, type, payload);
   },
 
   sendChromeEvent: function shell_sendChromeEvent(details) {
     if (!this.isHomeLoaded) {
       if (!('pendingChromeEvents' in this)) {
         this.pendingChromeEvents = [];
       }
 
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="456499c44d1ef39b602ea02e9ed460b6aab85b44"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="97a5b461686757dbb8ecab2aac5903e41d2e1afe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="456499c44d1ef39b602ea02e9ed460b6aab85b44"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "remote": "", 
         "branch": "", 
         "revision": ""
     }, 
-    "revision": "3d294ffa51afd8a8daafcdaa97cda8e38de81cb8", 
+    "revision": "c195232a409ee2234eca18a63bac2d48c2db0c13", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="97a5b461686757dbb8ecab2aac5903e41d2e1afe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="dfae3744607257206e37483dc3f431108baf70fb"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="fd883f4f086f95f2a9d2f94c7299fa7780aee19d"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b2f773d8320d30648b89767dfe5b25ef94bc7e62"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
new file mode 100644
--- /dev/null
+++ b/b2g/simulator/build_xpi.py
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Generate xpi for the simulator addon by:
+# - building a special gaia profile for it, as we need:
+#     * more languages, and,
+#     * less apps
+#   than b2g desktop's one
+# - retrieve usefull app version metadata from the build system
+# - finally, use addon sdk's cfx tool to build the addon xpi
+#   that ships:
+#     * a small firefox addon registering to the app manager
+#     * b2g desktop runtime
+#     * gaia profile
+
+import sys, os, re, subprocess
+from mozbuild.preprocessor import Preprocessor
+from mozbuild.base import MozbuildObject
+from mozbuild.util import ensureParentDir
+from zipfile import ZipFile
+from distutils.version import LooseVersion
+
+ftp_root_path = "/pub/mozilla.org/labs/fxos-simulator"
+UPDATE_LINK = "https://ftp.mozilla.org" + ftp_root_path + "/%(update_path)s/%(xpi_name)s"
+UPDATE_URL = "https://ftp.mozilla.org" + ftp_root_path + "/%(update_path)s/update.rdf"
+XPI_NAME = "fxos-simulator-%(version)s-%(platform)s.xpi"
+
+class GaiaBuilder(object):
+    def __init__(self, build, gaia_path):
+        self.build = build
+        self.gaia_path = gaia_path
+
+    def clean(self):
+        self.build._run_make(target="clean", directory=self.gaia_path)
+
+    def profile(self, env):
+        self.build._run_make(target="profile", directory=self.gaia_path, num_jobs=1, silent=False, append_env=env)
+
+    def override_prefs(self, srcfile):
+        # Note that each time we call `make profile` in gaia, a fresh new pref file is created
+        # cat srcfile >> profile/user.js
+        with open(os.path.join(self.gaia_path, "profile", "user.js"), "a") as userJs:
+            userJs.write(open(srcfile).read())
+
+def process_package_overload(src, dst, version, app_buildid):
+    ensureParentDir(dst)
+    # First replace numeric version like '1.3'
+    # Then replace with 'slashed' version like '1_4'
+    # Finally set the full length addon version like 1.3.20131230
+    defines = {
+        "NUM_VERSION": version,
+        "SLASH_VERSION": version.replace(".", "_"),
+        "FULL_VERSION": ("%s.%s" % (version, app_buildid))
+    }
+    pp = Preprocessor(defines=defines)
+    pp.do_filter("substitution")
+    with open(dst, "w") as output:
+        with open(src, "r") as input:
+            pp.processFile(input=input, output=output)
+
+def add_dir_to_zip(zip, top, pathInZip, blacklist=()):
+    zf = ZipFile(zip, "a")
+    for dirpath, subdirs, files in os.walk(top):
+        dir_relpath = os.path.relpath(dirpath, top)
+        if dir_relpath.startswith(blacklist):
+            continue
+        zf.write(dirpath, os.path.join(pathInZip, dir_relpath))
+        for filename in files:
+            relpath = os.path.join(dir_relpath, filename)
+            if relpath in blacklist:
+                continue
+            zf.write(os.path.join(dirpath, filename),
+                     os.path.join(pathInZip, relpath))
+    zf.close()
+
+def main(platform):
+    build = MozbuildObject.from_environment()
+    topsrcdir = build.topsrcdir
+    distdir = build.distdir
+
+    srcdir = os.path.join(topsrcdir, "b2g", "simulator")
+
+    app_buildid = open(os.path.join(build.topobjdir, "config", "buildid")).read().strip()
+
+    # The simulator uses a shorter version string,
+    # it only keeps the major version digits A.B
+    # whereas MOZ_B2G_VERSION is A.B.C.D
+    b2g_version = build.config_environment.defines["MOZ_B2G_VERSION"].replace('"', '')
+    version = ".".join(str(n) for n in LooseVersion(b2g_version).version[0:2])
+
+    # Build a gaia profile specific to the simulator in order to:
+    # - disable the FTU
+    # - set custom prefs to enable devtools debugger server
+    # - set custom settings to disable lockscreen and screen timeout
+    # - only ship production apps
+    gaia_path = build.config_environment.substs["GAIADIR"]
+    builder = GaiaBuilder(build, gaia_path)
+    builder.clean()
+    env = {
+      "NOFTU": "1",
+      "GAIA_APP_TARGET": "production",
+      "SETTINGS_PATH": os.path.join(srcdir, "custom-settings.json")
+    }
+    builder.profile(env)
+    builder.override_prefs(os.path.join(srcdir, "custom-prefs.js"))
+
+    # Substitute version strings in the package manifest overload file
+    manifest_overload = os.path.join(build.topobjdir, "b2g", "simulator", "package-overload.json")
+    process_package_overload(os.path.join(srcdir, "package-overload.json.in"),
+                             manifest_overload,
+                             version,
+                             app_buildid)
+
+    # Build the simulator addon xpi
+    xpi_name = XPI_NAME % {"version": version, "platform": platform}
+    xpi_path = os.path.join(distdir, xpi_name)
+
+    update_path = "%s/%s" % (version, platform)
+    update_link = UPDATE_LINK % {"update_path": update_path, "xpi_name": xpi_name}
+    update_url = UPDATE_URL % {"update_path": update_path}
+    subprocess.check_call([
+      build.virtualenv_manager.python_path, os.path.join(topsrcdir, "addon-sdk", "source", "bin", "cfx"), "xpi", \
+      "--pkgdir", srcdir, \
+      "--manifest-overload", manifest_overload, \
+      "--strip-sdk", \
+      "--update-link", update_link, \
+      "--update-url", update_url, \
+      "--static-args", "{\"label\": \"Firefox OS %s\"}" % version, \
+      "--output-file", xpi_path \
+    ])
+
+    # Ship b2g-desktop, but prevent its gaia profile to be shipped in the xpi
+    add_dir_to_zip(xpi_path, os.path.join(distdir, "b2g"), "b2g", ("gaia"))
+    # Then ship our own gaia profile
+    add_dir_to_zip(xpi_path, os.path.join(gaia_path, "profile"), "profile")
+
+if __name__ == '__main__':
+    if 2 != len(sys.argv):
+        print("""Usage:
+  python {0} MOZ_PKG_PLATFORM
+""".format(sys.argv[0]))
+        sys.exit(1)
+    main(*sys.argv[1:])
+
new file mode 100644
--- /dev/null
+++ b/b2g/simulator/custom-prefs.js
@@ -0,0 +1,2 @@
+user_pref("devtools.debugger.prompt-connection", false);
+user_pref("devtools.debugger.forbid-certified-apps", false);
new file mode 100644
--- /dev/null
+++ b/b2g/simulator/custom-settings.json
@@ -0,0 +1,6 @@
+{
+  "debugger.remote-mode": "adb-devtools",
+  "screen.timeout": 0,
+  "lockscreen.enabled": false,
+  "lockscreen.locked": false
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c4307fc8418436bb6b2fd3a6afc702c2db28aa77
GIT binary patch
literal 4762
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