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/. */
7 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
9 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
10 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
14 ChromeUtils.defineESModuleGetters(lazy, {
15 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
16 NativeManifests: "resource://gre/modules/NativeManifests.sys.mjs",
17 Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
20 const { ExtensionError, promiseTimeout } = ExtensionUtils;
22 // For a graceful shutdown (i.e., when the extension is unloaded or when it
23 // explicitly calls disconnect() on a native port), how long we give the native
24 // application to exit before we start trying to kill it. (in milliseconds)
25 const GRACEFUL_SHUTDOWN_TIME = 3000;
27 // Hard limits on maximum message size that can be read/written
28 // These are defined in the native messaging documentation, note that
29 // the write limit is imposed by the "wire protocol" in which message
30 // boundaries are defined by preceding each message with its length as
31 // 4-byte unsigned integer so this is the largest value that can be
32 // represented. Good luck generating a serialized message that large,
33 // the practical write limit is likely to be dictated by available memory.
34 const MAX_READ = 1024 * 1024;
35 const MAX_WRITE = 0xffffffff;
37 // Preferences that can lower the message size limits above,
38 // used for testing the limits.
39 const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
40 const PREF_MAX_WRITE =
41 "webextensions.native-messaging.max-output-message-bytes";
43 XPCOMUtils.defineLazyPreferenceGetter(lazy, "maxRead", PREF_MAX_READ, MAX_READ);
44 XPCOMUtils.defineLazyPreferenceGetter(
51 export class NativeApp extends EventEmitter {
53 * @param {BaseContext} context The context that initiated the native app.
54 * @param {string} application The identifier of the native app.
56 constructor(context, application) {
59 this.context = context;
60 this.name = application;
62 // We want a close() notification when the window is destroyed.
63 this.context.callOnClose(this);
66 this.readPromise = null;
68 this.writePromise = null;
69 this.cleanupStarted = false;
71 this.startupPromise = lazy.NativeManifests.lookupManifest(
77 // Report a generic error to not leak information about whether a native
78 // application is installed to addons that do not have the right permission.
80 throw new ExtensionError(`No such native application ${application}`);
83 let command = hostInfo.manifest.path;
84 if (AppConstants.platform == "win") {
85 // Normalize in case the extension used / instead of \.
86 command = command.replaceAll("/", "\\");
88 if (!PathUtils.isAbsolute(command)) {
89 // Note: hostInfo.path is an absolute path to the manifest.
90 const parentPath = PathUtils.parent(
91 hostInfo.path.replaceAll("/", "\\")
93 // PathUtils.joinRelative cannot be used because it throws for "..".
94 // but command is allowed to contain ".." to traverse the directory.
95 command = `${parentPath}\\${command}`;
97 } else if (!PathUtils.isAbsolute(command)) {
98 // Only windows supports relative paths.
100 "NativeApp requires absolute path to command on this platform"
104 let subprocessOpts = {
106 arguments: [hostInfo.path, context.extension.id],
107 workdir: PathUtils.parent(command),
112 return lazy.Subprocess.call(subprocessOpts);
115 this.startupPromise = null;
119 this._startStderrRead();
122 this.startupPromise = null;
123 Cu.reportError(err instanceof Error ? err : err.message);
129 * Open a connection to a native messaging host.
131 * @param {number} portId A unique internal ID that identifies the port.
132 * @param {import("ExtensionParent.sys.mjs").NativeMessenger} port Parent NativeMessenger used to send messages.
133 * @returns {import("ExtensionParent.sys.mjs").ParentPort}
135 onConnect(portId, port) {
136 // eslint-disable-next-line
137 this.on("message", (_, message) => {
138 port.sendPortMessage(
140 new StructuredCloneHolder(
141 `NativeMessaging/onConnect/${this.name}`,
147 this.once("disconnect", (_, error) => {
148 port.sendPortDisconnect(portId, error && new ClonedErrorHolder(error));
151 onPortMessage: holder => this.send(holder),
152 onPortDisconnect: () => this.close(),
157 * @param {BaseContext} context The scope from where `message` originates.
158 * @param {*} message A message from the extension, meant for a native app.
159 * @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app.
161 static encodeMessage(context, message) {
162 message = context.jsonStringify(message);
163 let buffer = new TextEncoder().encode(message).buffer;
164 if (buffer.byteLength > lazy.maxWrite) {
165 throw new context.Error("Write too big");
170 // A port is definitely "alive" if this.proc is non-null. But we have
171 // to provide a live port object immediately when connecting so we also
172 // need to consider a port alive if proc is null but the startupPromise
174 get _isDisconnected() {
175 return !this.proc && !this.startupPromise;
179 if (this.readPromise) {
180 throw new Error("Entered _startRead() while readPromise is non-null");
182 this.readPromise = this.proc.stdout
185 if (len > lazy.maxRead) {
186 throw new ExtensionError(
187 `Native application tried to send a message of ${len} bytes, which exceeds the limit of ${lazy.maxRead} bytes.`
190 return this.proc.stdout.readJSON(len);
193 this.emit("message", msg);
194 this.readPromise = null;
198 if (err.errorCode != lazy.Subprocess.ERROR_END_OF_FILE) {
199 Cu.reportError(err instanceof Error ? err : err.message);
206 if (!this.sendQueue.length) {
210 if (this.writePromise) {
211 throw new Error("Entered _startWrite() while writePromise is non-null");
214 let buffer = this.sendQueue.shift();
215 let uintArray = Uint32Array.of(buffer.byteLength);
217 this.writePromise = Promise.all([
218 this.proc.stdin.write(uintArray.buffer),
219 this.proc.stdin.write(buffer),
222 this.writePromise = null;
226 Cu.reportError(err.message);
232 let proc = this.proc;
237 let data = await proc.stderr.readString();
239 // We have hit EOF, just stop reading
241 Services.console.logStringMessage(
242 `stderr output from native app ${app}: ${partial}`
248 let lines = data.split(/\r?\n/);
249 lines[0] = partial + lines[0];
250 partial = lines.pop();
252 for (let line of lines) {
253 Services.console.logStringMessage(
254 `stderr output from native app ${app}: ${line}`
262 if (this._isDisconnected) {
263 throw new ExtensionError("Attempt to postMessage on disconnected port");
265 let msg = holder.deserialize(globalThis);
266 if (Cu.getClassName(msg, true) != "ArrayBuffer") {
267 // This error cannot be triggered by extensions; it indicates an error in
268 // our implementation.
270 "The message to the native messaging host is not an ArrayBuffer"
276 if (buffer.byteLength > lazy.maxWrite) {
277 throw new ExtensionError("Write too big");
280 this.sendQueue.push(buffer);
281 if (!this.startupPromise && !this.writePromise) {
286 // Shut down the native application and (by default) signal to the extension
287 // that the connect has been disconnected.
288 async _cleanup(err, fromExtension = false) {
289 if (this.cleanupStarted) {
292 this.cleanupStarted = true;
293 this.context.forgetOnClose(this);
295 if (!fromExtension) {
296 if (err && err.errorCode == lazy.Subprocess.ERROR_END_OF_FILE) {
299 this.emit("disconnect", err);
302 await this.startupPromise;
305 // Failed to initialize proc in the constructor.
309 // To prevent an uncooperative process from blocking shutdown, we take the
310 // following actions, and wait for GRACEFUL_SHUTDOWN_TIME in between.
312 // 1. Allow exit by closing the stdin pipe.
313 // 2. Allow exit by a kill signal.
314 // 3. Allow exit by forced kill signal.
315 // 4. Give up and unblock shutdown despite the process still being alive.
317 // Close the stdin stream and allow the process to exit on its own.
318 // proc.wait() below will resolve once the process has exited gracefully.
319 this.proc.stdin.close().catch(err => {
320 if (err.errorCode != lazy.Subprocess.ERROR_END_OF_FILE) {
324 let exitPromise = Promise.race([
325 // 1. Allow the process to exit on its own after closing stdin.
326 this.proc.wait().then(() => {
329 promiseTimeout(GRACEFUL_SHUTDOWN_TIME).then(() => {
331 // 2. Kill the process gracefully. 3. Force kill after a timeout.
332 this.proc.kill(GRACEFUL_SHUTDOWN_TIME);
334 // 4. If the process is still alive after a kill + timeout followed
335 // by a forced kill + timeout, give up and just resolve exitPromise.
337 // Note that waiting for just one interval is not enough, because the
338 // `proc.kill()` is asynchronous, so we need to wait a bit after the
339 // kill signal has been sent.
340 return promiseTimeout(2 * GRACEFUL_SHUTDOWN_TIME);
345 lazy.AsyncShutdown.profileBeforeChange.addBlocker(
346 `Native Messaging: Wait for application ${this.name} to exit`,
351 // Called when the Context or Port is closed.
353 this._cleanup(null, true);
356 sendMessage(holder) {
357 let responsePromise = new Promise((resolve, reject) => {
358 this.once("message", (what, msg) => {
361 this.once("disconnect", (what, err) => {
366 let result = this.startupPromise.then(() => {
367 // Skip .send() if _cleanup() has been called already;
368 // otherwise the error passed to _cleanup/"disconnect" would be hidden by the
369 // "Attempt to postMessage on disconnected port" error from this.send().
370 if (!this.cleanupStarted) {
373 return responsePromise;
381 // Prevent the response promise from being reported as an
382 // unchecked rejection if the startup promise fails.
383 responsePromise.catch(() => {});