Bug 1270359 Implement connectNative on windows r?kmag draft
authorAndrew Swan <aswan@mozilla.com>
Thu, 09 Jun 2016 15:10:54 -0700
changeset 378045 8ca88a0c81404f2f6193828536a2a6f106d92f1e
parent 378044 e757292c9868af732eeb8ac0e3a9deff4cce1309
child 523452 0482faa184d5a1f23f3c29c709bd9284e465b696
push id20920
push useraswan@mozilla.com
push dateTue, 14 Jun 2016 12:30:28 +0000
reviewerskmag
bugs1270359
milestone50.0a1
Bug 1270359 Implement connectNative on windows r?kmag MozReview-Commit-ID: IHJuGt2Us8e
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
toolkit/components/extensions/test/xpcshell/test_native_messaging.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -27,16 +27,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
                                   "resource://gre/modules/Subprocess.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
                                   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+                                  "resource://gre/modules/WindowsRegistry.jsm");
 
 const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json";
 const VALID_APPLICATION = /^\w+(\.\w+)*$/;
 
 // For a graceful shutdown (i.e., when the extension is unloaded or when it
 // explicitly calls disconnect() on a native port), how long we give the native
 // application to exit before we start trying to kill it.  (in milliseconds)
 const GRACEFUL_SHUTDOWN_TIME = 3000;
@@ -51,39 +53,55 @@ const GRACEFUL_SHUTDOWN_TIME = 3000;
 const MAX_READ = 1024 * 1024;
 const MAX_WRITE = 0xffffffff;
 
 // Preferences that can lower the message size limits above,
 // used for testing the limits.
 const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
 const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
 
