зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
ba7fba8f98
Коммит
db6763404e
|
@ -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
|
||||
.sendNativeMessage("badNativeApi", "errorerrorerror")
|
||||
// This message should not be handled
|
||||
.catch(runTest);
|
||||
// This message should not be handled
|
||||
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
|
||||
|
||||
async function runTest() {
|
||||
const response = await browser.runtime.sendNativeMessage(
|
||||
|
@ -27,3 +25,5 @@ async function runTest() {
|
|||
|
||||
port.postMessage("testContentPortMessage");
|
||||
}
|
||||
|
||||
runTest();
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
browser.runtime
|
||||
.sendNativeMessage("badNativeApi", "errorerrorerror")
|
||||
// This message should not be handled
|
||||
.catch(runTest);
|
||||
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
|
||||
|
||||
async function runTest() {
|
||||
await browser.runtime.sendNativeMessage(
|
||||
|
@ -10,3 +7,5 @@ async function runTest() {
|
|||
);
|
||||
browser.runtime.connectNative("browser");
|
||||
}
|
||||
|
||||
runTest();
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
browser.runtime
|
||||
.sendNativeMessage("badNativeApi", "errorerrorerror")
|
||||
// This message should not be handled
|
||||
.catch(runTest);
|
||||
browser.runtime.sendNativeMessage("badNativeApi", "errorerrorerror");
|
||||
|
||||
async function runTest() {
|
||||
const response = await browser.runtime.sendNativeMessage(
|
||||
|
@ -27,3 +24,5 @@ async function runTest() {
|
|||
|
||||
port.postMessage("testBackgroundPortMessage");
|
||||
}
|
||||
|
||||
runTest();
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.hamcrest.core.StringEndsWith.endsWith
|
|||
import org.hamcrest.core.IsEqual.equalTo
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Assume.assumeThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -45,6 +46,8 @@ class WebExtensionTest : BaseSessionTest() {
|
|||
"resource://android/assets/web_extensions/openoptionspage-1/"
|
||||
private const val OPENOPTIONSPAGE_2_BACKGROUND: String =
|
||||
"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
|
||||
|
@ -768,6 +771,94 @@ class WebExtensionTest : BaseSessionTest() {
|
|||
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
|
||||
// - Create and assign WebExtension TabDelegate to handle closing of tabs
|
||||
// - Create new GeckoSession for WebExtension to close
|
||||
|
|
|
@ -901,6 +901,11 @@ public class WebExtension {
|
|||
final WebExtension.MessageDelegate delegate,
|
||||
final String nativeApp) {
|
||||
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,
|
||||
|
|
|
@ -5,6 +5,8 @@ import androidx.annotation.IntDef;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
@ -17,9 +19,11 @@ import org.mozilla.gecko.util.GeckoBundle;
|
|||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED;
|
||||
|
@ -32,8 +36,8 @@ public class WebExtensionController {
|
|||
private PromptDelegate mPromptDelegate;
|
||||
private final WebExtension.Listener<WebExtension.TabDelegate> mListener;
|
||||
|
||||
// Map [ extensionId -> Message ]
|
||||
private final MultiMap<String, Message> mPendingMessages;
|
||||
// Map [ (extensionId, nativeApp, session) -> message ]
|
||||
private final MultiMap<MessageRecipient, Message> mPendingMessages;
|
||||
private final MultiMap<String, Message> mPendingNewTab;
|
||||
|
||||
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 WebExtension mExtension;
|
||||
private final WebExtension mExtension;
|
||||
|
||||
public DelegateController(final WebExtension extension) {
|
||||
mExtension = extension;
|
||||
|
@ -135,17 +157,6 @@ public class WebExtensionController {
|
|||
public void onMessageDelegate(final String nativeApp,
|
||||
final WebExtension.MessageDelegate delegate) {
|
||||
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
|
||||
|
@ -1112,12 +1123,45 @@ public class WebExtensionController {
|
|||
delegate = mListener.getMessageDelegate(sender.webExtension, nativeApp);
|
||||
}
|
||||
|
||||
if (delegate == null) {
|
||||
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;
|
||||
}
|
||||
|
||||
return delegate;
|
||||
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,
|
||||
|
@ -1132,7 +1176,9 @@ public class WebExtensionController {
|
|||
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
|
||||
message.callback);
|
||||
if (delegate == null) {
|
||||
mPendingMessages.add(sender.webExtension.id, message);
|
||||
mPendingMessages.add(
|
||||
new MessageRecipient(nativeApp, sender.webExtension.id, sender.session),
|
||||
message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1155,7 +1201,9 @@ public class WebExtensionController {
|
|||
final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender,
|
||||
callback);
|
||||
if (delegate == null) {
|
||||
mPendingMessages.add(sender.webExtension.id, message);
|
||||
mPendingMessages.add(
|
||||
new MessageRecipient(nativeApp, sender.webExtension.id, sender.session),
|
||||
message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче