1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 var EXPORTED_SYMBOLS = ["NativeApp"];
10 const { XPCOMUtils } = ChromeUtils.importESModule(
11 "resource://gre/modules/XPCOMUtils.sys.mjs"
13 const { AppConstants } = ChromeUtils.importESModule(
14 "resource://gre/modules/AppConstants.sys.mjs"
17 const { EventEmitter } = ChromeUtils.importESModule(
18 "resource://gre/modules/EventEmitter.sys.mjs"
22 ExtensionUtils: { ExtensionError, promiseTimeout },
23 } = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
27 ChromeUtils.defineESModuleGetters(lazy, {
28 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
29 Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
32 XPCOMUtils.defineLazyModuleGetters(lazy, {
33 NativeManifests: "resource://gre/modules/NativeManifests.jsm",
34 OS: "resource://gre/modules/osfile.jsm",
37 // For a graceful shutdown (i.e., when the extension is unloaded or when it
38 // explicitly calls disconnect() on a native port), how long we give the native
39 // application to exit before we start trying to kill it. (in milliseconds)
40 const GRACEFUL_SHUTDOWN_TIME = 3000;
42 // Hard limits on maximum message size that can be read/written
43 // These are defined in the native messaging documentation, note that
44 // the write limit is imposed by the "wire protocol" in which message
45 // boundaries are defined by preceding each message with its length as
46 // 4-byte unsigned integer so this is the largest value that can be
47 // represented. Good luck generating a serialized message that large,
48 // the practical write limit is likely to be dictated by available memory.
49 const MAX_READ = 1024 * 1024;
50 const MAX_WRITE = 0xffffffff;
52 // Preferences that can lower the message size limits above,
53 // used for testing the limits.
54 const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
55 const PREF_MAX_WRITE =
56 "webextensions.native-messaging.max-output-message-bytes";
58 var NativeApp = class extends EventEmitter {
60 * @param {BaseContext} context The context that initiated the native app.
61 * @param {string} application The identifier of the native app.
63 constructor(context, application) {
66 this.context = context;
67 this.name = application;
69 // We want a close() notification when the window is destroyed.
70 this.context.callOnClose(this);
73 this.readPromise = null;
75 this.writePromise = null;
76 this.cleanupStarted = false;
78 this.startupPromise = lazy.NativeManifests.lookupManifest(
84 // Report a generic error to not leak information about whether a native
85 // application is installed to addons that do not have the right permission.
87 throw new ExtensionError(`No such native application ${application}`);
90 let command = hostInfo.manifest.path;
91 if (AppConstants.platform == "win") {
92 // OS.Path.join() ignores anything before the last absolute path
93 // it sees, so if command is already absolute, it remains unchanged
94 // here. If it is relative, we get the proper absolute path here.
95 command = lazy.OS.Path.join(
96 lazy.OS.Path.dirname(hostInfo.path),
99 // Normalize in case the extension used / instead of \.
100 command = command.replaceAll("/", "\\");
103 let subprocessOpts = {
105 arguments: [hostInfo.path, context.extension.id],
106 workdir: lazy.OS.Path.dirname(command),
111 return lazy.Subprocess.call(subprocessOpts);
114 this.startupPromise = null;
118 this._startStderrRead();
121 this.startupPromise = null;
122 Cu.reportError(err instanceof Error ? err : err.message);
128 * Open a connection to a native messaging host.
130 * @param {number} portId A unique internal ID that identifies the port.
131 * @param {NativeMessenger} port Parent NativeMessenger used to send messages.
132 * @returns {ParentPort}
134 onConnect(portId, port) {
135 // eslint-disable-next-line
136 this.on("message", (_, message) => {
137 port.sendPortMessage(portId, new StructuredCloneHolder(message));
139 this.once("disconnect", (_, error) => {
140 port.sendPortDisconnect(portId, error && new ClonedErrorHolder(error));
143 onPortMessage: holder => this.send(holder),
144 onPortDisconnect: () => this.close(),
149 * @param {BaseContext} context The scope from where `message` originates.
150 * @param {*} message A message from the extension, meant for a native app.
151 * @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app.
153 static encodeMessage(context, message) {
154 message = context.jsonStringify(message);
155 let buffer = new TextEncoder().encode(message).buffer;
156 if (buffer.byteLength > NativeApp.maxWrite) {
157 throw new context.Error("Write too big");
162 // A port is definitely "alive" if this.proc is non-null. But we have
163 // to provide a live port object immediately when connecting so we also
164 // need to consider a port alive if proc is null but the startupPromise
166 get _isDisconnected() {
167 return !this.proc && !this.startupPromise;
171 if (this.readPromise) {
172 throw new Error("Entered _startRead() while readPromise is non-null");
174 this.readPromise = this.proc.stdout
177 if (len > NativeApp.maxRead) {
178 throw new ExtensionError(
179 `Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.`
182 return this.proc.stdout.readJSON(len);
185 this.emit("message", msg);
186 this.readPromise = null;
190 if (err.errorCode != lazy.Subprocess.ERROR_END_OF_FILE) {
191 Cu.reportError(err instanceof Error ? err : err.message);
198 if (!this.sendQueue.length) {
202 if (this.writePromise) {
203 throw new Error("Entered _startWrite() while writePromise is non-null");
206 let buffer = this.sendQueue.shift();
207 let uintArray = Uint32Array.of(buffer.byteLength);
209 this.writePromise = Promise.all([
210 this.proc.stdin.write(uintArray.buffer),
211 this.proc.stdin.write(buffer),
214 this.writePromise = null;
218 Cu.reportError(err.message);
224 let proc = this.proc;
229 let data = await proc.stderr.readString();
231 // We have hit EOF, just stop reading
233 Services.console.logStringMessage(
234 `stderr output from native app ${app}: ${partial}`
240 let lines = data.split(/\r?\n/);
241 lines[0] = partial + lines[0];
242 partial = lines.pop();
244 for (let line of lines) {
245 Services.console.logStringMessage(
246 `stderr output from native app ${app}: ${line}`
254 if (this._isDisconnected) {
255 throw new ExtensionError("Attempt to postMessage on disconnected port");
257 let msg = holder.deserialize(globalThis);
258 if (Cu.getClassName(msg, true) != "ArrayBuffer") {
259 // This error cannot be triggered by extensions; it indicates an error in
260 // our implementation.
262 "The message to the native messaging host is not an ArrayBuffer"
268 if (buffer.byteLength > NativeApp.maxWrite) {
269 throw new ExtensionError("Write too big");
272 this.sendQueue.push(buffer);
273 if (!this.startupPromise && !this.writePromise) {
278 // Shut down the native application and (by default) signal to the extension
279 // that the connect has been disconnected.
280 async _cleanup(err, fromExtension = false) {
281 if (this.cleanupStarted) {
284 this.cleanupStarted = true;
285 this.context.forgetOnClose(this);
287 if (!fromExtension) {
288 if (err && err.errorCode == lazy.Subprocess.ERROR_END_OF_FILE) {
291 this.emit("disconnect", err);
294 await this.startupPromise;
297 // Failed to initialize proc in the constructor.
301 // To prevent an uncooperative process from blocking shutdown, we take the
302 // following actions, and wait for GRACEFUL_SHUTDOWN_TIME in between.
304 // 1. Allow exit by closing the stdin pipe.
305 // 2. Allow exit by a kill signal.
306 // 3. Allow exit by forced kill signal.
307 // 4. Give up and unblock shutdown despite the process still being alive.
309 // Close the stdin stream and allow the process to exit on its own.
310 // proc.wait() below will resolve once the process has exited gracefully.
311 this.proc.stdin.close().catch(err => {
312 if (err.errorCode != lazy.Subprocess.ERROR_END_OF_FILE) {
316 let exitPromise = Promise.race([
317 // 1. Allow the process to exit on its own after closing stdin.
318 this.proc.wait().then(() => {
321 promiseTimeout(GRACEFUL_SHUTDOWN_TIME).then(() => {
323 // 2. Kill the process gracefully. 3. Force kill after a timeout.
324 this.proc.kill(GRACEFUL_SHUTDOWN_TIME);
326 // 4. If the process is still alive after a kill + timeout followed
327 // by a forced kill + timeout, give up and just resolve exitPromise.
329 // Note that waiting for just one interval is not enough, because the
330 // `proc.kill()` is asynchronous, so we need to wait a bit after the
331 // kill signal has been sent.
332 return promiseTimeout(2 * GRACEFUL_SHUTDOWN_TIME);
337 lazy.AsyncShutdown.profileBeforeChange.addBlocker(
338 `Native Messaging: Wait for application ${this.name} to exit`,
343 // Called when the Context or Port is closed.
345 this._cleanup(null, true);
348 sendMessage(holder) {
349 let responsePromise = new Promise((resolve, reject) => {
350 this.once("message", (what, msg) => {
353 this.once("disconnect", (what, err) => {
358 let result = this.startupPromise.then(() => {
359 // Skip .send() if _cleanup() has been called already;
360 // otherwise the error passed to _cleanup/"disconnect" would be hidden by the
361 // "Attempt to postMessage on disconnected port" error from this.send().
362 if (!this.cleanupStarted) {
365 return responsePromise;
373 // Prevent the response promise from being reported as an
374 // unchecked rejection if the startup promise fails.
375 responsePromise.catch(() => {});
385 XPCOMUtils.defineLazyPreferenceGetter(
391 XPCOMUtils.defineLazyPreferenceGetter(