forked from mirrors/gecko-dev
Bug 1675644 - Flush extension messages per-session and per-nativeApp. r=esawin
The extension code _tries_ to flush messages when the relevant delegate is attached. The logic, however, is pretty flawed: we currently only flush runtime-messages (i.e. not coming from a WebExtension Page) and we flush all messages when the first delegate is attached, even though there could be messages for different nativeApp values which don't have a delegate attached yet. We also erroneusly return a rejected promise to javascript when a message is queued up. This patch addresses the above by: - Never rejecting a pending connection request, the connection request will be resolved when the delegate for the right nativeApp is attached. - Making the pending messages queue per-nativeApp and per-session. - Flushing pending messages when a session delegate is attached. Differential Revision: https://phabricator.services.mozilla.com/D96645
This commit is contained in:
parent
ba7fba8f98
commit
db6763404e
9 changed files with 202 additions and 32 deletions
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "Test messages sent from extensions when restoring",
|
||||||
|
"version": "1.0",
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "extension-page-restoring@tests.mozilla.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"geckoViewAddons",
|
||||||
|
"nativeMessaging"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
browser.runtime.sendNativeMessage("browser1", "HELLO_FROM_PAGE_1");
|
||||||
|
browser.runtime.sendNativeMessage("browser2", "HELLO_FROM_PAGE_2");
|
||||||
|
|
||||||
|
const port = browser.runtime.connectNative("browser1");
|
||||||
|
port.postMessage("HELLO_FROM_PORT");
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html><html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World!</h1>
|
||||||
|
<script src=tab-script.js></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
browser.runtime
|
// This message should not be handled
|
||||||
.sendNativeMessage("badNativeApi", "errorerrorerror")
|
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
|
||||||
// This message should not be handled
|
|
||||||
.catch(runTest);
|
|
||||||
|
|
||||||
async function runTest() {
|
async function runTest() {
|
||||||
const response = await browser.runtime.sendNativeMessage(
|
const response = await browser.runtime.sendNativeMessage(
|
||||||
|
|
@ -27,3 +25,5 @@ async function runTest() {
|
||||||
|
|
||||||
port.postMessage("testContentPortMessage");
|
port.postMessage("testContentPortMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
browser.runtime
|
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
|
||||||
.sendNativeMessage("badNativeApi", "errorerrorerror")
|
|
||||||
// This message should not be handled
|
|
||||||
.catch(runTest);
|
|
||||||
|
|
||||||
async function runTest() {
|
async function runTest() {
|
||||||
await browser.runtime.sendNativeMessage(
|
await browser.runtime.sendNativeMessage(
|
||||||
|
|
@ -10,3 +7,5 @@ async function runTest() {
|
||||||
);
|
);
|
||||||
browser.runtime.connectNative("browser");
|
browser.runtime.connectNative("browser");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
browser.runtime
|
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
|
||||||
.sendNativeMessage("badNativeApi", "errorerrorerror")
|
|
||||||
// This message should not be handled
|
|
||||||
.catch(runTest);
|
|
||||||
|
|
||||||
async function runTest() {
|
async function runTest() {
|
||||||
const response = await browser.runtime.sendNativeMessage(
|
const response = await browser.runtime.sendNativeMessage(
|
||||||
|
|
@ -27,3 +24,5 @@ async function runTest() {
|
||||||
|
|
||||||
port.postMessage("testBackgroundPortMessage");
|
port.postMessage("testBackgroundPortMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import org.hamcrest.core.StringEndsWith.endsWith
|
||||||
import org.hamcrest.core.IsEqual.equalTo
|
import org.hamcrest.core.IsEqual.equalTo
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Assume.assumeThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -45,6 +46,8 @@ class WebExtensionTest : BaseSessionTest() {
|
||||||
"resource://android/assets/web_extensions/openoptionspage-1/"
|
"resource://android/assets/web_extensions/openoptionspage-1/"
|
||||||
private const val OPENOPTIONSPAGE_2_BACKGROUND: String =
|
private const val OPENOPTIONSPAGE_2_BACKGROUND: String =
|
||||||
"resource://android/assets/web_extensions/openoptionspage-2/"
|
"resource://android/assets/web_extensions/openoptionspage-2/"
|
||||||
|
private const val EXTENSION_PAGE_RESTORE: String =
|
||||||
|
"resource://android/assets/web_extensions/extension-page-restore/"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val controller
|
private val controller
|
||||||
|
|
@ -768,6 +771,94 @@ class WebExtensionTest : BaseSessionTest() {
|
||||||
newPrivateSession.close()
|
newPrivateSession.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verifies that the following messages are received from an extension page loaded in the session
|
||||||
|
// - HELLO_FROM_PAGE_1 from nativeApp browser1
|
||||||
|
// - HELLO_FROM_PAGE_2 from nativeApp browser2
|
||||||
|
// - connection request from browser1
|
||||||
|
// - HELLO_FROM_PORT from the port opened at the above step
|
||||||
|
private fun testExtensionMessages(extension: WebExtension, session: GeckoSession) {
|
||||||
|
val messageResult2 = GeckoResult<String>()
|
||||||
|
session.webExtensionController.setMessageDelegate(
|
||||||
|
extension, object : WebExtension.MessageDelegate {
|
||||||
|
override fun onMessage(nativeApp: String, message: Any,
|
||||||
|
sender: WebExtension.MessageSender): GeckoResult<Any>? {
|
||||||
|
messageResult2.complete(message as String);
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, "browser2")
|
||||||
|
|
||||||
|
val message2 = sessionRule.waitForResult(messageResult2)
|
||||||
|
assertThat("Message is received correctly", message2,
|
||||||
|
equalTo("HELLO_FROM_PAGE_2"))
|
||||||
|
|
||||||
|
val messageResult1 = GeckoResult<String>()
|
||||||
|
val portResult = GeckoResult<WebExtension.Port>()
|
||||||
|
session.webExtensionController.setMessageDelegate(
|
||||||
|
extension, object : WebExtension.MessageDelegate {
|
||||||
|
override fun onMessage(nativeApp: String, message: Any,
|
||||||
|
sender: WebExtension.MessageSender): GeckoResult<Any>? {
|
||||||
|
messageResult1.complete(message as String);
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnect(port: WebExtension.Port) {
|
||||||
|
portResult.complete(port)
|
||||||
|
}
|
||||||
|
}, "browser1")
|
||||||
|
|
||||||
|
val message1 = sessionRule.waitForResult(messageResult1)
|
||||||
|
assertThat("Message is received correctly", message1,
|
||||||
|
equalTo("HELLO_FROM_PAGE_1"))
|
||||||
|
|
||||||
|
val port = sessionRule.waitForResult(portResult)
|
||||||
|
val portMessageResult = GeckoResult<String>()
|
||||||
|
port.setDelegate(object : WebExtension.PortDelegate {
|
||||||
|
override fun onPortMessage(message: Any, port: WebExtension.Port) {
|
||||||
|
portMessageResult.complete(message as String)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val portMessage = sessionRule.waitForResult(portMessageResult)
|
||||||
|
assertThat("Message is received correctly", portMessage,
|
||||||
|
equalTo("HELLO_FROM_PORT"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test:
|
||||||
|
// - loads an extension that tries to send some messages when loading tab.html
|
||||||
|
// - verifies that the messages are received when loading the tab normally
|
||||||
|
// - verifies that the messages are received when restoring the tab in a fresh session
|
||||||
|
@Test
|
||||||
|
fun testRestoringExtensionPagePreservesMessages() {
|
||||||
|
// TODO: Bug 1648158
|
||||||
|
assumeThat(sessionRule.env.isFission, equalTo(false))
|
||||||
|
|
||||||
|
val extension = sessionRule.waitForResult(
|
||||||
|
controller.installBuiltIn(EXTENSION_PAGE_RESTORE))
|
||||||
|
|
||||||
|
sessionRule.session.loadUri("${extension.metaData.baseUrl}tab.html")
|
||||||
|
sessionRule.waitForPageStop()
|
||||||
|
|
||||||
|
var savedState : GeckoSession.SessionState? = null
|
||||||
|
sessionRule.waitUntilCalled(object : Callbacks.ProgressDelegate {
|
||||||
|
@AssertCalled(count=1)
|
||||||
|
override fun onSessionStateChange(session: GeckoSession, state: GeckoSession.SessionState) {
|
||||||
|
savedState = state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that messages are received in the main session
|
||||||
|
testExtensionMessages(extension, sessionRule.session)
|
||||||
|
|
||||||
|
val newSession = sessionRule.createOpenSession()
|
||||||
|
newSession.restoreState(savedState!!)
|
||||||
|
newSession.waitForPageStop()
|
||||||
|
|
||||||
|
// Test that messages are received in a restored state
|
||||||
|
testExtensionMessages(extension, newSession)
|
||||||
|
|
||||||
|
sessionRule.waitForResult(controller.uninstall(extension))
|
||||||
|
}
|
||||||
|
|
||||||
// This test
|
// This test
|
||||||
// - Create and assign WebExtension TabDelegate to handle closing of tabs
|
// - Create and assign WebExtension TabDelegate to handle closing of tabs
|
||||||
// - Create new GeckoSession for WebExtension to close
|
// - Create new GeckoSession for WebExtension to close
|
||||||
|
|
|
||||||
|
|
@ -901,6 +901,11 @@ public class WebExtension {
|
||||||
final WebExtension.MessageDelegate delegate,
|
final WebExtension.MessageDelegate delegate,
|
||||||
final String nativeApp) {
|
final String nativeApp) {
|
||||||
mMessageDelegates.put(new Sender(webExtension.id, nativeApp), delegate);
|
mMessageDelegates.put(new Sender(webExtension.id, nativeApp), delegate);
|
||||||
|
|
||||||
|
if (runtime != null && delegate != null) {
|
||||||
|
runtime.getWebExtensionController()
|
||||||
|
.releasePendingMessages(webExtension, nativeApp, mSession);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,
|
public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
@ -17,9 +19,11 @@ import org.mozilla.gecko.util.GeckoBundle;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED;
|
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED;
|
||||||
|
|
@ -32,8 +36,8 @@ public class WebExtensionController {
|
||||||
private PromptDelegate mPromptDelegate;
|
private PromptDelegate mPromptDelegate;
|
||||||
private final WebExtension.Listener<WebExtension.TabDelegate> mListener;
|
private final WebExtension.Listener<WebExtension.TabDelegate> mListener;
|
||||||
|
|
||||||
// Map [ extensionId -> Message ]
|
// Map [ (extensionId, nativeApp, session) -> message ]
|
||||||
private final MultiMap<String, Message> mPendingMessages;
|
private final MultiMap<MessageRecipient, Message> mPendingMessages;
|
||||||
private final MultiMap<String, Message> mPendingNewTab;
|
private final MultiMap<String, Message> mPendingNewTab;
|
||||||
|
|
||||||
private static class Message {
|
private static class Message {
|
||||||
|
|
@ -124,8 +128,26 @@ public class WebExtensionController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ void releasePendingMessages(final WebExtension extension, final String nativeApp,
|
||||||
|
final GeckoSession session) {
|
||||||
|
Log.i(LOGTAG, "releasePendingMessages:"
|
||||||
|
+ " extension=" + extension.id
|
||||||
|
+ " nativeApp=" + nativeApp
|
||||||
|
+ " session=" + session);
|
||||||
|
final List<Message> messages = mPendingMessages.remove(
|
||||||
|
new MessageRecipient(nativeApp, extension.id, session));
|
||||||
|
if (messages == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Message message : messages) {
|
||||||
|
WebExtensionController.this.handleMessage(message.event, message.bundle,
|
||||||
|
message.callback, message.session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class DelegateController implements WebExtension.DelegateController {
|
private class DelegateController implements WebExtension.DelegateController {
|
||||||
private WebExtension mExtension;
|
private final WebExtension mExtension;
|
||||||
|
|
||||||
public DelegateController(final WebExtension extension) {
|
public DelegateController(final WebExtension extension) {
|
||||||
mExtension = extension;
|
mExtension = extension;
|
||||||
|
|
@ -135,17 +157,6 @@ public class WebExtensionController {
|
||||||
public void onMessageDelegate(final String nativeApp,
|
public void onMessageDelegate(final String nativeApp,
|
||||||
final WebExtension.MessageDelegate delegate) {
|
final WebExtension.MessageDelegate delegate) {
|
||||||
mListener.setMessageDelegate(mExtension, delegate, nativeApp);
|
mListener.setMessageDelegate(mExtension, delegate, nativeApp);
|
||||||
|
|
||||||
if (delegate == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Message message : mPendingMessages.get(mExtension.id)) {
|
|
||||||
WebExtensionController.this.handleMessage(message.event, message.bundle,
|
|
||||||
message.callback, message.session);
|
|
||||||
}
|
|
||||||
|
|
||||||
mPendingMessages.remove(mExtension.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1112,12 +1123,45 @@ public class WebExtensionController {
|
||||||
delegate = mListener.getMessageDelegate(sender.webExtension, nativeApp);
|
delegate = mListener.getMessageDelegate(sender.webExtension, nativeApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delegate == null) {
|
return delegate;
|
||||||
callback.sendError("Native app not found or this WebExtension does not have permissions.");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return delegate;
|
private static class MessageRecipient {
|
||||||
|
final public String webExtensionId;
|
||||||
|
final public String nativeApp;
|
||||||
|
final public GeckoSession session;
|
||||||
|
|
||||||
|
public MessageRecipient(final String webExtensionId, final String nativeApp,
|
||||||
|
final GeckoSession session) {
|
||||||
|
this.webExtensionId = webExtensionId;
|
||||||
|
this.nativeApp = nativeApp;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean equals(final Object a, final Object b) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
return Objects.equals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (a == b) || (a != null && a.equals(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object other) {
|
||||||
|
if (!(other instanceof MessageRecipient)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MessageRecipient o = (MessageRecipient) other;
|
||||||
|
return equals(webExtensionId, o.webExtensionId) &&
|
||||||
|
equals(nativeApp, o.nativeApp) &&
|
||||||
|
equals(session, o.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Arrays.hashCode(new Object[] { webExtensionId, nativeApp, session });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connect(final String nativeApp, final long portId, final Message message,
|
private void connect(final String nativeApp, final long portId, final Message message,
|
||||||
|
|
@ -1132,7 +1176,9 @@ public class WebExtensionController {
|
||||||
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
|
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
|
||||||
message.callback);
|
message.callback);
|
||||||
if (delegate == null) {
|
if (delegate == null) {
|
||||||
mPendingMessages.add(sender.webExtension.id, message);
|
mPendingMessages.add(
|
||||||
|
new MessageRecipient(nativeApp, sender.webExtension.id, sender.session),
|
||||||
|
message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1155,7 +1201,9 @@ public class WebExtensionController {
|
||||||
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
|
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
|
||||||
callback);
|
callback);
|
||||||
if (delegate == null) {
|
if (delegate == null) {
|
||||||
mPendingMessages.add(sender.webExtension.id, message);
|
mPendingMessages.add(
|
||||||
|
new MessageRecipient(nativeApp, sender.webExtension.id, sender.session),
|
||||||
|
message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue