Bug 1270360 Implement runtime.sendNativeMessage() r?kmag draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 08 Jun 2016 14:52:35 -0700
changeset 379695 a94ddc26c891a4eaaccce6454ec2e440d37344f8
parent 378180 53f5b5c289fba6ad82c675578cf1c548ae37f0c1
child 523542 63836ea88986acdcc0f8519f0a5571620e06a23b
push id21024
push useraswan@mozilla.com
push dateThu, 16 Jun 2016 15:48:43 +0000
reviewerskmag
bugs1270360
milestone50.0a1
Bug 1270360 Implement runtime.sendNativeMessage() r?kmag MozReview-Commit-ID: 93FaGaYto5w
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/ext-runtime.js
toolkit/components/extensions/schemas/runtime.json
toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -351,9 +351,40 @@ this.NativeApp = class extends EventEmit
         return () => {
           this.off("message", listener);
         };
       }).api(),
     };
 
     return Cu.cloneInto(api, this.context.cloneScope, {cloneFunctions: true});
   }
+
+  sendMessage(msg) {
+    let responsePromise = new Promise((resolve, reject) => {
+      let disconnectListener;
+      let msgListener = (what, msg) => {
+        this.off("message", msgListener);
+        this.off("disconnect", disconnectListener);
+        resolve(msg);
+      };
+      disconnectListener = (what, err) => {
+        this.off("message", msgListener);
+        this.off("disconnect", disconnectListener);
+        reject(err);
+      };
+      this.on("message", msgListener);
+      this.on("disconnect", disconnectListener);
+    });
+
+    let result = this.startupPromise.then(() => {
+      this.send(msg);
+      return responsePromise;
+    });
+
+    result.then(() => {
+      this._cleanup();
+    }, () => {
+      this._cleanup();
+    });
+
+    return result;
+  }
 };
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -51,24 +51,25 @@ extensions.registerSchemaAPI("runtime", 
         if (!GlobalManager.extensionMap.has(recipient.extensionId)) {
           return context.wrapPromise(Promise.reject({message: "Invalid extension ID"}),
                                      responseCallback);
         }
         return context.messenger.sendMessage(Services.cpmm, message, recipient, responseCallback);
       },
 
       connectNative(application) {
-        if (!extension.hasPermission("nativeMessaging")) {
-          throw new context.cloneScope.Error("Permission denied because 'nativeMessaging' permission is missing.");
-        }
-
         let app = new NativeApp(extension, context, application);
         return app.portAPI();
       },
 
+      sendNativeMessage(application, message) {
+        let app = new NativeApp(extension, context, application);
+        return app.sendMessage(message);
+      },
+
       get lastError() {
         return context.lastError;
       },
 
       getManifest() {
         return Cu.cloneInto(extension.manifest, context.cloneScope);
       },
 
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -273,16 +273,17 @@
           "$ref": "Port",
           "description": "Port through which messages can be sent and received. The port's $(ref:runtime.Port onDisconnect) event is fired if the extension/app does not exist. "
         }
       },
       {
         "name": "connectNative",
         "type": "function",
         "description": "Connects to a native application in the host machine.",
+        "permissions": ["nativeMessaging"],
         "parameters": [
           {
             "type": "string",
             "name": "application",
             "description": "The name of the registered application to connect to."
           }
         ],
         "returns": {
@@ -317,32 +318,30 @@
                 "description": "The JSON response object sent by the handler of the message. If an error occurs while connecting to the extension, the callback will be called with no arguments and $(ref:runtime.lastError) will be set to the error message."
               }
             ]
           }
         ]
       },
       {
         "name": "sendNativeMessage",
-        "unsupported": true,
         "type": "function",
         "description": "Send a single message to a native application.",
+        "permissions": ["nativeMessaging"],
+        "async": "responseCallback",
         "parameters": [
           {
             "name": "application",
             "description": "The name of the native messaging host.",
             "type": "string"
           },
           {
             "name": "message",
             "description": "The message that will be passed to the native messaging host.",
-            "type": "object",
-            "additionalProperties": {
-              "type": "any"
-            }
+            "type": "any"
           },
           {
             "type": "function",
             "name": "responseCallback",
             "optional": true,
             "parameters": [
               {
                 "name": "response",
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
@@ -230,16 +230,54 @@ add_task(function* test_happy_path() {
 
   let procCount = yield getSubprocessCount();
   is(procCount, 1, "subprocess is still running");
   let exitPromise = waitForSubprocessExit();
   yield extension.unload();
   yield exitPromise;
 });
 
+// Test sendNativeMessage()
+add_task(function* test_sendNativeMessage() {
+  function background() {
+    let MSG = {test: "hello world"};
+
+    // Check error handling
+    browser.runtime.sendNativeMessage("nonexistent", MSG).then(() => {
+      browser.test.fail("sendNativeMessage() to a nonexistent app should have failed");
+    }, err => {
+      browser.test.succeed("sendNativeMessage() to a nonexistent app failed");
+    }).then(() => {
+      // Check regular message exchange
+      return browser.runtime.sendNativeMessage("echo", MSG);
+    }).then(reply => {
+      let expected = JSON.stringify(MSG);
+      let received = JSON.stringify(reply);
+      browser.test.assertEq(expected, received, "Received echoed native message");
+      browser.test.sendMessage("finished");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${background})()`,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+  yield extension.awaitMessage("finished");
+
+  // With sendNativeMessage(), the subprocess should be disconnected
+  // after exchanging a single message.
+  yield waitForSubprocessExit();
+
+  yield extension.unload();
+});
+
 // Test calling Port.disconnect()
 add_task(function* test_disconnect() {
   function background() {
     let port = browser.runtime.connectNative("echo");
     port.onMessage.addListener(msg => {
       browser.test.sendMessage("message", msg);
     });
     browser.test.onMessage.addListener((what, payload) => {
@@ -373,39 +411,31 @@ add_task(function* test_read_limit() {
 
   clearPref();
 });
 
 // Test that an extension without the nativeMessaging permission cannot
 // use native messaging.
 add_task(function* test_ext_permission() {
   function background() {
-    try {
-      browser.runtime.connectNative("test");
-      browser.test.sendMessage("result", null);
-    } catch (ex) {
-      browser.test.sendMessage("result", ex.message);
-    }
+    browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission");
+    browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission");
+    browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+    browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+    browser.test.sendMessage("finished");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${background})()`,
     manifest: {},
   });
 
   yield extension.startup();
-
-  let errmsg = yield extension.awaitMessage("result");
-  isnot(errmsg, null, "connectNative() failed without nativeMessaging permission");
-  ok(/Permission denied/.test(errmsg), "error message for missing extension permission is reasonable");
-
+  yield extension.awaitMessage("finished");
   yield extension.unload();
-
-  let procCount = yield getSubprocessCount();
-  is(procCount, 0, "No child process was started");
 });
 
 // Test that an extension that is not listed in allowed_extensions for
 // a native application cannot use that application.
 add_task(function* test_app_permission() {
   function background() {
     let port = browser.runtime.connectNative("echo");
     port.onDisconnect.addListener(() => {