diff --git a/mobile/android/components/geckoview/GeckoViewStartup.sys.mjs b/mobile/android/components/geckoview/GeckoViewStartup.sys.mjs index 97b6d099939b..768aa7316cb5 100644 --- a/mobile/android/components/geckoview/GeckoViewStartup.sys.mjs +++ b/mobile/android/components/geckoview/GeckoViewStartup.sys.mjs @@ -192,6 +192,7 @@ export class GeckoViewStartup { "GeckoView:WebExtension:SetPBAllowed", "GeckoView:WebExtension:Uninstall", "GeckoView:WebExtension:Update", + "GeckoView:WebExtension:EnableProcessSpawning", ], observers: [ "devtools-installed-addon", diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index 03610a90a6da..651bb6c3a39c 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -2491,6 +2491,7 @@ package org.mozilla.geckoview { method @Nullable @UiThread public WebExtension.Download createDownload(int); method @AnyThread @NonNull public GeckoResult disable(@NonNull WebExtension, int); method @AnyThread @NonNull public GeckoResult enable(@NonNull WebExtension, int); + method @AnyThread public void enableExtensionProcessSpawning(); method @AnyThread @NonNull public GeckoResult ensureBuiltIn(@NonNull String, @Nullable String); method @Nullable @UiThread public WebExtensionController.PromptDelegate getPromptDelegate(); method @AnyThread @NonNull public GeckoResult install(@NonNull String); @@ -2499,6 +2500,7 @@ package org.mozilla.geckoview { method @UiThread public void setAddonManagerDelegate(@Nullable WebExtensionController.AddonManagerDelegate); method @AnyThread @NonNull public GeckoResult setAllowedInPrivateBrowsing(@NonNull WebExtension, boolean); method @UiThread public void setDebuggerDelegate(@NonNull WebExtensionController.DebuggerDelegate); + method @UiThread public void setExtensionProcessDelegate(@Nullable WebExtensionController.ExtensionProcessDelegate); method @UiThread public void setPromptDelegate(@Nullable WebExtensionController.PromptDelegate); method @AnyThread public void setTabActive(@NonNull GeckoSession, boolean); method @AnyThread @NonNull public GeckoResult uninstall(@NonNull WebExtension); @@ -2529,6 +2531,10 @@ package org.mozilla.geckoview { @Retention(value=RetentionPolicy.SOURCE) public static interface WebExtensionController.EnableSources { } + public static interface WebExtensionController.ExtensionProcessDelegate { + method @UiThread default public void onDisabledProcessSpawning(); + } + @UiThread public static interface WebExtensionController.PromptDelegate { method @Nullable default public GeckoResult onInstallPrompt(@NonNull WebExtension); method @Nullable default public GeckoResult onOptionalPrompt(@NonNull WebExtension, @NonNull String[], @NonNull String[]); diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt index a12872a3e7a9..8074201029b2 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt @@ -3126,4 +3126,42 @@ class WebExtensionTest : BaseSessionTest() { equalTo(true), ) } + + fun extensionProcessCrash() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "extensions.webextensions.remote" to true, + "dom.ipc.keepProcessesAlive.extension" to 1, + "xpinstall.signatures.required" to false, + ), + ) + + sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { + @AssertCalled(count = 1) + override fun onInstallPrompt(extension: WebExtension): GeckoResult? { + return GeckoResult.allow() + } + }) + + sessionRule.addExternalDelegateUntilTestEnd( + WebExtensionController.ExtensionProcessDelegate::class, + { delegate -> controller.setExtensionProcessDelegate(delegate) }, + { controller.setExtensionProcessDelegate(null) }, + object : WebExtensionController.ExtensionProcessDelegate { + @AssertCalled(count = 1) + override fun onDisabledProcessSpawning() {} + }, + ) + + val borderify = sessionRule.waitForResult( + controller.install("resource://android/assets/web_extensions/borderify.xpi"), + ) + + val list = extensionsMap(sessionRule.waitForResult(controller.list())) + assertTrue(list.containsKey(borderify.id)) + + mainSession.loadUri("about:crashextensions") + + sessionRule.waitForResult(controller.uninstall(borderify)) + } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java index 9e55b79a0e78..18e5f6d47445 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java @@ -33,6 +33,7 @@ public class WebExtensionController { private static final String LOGTAG = "WebExtension"; private AddonManagerDelegate mAddonManagerDelegate; + private ExtensionProcessDelegate mExtensionProcessDelegate; private DebuggerDelegate mDebuggerDelegate; private PromptDelegate mPromptDelegate; private final WebExtension.Listener mListener; @@ -393,6 +394,13 @@ public class WebExtensionController { default void onInstalled(final @NonNull WebExtension extension) {} } + /** This delegate is used to notify of extension process state changes. */ + public interface ExtensionProcessDelegate { + /** Called when extension process spawning has been disabled. */ + @UiThread + default void onDisabledProcessSpawning() {} + } + /** * @return the current {@link PromptDelegate} instance. * @see PromptDelegate @@ -488,6 +496,42 @@ public class WebExtensionController { mAddonManagerDelegate = delegate; } + /** + * Set the {@link ExtensionProcessDelegate} for this instance. This delegate will be used to + * notify when the state of the extension process has changed. + * + * @param delegate the extension process delegate + * @see ExtensionProcessDelegate + */ + @UiThread + public void setExtensionProcessDelegate(final @Nullable ExtensionProcessDelegate delegate) { + if (delegate == null && mExtensionProcessDelegate != null) { + EventDispatcher.getInstance() + .unregisterUiThreadListener( + mInternals, "GeckoView:WebExtension:OnDisabledProcessSpawning"); + } else if (delegate != null && mExtensionProcessDelegate == null) { + EventDispatcher.getInstance() + .registerUiThreadListener(mInternals, "GeckoView:WebExtension:OnDisabledProcessSpawning"); + } + + mExtensionProcessDelegate = delegate; + } + + /** + * Enable extension process spawning. + * + *

Extension process spawning can be disabled when the extension process has been killed or + * crashed beyond the threshold set for Gecko. This method can be called to reset the threshold + * count and allow the spawning again. If the threshold is reached again, {@link + * ExtensionProcessDelegate#onDisabledProcessSpawning()} will still be called. + * + * @see ExtensionProcessDelegate#onDisabledProcessSpawning() + */ + @AnyThread + public void enableExtensionProcessSpawning() { + EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:EnableProcessSpawning", null); + } + private static class InstallCanceller implements GeckoResult.CancellationDelegate { public final String installId; @@ -860,6 +904,9 @@ public class WebExtensionController { } else if ("GeckoView:WebExtension:OnInstalled".equals(event)) { onInstalled(bundle); return; + } else if ("GeckoView:WebExtension:OnDisabledProcessSpawning".equals(event)) { + onDisabledProcessSpawning(); + return; } extensionFromBundle(bundle) @@ -1133,6 +1180,15 @@ public class WebExtensionController { mAddonManagerDelegate.onInstalled(extension); } + private void onDisabledProcessSpawning() { + if (mExtensionProcessDelegate == null) { + Log.e(LOGTAG, "no extension process delegate registered"); + return; + } + + mExtensionProcessDelegate.onDisabledProcessSpawning(); + } + @SuppressLint("WrongThread") // for .toGeckoBundle private void getSettings(final Message message, final WebExtension extension) { final WebExtension.BrowsingDataDelegate delegate = mListener.getBrowsingDataDelegate(extension); diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md index 1355dd9938e7..25895ddfe35d 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md @@ -25,6 +25,8 @@ exclude: true - Added [`getExperimentDelegate`][118.9] and [`setExperimentDelegate`][118.10] to the GeckoSession allow GeckoView to get and set the experiment delegate for the session. Default is to use the runtime delegate. - ⚠️ Deprecated [`onGetNimbusFeature`][115.5] by 122, please use `ExperimentDelegate.onGetExperimentFeature` instead. - Added [`GeckoRuntimeSettings.Builder.extensionsProcessEnabled`][118.11] for setting whether extensions process is enabled. ([bug 1843926]({{bugzilla}}1843926)) +- Added [`ExtensionProcessDelegate`][118.12] to allow GeckoView to notify disabling of the extension process spawning due to excessive crash/kill. ([bug 1819737]({{bugzilla}}1819737)) +- Added [`enableExtensionProcessSpawning`][118.13] for enabling the extension process spawning [118.1]: {{javadoc_uri}}/ExperimentDelegate.html [118.2]: {{javadoc_uri}}/WebExtension.InstallException.ErrorCodes.html#ERROR_BLOCKLISTED @@ -37,6 +39,8 @@ exclude: true [118.9]: {{javadoc_uri}}/GeckoSession.html#getExperimentDelegate() [118.10]: {{javadoc_uri}}/GeckoSession.html#setExperimentDelegate(org.mozilla.geckoview.ExperimentDelegate) [118.11]: {{javadoc_uri}}/GeckoRuntimeSettings.Builder.html#extensionsProcessEnabled(Boolean) +[118.12]: {{javadoc_uri}}/WebExtensionController.ExtensionProcessDelegate.html +[118.13]: {{javadoc_uri}}/WebExtensionController.html#enableExtensionProcessSpawning ## v116 - Added [`GeckoSession.didPrintPageContent`][116.1] to included extra print status for a standard print and new `GeckoPrintException.ERROR_NO_PRINT_DELEGATE` @@ -1417,4 +1421,4 @@ to allow adding gecko profiler markers. [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String) [65.25]: {{javadoc_uri}}/GeckoResult.html -[api-version]: 5743799f9da8509cfb6546e154f0e2d7989edc72 +[api-version]: 1b85a4f582547bb6ca01fad7bc36344a3179b28f diff --git a/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs b/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs index 9b202c1961de..690c3059ff53 100644 --- a/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs @@ -651,6 +651,45 @@ class AddonManagerListener { new AddonManagerListener(); +class ExtensionProcessListener { + constructor() { + this.onExtensionProcessCrash = this.onExtensionProcessCrash.bind(this); + lazy.Management.on("extension-process-crash", this.onExtensionProcessCrash); + + lazy.EventDispatcher.instance.registerListener(this, [ + "GeckoView:WebExtension:EnableProcessSpawning", + ]); + } + + async onEvent(aEvent, aData, aCallback) { + debug`onEvent ${aEvent} ${aData}`; + + switch (aEvent) { + case "GeckoView:WebExtension:EnableProcessSpawning": { + //TODO: Bug 1848426 - Add ability to reset threshold counters and allow for process spawning + } + } + } + + async onExtensionProcessCrash(name, { childID, disabledProcessSpawning }) { + debug`Extension process crash -> childID=${childID} disabledProcessSpawning=${disabledProcessSpawning}`; + + // When an extension process has crashed too many times, Gecko will set `disabledProcessSpawning` + // and no longer allow the extension process spawning. We only want to send a request + // to the embedder when we are disabling the process spawning. + // If process spawning is still enabled then we short circuit and don't notify the embedder. + if (!disabledProcessSpawning) { + return; + } + + lazy.EventDispatcher.instance.sendRequest({ + type: "GeckoView:WebExtension:OnDisabledProcessSpawning", + }); + } +} + +new ExtensionProcessListener(); + class MobileWindowTracker extends EventEmitter { constructor() { super();