mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 04:41:11 +00:00
Backed out 3 changesets (bug 1661935) for causing xpc failures @test_subprocess.js.
Backed out changeset 620790051502 (bug 1661935) Backed out changeset 7b10b4e274db (bug 1661935) Backed out changeset 594beeb5e424 (bug 1661935)
This commit is contained in:
parent
499ac89e77
commit
20c086f54e
@ -17553,16 +17553,6 @@
|
||||
value: 2
|
||||
mirror: always
|
||||
|
||||
# Whether to use XDG portal for native messaging.
|
||||
# https://github.com/flatpak/xdg-desktop-portal/issues/655
|
||||
# - 0: never
|
||||
# - 1: always
|
||||
# - 2: auto (true for snap and flatpak or GTK_USE_PORTAL=1, false otherwise)
|
||||
- name: widget.use-xdg-desktop-portal.native-messaging
|
||||
type: int32_t
|
||||
value: 0
|
||||
mirror: always
|
||||
|
||||
# Whether to try to use XDG portal for settings / look-and-feel information.
|
||||
# https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Settings
|
||||
# - 0: never
|
||||
|
@ -101,7 +101,6 @@ def build_dict(config, env=os.environ):
|
||||
substs.get("MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS") == "1"
|
||||
)
|
||||
d["automation"] = substs.get("MOZ_AUTOMATION") == "1"
|
||||
d["dbus_enabled"] = bool(substs.get("MOZ_ENABLE_DBUS"))
|
||||
|
||||
d["opt"] = not d["debug"] and not d["asan"] and not d["tsan"] and not d["ccov"]
|
||||
|
||||
|
@ -95,7 +95,6 @@ test-verify:
|
||||
- android/android-x86_64.py
|
||||
linux.*:
|
||||
- unittests/linux_unittest.py
|
||||
- unittests/linux_dbus-python.py
|
||||
- remove_executables.py
|
||||
macosx.*:
|
||||
- unittests/mac_unittest.py
|
||||
@ -106,18 +105,10 @@ test-verify:
|
||||
fetches:
|
||||
toolchain:
|
||||
by-test-platform:
|
||||
linux1804.*:
|
||||
linux.*:
|
||||
- linux64-node
|
||||
- linux64-minidump-stackwalk
|
||||
- linux64-fix-stacks
|
||||
- linux-python-dbusmock
|
||||
- linux64-1804-dbus-python
|
||||
linux2204.*:
|
||||
- linux64-node
|
||||
- linux64-minidump-stackwalk
|
||||
- linux64-fix-stacks
|
||||
- linux-python-dbusmock
|
||||
- linux64-2204-dbus-python
|
||||
macosx.*:
|
||||
- macosx64-node
|
||||
- macosx64-minidump-stackwalk
|
||||
|
@ -26,7 +26,6 @@ task-defaults:
|
||||
- android/android-x86_64.py
|
||||
linux.*:
|
||||
- unittests/linux_unittest.py
|
||||
- unittests/linux_dbus-python.py
|
||||
- remove_executables.py
|
||||
macosx.*:
|
||||
- unittests/mac_unittest.py
|
||||
@ -134,18 +133,10 @@ xpcshell:
|
||||
fetches:
|
||||
toolchain:
|
||||
by-test-platform:
|
||||
linux1804.*:
|
||||
linux.*:
|
||||
- linux64-node
|
||||
- linux64-minidump-stackwalk
|
||||
- linux64-fix-stacks
|
||||
- linux-python-dbusmock
|
||||
- linux64-1804-dbus-python
|
||||
linux2204.*:
|
||||
- linux64-node
|
||||
- linux64-minidump-stackwalk
|
||||
- linux64-fix-stacks
|
||||
- linux-python-dbusmock
|
||||
- linux64-2204-dbus-python
|
||||
macosx.*:
|
||||
- macosx64-node
|
||||
- macosx64-minidump-stackwalk
|
||||
|
@ -1,18 +0,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/.
|
||||
|
||||
import os
|
||||
|
||||
#####
|
||||
config = {
|
||||
###
|
||||
"virtualenv_modules": [
|
||||
"dbus-python>=1.2.18,<=1.3.2",
|
||||
"python-dbusmock==0.32.2",
|
||||
],
|
||||
"find_links": [
|
||||
"https://pypi.pub.build.mozilla.org/pub/",
|
||||
os.path.abspath(os.environ.get("MOZ_FETCHES_DIR")),
|
||||
],
|
||||
}
|
@ -279,19 +279,6 @@ def run_xpcshell_test(command_context, test_objects=None, **params):
|
||||
xpcshell = command_context._spawn(XPCShellRunner)
|
||||
xpcshell.cwd = command_context._mach_context.cwd
|
||||
|
||||
tags = " ".join(params["manifest"].get("tags")).split(" ")
|
||||
if "webextensions" in tags and "portal" in tags and sys.platform == "linux":
|
||||
dir_relpath = params["manifest"].get("dir_relpath")[0]
|
||||
# Only Linux Native Messaging Portal xpcshell tests need this.
|
||||
req = os.path.join(
|
||||
dir_relpath,
|
||||
"linux_native-messaging-portal_requirements.txt",
|
||||
)
|
||||
command_context.virtualenv_manager.activate()
|
||||
command_context.virtualenv_manager.install_pip_requirements(
|
||||
req, require_hashes=False
|
||||
)
|
||||
|
||||
try:
|
||||
return xpcshell.run_test(**params)
|
||||
except InvalidTestPathError as e:
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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
|
||||
@ -89,20 +89,26 @@ export var NativeManifests = {
|
||||
return manifest ? { path, manifest } : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a native manifest of the given type and name.
|
||||
*
|
||||
* @param {string} type The type, one of: "pkcs11", "stdio" or "storage".
|
||||
* @param {string} path The path to the manifest file.
|
||||
* @param {string} name The name of the application.
|
||||
* @param {object} context A context object as expected by Schemas.normalize.
|
||||
* @param {object} data The JSON object of the manifest.
|
||||
* @returns {object} The contents of the validated manifest, or null if
|
||||
* the manifest is not valid.
|
||||
*/
|
||||
async parseManifest(type, path, name, context, data) {
|
||||
await this.init();
|
||||
let manifest = data;
|
||||
async _tryPath(type, path, name, context, logIfNotFound) {
|
||||
let manifest;
|
||||
try {
|
||||
manifest = await IOUtils.readJSON(path);
|
||||
} catch (ex) {
|
||||
if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) {
|
||||
Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`);
|
||||
return null;
|
||||
}
|
||||
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
|
||||
if (logIfNotFound) {
|
||||
Cu.reportError(
|
||||
`Error reading native manifest file ${path}: file is referenced in the registry but does not exist`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Cu.reportError(ex);
|
||||
return null;
|
||||
}
|
||||
let normalized = lazy.Schemas.normalize(
|
||||
manifest,
|
||||
"manifest.NativeManifest",
|
||||
@ -152,30 +158,6 @@ export var NativeManifests = {
|
||||
return manifest;
|
||||
},
|
||||
|
||||
async _tryPath(type, path, name, context, logIfNotFound) {
|
||||
let manifest;
|
||||
try {
|
||||
manifest = await IOUtils.readJSON(path);
|
||||
} catch (ex) {
|
||||
if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) {
|
||||
Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`);
|
||||
return null;
|
||||
}
|
||||
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
|
||||
if (logIfNotFound) {
|
||||
Cu.reportError(
|
||||
`Error reading native manifest file ${path}: file is referenced in the registry but does not exist`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Cu.reportError(ex);
|
||||
return null;
|
||||
}
|
||||
manifest = await this.parseManifest(type, path, name, context, manifest);
|
||||
return manifest;
|
||||
},
|
||||
|
||||
async _tryPaths(type, name, dirs, context) {
|
||||
for (let dir of dirs) {
|
||||
let path = PathUtils.join(dir, TYPES[type], `${name}.json`);
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
/* 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
|
||||
@ -19,13 +19,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
|
||||
const { ExtensionError, promiseTimeout } = ExtensionUtils;
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
lazy,
|
||||
"portal",
|
||||
"@mozilla.org/extensions/native-messaging-portal;1",
|
||||
"nsINativeMessagingPortal"
|
||||
);
|
||||
|
||||
// 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)
|
||||
@ -56,12 +49,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
);
|
||||
|
||||
export class NativeApp extends EventEmitter {
|
||||
_throwGenericError(application) {
|
||||
// Report a generic error to not leak information about whether a native
|
||||
// application is installed to addons that do not have the right permission.
|
||||
throw new ExtensionError(`No such native application ${application}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BaseContext} context The context that initiated the native app.
|
||||
* @param {string} application The identifier of the native app.
|
||||
@ -80,18 +67,6 @@ export class NativeApp extends EventEmitter {
|
||||
this.sendQueue = [];
|
||||
this.writePromise = null;
|
||||
this.cleanupStarted = false;
|
||||
this.portalSessionHandle = null;
|
||||
|
||||
if ("@mozilla.org/extensions/native-messaging-portal;1" in Cc) {
|
||||
if (lazy.portal.shouldUse()) {
|
||||
this.startupPromise = this._doInitPortal().catch(err => {
|
||||
this.startupPromise = null;
|
||||
Cu.reportError(err instanceof Error ? err : err.message);
|
||||
this._cleanup(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.startupPromise = lazy.NativeManifests.lookupManifest(
|
||||
"stdio",
|
||||
@ -99,8 +74,10 @@ export class NativeApp extends EventEmitter {
|
||||
context
|
||||
)
|
||||
.then(hostInfo => {
|
||||
// Report a generic error to not leak information about whether a native
|
||||
// application is installed to addons that do not have the right permission.
|
||||
if (!hostInfo) {
|
||||
this._throwGenericError(application);
|
||||
throw new ExtensionError(`No such native application ${application}`);
|
||||
}
|
||||
|
||||
let command = hostInfo.manifest.path;
|
||||
@ -146,67 +123,6 @@ export class NativeApp extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
async _doInitPortal() {
|
||||
let available = await lazy.portal.available;
|
||||
if (!available) {
|
||||
Cu.reportError("Native messaging portal is not available");
|
||||
this._throwGenericError(this.name);
|
||||
}
|
||||
|
||||
let handle = await lazy.portal.createSession(this.name);
|
||||
this.portalSessionHandle = handle;
|
||||
|
||||
let hostInfo = null;
|
||||
let path;
|
||||
try {
|
||||
let manifest = await lazy.portal.getManifest(
|
||||
handle,
|
||||
this.name,
|
||||
this.context.extension.id
|
||||
);
|
||||
path = manifest.substring(0, 30) + "...";
|
||||
hostInfo = await lazy.NativeManifests.parseManifest(
|
||||
"stdio",
|
||||
path,
|
||||
this.name,
|
||||
this.context,
|
||||
JSON.parse(manifest)
|
||||
);
|
||||
} catch (ex) {
|
||||
if (ex instanceof SyntaxError && ex.message.startsWith("JSON.parse:")) {
|
||||
Cu.reportError(`Error parsing native manifest ${path}: ${ex.message}`);
|
||||
this._throwGenericError(this.name);
|
||||
}
|
||||
}
|
||||
if (!hostInfo) {
|
||||
this._throwGenericError(this.name);
|
||||
}
|
||||
|
||||
let pipes;
|
||||
try {
|
||||
pipes = await lazy.portal.start(
|
||||
handle,
|
||||
this.name,
|
||||
this.context.extension.id
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.name == "NotFoundError") {
|
||||
this._throwGenericError(this.name);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.proc = await lazy.Subprocess.connectRunning([
|
||||
pipes.stdin,
|
||||
pipes.stdout,
|
||||
pipes.stderr,
|
||||
]);
|
||||
this.startupPromise = null;
|
||||
this._startRead();
|
||||
this._startWrite();
|
||||
this._startStderrRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a connection to a native messaging host.
|
||||
*
|
||||
@ -383,21 +299,6 @@ export class NativeApp extends EventEmitter {
|
||||
|
||||
await this.startupPromise;
|
||||
|
||||
if (this.portalSessionHandle) {
|
||||
if (this.writePromise) {
|
||||
await this.writePromise.catch(Cu.reportError);
|
||||
}
|
||||
// When using the WebExtensions portal, we don't control the external
|
||||
// process, the portal does. So let the portal handle waiting/killing the
|
||||
// external process as it sees fit.
|
||||
await lazy.portal
|
||||
.closeSession(this.portalSessionHandle)
|
||||
.catch(Cu.reportError);
|
||||
this.portalSessionHandle = null;
|
||||
this.proc = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.proc) {
|
||||
// Failed to initialize proc in the constructor.
|
||||
return;
|
||||
|
@ -1,696 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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 "NativeMessagingPortal.h"
|
||||
|
||||
#include <gio/gunixfdlist.h>
|
||||
#include <glib.h>
|
||||
|
||||
#include "mozilla/ClearOnShutdown.h"
|
||||
#include "mozilla/GUniquePtr.h"
|
||||
#include "mozilla/Logging.h"
|
||||
#include "mozilla/UniquePtrExtensions.h"
|
||||
#include "mozilla/WidgetUtilsGtk.h"
|
||||
#include "mozilla/dom/Promise.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static mozilla::LazyLogModule gNativeMessagingPortalLog(
|
||||
"NativeMessagingPortal");
|
||||
|
||||
#ifdef MOZ_LOGGING
|
||||
# define LOG_NMP(...) \
|
||||
MOZ_LOG(gNativeMessagingPortalLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
|
||||
#else
|
||||
# define LOG_NMP(args)
|
||||
#endif
|
||||
|
||||
namespace mozilla::extensions {
|
||||
|
||||
NS_IMPL_ISUPPORTS(NativeMessagingPortal, nsINativeMessagingPortal)
|
||||
|
||||
/* static */
|
||||
already_AddRefed<NativeMessagingPortal> NativeMessagingPortal::GetSingleton() {
|
||||
static StaticRefPtr<NativeMessagingPortal> sInstance;
|
||||
|
||||
if (MOZ_UNLIKELY(!sInstance)) {
|
||||
sInstance = new NativeMessagingPortal();
|
||||
ClearOnShutdown(&sInstance);
|
||||
}
|
||||
|
||||
return do_AddRef(sInstance);
|
||||
}
|
||||
|
||||
static void LogError(const char* aMethod, const GError& aError) {
|
||||
g_warning("%s error: %s", aMethod, aError.message);
|
||||
}
|
||||
|
||||
static void RejectPromiseWithErrorMessage(dom::Promise& aPromise,
|
||||
const GError& aError) {
|
||||
aPromise.MaybeRejectWithOperationError(nsDependentCString(aError.message));
|
||||
}
|
||||
|
||||
static nsresult GetPromise(JSContext* aCx, RefPtr<dom::Promise>& aPromise) {
|
||||
nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
|
||||
if (NS_WARN_IF(!globalObject)) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
ErrorResult result;
|
||||
aPromise = dom::Promise::Create(globalObject, result);
|
||||
if (NS_WARN_IF(result.Failed())) {
|
||||
return result.StealNSResult();
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
struct CallbackData {
|
||||
explicit CallbackData(dom::Promise& aPromise,
|
||||
const gchar* aSessionHandle = nullptr)
|
||||
: promise(&aPromise), sessionHandle(g_strdup(aSessionHandle)) {}
|
||||
RefPtr<dom::Promise> promise;
|
||||
GUniquePtr<gchar> sessionHandle;
|
||||
guint subscription_id = 0;
|
||||
};
|
||||
|
||||
NativeMessagingPortal::NativeMessagingPortal() {
|
||||
LOG_NMP("NativeMessagingPortal::NativeMessagingPortal()");
|
||||
mCancellable = dont_AddRef(g_cancellable_new());
|
||||
g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.WebExtensions", mCancellable,
|
||||
&NativeMessagingPortal::OnProxyReady, this);
|
||||
}
|
||||
|
||||
NativeMessagingPortal::~NativeMessagingPortal() {
|
||||
LOG_NMP("NativeMessagingPortal::~NativeMessagingPortal()");
|
||||
|
||||
g_cancellable_cancel(mCancellable);
|
||||
|
||||
// Close all active sessions
|
||||
for (const auto& it : mSessions) {
|
||||
if (it.second != SessionState::Active) {
|
||||
continue;
|
||||
}
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GDBusProxy> proxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync(
|
||||
G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
|
||||
"org.freedesktop.portal.Desktop", it.first.c_str(),
|
||||
"org.freedesktop.portal.Session", nullptr, getter_Transfers(error)));
|
||||
if (!proxy) {
|
||||
LOG_NMP("failed to get a D-Bus proxy: %s", error->message);
|
||||
LogError(__func__, *error);
|
||||
continue;
|
||||
}
|
||||
RefPtr<GVariant> res = dont_AddRef(
|
||||
g_dbus_proxy_call_sync(proxy, "Close", nullptr, G_DBUS_CALL_FLAGS_NONE,
|
||||
-1, nullptr, getter_Transfers(error)));
|
||||
if (!res) {
|
||||
LOG_NMP("failed to close session: %s", error->message);
|
||||
LogError(__func__, *error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
NativeMessagingPortal::ShouldUse(bool* aResult) {
|
||||
*aResult = widget::ShouldUsePortal(widget::PortalKind::NativeMessaging);
|
||||
LOG_NMP("will %sbe used", *aResult ? "" : "not ");
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
struct NativeMessagingPortal::DelayedCall {
|
||||
using DelayedMethodCall = void (NativeMessagingPortal::*)(dom::Promise&,
|
||||
GVariant*);
|
||||
|
||||
DelayedCall(DelayedMethodCall aCallback, dom::Promise& aPromise,
|
||||
GVariant* aArgs = nullptr)
|
||||
: callback(aCallback), promise(&aPromise), args(aArgs) {
|
||||
LOG_NMP("NativeMessagingPortal::DelayedCall::DelayedCall()");
|
||||
}
|
||||
~DelayedCall() {
|
||||
LOG_NMP("NativeMessagingPortal::DelayedCall::~DelayedCall()");
|
||||
}
|
||||
|
||||
DelayedMethodCall callback;
|
||||
RefPtr<dom::Promise> promise;
|
||||
RefPtr<GVariant> args;
|
||||
};
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnProxyReady(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
NativeMessagingPortal* self = static_cast<NativeMessagingPortal*>(user_data);
|
||||
GUniquePtr<GError> error;
|
||||
self->mProxy = dont_AddRef(
|
||||
g_dbus_proxy_new_for_bus_finish(result, getter_Transfers(error)));
|
||||
if (self->mProxy) {
|
||||
LOG_NMP("D-Bus proxy ready for name %s, path %s, interface %s",
|
||||
g_dbus_proxy_get_name(self->mProxy),
|
||||
g_dbus_proxy_get_object_path(self->mProxy),
|
||||
g_dbus_proxy_get_interface_name(self->mProxy));
|
||||
} else {
|
||||
LOG_NMP("failed to get a D-Bus proxy: %s", error->message);
|
||||
LogError(__func__, *error);
|
||||
}
|
||||
self->mInitialized = true;
|
||||
while (!self->mPending.empty()) {
|
||||
auto pending = std::move(self->mPending.front());
|
||||
self->mPending.pop_front();
|
||||
(self->*pending->callback)(*pending->promise, pending->args.get());
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
NativeMessagingPortal::GetAvailable(JSContext* aCx, dom::Promise** aPromise) {
|
||||
RefPtr<dom::Promise> promise;
|
||||
MOZ_TRY(GetPromise(aCx, promise));
|
||||
|
||||
if (mInitialized) {
|
||||
MaybeDelayedIsAvailable(*promise, nullptr);
|
||||
} else {
|
||||
auto delayed = MakeUnique<DelayedCall>(
|
||||
&NativeMessagingPortal::MaybeDelayedIsAvailable, *promise);
|
||||
mPending.push_back(std::move(delayed));
|
||||
}
|
||||
|
||||
promise.forget(aPromise);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void NativeMessagingPortal::MaybeDelayedIsAvailable(dom::Promise& aPromise,
|
||||
GVariant* aArgs) {
|
||||
MOZ_ASSERT(!aArgs);
|
||||
|
||||
bool available = false;
|
||||
if (mProxy) {
|
||||
RefPtr<GVariant> version =
|
||||
dont_AddRef(g_dbus_proxy_get_cached_property(mProxy, "version"));
|
||||
if (version) {
|
||||
if (g_variant_get_uint32(version) >= 1) {
|
||||
available = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_NMP("is %savailable", available ? "" : "not ");
|
||||
aPromise.MaybeResolve(available);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
NativeMessagingPortal::CreateSession(const nsACString& aApplication,
|
||||
JSContext* aCx, dom::Promise** aPromise) {
|
||||
RefPtr<dom::Promise> promise;
|
||||
MOZ_TRY(GetPromise(aCx, promise));
|
||||
|
||||
// Creating a session requires passing a unique token that will be used as the
|
||||
// suffix for the session handle, and it should be a valid D-Bus object path
|
||||
// component (i.e. it contains only the characters "[A-Z][a-z][0-9]_", see
|
||||
// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
|
||||
// and
|
||||
// https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Session).
|
||||
// The token should be unique and not guessable. To avoid clashes with calls
|
||||
// made from unrelated libraries, it is a good idea to use a per-library
|
||||
// prefix combined with a random number.
|
||||
// Here, we build the token by concatenating MOZ_APP_NAME (e.g. "firefox"),
|
||||
// with the name of the native application (sanitized to remove invalid
|
||||
// characters, see
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests),
|
||||
// and a random number.
|
||||
const nsCString& application = PromiseFlatCString(aApplication);
|
||||
GUniquePtr<gchar> sanitizedApplicationName(g_strdup(application.get()));
|
||||
g_strdelimit(sanitizedApplicationName.get(), ".", '_');
|
||||
GUniquePtr<gchar> token(g_strdup_printf("%s_%s_%u", MOZ_APP_NAME,
|
||||
sanitizedApplicationName.get(),
|
||||
g_random_int()));
|
||||
RefPtr<GVariant> args = dont_AddRef(g_variant_new_string(token.get()));
|
||||
|
||||
if (mInitialized) {
|
||||
MaybeDelayedCreateSession(*promise, args);
|
||||
} else {
|
||||
auto delayed = MakeUnique<DelayedCall>(
|
||||
&NativeMessagingPortal::MaybeDelayedCreateSession, *promise, args);
|
||||
mPending.push_back(std::move(delayed));
|
||||
}
|
||||
|
||||
promise.forget(aPromise);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void NativeMessagingPortal::MaybeDelayedCreateSession(dom::Promise& aPromise,
|
||||
GVariant* aArgs) {
|
||||
MOZ_ASSERT(g_variant_is_of_type(aArgs, G_VARIANT_TYPE_STRING));
|
||||
|
||||
if (!mProxy) {
|
||||
return aPromise.MaybeRejectWithOperationError(
|
||||
"No D-Bus proxy for the native messaging portal");
|
||||
}
|
||||
|
||||
LOG_NMP("creating session with handle suffix %s",
|
||||
g_variant_get_string(aArgs, nullptr));
|
||||
|
||||
GVariantBuilder options;
|
||||
g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
|
||||
g_variant_builder_add(&options, "{sv}", "session_handle_token",
|
||||
g_variant_ref_sink(aArgs));
|
||||
auto callbackData = MakeUnique<CallbackData>(aPromise);
|
||||
g_dbus_proxy_call(mProxy, "CreateSession", g_variant_new("(a{sv})", &options),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
|
||||
&NativeMessagingPortal::OnCreateSessionDone,
|
||||
callbackData.release());
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnCreateSessionDone(GObject* source,
|
||||
GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
GDBusProxy* proxy = G_DBUS_PROXY(source);
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GVariant> res = dont_AddRef(
|
||||
g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
|
||||
if (res) {
|
||||
RefPtr<GVariant> sessionHandle =
|
||||
dont_AddRef(g_variant_get_child_value(res, 0));
|
||||
gsize length;
|
||||
const char* value = g_variant_get_string(sessionHandle, &length);
|
||||
LOG_NMP("session created with handle %s", value);
|
||||
RefPtr<NativeMessagingPortal> portal = GetSingleton();
|
||||
portal->mSessions[value] = SessionState::Active;
|
||||
|
||||
GDBusConnection* connection = g_dbus_proxy_get_connection(proxy);
|
||||
// The "Closed" signal is emitted e.g. when the user denies access to the
|
||||
// native application when the shell prompts them.
|
||||
auto subscription_id_ptr = MakeUnique<guint>(0);
|
||||
*subscription_id_ptr = g_dbus_connection_signal_subscribe(
|
||||
connection, "org.freedesktop.portal.Desktop",
|
||||
"org.freedesktop.portal.Session", "Closed", value, nullptr,
|
||||
G_DBUS_SIGNAL_FLAGS_NONE, &NativeMessagingPortal::OnSessionClosedSignal,
|
||||
subscription_id_ptr.get(), [](gpointer aUserData) {
|
||||
UniquePtr<guint> release(reinterpret_cast<guint*>(aUserData));
|
||||
});
|
||||
Unused << subscription_id_ptr.release(); // Ownership transferred above.
|
||||
|
||||
callbackData->promise->MaybeResolve(nsDependentCString(value, length));
|
||||
} else {
|
||||
LOG_NMP("failed to create session: %s", error->message);
|
||||
LogError(__func__, *error);
|
||||
RejectPromiseWithErrorMessage(*callbackData->promise, *error);
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
NativeMessagingPortal::CloseSession(const nsACString& aHandle, JSContext* aCx,
|
||||
dom::Promise** aPromise) {
|
||||
const nsCString& sessionHandle = PromiseFlatCString(aHandle);
|
||||
|
||||
if (!g_variant_is_object_path(sessionHandle.get())) {
|
||||
LOG_NMP("cannot close session %s, invalid handle", sessionHandle.get());
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
auto sessionIterator = mSessions.find(sessionHandle.get());
|
||||
if (sessionIterator == mSessions.end()) {
|
||||
LOG_NMP("cannot close session %s, unknown handle", sessionHandle.get());
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (sessionIterator->second != SessionState::Active) {
|
||||
LOG_NMP("cannot close session %s, not active", sessionHandle.get());
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
RefPtr<dom::Promise> promise;
|
||||
MOZ_TRY(GetPromise(aCx, promise));
|
||||
|
||||
sessionIterator->second = SessionState::Closing;
|
||||
LOG_NMP("closing session %s", sessionHandle.get());
|
||||
auto callbackData = MakeUnique<CallbackData>(*promise, sessionHandle.get());
|
||||
g_dbus_proxy_new_for_bus(
|
||||
G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
|
||||
"org.freedesktop.portal.Desktop", sessionHandle.get(),
|
||||
"org.freedesktop.portal.Session", nullptr,
|
||||
&NativeMessagingPortal::OnCloseSessionProxyReady, callbackData.release());
|
||||
|
||||
promise.forget(aPromise);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnCloseSessionProxyReady(GObject* source,
|
||||
GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GDBusProxy> proxy = dont_AddRef(
|
||||
g_dbus_proxy_new_for_bus_finish(result, getter_Transfers(error)));
|
||||
if (!proxy) {
|
||||
LOG_NMP("failed to close session: %s", error->message);
|
||||
LogError(__func__, *error);
|
||||
return RejectPromiseWithErrorMessage(*callbackData->promise, *error);
|
||||
}
|
||||
|
||||
g_dbus_proxy_call(proxy, "Close", nullptr, G_DBUS_CALL_FLAGS_NONE, -1,
|
||||
nullptr, &NativeMessagingPortal::OnCloseSessionDone,
|
||||
callbackData.release());
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnCloseSessionDone(GObject* source,
|
||||
GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
GDBusProxy* proxy = G_DBUS_PROXY(source);
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
|
||||
RefPtr<NativeMessagingPortal> portal = GetSingleton();
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GVariant> res = dont_AddRef(
|
||||
g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
|
||||
if (res) {
|
||||
LOG_NMP("session %s closed", callbackData->sessionHandle.get());
|
||||
portal->mSessions.erase(callbackData->sessionHandle.get());
|
||||
callbackData->promise->MaybeResolve(NS_OK);
|
||||
} else {
|
||||
LOG_NMP("failed to close session %s: %s", callbackData->sessionHandle.get(),
|
||||
error->message);
|
||||
LogError(__func__, *error);
|
||||
portal->mSessions[callbackData->sessionHandle.get()] = SessionState::Error;
|
||||
RejectPromiseWithErrorMessage(*callbackData->promise, *error);
|
||||
}
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnSessionClosedSignal(
|
||||
GDBusConnection* bus, const gchar* sender_name, const gchar* object_path,
|
||||
const gchar* interface_name, const gchar* signal_name, GVariant* parameters,
|
||||
gpointer user_data) {
|
||||
guint subscription_id = *reinterpret_cast<guint*>(user_data);
|
||||
LOG_NMP("session %s was closed by the portal", object_path);
|
||||
g_dbus_connection_signal_unsubscribe(bus, subscription_id);
|
||||
RefPtr<NativeMessagingPortal> portal = GetSingleton();
|
||||
portal->mSessions.erase(object_path);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
NativeMessagingPortal::GetManifest(const nsACString& aHandle,
|
||||
const nsACString& aName,
|
||||
const nsACString& aExtension, JSContext* aCx,
|
||||
dom::Promise** aPromise) {
|
||||
const nsCString& sessionHandle = PromiseFlatCString(aHandle);
|
||||
const nsCString& name = PromiseFlatCString(aName);
|
||||
const nsCString& extension = PromiseFlatCString(aExtension);
|
||||
|
||||
if (!g_variant_is_object_path(sessionHandle.get())) {
|
||||
LOG_NMP("cannot find manifest for %s, invalid session handle %s",
|
||||
name.get(), sessionHandle.get());
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
auto sessionIterator = mSessions.find(sessionHandle.get());
|
||||
if (sessionIterator == mSessions.end()) {
|
||||
LOG_NMP("cannot find manifest for %s, unknown session handle %s",
|
||||
name.get(), sessionHandle.get());
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (sessionIterator->second != SessionState::Active) {
|
||||
LOG_NMP("cannot find manifest for %s, inactive session %s", name.get(),
|
||||
sessionHandle.get());
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (!mProxy) {
|
||||
LOG_NMP("cannot find manifest for %s, missing D-Bus proxy", name.get());
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
RefPtr<dom::Promise> promise;
|
||||
MOZ_TRY(GetPromise(aCx, promise));
|
||||
|
||||
auto callbackData = MakeUnique<CallbackData>(*promise, sessionHandle.get());
|
||||
g_dbus_proxy_call(
|
||||
mProxy, "GetManifest",
|
||||
g_variant_new("(oss)", sessionHandle.get(), name.get(), extension.get()),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
|
||||
&NativeMessagingPortal::OnGetManifestDone, callbackData.release());
|
||||
|
||||
promise.forget(aPromise);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnGetManifestDone(GObject* source,
|
||||
GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
GDBusProxy* proxy = G_DBUS_PROXY(source);
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GVariant> jsonManifest = dont_AddRef(
|
||||
g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
|
||||
if (jsonManifest) {
|
||||
jsonManifest = dont_AddRef(g_variant_get_child_value(jsonManifest, 0));
|
||||
gsize length;
|
||||
const char* value = g_variant_get_string(jsonManifest, &length);
|
||||
LOG_NMP("manifest found in session %s: %s",
|
||||
callbackData->sessionHandle.get(), value);
|
||||
callbackData->promise->MaybeResolve(nsDependentCString(value, length));
|
||||
} else {
|
||||
LOG_NMP("failed to find a manifest in session %s: %s",
|
||||
callbackData->sessionHandle.get(), error->message);
|
||||
LogError(__func__, *error);
|
||||
RejectPromiseWithErrorMessage(*callbackData->promise, *error);
|
||||
}
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
NativeMessagingPortal::Start(const nsACString& aHandle, const nsACString& aName,
|
||||
const nsACString& aExtension, JSContext* aCx,
|
||||
dom::Promise** aPromise) {
|
||||
const nsCString& sessionHandle = PromiseFlatCString(aHandle);
|
||||
const nsCString& name = PromiseFlatCString(aName);
|
||||
const nsCString& extension = PromiseFlatCString(aExtension);
|
||||
|
||||
if (!g_variant_is_object_path(sessionHandle.get())) {
|
||||
LOG_NMP("cannot start %s, invalid session handle %s", name.get(),
|
||||
sessionHandle.get());
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
auto sessionIterator = mSessions.find(sessionHandle.get());
|
||||
if (sessionIterator == mSessions.end()) {
|
||||
LOG_NMP("cannot start %s, unknown session handle %s", name.get(),
|
||||
sessionHandle.get());
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (sessionIterator->second != SessionState::Active) {
|
||||
LOG_NMP("cannot start %s, inactive session %s", name.get(),
|
||||
sessionHandle.get());
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (!mProxy) {
|
||||
LOG_NMP("cannot start %s, missing D-Bus proxy", name.get());
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
RefPtr<dom::Promise> promise;
|
||||
MOZ_TRY(GetPromise(aCx, promise));
|
||||
|
||||
auto callbackData = MakeUnique<CallbackData>(*promise, sessionHandle.get());
|
||||
auto* releasedCallbackData = callbackData.release();
|
||||
|
||||
LOG_NMP("starting %s, requested by %s in session %s", name.get(),
|
||||
extension.get(), sessionHandle.get());
|
||||
|
||||
GDBusConnection* connection = g_dbus_proxy_get_connection(mProxy);
|
||||
GUniquePtr<gchar> senderName(
|
||||
g_strdup(g_dbus_connection_get_unique_name(connection)));
|
||||
g_strdelimit(senderName.get(), ".", '_');
|
||||
GUniquePtr<gchar> handleToken(
|
||||
g_strdup_printf("%s/%d", MOZ_APP_NAME, g_random_int_range(0, G_MAXINT)));
|
||||
GUniquePtr<gchar> requestPath(
|
||||
g_strdup_printf("/org/freedesktop/portal/desktop/request/%s/%s",
|
||||
senderName.get() + 1, handleToken.get()));
|
||||
releasedCallbackData->subscription_id = g_dbus_connection_signal_subscribe(
|
||||
connection, "org.freedesktop.portal.Desktop",
|
||||
"org.freedesktop.portal.Request", "Response", requestPath.get(), nullptr,
|
||||
G_DBUS_SIGNAL_FLAGS_NONE,
|
||||
&NativeMessagingPortal::OnStartRequestResponseSignal,
|
||||
releasedCallbackData, nullptr);
|
||||
|
||||
auto callbackDataCopy =
|
||||
MakeUnique<CallbackData>(*promise, sessionHandle.get());
|
||||
GVariantBuilder options;
|
||||
g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
|
||||
g_variant_builder_add(&options, "{sv}", "handle_token",
|
||||
g_variant_new_string(handleToken.get()));
|
||||
g_dbus_proxy_call(mProxy, "Start",
|
||||
g_variant_new("(ossa{sv})", sessionHandle.get(), name.get(),
|
||||
extension.get(), &options),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, nullptr,
|
||||
&NativeMessagingPortal::OnStartDone,
|
||||
callbackDataCopy.release());
|
||||
|
||||
promise.forget(aPromise);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnStartDone(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
GDBusProxy* proxy = G_DBUS_PROXY(source);
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GVariant> handle = dont_AddRef(
|
||||
g_dbus_proxy_call_finish(proxy, result, getter_Transfers(error)));
|
||||
if (handle) {
|
||||
handle = dont_AddRef(g_variant_get_child_value(handle, 0));
|
||||
LOG_NMP(
|
||||
"native application start requested in session %s, pending response "
|
||||
"for %s",
|
||||
callbackData->sessionHandle.get(),
|
||||
g_variant_get_string(handle, nullptr));
|
||||
} else {
|
||||
LOG_NMP("failed to start native application in session %s: %s",
|
||||
callbackData->sessionHandle.get(), error->message);
|
||||
LogError(__func__, *error);
|
||||
RejectPromiseWithErrorMessage(*callbackData->promise, *error);
|
||||
}
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnStartRequestResponseSignal(
|
||||
GDBusConnection* bus, const gchar* sender_name, const gchar* object_path,
|
||||
const gchar* interface_name, const gchar* signal_name, GVariant* parameters,
|
||||
gpointer user_data) {
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
|
||||
LOG_NMP("got response signal for %s in session %s", object_path,
|
||||
callbackData->sessionHandle.get());
|
||||
g_dbus_connection_signal_unsubscribe(bus, callbackData->subscription_id);
|
||||
|
||||
RefPtr<GVariant> result =
|
||||
dont_AddRef(g_variant_get_child_value(parameters, 0));
|
||||
guint32 response = g_variant_get_uint32(result);
|
||||
// Possible values for response
|
||||
// (https://flatpak.github.io/xdg-desktop-portal/#gdbus-signal-org-freedesktop-portal-Request.Response):
|
||||
// 0: Success, the request is carried out
|
||||
// 1: The user cancelled the interaction
|
||||
// 2: The user interaction was ended in some other way
|
||||
if (response == 0) {
|
||||
LOG_NMP(
|
||||
"native application start successful in session %s, requesting file "
|
||||
"descriptors",
|
||||
callbackData->sessionHandle.get());
|
||||
RefPtr<NativeMessagingPortal> portal = GetSingleton();
|
||||
GVariantBuilder options;
|
||||
g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
|
||||
g_dbus_proxy_call_with_unix_fd_list(
|
||||
portal->mProxy.get(), "GetPipes",
|
||||
g_variant_new("(oa{sv})", callbackData->sessionHandle.get(), &options),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr,
|
||||
&NativeMessagingPortal::OnGetPipesDone, callbackData.release());
|
||||
} else if (response == 1) {
|
||||
LOG_NMP("native application start canceled by user in session %s",
|
||||
callbackData->sessionHandle.get());
|
||||
callbackData->promise->MaybeRejectWithAbortError(
|
||||
"Native application start canceled by user");
|
||||
} else {
|
||||
LOG_NMP("native application start failed in session %s",
|
||||
callbackData->sessionHandle.get());
|
||||
callbackData->promise->MaybeRejectWithNotFoundError(
|
||||
"Native application start failed");
|
||||
}
|
||||
}
|
||||
|
||||
static gint GetFD(const RefPtr<GVariant>& result, GUnixFDList* fds,
|
||||
gint index) {
|
||||
RefPtr<GVariant> value =
|
||||
dont_AddRef(g_variant_get_child_value(result, index));
|
||||
GUniquePtr<GError> error;
|
||||
gint fd = g_unix_fd_list_get(fds, g_variant_get_handle(value),
|
||||
getter_Transfers(error));
|
||||
if (fd == -1) {
|
||||
LOG_NMP("failed to get file descriptor at index %d: %s", index,
|
||||
error->message);
|
||||
LogError("GetFD", *error);
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* static */
|
||||
void NativeMessagingPortal::OnGetPipesDone(GObject* source,
|
||||
GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
GDBusProxy* proxy = G_DBUS_PROXY(source);
|
||||
UniquePtr<CallbackData> callbackData(static_cast<CallbackData*>(user_data));
|
||||
auto promise = callbackData->promise;
|
||||
|
||||
RefPtr<GUnixFDList> fds;
|
||||
GUniquePtr<GError> error;
|
||||
RefPtr<GVariant> pipes =
|
||||
dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish(
|
||||
proxy, getter_AddRefs(fds), result, getter_Transfers(error)));
|
||||
|
||||
if (!pipes) {
|
||||
LOG_NMP(
|
||||
"failed to get file descriptors for native application in session %s: "
|
||||
"%s",
|
||||
callbackData->sessionHandle.get(), error->message);
|
||||
LogError(__func__, *error);
|
||||
return RejectPromiseWithErrorMessage(*promise, *error);
|
||||
}
|
||||
|
||||
gint32 _stdin = GetFD(pipes, fds, 0);
|
||||
gint32 _stdout = GetFD(pipes, fds, 1);
|
||||
gint32 _stderr = GetFD(pipes, fds, 2);
|
||||
LOG_NMP(
|
||||
"got file descriptors for native application in session %s: (%d, %d, %d)",
|
||||
callbackData->sessionHandle.get(), _stdin, _stdout, _stderr);
|
||||
|
||||
if (_stdin == -1 || _stdout == -1 || _stderr == -1) {
|
||||
return promise->MaybeRejectWithOperationError("Invalid file descriptor");
|
||||
}
|
||||
|
||||
dom::AutoJSAPI jsapi;
|
||||
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
|
||||
return promise->MaybeRejectWithUnknownError(
|
||||
"Failed to initialize JS context");
|
||||
}
|
||||
JSContext* cx = jsapi.cx();
|
||||
|
||||
JS::Rooted<JSObject*> jsPipes(cx, JS_NewPlainObject(cx));
|
||||
if (!jsPipes) {
|
||||
return promise->MaybeRejectWithOperationError(
|
||||
"Failed to create a JS object to hold the file descriptors");
|
||||
}
|
||||
|
||||
auto setPipeProperty = [&](const char* name, int32_t value) {
|
||||
JS::Rooted<JS::Value> jsValue(cx, JS::Value::fromInt32(value));
|
||||
return JS_SetProperty(cx, jsPipes, name, jsValue);
|
||||
};
|
||||
if (!setPipeProperty("stdin", _stdin)) {
|
||||
return promise->MaybeRejectWithOperationError(
|
||||
"Failed to set the 'stdin' property on the JS object");
|
||||
}
|
||||
if (!setPipeProperty("stdout", _stdout)) {
|
||||
return promise->MaybeRejectWithOperationError(
|
||||
"Failed to set the 'stdout' property on the JS object");
|
||||
}
|
||||
if (!setPipeProperty("stderr", _stderr)) {
|
||||
return promise->MaybeRejectWithOperationError(
|
||||
"Failed to set the 'stderr' property on the JS object");
|
||||
}
|
||||
|
||||
promise->MaybeResolve(jsPipes);
|
||||
}
|
||||
|
||||
} // namespace mozilla::extensions
|
@ -1,76 +0,0 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_extensions_NativeMessagingPortal_h
|
||||
#define mozilla_extensions_NativeMessagingPortal_h
|
||||
|
||||
#include "nsINativeMessagingPortal.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include "mozilla/GRefPtr.h"
|
||||
#include "mozilla/UniquePtr.h"
|
||||
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace mozilla::extensions {
|
||||
|
||||
enum class SessionState { Active, Closing, Error };
|
||||
|
||||
class NativeMessagingPortal : public nsINativeMessagingPortal {
|
||||
public:
|
||||
NS_DECL_NSINATIVEMESSAGINGPORTAL
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
static already_AddRefed<NativeMessagingPortal> GetSingleton();
|
||||
|
||||
private:
|
||||
NativeMessagingPortal();
|
||||
virtual ~NativeMessagingPortal();
|
||||
|
||||
RefPtr<GDBusProxy> mProxy;
|
||||
bool mInitialized = false;
|
||||
RefPtr<GCancellable> mCancellable;
|
||||
|
||||
struct DelayedCall;
|
||||
std::deque<UniquePtr<DelayedCall>> mPending;
|
||||
|
||||
using SessionsMap = std::unordered_map<std::string, SessionState>;
|
||||
SessionsMap mSessions;
|
||||
|
||||
// Callbacks
|
||||
static void OnProxyReady(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
void MaybeDelayedIsAvailable(dom::Promise&, GVariant*);
|
||||
void MaybeDelayedCreateSession(dom::Promise&, GVariant*);
|
||||
static void OnCreateSessionDone(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
static void OnCloseSessionProxyReady(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
static void OnCloseSessionDone(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
static void OnSessionClosedSignal(GDBusConnection* bus,
|
||||
const gchar* sender_name,
|
||||
const gchar* object_path,
|
||||
const gchar* interface_name,
|
||||
const gchar* signal_name,
|
||||
GVariant* parameters, gpointer user_data);
|
||||
static void OnGetManifestDone(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
static void OnStartDone(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
static void OnStartRequestResponseSignal(
|
||||
GDBusConnection* bus, const gchar* sender_name, const gchar* object_path,
|
||||
const gchar* interface_name, const gchar* signal_name,
|
||||
GVariant* parameters, gpointer user_data);
|
||||
static void OnGetPipesDone(GObject* source, GAsyncResult* result,
|
||||
gpointer user_data);
|
||||
};
|
||||
|
||||
} // namespace mozilla::extensions
|
||||
|
||||
#endif // mozilla_extensions_NativeMessagingPortal_h
|
@ -14,15 +14,3 @@ Classes = [
|
||||
'categories': {'app-startup': 'ExtensionsChild'},
|
||||
},
|
||||
]
|
||||
|
||||
if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'gtk' and defined('MOZ_ENABLE_DBUS'):
|
||||
Classes += [
|
||||
{
|
||||
'cid': '{8a9a1406-d700-4221-8615-1d84b0d213fb}',
|
||||
'contract_ids': ['@mozilla.org/extensions/native-messaging-portal;1'],
|
||||
'singleton': True,
|
||||
'type': 'mozilla::extensions::NativeMessagingPortal',
|
||||
'constructor': 'mozilla::extensions::NativeMessagingPortal::GetSingleton',
|
||||
'headers': ['mozilla/extensions/NativeMessagingPortal.h'],
|
||||
},
|
||||
]
|
||||
|
@ -1,80 +0,0 @@
|
||||
Native messaging for a strictly-confined Firefox
|
||||
================================================
|
||||
|
||||
Rationale
|
||||
---------
|
||||
|
||||
Firefox, when packaged as a snap or flatpak, is confined in a way that the browser only has a very partial view of the host filesystem and limited capabilities.
|
||||
Because of this, when an extension attempts to use the `nativeMessaging API <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging>`_, the browser cannot locate the corresponding `native manifest <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests>`_, and it cannot launch the native messaging host (native application) either.
|
||||
Instead, it can use the `WebExtensions XDG desktop portal <https://github.com/flatpak/xdg-desktop-portal/pull/705>`_ (work in progress). The portal is responsible for mediating accesses to otherwise unavailable files on the host filesystem, prompting the user whether they want to allow a given extension to launch a given native application (and remembering the user's choice), and spawning the native application on behalf of the browser.
|
||||
The portal is browser-agnostic, although currently its only known use is in Firefox.
|
||||
|
||||
Workflow
|
||||
--------
|
||||
|
||||
When Firefox detects that it is running strictly confined, and if the value of the ``widget.use-xdg-desktop-portal.native-messaging`` preference is ≠ ``0``, it queries the existence of the WebExtensions portal on the D-Bus session bus. If the portal is not available, native messaging will not work (a generic error is reported). A value of ``1`` will enable the portal, while a value of ``2`` will try to autodetect its use.
|
||||
|
||||
If the portal is available, Firefox starts by creating a session (`CreateSession method <https://github.com/jhenstridge/xdg-desktop-portal/blob/557d3c1b22ce393358d2fecb6862566321a57983/data/org.freedesktop.portal.WebExtensions.xml#L31>`_). The resulting Session object will be used to communicate with the portal until it is closed (`Close method <https://flatpak.github.io/xdg-desktop-portal/#gdbus-method-org-freedesktop-portal-Session.Close>`_).
|
||||
|
||||
Firefox then calls `the GetManifest method <https://github.com/jhenstridge/xdg-desktop-portal/blob/557d3c1b22ce393358d2fecb6862566321a57983/data/org.freedesktop.portal.WebExtensions.xml#L58>`_ on the portal, and the portal looks up a host manifest matching the name of the native application and the extension ID, and returns the JSON manifest, which Firefox can use to do its own validation before pursuing.
|
||||
|
||||
Firefox then calls `the Start method <https://github.com/jhenstridge/xdg-desktop-portal/blob/557d3c1b22ce393358d2fecb6862566321a57983/data/org.freedesktop.portal.WebExtensions.xml#L74>`_ on the Session object, which creates and returns `a Request object <https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Request>`_. The portal asynchronously spawns the native application and emits `the Response signal <https://flatpak.github.io/xdg-desktop-portal/#gdbus-signal-org-freedesktop-portal-Request.Response>`_ on the Request object.
|
||||
|
||||
Firefox then calls `the GetPipes method <https://github.com/jhenstridge/xdg-desktop-portal/blob/557d3c1b22ce393358d2fecb6862566321a57983/data/org.freedesktop.portal.WebExtensions.xml#L109>`_ on the portal, which returns open file descriptors for stdin, stdout and stderr of the spawned process.
|
||||
|
||||
From that point on, Firefox can talk to the native process exactly as it does when running unconfined (i.e. when it is responsible for launching the process itself).
|
||||
|
||||
Closing the session will have the portal terminate the native process cleanly.
|
||||
|
||||
From an end user's perspective, assuming the portal is present and in use, the only visible difference is going to be a one-time prompt for each extension requesting to launch a given native application. There is currently no GUI tool to edit the saved authorizations, but there is a CLI tool (``flatpak permissions webextensions``, whose name is confusing because it's not flatpak-specific).
|
||||
|
||||
Implementation details
|
||||
----------------------
|
||||
|
||||
Some complexity that is specific to XDG desktop portals architecture is hidden away in the XPCOM interface used by Firefox to talk to the portal: the Request and Response objects aren't exposed (instead the relevant methods are asynchronous and return a Promise that resolves when the response has arrived), and the GetPipes method has been folded into the Start method.
|
||||
|
||||
A ``connectRunning()`` method was added to the ``Subprocess`` javascript module to wrap a process spawned externally. Interaction with a ``Process`` object created this way is limited to communication through its open file descriptors, the caller cannot kill or wait on the process.
|
||||
|
||||
Extensions with the "nativeMessaging" permission should know nothing about the underlying mechanism used to talk to native applications, so it is important that the errors thrown in this separate code path aren't distinguishable from the generic errors thrown in the usual code path where the browser is responsible for managing the lifecycle of the native applications itself.
|
||||
|
||||
Debugging via ``MOZ_LOG`` environment variable or ``about:logging`` can be triggered with the log module ``NativeMessagingPortal``. It will enable more verbose logs to be emitted by the Firefox side of the portal client implementation.
|
||||
|
||||
The ``IDL`` interface to the portal is ``nsINativeMessagingPortal``.
|
||||
|
||||
|
||||
Future work
|
||||
-----------
|
||||
|
||||
The WebExtensions portal isn't widely available yet in a release of the XDG desktop portals project, however an agreement in principle was reached with its maintainers, pending minor changes to the current implementation, and the goal is to land it with the next stable release, 1.18.
|
||||
In the meantime, the portal has been available in Ubuntu `as a distro patch <https://launchpad.net/bugs/1968215>`_ starting with release 22.04.
|
||||
|
||||
The functionality is exercised with XPCShell tests that mock the portal's DBus interface. There are currently no integration tests that exercise the real portal.
|
||||
|
||||
Security Considerations
|
||||
_______________________
|
||||
|
||||
Baseline
|
||||
~~~~~~~~
|
||||
|
||||
Without confinement, the following stakeholders are relevant. The tree structure reflects the relative trust between components:
|
||||
|
||||
- User
|
||||
+ Browser
|
||||
o Extension run by browser
|
||||
+ Installer of native messaging host (native manifest and native app)
|
||||
o Native messaging host ("native app")
|
||||
|
||||
The browser is responsible for mediating access from the extension to native apps. This includes verifying the extension ID and user consent to the nativeMessaging permission, and verifying that the native manifest permits access to the extension. If permitted, the browser is expected to relay messages between the extension and the native app.
|
||||
The native app is distrustful by default, and is only willing to accept messages from extensions that have been allowlisted in the native manifest.
|
||||
The extension, the native app and its installer are commonly (but not always!) provided by the same developer. E.g. a distro may create a package for installation independently of the original NMH developer.
|
||||
|
||||
Installation to the user profile or even system directories require privileges. Therefore the browser and NMH installer are considered trusted by the user.
|
||||
|
||||
Without confinement, the native app runs with the same privileges as the browser, because it's the default behavior when an external process is launched. The trust of the browser in the native app is acceptable because the existence of a registered native messaging host implies that the user has at some point consented to write access to the user profile or at a system location. Conversely, the native app trusts the browser implicitly, because the intent to communicate with the browser was declared by the existence of the native manifest.
|
||||
|
||||
With confinement
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
With confinement of the browser, the browser cannot solely act as a mediator to launch the native app, because of the restricted access to the filesystem and ability to launch external applications. These two tasks have been delegated to the portal.
|
||||
|
||||
The portal will launch external applications as specified in the native messaging manifest. To avoid sandbox escapes and privilege escalation, the host system should make sure that the native messaging manifests and referenced applications cannot be modified by the confined browser. At the time of writing, this concern was accounted for in `the portal implementation <https://github.com/flatpak/xdg-desktop-portal/pull/705#issuecomment-2262776394>`_.
|
@ -77,7 +77,6 @@ XPIDL_SOURCES += [
|
||||
"extIWebNavigation.idl",
|
||||
"mozIExtensionAPIRequestHandling.idl",
|
||||
"mozIExtensionProcessScript.idl",
|
||||
"nsINativeMessagingPortal.idl",
|
||||
]
|
||||
|
||||
XPIDL_MODULE = "webextensions"
|
||||
@ -104,13 +103,6 @@ UNIFIED_SOURCES += [
|
||||
"WebExtensionPolicy.cpp",
|
||||
]
|
||||
|
||||
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk" and CONFIG["MOZ_ENABLE_DBUS"]:
|
||||
EXPORTS.mozilla.extensions += ["NativeMessagingPortal.h"]
|
||||
UNIFIED_SOURCES += ["NativeMessagingPortal.cpp"]
|
||||
CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"]
|
||||
CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
|
||||
DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"]
|
||||
|
||||
XPCOM_MANIFESTS += [
|
||||
"components.conf",
|
||||
]
|
||||
|
@ -1,86 +0,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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
/**
|
||||
* An interface to talk to the WebExtensions XDG desktop portal,
|
||||
* for sandboxed browsers (e.g. packaged as a snap or a flatpak).
|
||||
* See https://github.com/flatpak/xdg-desktop-portal/issues/655.
|
||||
*/
|
||||
[scriptable, builtinclass, uuid(7c3003e8-6d10-46cc-b754-70cd889871e7)]
|
||||
interface nsINativeMessagingPortal : nsISupports
|
||||
{
|
||||
/**
|
||||
* Whether client code should use the portal, or fall back to the "legacy"
|
||||
* implementation that spawns and communicates directly with native
|
||||
* applications.
|
||||
*/
|
||||
boolean shouldUse();
|
||||
|
||||
/**
|
||||
* Whether the portal is available and can be talked to. It is an error to
|
||||
* call other methods in this interface if the portal isn't available.
|
||||
*
|
||||
* @returns Promise that resolves with a boolean that reflects
|
||||
the availability of the portal.
|
||||
*/
|
||||
[implicit_jscontext]
|
||||
readonly attribute Promise available;
|
||||
|
||||
/**
|
||||
* Create a native messaging session.
|
||||
*
|
||||
* @param aApplication The name of the native application which the portal is
|
||||
* being requested to talk to. See
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests.
|
||||
*
|
||||
* @returns Promise that resolves with a string that represents the
|
||||
session handle (a D-Bus object path of the form
|
||||
/org/freedesktop/portal/desktop/session/SENDER/TOKEN).
|
||||
*/
|
||||
[implicit_jscontext]
|
||||
Promise createSession(in ACString aApplication);
|
||||
|
||||
/**
|
||||
* Close a previously open session.
|
||||
*
|
||||
* @param aHandle The handle of a valid session.
|
||||
*
|
||||
* @returns Promise that resolves when the session is successfully closed.
|
||||
*/
|
||||
[implicit_jscontext]
|
||||
Promise closeSession(in ACString aHandle);
|
||||
|
||||
/**
|
||||
* Find and return the JSON manifest for the named native messaging server
|
||||
* as a string. This allows the browser to validate the manifest before
|
||||
* deciding to start the server.
|
||||
*
|
||||
* @param aHandle The handle of a valid session.
|
||||
* @param aName The name of the native messaging server to start.
|
||||
* @param aExtension The ID of the extension that issues the request.
|
||||
*
|
||||
* @returns Promise that resolves with an UTF8-encoded string containing
|
||||
the raw JSON manifest.
|
||||
*/
|
||||
[implicit_jscontext]
|
||||
Promise getManifest(in ACString aHandle, in ACString aName, in ACString aExtension);
|
||||
|
||||
/**
|
||||
* Start the named native messaging server, in a previously open session.
|
||||
* The caller must indicate the requesting web extension (by extension ID).
|
||||
*
|
||||
* @param aHandle The handle of a valid session.
|
||||
* @param aName The name of the native messaging server to start.
|
||||
* @param aExtension The ID of the extension that issues the request.
|
||||
*
|
||||
* @returns Promise that resolves with an object that has 'stdin', 'stdout'
|
||||
and 'stderr' attributes for the open file descriptors that the
|
||||
caller can use to communicate with the native application once
|
||||
successfully started.
|
||||
*/
|
||||
[implicit_jscontext]
|
||||
Promise start(in ACString aHandle, in ACString aName, in ACString aExtension);
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
# dbus and dbusmock are needed for some xpcshell tests on linux
|
||||
# e.g. toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_portal.js
|
||||
dbus-python==1.3.2
|
||||
python-dbusmock==0.32.2
|
@ -16,8 +16,4 @@ run-sequentially = "very high failure rate in parallel"
|
||||
["test_ext_native_messaging_perf.js"]
|
||||
skip-if = ["tsan"] # Unreasonably slow, bug 1612707
|
||||
|
||||
["test_ext_native_messaging_portal.js"]
|
||||
run-if = ["os == 'linux' && toolkit == 'gtk' && dbus_enabled"]
|
||||
tags = "portal"
|
||||
|
||||
["test_ext_native_messaging_unresponsive.js"]
|
||||
|
@ -1,397 +0,0 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
|
||||
});
|
||||
|
||||
AddonTestUtils.init(this);
|
||||
AddonTestUtils.overrideCertDB();
|
||||
AddonTestUtils.createAppInfo(
|
||||
"xpcshell@tests.mozilla.org",
|
||||
"XPCShell",
|
||||
"1",
|
||||
"42"
|
||||
);
|
||||
|
||||
// Helpful documentation on the WebExtensions portal that is being tested here:
|
||||
// - feature request: https://github.com/flatpak/xdg-desktop-portal/issues/655
|
||||
// - pull request: https://github.com/flatpak/xdg-desktop-portal/pull/705
|
||||
// - D-Bus API: https://github.com/jhenstridge/xdg-desktop-portal/blob/native-messaging-portal/data/org.freedesktop.portal.WebExtensions.xml
|
||||
|
||||
const SESSION_HANDLE =
|
||||
"/org/freedesktop/portal/desktop/session/foobar/firefox_xpcshell_tests_mozilla_org_42";
|
||||
|
||||
const portalBusName = "org.freedesktop.portal.Desktop";
|
||||
const portalObjectPath = "/org/freedesktop/portal/desktop";
|
||||
const portalInterfaceName = "org.freedesktop.portal.WebExtensions";
|
||||
const sessionInterfaceName = "org.freedesktop.portal.Session";
|
||||
const dbusMockInterface = "org.freedesktop.DBus.Mock";
|
||||
const addObjectMethod = `${dbusMockInterface}.AddObject`;
|
||||
const addMethodMethod = `${dbusMockInterface}.AddMethod`;
|
||||
const addPropertyMethod = `${dbusMockInterface}.AddProperty`;
|
||||
const updatePropertiesMethod = `${dbusMockInterface}.UpdateProperties`;
|
||||
const emitSignalDetailedMethod = `${dbusMockInterface}.EmitSignalDetailed`;
|
||||
const getCallsMethod = `${dbusMockInterface}.GetCalls`;
|
||||
const clearCallsMethod = `${dbusMockInterface}.ClearCalls`;
|
||||
const resetMethod = `${dbusMockInterface}.Reset`;
|
||||
const mockRequestObjectPath = "/org/freedesktop/portal/desktop/request";
|
||||
const mockManifest =
|
||||
'{"name":"echo","description":"a native connector","type":"stdio","path":"/usr/bin/echo","allowed_extensions":["native@tests.mozilla.org"]}';
|
||||
const nativeMessagingPref = "widget.use-xdg-desktop-portal.native-messaging";
|
||||
|
||||
var DBUS_SESSION_BUS_ADDRESS = "";
|
||||
var DBUS_SESSION_BUS_PID = 0; // eslint-disable-line no-unused-vars
|
||||
var DBUS_MOCK = null;
|
||||
var FDS_MOCK = null;
|
||||
|
||||
async function background() {
|
||||
let port;
|
||||
browser.test.onMessage.addListener(async (what, payload) => {
|
||||
if (what == "request") {
|
||||
await browser.permissions.request({ permissions: ["nativeMessaging"] });
|
||||
// connectNative requires permission
|
||||
port = browser.runtime.connectNative("echo");
|
||||
port.onMessage.addListener(msg => {
|
||||
browser.test.sendMessage("message", msg);
|
||||
});
|
||||
browser.test.sendMessage("ready");
|
||||
} else if (what == "send") {
|
||||
if (payload._json) {
|
||||
let json = payload._json;
|
||||
payload.toJSON = () => json;
|
||||
delete payload._json;
|
||||
}
|
||||
port.postMessage(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function mockSetup(objectPath, methodName, args) {
|
||||
let mockProcess = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("gdbus"),
|
||||
arguments: [
|
||||
"call",
|
||||
"--session",
|
||||
"-d",
|
||||
portalBusName,
|
||||
"-o",
|
||||
objectPath,
|
||||
"-m",
|
||||
methodName,
|
||||
...args,
|
||||
],
|
||||
});
|
||||
return mockProcess.wait();
|
||||
}
|
||||
|
||||
add_setup(async function () {
|
||||
// Start and use a separate message bus for the tests, to not interfere with
|
||||
// the current's session message bus.
|
||||
let dbus = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("dbus-launch"),
|
||||
});
|
||||
let stdout = await dbus.stdout.readString();
|
||||
let lines = stdout.split("\n");
|
||||
for (let i in lines) {
|
||||
let tokens = lines[i].split("=");
|
||||
switch (tokens.shift()) {
|
||||
case "DBUS_SESSION_BUS_ADDRESS":
|
||||
DBUS_SESSION_BUS_ADDRESS = tokens.join("=");
|
||||
break;
|
||||
case "DBUS_SESSION_BUS_PID":
|
||||
DBUS_SESSION_BUS_PID = tokens.join();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
let prefValue = Services.prefs.getIntPref(nativeMessagingPref, 0);
|
||||
Services.prefs.setIntPref(nativeMessagingPref, 2);
|
||||
|
||||
Services.env.set("DBUS_SESSION_BUS_ADDRESS", DBUS_SESSION_BUS_ADDRESS);
|
||||
Services.env.set("GTK_USE_PORTAL", "1");
|
||||
|
||||
// dbusmock is used to mock the native messaging portal's D-Bus API.
|
||||
DBUS_MOCK = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("python3"),
|
||||
arguments: [
|
||||
"-m",
|
||||
"dbusmock",
|
||||
portalBusName,
|
||||
portalObjectPath,
|
||||
portalInterfaceName,
|
||||
],
|
||||
});
|
||||
|
||||
// When talking to the native messaging portal over D-Bus, it returns a tuple
|
||||
// of file descriptors. For the mock to work correctly, the file descriptors
|
||||
// must exist, so create a dummy process in order to use its stdin, stdout
|
||||
// and stderr file descriptors.
|
||||
FDS_MOCK = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("tail"),
|
||||
arguments: ["-f", "/dev/null"],
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
registerCleanupFunction(async function () {
|
||||
await FDS_MOCK.kill();
|
||||
await mockSetup(portalObjectPath, resetMethod, []);
|
||||
await DBUS_MOCK.kill();
|
||||
// XXX: While this works locally, it consistently fails when tests are run
|
||||
// in CI, with "xpcshell return code: -15". This needs to be investigated
|
||||
// further. This leaves a stray dbus-daemon process behind,
|
||||
// which isn't ideal, but is harmless.
|
||||
/*await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("kill"),
|
||||
arguments: ["-SIGQUIT", DBUS_SESSION_BUS_PID],
|
||||
});*/
|
||||
Services.prefs.setIntPref(nativeMessagingPref, prefValue);
|
||||
});
|
||||
|
||||
// Set up the mock objects and methods.
|
||||
await mockSetup(portalObjectPath, addPropertyMethod, [
|
||||
portalInterfaceName,
|
||||
"version",
|
||||
"<uint32 1>",
|
||||
]);
|
||||
await mockSetup(portalObjectPath, addMethodMethod, [
|
||||
portalInterfaceName,
|
||||
"CreateSession",
|
||||
"a{sv}",
|
||||
"o",
|
||||
`ret = "${SESSION_HANDLE}"`,
|
||||
]);
|
||||
await mockSetup(portalObjectPath, addObjectMethod, [
|
||||
SESSION_HANDLE,
|
||||
sessionInterfaceName,
|
||||
"@a{sv} {}",
|
||||
"@a(ssss) [('Close', '', '', '')]",
|
||||
]);
|
||||
await mockSetup(portalObjectPath, addMethodMethod, [
|
||||
portalInterfaceName,
|
||||
"GetManifest",
|
||||
"oss",
|
||||
"s",
|
||||
`ret = '${mockManifest}'`,
|
||||
]);
|
||||
await mockSetup(portalObjectPath, addMethodMethod, [
|
||||
portalInterfaceName,
|
||||
"Start",
|
||||
"ossa{sv}",
|
||||
"o",
|
||||
`ret = "${mockRequestObjectPath}/foobar"`,
|
||||
]);
|
||||
await mockSetup(portalObjectPath, addMethodMethod, [
|
||||
portalInterfaceName,
|
||||
"GetPipes",
|
||||
"oa{sv}",
|
||||
"hhh",
|
||||
`ret = (dbus.types.UnixFd(${FDS_MOCK.stdin.fd}), dbus.types.UnixFd(${FDS_MOCK.stdout.fd}), dbus.types.UnixFd(${FDS_MOCK.stderr.fd}))`,
|
||||
]);
|
||||
|
||||
optionalPermissionsPromptHandler.init();
|
||||
optionalPermissionsPromptHandler.acceptPrompt = true;
|
||||
await AddonTestUtils.promiseStartupManager();
|
||||
await setupHosts([]); // these tests don't use any native app script
|
||||
});
|
||||
|
||||
async function verifyDbusMockCall(objectPath, method, offset) {
|
||||
let getCalls = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("gdbus"),
|
||||
arguments: [
|
||||
"call",
|
||||
"--session",
|
||||
"-d",
|
||||
portalBusName,
|
||||
"-o",
|
||||
objectPath,
|
||||
"-m",
|
||||
getCallsMethod,
|
||||
],
|
||||
});
|
||||
let out = await getCalls.stdout.readString();
|
||||
out = out.match(/\((@a\(tsav\) )?\[(.*)\],\)/)[2];
|
||||
let calls = out.matchAll(/\(.*?\),?/g);
|
||||
let methodCalled = false;
|
||||
let params = {};
|
||||
let i = 0;
|
||||
for (let call of calls) {
|
||||
if (i++ < offset) {
|
||||
continue;
|
||||
}
|
||||
let matches = call[0].match(
|
||||
/\((uint64 )?(?<timestamp>\d+), '(?<method>\w+)', (@av )?\[(?<params>.*)\]\),?/
|
||||
);
|
||||
ok(parseFloat(matches.groups.timestamp), "timestamp is valid");
|
||||
if (matches.groups.method == method) {
|
||||
methodCalled = true;
|
||||
params = matches.groups.params;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (method) {
|
||||
ok(methodCalled, `The ${method} mock was called`);
|
||||
} else {
|
||||
equal(i, 0, "No method mock was called");
|
||||
}
|
||||
return { offset: i, params: params };
|
||||
}
|
||||
|
||||
add_task(async function test_talk_to_portal() {
|
||||
await mockSetup(portalObjectPath, clearCallsMethod, []);
|
||||
|
||||
// Make sure the portal is considered available
|
||||
await mockSetup(portalObjectPath, updatePropertiesMethod, [
|
||||
portalInterfaceName,
|
||||
"{'version': <uint32 1>}",
|
||||
]);
|
||||
|
||||
// dbusmock's logging output doesn't reveal the sender name,
|
||||
// so run dbus-monitor in parallel. The sender name is needed to build the
|
||||
// object path of the Request that is returned by the portal's Start method.
|
||||
let dbusMonitor = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("dbus-monitor"),
|
||||
arguments: [
|
||||
"--session",
|
||||
`interface='${portalInterfaceName}', member='CreateSession'`,
|
||||
],
|
||||
});
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
background,
|
||||
manifest: {
|
||||
applications: { gecko: { id: ID } },
|
||||
optional_permissions: ["nativeMessaging"],
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await withHandlingUserInput(extension, async () => {
|
||||
extension.sendMessage("request");
|
||||
await extension.awaitMessage("ready");
|
||||
});
|
||||
|
||||
let handleToken = "";
|
||||
let senderName = "";
|
||||
|
||||
// Verify that starting the extension talks to the mock native messaging
|
||||
// portal (i.e. CreateSession and Start are called with the expected
|
||||
// arguments).
|
||||
let result = await verifyDbusMockCall(portalObjectPath, "CreateSession", 0);
|
||||
result = await verifyDbusMockCall(
|
||||
portalObjectPath,
|
||||
"GetManifest",
|
||||
result.offset
|
||||
);
|
||||
result = await verifyDbusMockCall(portalObjectPath, "Start", result.offset);
|
||||
let match = result.params.match(/{'handle_token': <'(?<token>.*)'>}/);
|
||||
ok(match, "Start arguments contain a handle token");
|
||||
handleToken = match.groups.token;
|
||||
|
||||
// Extract the sender name from the dbus-monitor process's output.
|
||||
let dbusMonitorOutput = await dbusMonitor.stdout.readString();
|
||||
let lines = dbusMonitorOutput.split("\n");
|
||||
for (let i in lines) {
|
||||
let line = lines[i];
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("method call")) {
|
||||
let match = line.match(/sender=(\S*)/);
|
||||
ok(match, "dbus-monitor output informs us of the sender");
|
||||
senderName = match[1];
|
||||
}
|
||||
}
|
||||
ok(senderName, "Got the sender name");
|
||||
await dbusMonitor.kill();
|
||||
|
||||
// Mock the Request object that is expected to be created in response to
|
||||
// calling the Start method on the native messaging portal, wait for it to be
|
||||
// available, and emit its Response signal.
|
||||
let requestPath = `${mockRequestObjectPath}/${senderName
|
||||
.slice(1)
|
||||
.replace(".", "_")}/${handleToken}`;
|
||||
await mockSetup(portalObjectPath, addObjectMethod, [
|
||||
requestPath,
|
||||
"org.freedesktop.portal.Request",
|
||||
"@a{sv} {}",
|
||||
"@a(ssss) []",
|
||||
]);
|
||||
let waitForRequestObject = await lazy.Subprocess.call({
|
||||
command: await lazy.Subprocess.pathSearch("gdbus"),
|
||||
arguments: [
|
||||
"introspect",
|
||||
"--session",
|
||||
"-d",
|
||||
portalBusName,
|
||||
"-o",
|
||||
requestPath,
|
||||
"-p",
|
||||
],
|
||||
});
|
||||
await waitForRequestObject.wait();
|
||||
await mockSetup(requestPath, emitSignalDetailedMethod, [
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
"ua{sv}",
|
||||
"[<uint32 0>, <@a{sv} {}>]",
|
||||
`{'destination': <'${senderName}'>}`,
|
||||
]);
|
||||
|
||||
// Verify that the GetPipes method of the native messaging portal mock was
|
||||
// called as expected after the Start request completed.
|
||||
await verifyDbusMockCall(portalObjectPath, "GetPipes", result.offset);
|
||||
|
||||
await extension.unload();
|
||||
|
||||
// Verify that the native messaging portal session is properly closed when
|
||||
// the extension is unloaded.
|
||||
await verifyDbusMockCall(SESSION_HANDLE, "Close", 0);
|
||||
});
|
||||
|
||||
add_task(async function test_portal_unavailable() {
|
||||
await mockSetup(portalObjectPath, clearCallsMethod, []);
|
||||
|
||||
// Make sure the portal is NOT considered available
|
||||
await mockSetup(portalObjectPath, updatePropertiesMethod, [
|
||||
portalInterfaceName,
|
||||
"{'version': <uint32 0>}",
|
||||
]);
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
background,
|
||||
manifest: {
|
||||
applications: { gecko: { id: ID } },
|
||||
optional_permissions: ["nativeMessaging"],
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
});
|
||||
|
||||
let logged = false;
|
||||
function listener(msg) {
|
||||
logged ||= /Native messaging portal is not available/.test(msg.message);
|
||||
}
|
||||
Services.console.registerListener(listener);
|
||||
registerCleanupFunction(() => {
|
||||
Services.console.unregisterListener(listener);
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await withHandlingUserInput(extension, async () => {
|
||||
extension.sendMessage("request");
|
||||
await extension.awaitMessage("ready");
|
||||
});
|
||||
|
||||
ok(logged, "Non availability of the portal was logged");
|
||||
|
||||
// Verify that the native messaging portal wasn't talked to,
|
||||
// because it advertised itself as not available.
|
||||
await verifyDbusMockCall(portalObjectPath, null, 0);
|
||||
|
||||
await extension.unload();
|
||||
});
|
@ -150,100 +150,6 @@ function lookupApplication(app, ctx) {
|
||||
return NativeManifests.lookupManifest("stdio", app, ctx);
|
||||
}
|
||||
|
||||
add_task(async function test_parse_good_manifest() {
|
||||
let manifest = await NativeManifests.parseManifest(
|
||||
"stdio",
|
||||
"/some/path",
|
||||
"test",
|
||||
context,
|
||||
templateManifest
|
||||
);
|
||||
deepEqual(
|
||||
manifest,
|
||||
templateManifest,
|
||||
"parseManifest returns the manifest contents"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_parse_invalid_manifest() {
|
||||
function matchLastConsoleMessage(regex) {
|
||||
ok(Services.console.getMessageArray().pop().message.match(regex));
|
||||
}
|
||||
|
||||
equal(
|
||||
null,
|
||||
await NativeManifests.parseManifest(
|
||||
"pkcs11",
|
||||
"/some/path",
|
||||
"test",
|
||||
context,
|
||||
templateManifest
|
||||
)
|
||||
);
|
||||
matchLastConsoleMessage(
|
||||
/Native manifest \/some\/path has type property stdio \(expected pkcs11\)/
|
||||
);
|
||||
|
||||
equal(
|
||||
null,
|
||||
await NativeManifests.parseManifest(
|
||||
"stdio",
|
||||
"/some/path",
|
||||
"foobar",
|
||||
context,
|
||||
templateManifest
|
||||
)
|
||||
);
|
||||
matchLastConsoleMessage(
|
||||
/Native manifest \/some\/path has name property test \(expected foobar\)/
|
||||
);
|
||||
|
||||
const incompleteManifest = { ...templateManifest };
|
||||
delete incompleteManifest.description;
|
||||
equal(
|
||||
null,
|
||||
await NativeManifests.parseManifest(
|
||||
"stdio",
|
||||
"/some/path",
|
||||
"test",
|
||||
context,
|
||||
incompleteManifest
|
||||
)
|
||||
);
|
||||
matchLastConsoleMessage(/Value must either: match the pattern/);
|
||||
|
||||
const unauthorizedManifest = { ...templateManifest };
|
||||
unauthorizedManifest.allowed_extensions = [];
|
||||
equal(
|
||||
null,
|
||||
await NativeManifests.parseManifest(
|
||||
"stdio",
|
||||
"/some/path",
|
||||
"test",
|
||||
context,
|
||||
unauthorizedManifest
|
||||
)
|
||||
);
|
||||
matchLastConsoleMessage(
|
||||
/Value must either: .allowed_extensions must have at least 1 items/
|
||||
);
|
||||
|
||||
unauthorizedManifest.allowed_extensions = ["unauthorized@tests.mozilla.org"];
|
||||
equal(
|
||||
null,
|
||||
await NativeManifests.parseManifest(
|
||||
"stdio",
|
||||
"/some/path",
|
||||
"test",
|
||||
context,
|
||||
unauthorizedManifest
|
||||
)
|
||||
);
|
||||
matchLastConsoleMessage(
|
||||
/This extension does not have permission to use native manifest \/some\/path/
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_nonexistent_manifest() {
|
||||
let result = await lookupApplication("test", context);
|
||||
equal(
|
||||
|
@ -188,19 +188,6 @@ export var Subprocess = {
|
||||
let path = lazy.SubprocessImpl.pathSearch(command, environment);
|
||||
return Promise.resolve(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Connect to an already-running subprocess
|
||||
* given the file descriptors for its stdin, stdout and stderr.
|
||||
*
|
||||
* @param {int[]} [fds]
|
||||
* A list of three file descriptors [stdin, stdout, stderr].
|
||||
*
|
||||
* @returns {Process}
|
||||
*/
|
||||
connectRunning(fds) {
|
||||
return lazy.SubprocessImpl.connectRunning(fds);
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(Subprocess, SubprocessConstants);
|
||||
|
@ -581,22 +581,13 @@ export class BaseProcess {
|
||||
*/
|
||||
this.pid = pid;
|
||||
|
||||
/**
|
||||
* @property {boolean} managed
|
||||
* Whether the process is externally managed, or spawned by us.
|
||||
* @readonly
|
||||
*/
|
||||
this.managed = pid == 0;
|
||||
|
||||
this.exitCode = null;
|
||||
|
||||
this.exitPromise = new Promise(resolve => {
|
||||
if (!this.managed) {
|
||||
this.worker.call("wait", [this.id]).then(({ exitCode }) => {
|
||||
resolve(Object.freeze({ exitCode }));
|
||||
this.exitCode = exitCode;
|
||||
});
|
||||
}
|
||||
this.worker.call("wait", [this.id]).then(({ exitCode }) => {
|
||||
resolve(Object.freeze({ exitCode }));
|
||||
this.exitCode = exitCode;
|
||||
});
|
||||
});
|
||||
|
||||
if (fds[0] !== undefined) {
|
||||
@ -644,16 +635,6 @@ export class BaseProcess {
|
||||
});
|
||||
}
|
||||
|
||||
static fromRunning(options) {
|
||||
let worker = this.getWorker();
|
||||
|
||||
return worker
|
||||
.call("connectRunning", [options])
|
||||
.then(({ processId, fds }) => {
|
||||
return new this(worker, processId, fds, 0);
|
||||
});
|
||||
}
|
||||
|
||||
static get WORKER_URL() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
@ -691,10 +672,6 @@ export class BaseProcess {
|
||||
* has exited.
|
||||
*/
|
||||
kill(timeout = 300) {
|
||||
if (this.managed) {
|
||||
throw new Error("Cannot kill a process managed externally");
|
||||
}
|
||||
|
||||
// If the process has already exited, don't bother sending a signal.
|
||||
if (this.exitCode != null) {
|
||||
return this.wait();
|
||||
@ -729,9 +706,6 @@ export class BaseProcess {
|
||||
* method.
|
||||
*/
|
||||
wait() {
|
||||
if (this.managed) {
|
||||
throw new Error("Cannot wait on a process managed externally");
|
||||
}
|
||||
return this.exitPromise;
|
||||
}
|
||||
}
|
||||
|
@ -198,10 +198,6 @@ var SubprocessUnix = {
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
},
|
||||
|
||||
connectRunning(options) {
|
||||
return Process.fromRunning(options);
|
||||
},
|
||||
};
|
||||
|
||||
export var SubprocessImpl = SubprocessUnix;
|
||||
|
@ -405,22 +405,6 @@ class Process extends BaseProcess {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to an already running process that was spawned externally.
|
||||
*
|
||||
* @param {object} options
|
||||
An object with a 'fds' attribute that's an array
|
||||
of file descriptors (stdin, stdout and stderr).
|
||||
*/
|
||||
connectRunning(options) {
|
||||
this.pid = 0;
|
||||
this.pipes = [];
|
||||
this.pipes.push(new OutputPipe(this, unix.Fd(options.fds[0])));
|
||||
this.pipes.push(new InputPipe(this, unix.Fd(options.fds[1])));
|
||||
this.pipes.push(new InputPipe(this, unix.Fd(options.fds[2])));
|
||||
// Not creating a poll fd here, because this process is managed externally.
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when input is available on our sentinel file descriptor.
|
||||
*
|
||||
@ -473,9 +457,7 @@ class Process extends BaseProcess {
|
||||
this.exitCode = unix.WEXITSTATUS(status.value);
|
||||
}
|
||||
|
||||
if (this.fd !== undefined) {
|
||||
this.fd.dispose();
|
||||
}
|
||||
this.fd.dispose();
|
||||
io.updatePollFds();
|
||||
this.resolveExit(this.exitCode);
|
||||
return this.exitCode;
|
||||
@ -539,9 +521,7 @@ io = {
|
||||
let handlers = [
|
||||
this.signal,
|
||||
...this.pipes.values(),
|
||||
// Filter out processes without a poll fd, because those are managed
|
||||
// externally, not spawned by us.
|
||||
...Array.from(this.processes.values()).filter(p => p.fd !== undefined),
|
||||
...this.processes.values(),
|
||||
];
|
||||
|
||||
handlers = handlers.filter(handler => handler.pollEvents);
|
||||
|
@ -168,12 +168,6 @@ var SubprocessWin = {
|
||||
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
|
||||
throw error;
|
||||
},
|
||||
|
||||
connectRunning(_options) {
|
||||
// Not relevant (yet?) on Windows. This is currently used only on Unix
|
||||
// for native messaging through the WebExtensions portal.
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
};
|
||||
|
||||
export var SubprocessImpl = SubprocessWin;
|
||||
|
@ -601,12 +601,6 @@ class Process extends BaseProcess {
|
||||
libc.CloseHandle(procInfo.hThread);
|
||||
}
|
||||
|
||||
connectRunning(_options) {
|
||||
// Not relevant (yet?) on Windows. This is currently used only on Unix
|
||||
// for native messaging through the WebExtensions portal.
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when our process handle is signaled as active, meaning the process
|
||||
* has exited.
|
||||
|
@ -59,11 +59,7 @@ class BaseProcess {
|
||||
this.pid = null;
|
||||
this.pipes = [];
|
||||
|
||||
if (options.managed) {
|
||||
this.connectRunning(options);
|
||||
} else {
|
||||
this.spawn(options);
|
||||
}
|
||||
this.spawn(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,7 +96,6 @@ let requests = {
|
||||
},
|
||||
|
||||
spawn(options) {
|
||||
options.managed = false;
|
||||
let process = new Process(options);
|
||||
let processId = process.id;
|
||||
|
||||
@ -111,18 +106,6 @@ let requests = {
|
||||
return { data: { processId, fds, pid: process.pid } };
|
||||
},
|
||||
|
||||
connectRunning(fds) {
|
||||
let options = {};
|
||||
options.managed = true;
|
||||
options.fds = fds;
|
||||
let process = new Process(options);
|
||||
let processId = process.id;
|
||||
|
||||
io.addProcess(process);
|
||||
|
||||
return { data: { processId, fds: process.pipes.map(pipe => pipe.id) } };
|
||||
},
|
||||
|
||||
kill(processId, force = false) {
|
||||
let process = io.getProcess(processId);
|
||||
|
||||
@ -179,18 +162,6 @@ let requests = {
|
||||
Array.from(io.processes.values(), proc => proc.awaitFinished())
|
||||
);
|
||||
},
|
||||
|
||||
getFds(processId) {
|
||||
let process = io.getProcess(processId);
|
||||
let pipes = process.pipes;
|
||||
return {
|
||||
data: [
|
||||
pipes[0].fd.toString(),
|
||||
pipes[1].fd.toString(),
|
||||
pipes[2].fd.toString(),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
onmessage = event => {
|
||||
|
@ -855,83 +855,6 @@ add_task(async function test_bad_executable() {
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_subprocess_connectRunning() {
|
||||
if (AppConstants.platform != "win") {
|
||||
let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||
tempFile.append("test-subprocess-connectRunning.txt");
|
||||
if (tempFile.exists()) {
|
||||
tempFile.remove(true);
|
||||
}
|
||||
registerCleanupFunction(async function () {
|
||||
tempFile.remove(true);
|
||||
});
|
||||
|
||||
let running = await Subprocess.call({
|
||||
command: await Subprocess.pathSearch("tee"),
|
||||
arguments: [tempFile.path],
|
||||
environment: {},
|
||||
stderr: "pipe",
|
||||
});
|
||||
equal(
|
||||
running.managed,
|
||||
false,
|
||||
"A process spawned by us is not externally managed"
|
||||
);
|
||||
let { getSubprocessImplForTest } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Subprocess.sys.mjs"
|
||||
);
|
||||
let worker = getSubprocessImplForTest().Process.getWorker();
|
||||
let fds = await worker.call("getFds", [running.id]);
|
||||
let proc = await Subprocess.connectRunning(fds);
|
||||
greater(proc.id, 0, "Already running process id is valid");
|
||||
equal(proc.pid, 0, "Already running process pid is 0");
|
||||
equal(
|
||||
proc.managed,
|
||||
true,
|
||||
"A process not spawned by us is externally managed"
|
||||
);
|
||||
Assert.throws(
|
||||
() => proc.wait(),
|
||||
/Cannot wait/,
|
||||
"A process externally managed cannot be waited on"
|
||||
);
|
||||
Assert.throws(
|
||||
() => proc.kill(),
|
||||
/Cannot kill/,
|
||||
"A process externally managed cannot be killed"
|
||||
);
|
||||
[proc.stdin, proc.stdout, proc.stderr].forEach((pipe, _i) =>
|
||||
greater(
|
||||
pipe.id,
|
||||
0,
|
||||
"File descriptor (stdin) for already running process is valid"
|
||||
)
|
||||
);
|
||||
let contents = "lorem ipsum";
|
||||
let writeOp = proc.stdin.write(contents);
|
||||
equal(
|
||||
(await writeOp).bytesWritten,
|
||||
contents.length,
|
||||
"Contents correctly written to stdin"
|
||||
);
|
||||
let readOp = running.stdout.readString(contents.length);
|
||||
equal(await readOp, contents, "Pipes communication is functional");
|
||||
await running.kill();
|
||||
|
||||
ok(tempFile.exists(), "temp file was written to");
|
||||
equal(
|
||||
await IOUtils.readUTF8(tempFile.path),
|
||||
contents,
|
||||
"Contents correctly written to temp file"
|
||||
);
|
||||
} else {
|
||||
Assert.throws(
|
||||
() => Subprocess.connectRunning([42, 58, 63]),
|
||||
/Not implemented/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_cleanup() {
|
||||
let { getSubprocessImplForTest } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Subprocess.sys.mjs"
|
||||
|
@ -214,8 +214,6 @@ bool ShouldUsePortal(PortalKind aPortalKind) {
|
||||
// Mime portal breaks default browser handling, see bug 1516290.
|
||||
autoBehavior = IsRunningUnderFlatpakOrSnap();
|
||||
return StaticPrefs::widget_use_xdg_desktop_portal_mime_handler();
|
||||
case PortalKind::NativeMessaging:
|
||||
return StaticPrefs::widget_use_xdg_desktop_portal_native_messaging();
|
||||
case PortalKind::Settings:
|
||||
autoBehavior = true;
|
||||
return StaticPrefs::widget_use_xdg_desktop_portal_settings();
|
||||
|
@ -53,7 +53,6 @@ inline bool IsRunningUnderFlatpakOrSnap() {
|
||||
enum class PortalKind {
|
||||
FilePicker,
|
||||
MimeHandler,
|
||||
NativeMessaging,
|
||||
Settings,
|
||||
Location,
|
||||
OpenUri,
|
||||
|
Loading…
Reference in New Issue
Block a user