+const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
+
 this.HostManifestManager = {
   _initializePromise: null,
   _lookup: null,
 
   init() {
     if (!this._initializePromise) {
       let platform = AppConstants.platform;
       if (platform == "win") {
-        throw new Error("Windows not yet implemented (bug 1270359)");
+        this._lookup = this._winLookup;
       } else if (platform == "macosx" || platform == "linux") {
         let dirs = [
           Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path,
           Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path,
         ];
         this._lookup = (application, context) => this._tryPaths(application, dirs, context);
       } else {
         throw new Error(`Native messaging is not supported on ${AppConstants.platform}`);
       }
       this._initializePromise = Schemas.load(HOST_MANIFEST_SCHEMA);
     }
     return this._initializePromise;
   },
 
+  _winLookup(application, context) {
+    let path = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                                          REGPATH, application);
+    if (!path) {
+      path = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+                                        REGPATH, application);
+    }
+    if (!path) {
+      return null;
+    }
+    return this._tryPath(path, application, context)
+      .then(manifest => manifest ? {path, manifest} : null);
+  },
+
   _tryPath(path, application, context) {
     return Promise.resolve()
       .then(() => OS.File.read(path, {encoding: "utf-8"}))
       .then(data => {
         let manifest;
         try {
           manifest = JSON.parse(data);
         } catch (ex) {
@@ -180,17 +198,17 @@ this.NativeApp = class extends EventEmit
         return Subprocess.call(subprocessOpts);
       }).then(proc => {
         this.startupPromise = null;
         this.proc = proc;
         this._startRead();
         this._startWrite();
       }).catch(err => {
         this.startupPromise = null;
-        Cu.reportError(err);
+        Cu.reportError(err.message);
         this._cleanup(err);
       });
   }
 
   // A port is definitely "alive" if this.proc is non-null.  But we have
   // to provide a live port object immediately when connecting so we also
   // need to consider a port alive if proc is null but the startupPromise
   // is still pending.
@@ -208,17 +226,17 @@ this.NativeApp = class extends EventEmit
           throw new Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${this.maxRead} bytes.`);
         }
         return this.proc.stdout.readJSON(len);
       }).then(msg => {
         this.emit("message", msg);
         this.readPromise = null;
         this._startRead();
       }).catch(err => {
-        Cu.reportError(err);
+        Cu.reportError(err.message);
         this._cleanup(err);
       });
   }
 
   _startWrite() {
     if (this.sendQueue.length == 0) {
       return;
     }
@@ -232,17 +250,17 @@ this.NativeApp = class extends EventEmit
 
     this.writePromise = Promise.all([
       this.proc.stdin.write(uintArray.buffer),
       this.proc.stdin.write(buffer),
     ]).then(() => {
       this.writePromise = null;
       this._startWrite();
     }).catch(err => {
-      Cu.reportError(err);
+      Cu.reportError(err.message);
       this._cleanup(err);
     });
   }
 
   send(msg) {
     if (this._isDisconnected) {
       throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
     }
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -9,24 +9,22 @@ support-files =
 skip-if = (os == 'android') # android doesn't have devtools
 [test_chrome_ext_background_page.html]
 skip-if = (toolkit == 'android') # android doesn't have devtools
 [test_chrome_ext_downloads_download.html]
 [test_chrome_ext_downloads_misc.html]
 [test_chrome_ext_downloads_search.html]
 [test_chrome_ext_eventpage_warning.html]
 [test_chrome_ext_native_messaging.html]
-# Re-enable for Windows with bug 1270359.
-skip-if = os != "mac" && os != "linux"
+skip-if = os == "android"  # native messaging is not supported on android
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_ext_webnavigation_resolved_urls.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_native_messaging_paths.html]
-# Re-enable for Windows with bug 1270359.
 skip-if = os != "mac" && os != "linux"
 [test_ext_cookies_expiry.html]
 skip-if = buildapp == 'b2g'
 [test_ext_cookies_permissions.html]
 skip-if = buildapp == 'b2g'
 [test_ext_jsversion.html]
 skip-if = buildapp == 'b2g'
 [test_ext_schema.html]
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
@@ -13,134 +13,77 @@
 
 <script type="text/javascript">
 "use strict";
 
 /* globals OS */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
 
+if (AppConstants.platform == "win") {
+  Cu.import("resource://testing-common/MockRegistry.jsm");
+}
+
 const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
 const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
 
 function getSubprocessCount() {
   return SubprocessImpl.Process.getWorker().call("getProcesses", [])
                        .then(result => result.size);
 }
 function waitForSubprocessExit() {
   return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []);
 }
 
 let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 info(`Using local directory ${dir.path}\n`);
 
-let dirProvider = {
-  getFile(property) {
-    if (property == "XREUserNativeMessaging") {
-      return dir.clone();
-    }
-    return null;
-  },
-};
-
-Services.dirsvc.registerProvider(dirProvider);
-SimpleTest.registerCleanupFunction(() => {
-  Services.dirsvc.unregisterProvider(dirProvider);
-  dir.remove(true);
-});
-
 function getPath(filename) {
   return OS.Path.join(dir.path, filename);
 }
 
 // Set up a couple of native applications and their manifests for
 // test to use.
 const ID = "native@tests.mozilla.org";
 
-const ECHO_PATH = getPath("echo.py");
-const ECHO_MANIFEST_PATH = getPath("echo.json");
-const ECHO_MANIFEST = {
-  name: "echo",
-  description: "a native app that echoes back messages it receives",
-  path: ECHO_PATH,
-  type: "stdio",
-  allowed_extensions: [ID],
-};
-
-const INFO_PATH = getPath("info.py");
-const INFO_MANIFEST_PATH = getPath("info.json");
-const INFO_MANIFEST = {
-  name: "info",
-  description: "a native app that gives some info about how it was started",
-  path: INFO_PATH,
-  type: "stdio",
-  allowed_extensions: [ID],
-};
-
-const WONTDIE_PATH = getPath("wontdie.py");
-const WONTDIE_MANIFEST_PATH = getPath("wontdie.json");
-const WONTDIE_MANIFEST = {
-  name: "wontdie",
-  description: "a native app that does not exit when stdin closes or on SIGTERM",
-  path: WONTDIE_PATH,
-  type: "stdio",
-  allowed_extensions: [ID],
-};
-
-add_task(function* setup_scripts() {
-  const PERMS = {unixMode: 0o755};
-  let pythonPath = yield Subprocess.pathSearch("python2.7").catch(err => {
-    if (err.errorCode != Subprocess.ERROR_BAD_EXECUTABLE) {
-      throw err;
-    }
-    return Subprocess.pathSearch("python");
-  });
-
-  const ECHO_SCRIPT = String.raw`#!${pythonPath} -u
+const ECHO_BODY = String.raw`
 import struct
 import sys
 
 while True:
     rawlen = sys.stdin.read(4)
     if len(rawlen) == 0:
         sys.exit(0)
     msglen = struct.unpack('@I', rawlen)[0]
     msg = sys.stdin.read(msglen)
 
     sys.stdout.write(struct.pack('@I', msglen))
     sys.stdout.write(msg)
 `;
 
-  yield OS.File.writeAtomic(ECHO_PATH, ECHO_SCRIPT);
-  yield OS.File.setPermissions(ECHO_PATH, PERMS);
-  yield OS.File.writeAtomic(ECHO_MANIFEST_PATH, JSON.stringify(ECHO_MANIFEST));
-
-  const INFO_SCRIPT = String.raw`#!${pythonPath} -u
+const INFO_BODY = String.raw`
 import json
 import os
 import struct
 import sys
 
 msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
 sys.stdout.write(struct.pack('@I', len(msg)))
 sys.stdout.write(msg)
 sys.exit(0)
 `;
 
-  yield OS.File.writeAtomic(INFO_PATH, INFO_SCRIPT);
-  yield OS.File.setPermissions(INFO_PATH, PERMS);
-  yield OS.File.writeAtomic(INFO_MANIFEST_PATH, JSON.stringify(INFO_MANIFEST));
-
-  const WONTDIE_SCRIPT = String.raw`#!${pythonPath} -u
+const WONTDIE_BODY = String.raw`
 import signal
 import struct
 import sys
 
 signal.signal(signal.SIGTERM, signal.SIG_IGN)
 
 while True:
     rawlen = sys.stdin.read(4)
@@ -148,19 +91,113 @@ while True:
         signal.pause()
     msglen = struct.unpack('@I', rawlen)[0]
     msg = sys.stdin.read(msglen)
 
     sys.stdout.write(struct.pack('@I', msglen))
     sys.stdout.write(msg)
 `;
 
-  yield OS.File.writeAtomic(WONTDIE_PATH, WONTDIE_SCRIPT);
-  yield OS.File.setPermissions(WONTDIE_PATH, PERMS);
-  yield OS.File.writeAtomic(WONTDIE_MANIFEST_PATH, JSON.stringify(WONTDIE_MANIFEST));
+const SCRIPTS = [
+  {
+    name: "echo",
+    description: "a native app that echoes back messages it receives",
+    script: ECHO_BODY,
+  },
+  {
+    name: "info",
+    description: "a native app that gives some info about how it was started",
+    script: INFO_BODY,
+  },
+  {
+    name: "wontdie",
+    description: "a native app that does not exit when stdin closes or on SIGTERM",
+    script: WONTDIE_BODY,
+  },
+];
+
+add_task(function* setup() {
+  const PERMS = {unixMode: 0o755};
+  let pythonPath = yield Subprocess.pathSearch("python2.7").catch(err => {
+    if (err.errorCode != Subprocess.ERROR_BAD_EXECUTABLE) {
+      throw err;
+    }
+    return Subprocess.pathSearch("python");
+  });
+
+  switch (AppConstants.platform) {
+    case "macosx":
+    case "linux":
+      let dirProvider = {
+        getFile(property) {
+          if (property == "XREUserNativeMessaging") {
+            return dir.clone();
+          }
+          return null;
+        },
+      };
+
+      Services.dirsvc.registerProvider(dirProvider);
+      SimpleTest.registerCleanupFunction(() => {
+        Services.dirsvc.unregisterProvider(dirProvider);
+        dir.remove(true);
+      });
+
+      for (let script of SCRIPTS) {
+        let path = getPath(`${script.name}.py`);
+        let body = `#!${pythonPath} -u\n${script.script}`;
+        yield OS.File.writeAtomic(path, body);
+        yield OS.File.setPermissions(path, PERMS);
+
+        let manifest = {
+          name: script.name,
+          description: script.description,
+          path,
+          type: "stdio",
+          allowed_extensions: [ID],
+        };
+
+        yield OS.File.writeAtomic(getPath(`${script.name}.json`), JSON.stringify(manifest));
+      }
+      break;
+
+    case "win":
+      const REGKEY = "Software\\Mozilla\\NativeMessagingHosts";
+      let registry = new MockRegistry();
+      SimpleTest.registerCleanupFunction(() => {
+        registry.shutdown();
+      });
+
+      for (let script of SCRIPTS) {
+        let pyPath = getPath(`${script.name}.py`);
+        yield OS.File.writeAtomic(pyPath, script.script);
+
+        let batPath = getPath(`${script.name}.bat`);
+        let batBody = `@ECHO OFF\n${pythonPath} -u ${pyPath} %*\n`;
+        yield OS.File.writeAtomic(batPath, batBody);
+        yield OS.File.setPermissions(pyPath, PERMS);
+
+        let manifest = {
+          name: script.name,
+          description: script.description,
+          path: batPath,
+          type: "stdio",
+          allowed_extensions: [ID],
+        };
+        let manifestPath = getPath(`${script.name}.json`);
+        yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
+
+        registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                          REGKEY, script.name, manifestPath);
+      }
+      break;
+
+    default:
+      ok(false, `Native messaing is not supported on ${AppConstants.platform}`);
+  }
 });
 
 // Test the basic operation of native messaging with a simple
 // script that echoes back whatever message is sent to it.
 add_task(function* test_happy_path() {
   function background() {
     let port = browser.runtime.connectNative("echo");
     port.onMessage.addListener(msg => {
@@ -451,18 +488,18 @@ add_task(function* test_child_process() 
       permissions: ["nativeMessaging"],
     },
   }, ID);
 
   yield extension.startup();
 
   let msg = yield extension.awaitMessage("result");
   is(msg.args.length, 2, "Received one command line argument");
-  is(msg.args[1], INFO_MANIFEST_PATH, "Command line argument is the path to the native host manifest");
-  is(msg.cwd, OS.Path.dirname(INFO_PATH), "Working directory is the directory containing the native appliation");
+  is(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
+  is(msg.cwd, dir.path, "Working directory is the directory containing the native appliation");
 
   let exitPromise = waitForSubprocessExit();
   yield extension.unload();
   yield exitPromise;
 });
 
 // Test that an unresponsive native application still gets killed eventually
 add_task(function* test_unresponsive_native_app() {
--- a/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
@@ -38,17 +38,19 @@ add_task(function* test_default_paths() 
       expectUser = OS.Path.join(OS.Constants.Path.homeDir, ".mozilla/native-messaging-hosts");
 
       const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib";
       expectGlobal = OS.Path.join("/usr", libdir, "mozilla/native-messaging-hosts");
       break;
     }
 
     default:
-      ok(false, `This test should be skipped on ${AppConstants.platform}`);
+      // Fixed filesystem paths are only defined for MacOS and Linux,
+      // there's nothing to test on other platforms.
+      ok(false, `This test does not apply on ${AppConstants.platform}`);
       break;
   }
 
   let userDir = Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path;
   is(userDir, expectUser, "user-specific native messaging directory is correct");
 
   let globalDir = Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path;
   is(globalDir, expectGlobal, "system-wide native messaing directory is correct");
--- a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -1,19 +1,31 @@
 "use strict";
 
 /* global OS, HostManifestManager, NativeApp */
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Schemas.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
 Cu.import("resource://gre/modules/NativeMessaging.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
+let registry = null;
+if (AppConstants.platform == "win") {
+  Cu.import("resource://testing-common/MockRegistry.jsm");
+  registry = new MockRegistry();
+  do_register_cleanup(() => {
+    registry.shutdown();
+  });
+}
+
+const REGKEY = "Software\\Mozilla\\NativeMessagingHosts";
+
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 
 let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
 dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
 let userDir = dir.clone();
 userDir.append("user");
 userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
@@ -79,16 +91,21 @@ add_task(function* test_nonexistent_mani
   let result = yield HostManifestManager.lookupApplication("test", context);
   equal(result, null, "lookupApplication returns null for non-existent application");
 });
 
 const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json");
 
 add_task(function* test_good_manifest() {
   yield writeManifest(USER_TEST_JSON, templateManifest);
+  if (registry) {
+    registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                      REGKEY, "test", USER_TEST_JSON);
+  }
+
   let result = yield HostManifestManager.lookupApplication("test", context);
   notEqual(result, null, "lookupApplication finds a good manifest");
   equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path");
   deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents");
 });
 
 add_task(function* test_invalid_json() {
   yield writeManifest(USER_TEST_JSON, "this is not valid json");
@@ -104,17 +121,18 @@ add_task(function* test_invalid_name() {
   equal(result, null, "lookupApplication ignores an invalid name");
 });
 
 add_task(function* test_name_mismatch() {
   let manifest = Object.assign({}, templateManifest);
   manifest.name = "not test";
   yield writeManifest(USER_TEST_JSON, manifest);
   let result = yield HostManifestManager.lookupApplication("test", context);
-  equal(result, null, "lookupApplication ignores mistmatch between json filename and name property");
+  let what = (AppConstants.platform == "win") ? "registry key" : "json filename";
+  equal(result, null, `lookupApplication ignores mistmatch between ${what} and name property`);
 });
 
 add_task(function* test_missing_props() {
   const PROPS = [
     "name",
     "description",
     "path",
     "type",
@@ -148,66 +166,95 @@ add_task(function* test_no_allowed_exten
 
 const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json");
 let globalManifest = Object.assign({}, templateManifest);
 globalManifest.description = "This manifest is from the systemwide directory";
 
 add_task(function* good_manifest_system_dir() {
   yield OS.File.remove(USER_TEST_JSON);
   yield writeManifest(GLOBAL_TEST_JSON, globalManifest);
+  if (registry) {
+    registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                      REGKEY, "test", null);
+    registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+                      REGKEY, "test", GLOBAL_TEST_JSON);
+  }
 
+  let where = (AppConstants.platform == "win") ? "registry location" : "directory";
   let result = yield HostManifestManager.lookupApplication("test", context);
-  notEqual(result, null, "lookupApplication finds a manifest in the system-wide directory");
-  equal(result.path, GLOBAL_TEST_JSON, "lookupApplication returns path in the system-wide directory");
-  deepEqual(result.manifest, globalManifest, "lookupApplication returns manifest contents from the system-wide directory");
+  notEqual(result, null, `lookupApplication finds a manifest in the system-wide ${where}`);
+  equal(result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}`);
+  deepEqual(result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}`);
 });
 
 add_task(function* test_user_dir_precedence() {
   yield writeManifest(USER_TEST_JSON, templateManifest);
-  // test.json is still in the global directory from the previous test
+  if (registry) {
+    registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                      REGKEY, "test", USER_TEST_JSON);
+  }
+  // global test.json and LOCAL_MACHINE registry key on windows are
+  // still present from the previous test
 
   let result = yield HostManifestManager.lookupApplication("test", context);
-  notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide directories");
+  notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations");
   equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist");
   deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist");
 });
 
 // Test shutdown handling in NativeApp
 add_task(function* test_native_app_shutdown() {
-  const SCRIPT = String.raw`#!${PYTHON} -u
+  const SCRIPT = String.raw`
 import signal
 import struct
 import sys
 
 signal.signal(signal.SIGTERM, signal.SIG_IGN)
 
 while True:
     rawlen = sys.stdin.read(4)
     if len(rawlen) == 0:
         signal.pause()
     msglen = struct.unpack('@I', rawlen)[0]
     msg = sys.stdin.read(msglen)
 
     sys.stdout.write(struct.pack('@I', msglen))
     sys.stdout.write(msg)
-`;
+  `;
 
   let scriptPath = OS.Path.join(userDir.path, "wontdie.py");
-  yield OS.File.writeAtomic(scriptPath, SCRIPT);
-  yield OS.File.setPermissions(scriptPath, {unixMode: 0o755});
+  let manifestPath = OS.Path.join(userDir.path, "wontdie.json");
 
   const ID = "native@tests.mozilla.org";
-  const MANIFEST = {
+  let manifest = {
     name: "wontdie",
     description: "test async shutdown of native apps",
-    path: scriptPath,
     type: "stdio",
     allowed_extensions: [ID],
   };
-  yield writeManifest(OS.Path.join(userDir.path, "wontdie.json"), MANIFEST);
+
+  if (AppConstants.platform == "win") {
+    yield OS.File.writeAtomic(scriptPath, SCRIPT);
+
+    let batPath = OS.Path.join(userDir.path, "wontdie.bat");
+    let batBody = `@ECHO OFF\n${PYTHON} -u ${scriptPath} %*\n`;
+    yield OS.File.writeAtomic(batPath, batBody);
+    yield OS.File.setPermissions(batPath, {unixMode: 0o755});
+
+    manifest.path = batPath;
+    yield writeManifest(manifestPath, manifest);
+
+    registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                      REGKEY, "wontdie", manifestPath);
+  } else {
+    yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`);
+    yield OS.File.setPermissions(scriptPath, {unixMode: 0o755});
+    manifest.path = scriptPath;
+    yield writeManifest(manifestPath, manifest);
+  }
 
   let extension = {id: ID};
   let app = new NativeApp(extension, context, "wontdie");
 
   // send a message and wait for the reply to make sure the app is running
   let MSG = "test";
   let recvPromise = new Promise(resolve => {
     let listener = (what, msg) => {
@@ -226,9 +273,8 @@ while True:
   do_print("waiting for async shutdown");
   Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
   AsyncShutdown.profileBeforeChange._trigger();
   Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
 
   let procs = yield SubprocessImpl.Process.getWorker().call("getProcesses", []);
   equal(procs.size, 0, "native process exited");
 });
-
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -9,10 +9,9 @@ skip-if = toolkit == 'gonk' || appname =
 [test_locale_data.js]
 [test_locale_converter.js]
 [test_ext_contexts.js]
 [test_ext_json_parser.js]
 [test_ext_manifest_content_security_policy.js]
 [test_ext_schemas.js]
 [test_getAPILevelForWindow.js]
 [test_native_messaging.js]
-# Re-enable for Windows with bug 1270359.
-skip-if = os != "mac" && os != "linux"
+skip-if = os == "android"