diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /mobile/android/geckoview/src/main/java/org/mozilla/gecko/process | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/process')
8 files changed, 2090 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja new file mode 100644 index 0000000000..fa2f336566 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja @@ -0,0 +1,19 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.process; + +public class GeckoChildProcessServices { + /* package */ static final int MAX_NUM_ISOLATED_CONTENT_SERVICES = {{MOZ_ANDROID_CONTENT_SERVICE_COUNT}}; + public static final class gmplugin extends GeckoServiceChildProcess {} + public static final class socket extends GeckoServiceChildProcess {} + public static final class gpu extends GeckoServiceGpuProcess {} + public static final class utility extends GeckoServiceChildProcess {} + public static final class ipdlunittest extends GeckoServiceChildProcess {} + +{% for id in range(0, MOZ_ANDROID_CONTENT_SERVICE_COUNT | int) %} + public static final class tab{{ id }} extends GeckoServiceChildProcess {} +{% endfor %} +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java new file mode 100644 index 0000000000..039396f9e8 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java @@ -0,0 +1,927 @@ +/* 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/. */ + +package org.mozilla.gecko.process; + +import android.os.DeadObjectException; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.collection.ArrayMap; +import androidx.collection.ArraySet; +import androidx.collection.SimpleArrayMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoNetworkManager; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.GeckoThread.FileDescriptors; +import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors; +import org.mozilla.gecko.IGeckoEditableChild; +import org.mozilla.gecko.IGeckoEditableParent; +import org.mozilla.gecko.TelemetryUtils; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.gfx.CompositorSurfaceManager; +import org.mozilla.gecko.gfx.ISurfaceAllocator; +import org.mozilla.gecko.gfx.RemoteSurfaceAllocator; +import org.mozilla.gecko.mozglue.JNIObject; +import org.mozilla.gecko.process.ServiceAllocator.PriorityLevel; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.util.XPCOMEventTarget; +import org.mozilla.geckoview.GeckoResult; + +public final class GeckoProcessManager extends IProcessManager.Stub { + private static final String LOGTAG = "GeckoProcessManager"; + private static final GeckoProcessManager INSTANCE = new GeckoProcessManager(); + private static final int INVALID_PID = 0; + + // This id univocally identifies the current process manager instance + private final String mInstanceId; + + public static GeckoProcessManager getInstance() { + return INSTANCE; + } + + @WrapForJNI(calledFrom = "gecko") + private static void setEditableChildParent( + final IGeckoEditableChild child, final IGeckoEditableParent parent) { + try { + child.transferParent(parent); + } catch (final RemoteException e) { + Log.e(LOGTAG, "Cannot set parent", e); + } + } + + @WrapForJNI(stubName = "GetEditableParent", dispatchTo = "gecko") + private static native void nativeGetEditableParent( + IGeckoEditableChild child, long contentId, long tabId); + + @Override // IProcessManager + public void getEditableParent( + final IGeckoEditableChild child, final long contentId, final long tabId) { + nativeGetEditableParent(child, contentId, tabId); + } + + /** + * Returns the surface allocator interface to be used by child processes to allocate Surfaces. The + * service bound to the returned interface may live in either the GPU process or parent process. + */ + @Override // IProcessManager + public ISurfaceAllocator getSurfaceAllocator() { + final GeckoResult<Boolean> gpuEnabled = GeckoAppShell.isGpuProcessEnabled(); + + try { + final GeckoResult<ISurfaceAllocator> allocator = new GeckoResult<>(); + if (gpuEnabled.poll(1000)) { + // The GPU process is enabled, so look it up and ask it for its surface allocator. + XPCOMEventTarget.runOnLauncherThread( + () -> { + final Selector selector = new Selector(GeckoProcessType.GPU); + final GpuProcessConnection conn = + (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector); + if (conn != null) { + allocator.complete(conn.getSurfaceAllocator()); + } else { + // If we cannot find a GPU process, it has probably been killed and not yet + // restarted. Return null here, and allow the caller to try again later. + // We definitely do *not* want to return the parent process allocator instead, as + // that will result in surfaces being allocated in the parent process, which + // therefore won't be usable when the GPU process is eventually launched. + allocator.complete(null); + } + }); + } else { + // The GPU process is disabled, so return the parent process allocator instance. + allocator.complete(RemoteSurfaceAllocator.getInstance(0)); + } + return allocator.poll(100); + } catch (final Throwable e) { + Log.e(LOGTAG, "Error in getSurfaceAllocator", e); + return null; + } + } + + @WrapForJNI + public static CompositorSurfaceManager getCompositorSurfaceManager() { + final Selector selector = new Selector(GeckoProcessType.GPU); + final GpuProcessConnection conn = + (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector); + if (conn == null) { + return null; + } + return conn.getCompositorSurfaceManager(); + } + + /** Gecko uses this class to uniquely identify a process managed by GeckoProcessManager. */ + public static final class Selector { + private final GeckoProcessType mType; + private final int mPid; + + @WrapForJNI + private Selector(@NonNull final GeckoProcessType type, final int pid) { + if (pid == INVALID_PID) { + throw new RuntimeException("Invalid PID"); + } + + mType = type; + mPid = pid; + } + + @WrapForJNI + private Selector(@NonNull final GeckoProcessType type) { + mType = type; + mPid = INVALID_PID; + } + + public GeckoProcessType getType() { + return mType; + } + + public int getPid() { + return mPid; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + + if (obj == ((Object) this)) { + return true; + } + + final Selector other = (Selector) obj; + return mType == other.mType && mPid == other.mPid; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {mType, mPid}); + } + } + + private static final class IncompleteChildConnectionException extends RuntimeException { + public IncompleteChildConnectionException(@NonNull final String msg) { + super(msg); + } + } + + /** + * Maintains state pertaining to an individual child process. Inheriting from + * ServiceAllocator.InstanceInfo enables this class to work with ServiceAllocator. + */ + private static class ChildConnection extends ServiceAllocator.InstanceInfo { + private IChildProcess mChild; + private GeckoResult<IChildProcess> mPendingBind; + private int mPid; + + protected ChildConnection( + @NonNull final ServiceAllocator allocator, + @NonNull final GeckoProcessType type, + @NonNull final PriorityLevel initialPriority) { + super(allocator, type, initialPriority); + mPid = INVALID_PID; + } + + public int getPid() { + XPCOMEventTarget.assertOnLauncherThread(); + if (mChild == null) { + throw new IncompleteChildConnectionException( + "Calling ChildConnection.getPid() on an incomplete connection"); + } + + return mPid; + } + + private GeckoResult<IChildProcess> completeFailedBind( + @NonNull final ServiceAllocator.BindException e) { + XPCOMEventTarget.assertOnLauncherThread(); + Log.e(LOGTAG, "Failed bind", e); + + if (mPendingBind == null) { + throw new IllegalStateException("Bind failed with null mPendingBind"); + } + + final GeckoResult<IChildProcess> bindResult = mPendingBind; + mPendingBind = null; + unbind().accept(v -> bindResult.completeExceptionally(e)); + return bindResult; + } + + public GeckoResult<IChildProcess> bind() { + XPCOMEventTarget.assertOnLauncherThread(); + + if (mChild != null) { + // Already bound + return GeckoResult.fromValue(mChild); + } + + if (mPendingBind != null) { + // Bind in progress + return mPendingBind; + } + + mPendingBind = new GeckoResult<>(); + try { + if (!bindService()) { + throw new ServiceAllocator.BindException("Cannot connect to process"); + } + } catch (final ServiceAllocator.BindException e) { + return completeFailedBind(e); + } + + return mPendingBind; + } + + public GeckoResult<Void> unbind() { + XPCOMEventTarget.assertOnLauncherThread(); + + if (mPendingBind != null) { + // We called unbind() while bind() was still pending completion + return mPendingBind.then(child -> unbind()); + } + + if (mChild == null) { + // Not bound in the first place + return GeckoResult.fromValue(null); + } + + unbindService(); + + return GeckoResult.fromValue(null); + } + + @Override + protected void onBinderConnected(final IBinder service) { + XPCOMEventTarget.assertOnLauncherThread(); + + final IChildProcess child = IChildProcess.Stub.asInterface(service); + try { + mPid = child.getPid(); + onBinderConnected(child); + } catch (final DeadObjectException e) { + unbindService(); + + // mPendingBind might be null if a bind was initiated by the system (eg Service Restart) + if (mPendingBind != null) { + mPendingBind.completeExceptionally(e); + mPendingBind = null; + } + + return; + } catch (final RemoteException e) { + throw new RuntimeException(e); + } + + mChild = child; + GeckoProcessManager.INSTANCE.mConnections.onBindComplete(this); + + // mPendingBind might be null if a bind was initiated by the system (eg Service Restart) + if (mPendingBind != null) { + mPendingBind.complete(mChild); + mPendingBind = null; + } + } + + // Subclasses of ChildConnection can override this method to make any IChildProcess calls + // specific to their process type immediately after connection. + protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException {} + + @Override + protected void onReleaseResources() { + XPCOMEventTarget.assertOnLauncherThread(); + + // NB: This must happen *before* resetting mPid! + GeckoProcessManager.INSTANCE.mConnections.removeConnection(this); + + mChild = null; + mPid = INVALID_PID; + } + } + + private static class NonContentConnection extends ChildConnection { + public NonContentConnection( + @NonNull final ServiceAllocator allocator, @NonNull final GeckoProcessType type) { + super(allocator, type, PriorityLevel.FOREGROUND); + if (type == GeckoProcessType.CONTENT) { + throw new AssertionError("Attempt to create a NonContentConnection as CONTENT"); + } + } + + protected void onAppForeground() { + setPriorityLevel(PriorityLevel.FOREGROUND); + } + + protected void onAppBackground() { + setPriorityLevel(PriorityLevel.IDLE); + } + } + + private static final class GpuProcessConnection extends NonContentConnection { + private CompositorSurfaceManager mCompositorSurfaceManager; + private ISurfaceAllocator mSurfaceAllocator; + + // Unique ID used to identify each GPU process instance. Will always be non-zero, + // and unlike the process' pid cannot be the same value for successive instances. + private int mUniqueGpuProcessId; + // Static counter used to initialize each instance's mUniqueGpuProcessId + private static int sUniqueGpuProcessIdCounter = 0; + + public GpuProcessConnection(@NonNull final ServiceAllocator allocator) { + super(allocator, GeckoProcessType.GPU); + + // Initialize the unique ID ensuring we skip 0 (as that is reserved for parent process + // allocators). + if (sUniqueGpuProcessIdCounter == 0) { + sUniqueGpuProcessIdCounter++; + } + mUniqueGpuProcessId = sUniqueGpuProcessIdCounter++; + } + + @Override + protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException { + mCompositorSurfaceManager = new CompositorSurfaceManager(child.getCompositorSurfaceManager()); + mSurfaceAllocator = child.getSurfaceAllocator(mUniqueGpuProcessId); + } + + public CompositorSurfaceManager getCompositorSurfaceManager() { + return mCompositorSurfaceManager; + } + + public ISurfaceAllocator getSurfaceAllocator() { + return mSurfaceAllocator; + } + } + + private static final class SocketProcessConnection extends NonContentConnection { + private boolean mIsForeground = true; + private boolean mIsNetworkUp = true; + + public SocketProcessConnection(@NonNull final ServiceAllocator allocator) { + super(allocator, GeckoProcessType.SOCKET); + GeckoProcessManager.INSTANCE.mConnections.enableNetworkNotifications(); + } + + public void onNetworkStateChange(final boolean isNetworkUp) { + mIsNetworkUp = isNetworkUp; + prioritize(); + } + + @Override + protected void onAppForeground() { + mIsForeground = true; + prioritize(); + } + + @Override + protected void onAppBackground() { + mIsForeground = false; + prioritize(); + } + + private static final PriorityLevel[][] sPriorityStates = initPriorityStates(); + + private static PriorityLevel[][] initPriorityStates() { + final PriorityLevel[][] states = new PriorityLevel[2][2]; + // Background, no network + states[0][0] = PriorityLevel.IDLE; + // Background, network + states[0][1] = PriorityLevel.BACKGROUND; + // Foreground, no network + states[1][0] = PriorityLevel.IDLE; + // Foreground, network + states[1][1] = PriorityLevel.FOREGROUND; + return states; + } + + private void prioritize() { + final PriorityLevel nextPriority = + sPriorityStates[mIsForeground ? 1 : 0][mIsNetworkUp ? 1 : 0]; + setPriorityLevel(nextPriority); + } + } + + private static final class ContentConnection extends ChildConnection { + private static final String TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME = + "GV_CONTENT_PROCESS_LIFETIME_MS"; + + private TelemetryUtils.UptimeTimer mLifetimeTimer = null; + + public ContentConnection( + @NonNull final ServiceAllocator allocator, @NonNull final PriorityLevel initialPriority) { + super(allocator, GeckoProcessType.CONTENT, initialPriority); + } + + @Override + protected void onBinderConnected(final IBinder service) { + mLifetimeTimer = new TelemetryUtils.UptimeTimer(TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME); + super.onBinderConnected(service); + } + + @Override + protected void onReleaseResources() { + if (mLifetimeTimer != null) { + mLifetimeTimer.stop(); + mLifetimeTimer = null; + } + + super.onReleaseResources(); + } + } + + /** This class manages the state surrounding existing connections and their priorities. */ + private static final class ConnectionManager extends JNIObject { + // Connections to non-content processes + private final ArrayMap<GeckoProcessType, NonContentConnection> mNonContentConnections; + // Mapping of pid to content process + private final SimpleArrayMap<Integer, ContentConnection> mContentPids; + // Set of initialized content process connections + private final ArraySet<ContentConnection> mContentConnections; + // Set of bound but uninitialized content connections + private final ArraySet<ContentConnection> mNonStartedContentConnections; + // Allocator for service IDs + private final ServiceAllocator mServiceAllocator; + private boolean mIsObservingNetwork = false; + + public ConnectionManager() { + mNonContentConnections = new ArrayMap<GeckoProcessType, NonContentConnection>(); + mContentPids = new SimpleArrayMap<Integer, ContentConnection>(); + mContentConnections = new ArraySet<ContentConnection>(); + mNonStartedContentConnections = new ArraySet<ContentConnection>(); + mServiceAllocator = new ServiceAllocator(); + + // Attach to native once JNI is ready. + if (GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) { + attachTo(this); + } else { + GeckoThread.queueNativeCallUntil( + GeckoThread.State.JNI_READY, ConnectionManager.class, "attachTo", this); + } + } + + private void enableNetworkNotifications() { + if (mIsObservingNetwork) { + return; + } + + mIsObservingNetwork = true; + + // Ensure that GeckoNetworkManager is monitoring network events so that we can + // prioritize the socket process. + ThreadUtils.runOnUiThread( + () -> { + GeckoNetworkManager.getInstance().enableNotifications(); + }); + + observeNetworkNotifications(); + } + + @WrapForJNI(dispatchTo = "gecko") + private static native void attachTo(ConnectionManager instance); + + @WrapForJNI(dispatchTo = "gecko") + private native void observeNetworkNotifications(); + + @WrapForJNI(calledFrom = "gecko") + private void onBackground() { + XPCOMEventTarget.runOnLauncherThread(() -> onAppBackgroundInternal()); + } + + @WrapForJNI(calledFrom = "gecko") + private void onForeground() { + XPCOMEventTarget.runOnLauncherThread(() -> onAppForegroundInternal()); + } + + @WrapForJNI(calledFrom = "gecko") + private void onNetworkStateChange(final boolean isUp) { + XPCOMEventTarget.runOnLauncherThread(() -> onNetworkStateChangeInternal(isUp)); + } + + @Override + protected native void disposeNative(); + + private void onAppBackgroundInternal() { + XPCOMEventTarget.assertOnLauncherThread(); + + for (final NonContentConnection conn : mNonContentConnections.values()) { + conn.onAppBackground(); + } + } + + private void onAppForegroundInternal() { + XPCOMEventTarget.assertOnLauncherThread(); + + for (final NonContentConnection conn : mNonContentConnections.values()) { + conn.onAppForeground(); + } + } + + private void onNetworkStateChangeInternal(final boolean isUp) { + XPCOMEventTarget.assertOnLauncherThread(); + + final SocketProcessConnection conn = + (SocketProcessConnection) mNonContentConnections.get(GeckoProcessType.SOCKET); + if (conn == null) { + return; + } + + conn.onNetworkStateChange(isUp); + } + + private void removeContentConnection(@NonNull final ChildConnection conn) { + if (!mContentConnections.remove(conn)) { + throw new RuntimeException("Attempt to remove non-registered connection"); + } + mNonStartedContentConnections.remove(conn); + + final int pid; + + try { + pid = conn.getPid(); + } catch (final IncompleteChildConnectionException e) { + // conn lost its binding before it was able to retrieve its pid. It follows that + // mContentPids does not have an entry for this connection, so we can just return. + return; + } + + if (pid == INVALID_PID) { + return; + } + + final ChildConnection removed = mContentPids.remove(Integer.valueOf(pid)); + if (removed != null && removed != conn) { + throw new RuntimeException( + "Integrity error - connection mismatch for pid " + Integer.toString(pid)); + } + } + + public void removeConnection(@NonNull final ChildConnection conn) { + XPCOMEventTarget.assertOnLauncherThread(); + + if (conn.getType() == GeckoProcessType.CONTENT) { + removeContentConnection(conn); + return; + } + + final ChildConnection removed = mNonContentConnections.remove(conn.getType()); + if (removed != conn) { + throw new RuntimeException( + "Integrity error - connection mismatch for process type " + conn.getType().toString()); + } + } + + /** Saves any state information that was acquired upon start completion. */ + public void onBindComplete(@NonNull final ChildConnection conn) { + if (conn.getType() == GeckoProcessType.CONTENT) { + final int pid = conn.getPid(); + if (pid == INVALID_PID) { + throw new AssertionError( + "PID is invalid even though our caller just successfully retrieved it after binding"); + } + + mContentPids.put(pid, (ContentConnection) conn); + } + } + + /** Retrieve the ChildConnection for an already running content process. */ + private ContentConnection getExistingContentConnection(@NonNull final Selector selector) { + XPCOMEventTarget.assertOnLauncherThread(); + if (selector.getType() != GeckoProcessType.CONTENT) { + throw new IllegalArgumentException("Selector is not for content!"); + } + + return mContentPids.get(selector.getPid()); + } + + /** Unconditionally create a new content connection for the specified priority. */ + private ContentConnection getNewContentConnection(@NonNull final PriorityLevel newPriority) { + final ContentConnection result = new ContentConnection(mServiceAllocator, newPriority); + mContentConnections.add(result); + + return result; + } + + /** Retrieve the ChildConnection for an already running child process of any type. */ + public ChildConnection getExistingConnection(@NonNull final Selector selector) { + XPCOMEventTarget.assertOnLauncherThread(); + + final GeckoProcessType type = selector.getType(); + + if (type == GeckoProcessType.CONTENT) { + return getExistingContentConnection(selector); + } + + return mNonContentConnections.get(type); + } + + /** + * Retrieve a ChildConnection for a content process for the purposes of starting. If there are + * any preloaded content processes already running, we will use one of those. Otherwise we will + * allocate a new ChildConnection. + */ + private ChildConnection getContentConnectionForStart() { + XPCOMEventTarget.assertOnLauncherThread(); + + if (mNonStartedContentConnections.isEmpty()) { + return getNewContentConnection(PriorityLevel.FOREGROUND); + } + + final ChildConnection conn = + mNonStartedContentConnections.removeAt(mNonStartedContentConnections.size() - 1); + conn.setPriorityLevel(PriorityLevel.FOREGROUND); + return conn; + } + + /** Retrieve or create a new child process for the specified non-content process. */ + private ChildConnection getNonContentConnection(@NonNull final GeckoProcessType type) { + XPCOMEventTarget.assertOnLauncherThread(); + if (type == GeckoProcessType.CONTENT) { + throw new IllegalArgumentException("Content processes not supported by this method"); + } + + NonContentConnection connection = mNonContentConnections.get(type); + if (connection == null) { + if (type == GeckoProcessType.SOCKET) { + connection = new SocketProcessConnection(mServiceAllocator); + } else if (type == GeckoProcessType.GPU) { + connection = new GpuProcessConnection(mServiceAllocator); + } else { + connection = new NonContentConnection(mServiceAllocator, type); + } + + mNonContentConnections.put(type, connection); + } + + return connection; + } + + /** Retrieve a ChildConnection for the purposes of starting a new child process. */ + public ChildConnection getConnectionForStart(@NonNull final GeckoProcessType type) { + if (type == GeckoProcessType.CONTENT) { + return getContentConnectionForStart(); + } + + return getNonContentConnection(type); + } + + /** Retrieve a ChildConnection for the purposes of preloading a new child process. */ + public ChildConnection getConnectionForPreload(@NonNull final GeckoProcessType type) { + if (type == GeckoProcessType.CONTENT) { + final ContentConnection conn = getNewContentConnection(PriorityLevel.BACKGROUND); + mNonStartedContentConnections.add(conn); + return conn; + } + + return getNonContentConnection(type); + } + } + + private final ConnectionManager mConnections; + + private GeckoProcessManager() { + mConnections = new ConnectionManager(); + mInstanceId = UUID.randomUUID().toString(); + } + + public void preload(final GeckoProcessType... types) { + XPCOMEventTarget.launcherThread() + .execute( + () -> { + for (final GeckoProcessType type : types) { + final ChildConnection connection = mConnections.getConnectionForPreload(type); + connection.bind(); + } + }); + } + + public void crashChild(@NonNull final Selector selector) { + XPCOMEventTarget.launcherThread() + .execute( + () -> { + final ChildConnection conn = mConnections.getExistingConnection(selector); + if (conn == null) { + return; + } + + conn.bind() + .accept( + proc -> { + try { + proc.crash(); + } catch (final RemoteException e) { + } + }); + }); + } + + @WrapForJNI + private static void shutdownProcess(final Selector selector) { + XPCOMEventTarget.assertOnLauncherThread(); + final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector); + if (conn == null) { + return; + } + + conn.unbind(); + } + + @WrapForJNI + private static void setProcessPriority( + @NonNull final Selector selector, + @NonNull final PriorityLevel priorityLevel, + final int relativeImportance) { + XPCOMEventTarget.runOnLauncherThread( + () -> { + final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector); + if (conn == null) { + return; + } + + conn.setPriorityLevel(priorityLevel, relativeImportance); + }); + } + + @WrapForJNI + private static GeckoResult<Integer> start( + final GeckoProcessType type, + final String[] args, + final int prefsFd, + final int prefMapFd, + final int ipcFd, + final int crashFd, + final int crashAnnotationFd) { + final GeckoResult<Integer> result = new GeckoResult<>(); + final StartInfo info = + new StartInfo( + type, + GeckoThread.InitInfo.builder() + .args(args) + .userSerialNumber(System.getenv("MOZ_ANDROID_USER_SERIAL_NUMBER")) + .extras(GeckoThread.getActiveExtras()) + .flags(filterFlagsForChild(GeckoThread.getActiveFlags())) + .fds( + FileDescriptors.builder() + .prefs(prefsFd) + .prefMap(prefMapFd) + .ipc(ipcFd) + .crashReporter(crashFd) + .crashAnnotation(crashAnnotationFd) + .build()) + .build()); + + XPCOMEventTarget.runOnLauncherThread( + () -> { + INSTANCE + .start(info) + .accept(result::complete, result::completeExceptionally) + .finally_(info.pfds::close); + }); + + return result; + } + + private static int filterFlagsForChild(final int flags) { + return flags & GeckoThread.FLAG_ENABLE_NATIVE_CRASHREPORTER; + } + + private static class StartInfo { + final GeckoProcessType type; + final String crashHandler; + final GeckoThread.InitInfo init; + + final ParcelFileDescriptors pfds; + + private StartInfo(final GeckoProcessType type, final GeckoThread.InitInfo initInfo) { + this.type = type; + this.init = initInfo; + crashHandler = + GeckoAppShell.getCrashHandlerService() != null + ? GeckoAppShell.getCrashHandlerService().getName() + : null; + // The native side owns the File Descriptors so we cannot call adopt here. + pfds = ParcelFileDescriptors.from(initInfo.fds); + } + } + + private static final int MAX_RETRIES = 3; + + private GeckoResult<Integer> start(final StartInfo info) { + return start(info, new ArrayList<>()); + } + + private GeckoResult<Integer> retry( + final StartInfo info, final List<Throwable> retryLog, final Throwable error) { + retryLog.add(error); + + if (error instanceof StartException) { + final StartException startError = (StartException) error; + if (startError.errorCode == IChildProcess.STARTED_BUSY) { + // This process is owned by a different runtime, so we can't use + // it. We will keep retrying indefinitely until we find a non-busy process. + // Note: this strategy is pretty bad, we go through each process in + // sequence until one works, the multiple runtime case is test-only + // for now, so that's ok. We can improve on this if we eventually + // end up needing something fancier. + return start(info, retryLog); + } + } + + // If we couldn't unbind there's something very wrong going on and we bail + // immediately. + if (retryLog.size() >= MAX_RETRIES || error instanceof UnbindException) { + return GeckoResult.fromException(fromRetryLog(retryLog)); + } + + return start(info, retryLog); + } + + private String serializeLog(final List<Throwable> retryLog) { + if (retryLog == null || retryLog.size() == 0) { + return "Empty log."; + } + + final StringBuilder message = new StringBuilder(); + + for (final Throwable error : retryLog) { + if (error instanceof UnbindException) { + message.append("Could not unbind: "); + } else if (error instanceof StartException) { + message.append("Cannot restart child: "); + } else { + message.append("Error while binding: "); + } + message.append(error); + message.append(";"); + } + + return message.toString(); + } + + private RuntimeException fromRetryLog(final List<Throwable> retryLog) { + return new RuntimeException(serializeLog(retryLog), retryLog.get(retryLog.size() - 1)); + } + + private GeckoResult<Integer> start(final StartInfo info, final List<Throwable> retryLog) { + return startInternal(info).then(GeckoResult::fromValue, error -> retry(info, retryLog, error)); + } + + private static class StartException extends RuntimeException { + public final int errorCode; + + public StartException(final int errorCode, final int pid) { + super("Could not start process, errorCode: " + errorCode + " PID: " + pid); + this.errorCode = errorCode; + } + } + + private GeckoResult<Integer> startInternal(final StartInfo info) { + XPCOMEventTarget.assertOnLauncherThread(); + + final ChildConnection connection = mConnections.getConnectionForStart(info.type); + return connection + .bind() + .map( + child -> { + final int result = + child.start( + this, + mInstanceId, + info.init.args, + info.init.extras, + info.init.flags, + info.init.userSerialNumber, + info.crashHandler, + info.pfds.prefs, + info.pfds.prefMap, + info.pfds.ipc, + info.pfds.crashReporter, + info.pfds.crashAnnotation); + if (result == IChildProcess.STARTED_OK) { + return connection.getPid(); + } else { + throw new StartException(result, connection.getPid()); + } + }) + .then(GeckoResult::fromValue, error -> handleBindError(connection, error)); + } + + private GeckoResult<Integer> handleBindError( + final ChildConnection connection, final Throwable error) { + return connection + .unbind() + .then( + unused -> GeckoResult.fromException(error), + unbindError -> GeckoResult.fromException(new UnbindException(unbindError))); + } + + private static class UnbindException extends RuntimeException { + public UnbindException(final Throwable cause) { + super(cause); + } + } +} // GeckoProcessManager diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java new file mode 100644 index 0000000000..812a27614c --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java @@ -0,0 +1,40 @@ +/* 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/. */ + +package org.mozilla.gecko.process; + +import org.mozilla.gecko.annotation.WrapForJNI; + +@WrapForJNI +public enum GeckoProcessType { + // These need to match the stringified names from the GeckoProcessType enum + PARENT("default"), + PLUGIN("plugin"), + CONTENT("tab"), + IPDLUNITTEST("ipdlunittest"), + GMPLUGIN("gmplugin"), + GPU("gpu"), + VR("vr"), + RDD("rdd"), + SOCKET("socket"), + REMOTESANDBOXBROKER("sandboxbroker"), + FORKSERVER("forkserver"), + UTILITY("utility"); + + private final String mGeckoName; + + private GeckoProcessType(final String geckoName) { + mGeckoName = geckoName; + } + + @Override + public String toString() { + return mGeckoName; + } + + @WrapForJNI + private static final GeckoProcessType fromInt(final int type) { + return values()[type]; + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java new file mode 100644 index 0000000000..e030a47c74 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java @@ -0,0 +1,213 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.process; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.GeckoThread.FileDescriptors; +import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors; +import org.mozilla.gecko.IGeckoEditableChild; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.gfx.ICompositorSurfaceManager; +import org.mozilla.gecko.gfx.ISurfaceAllocator; +import org.mozilla.gecko.util.ThreadUtils; + +public class GeckoServiceChildProcess extends Service { + private static final String LOGTAG = "ServiceChildProcess"; + + private static IProcessManager sProcessManager; + private static String sOwnerProcessId; + private final MemoryController mMemoryController = new MemoryController(); + + // Makes sure we don't reuse this process + private static boolean sCreateCalled; + + @WrapForJNI(calledFrom = "gecko") + private static void getEditableParent( + final IGeckoEditableChild child, final long contentId, final long tabId) { + try { + sProcessManager.getEditableParent(child, contentId, tabId); + } catch (final RemoteException e) { + Log.e(LOGTAG, "Cannot get editable", e); + } + } + + @Override + public void onCreate() { + super.onCreate(); + Log.i(LOGTAG, "onCreate"); + + if (sCreateCalled) { + // We don't support reusing processes, and this could get us in a really weird state, + // so let's throw here. + throw new RuntimeException("Cannot reuse process."); + } + sCreateCalled = true; + + GeckoAppShell.setApplicationContext(getApplicationContext()); + GeckoThread.launch(); // Preload Gecko. + } + + protected static class ChildProcessBinder extends IChildProcess.Stub { + @Override + public int getPid() { + return Process.myPid(); + } + + @Override + public int start( + final IProcessManager procMan, + final String mainProcessId, + final String[] args, + final Bundle extras, + final int flags, + final String userSerialNumber, + final String crashHandlerService, + final ParcelFileDescriptor prefsPfd, + final ParcelFileDescriptor prefMapPfd, + final ParcelFileDescriptor ipcPfd, + final ParcelFileDescriptor crashReporterPfd, + final ParcelFileDescriptor crashAnnotationPfd) { + + final ParcelFileDescriptors pfds = + ParcelFileDescriptors.builder() + .prefs(prefsPfd) + .prefMap(prefMapPfd) + .ipc(ipcPfd) + .crashReporter(crashReporterPfd) + .crashAnnotation(crashAnnotationPfd) + .build(); + + synchronized (GeckoServiceChildProcess.class) { + if (sOwnerProcessId != null && !sOwnerProcessId.equals(mainProcessId)) { + Log.w( + LOGTAG, + "This process belongs to a different GeckoRuntime owner: " + + sOwnerProcessId + + " process: " + + mainProcessId); + // We need to close the File Descriptors here otherwise we will leak them causing a + // shutdown hang. + pfds.close(); + return IChildProcess.STARTED_BUSY; + } + if (sProcessManager != null) { + Log.e(LOGTAG, "Child process already started"); + pfds.close(); + return IChildProcess.STARTED_FAIL; + } + sProcessManager = procMan; + sOwnerProcessId = mainProcessId; + } + + final FileDescriptors fds = pfds.detach(); + ThreadUtils.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (crashHandlerService != null) { + try { + @SuppressWarnings("unchecked") + final Class<? extends Service> crashHandler = + (Class<? extends Service>) Class.forName(crashHandlerService); + + // Native crashes are reported through pipes, so we don't have to + // do anything special for that. + GeckoAppShell.setCrashHandlerService(crashHandler); + GeckoAppShell.ensureCrashHandling(crashHandler); + } catch (final ClassNotFoundException e) { + Log.w(LOGTAG, "Couldn't find crash handler service " + crashHandlerService); + } + } + + final GeckoThread.InitInfo info = + GeckoThread.InitInfo.builder() + .args(args) + .extras(extras) + .flags(flags) + .userSerialNumber(userSerialNumber) + .fds(fds) + .build(); + + if (GeckoThread.init(info)) { + GeckoThread.launch(); + } + } + }); + return IChildProcess.STARTED_OK; + } + + @Override + public void crash() { + GeckoThread.crash(); + } + + @Override + public ICompositorSurfaceManager getCompositorSurfaceManager() { + Log.e( + LOGTAG, "Invalid call to IChildProcess.getCompositorSurfaceManager for non-GPU process"); + throw new AssertionError( + "Invalid call to IChildProcess.getCompositorSurfaceManager for non-GPU process."); + } + + @Override + public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) { + Log.e(LOGTAG, "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process"); + throw new AssertionError( + "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process."); + } + } + + protected Binder createBinder() { + return new ChildProcessBinder(); + } + + private final Binder mBinder = createBinder(); + + @Override + public void onDestroy() { + Log.i(LOGTAG, "Destroying GeckoServiceChildProcess"); + System.exit(0); + } + + @Override + public IBinder onBind(final Intent intent) { + // Calling stopSelf ensures that whenever the client unbinds the process dies immediately. + stopSelf(); + return mBinder; + } + + @Override + public void onTrimMemory(final int level) { + mMemoryController.onTrimMemory(level); + + // This is currently a no-op in Service, but let's future-proof. + super.onTrimMemory(level); + } + + @Override + public void onLowMemory() { + mMemoryController.onLowMemory(); + super.onLowMemory(); + } + + /** + * Returns the surface allocator interface that should be used by this process to allocate + * Surfaces, for consumption in either the GPU process or parent process. + */ + public static ISurfaceAllocator getSurfaceAllocator() throws RemoteException { + return sProcessManager.getSurfaceAllocator(); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java new file mode 100644 index 0000000000..e4312c7e67 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java @@ -0,0 +1,63 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.process; + +import android.os.Binder; +import android.util.SparseArray; +import android.view.Surface; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.gfx.ICompositorSurfaceManager; +import org.mozilla.gecko.gfx.ISurfaceAllocator; +import org.mozilla.gecko.gfx.RemoteSurfaceAllocator; + +public class GeckoServiceGpuProcess extends GeckoServiceChildProcess { + private static final String LOGTAG = "ServiceGpuProcess"; + + private static final class GpuProcessBinder extends GeckoServiceChildProcess.ChildProcessBinder { + @Override + public ICompositorSurfaceManager getCompositorSurfaceManager() { + return RemoteCompositorSurfaceManager.getInstance(); + } + + @Override + public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) { + return RemoteSurfaceAllocator.getInstance(allocatorId); + } + } + + @Override + protected Binder createBinder() { + return new GpuProcessBinder(); + } + + public static final class RemoteCompositorSurfaceManager extends ICompositorSurfaceManager.Stub { + private static RemoteCompositorSurfaceManager mInstance; + + @WrapForJNI + private static synchronized RemoteCompositorSurfaceManager getInstance() { + if (mInstance == null) { + mInstance = new RemoteCompositorSurfaceManager(); + } + return mInstance; + } + + private final SparseArray<Surface> mSurfaces = new SparseArray<Surface>(); + + @Override + public synchronized void onSurfaceChanged(final int widgetId, final Surface surface) { + if (surface != null) { + mSurfaces.put(widgetId, surface); + } else { + mSurfaces.remove(widgetId); + } + } + + @WrapForJNI + public synchronized Surface getCompositorSurface(final int widgetId) { + return mSurfaces.get(widgetId); + } + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java new file mode 100644 index 0000000000..f2dcb7a52b --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java @@ -0,0 +1,74 @@ +/* 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/. */ + +package org.mozilla.gecko.process; + +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; +import android.util.Log; +import androidx.annotation.NonNull; +import org.mozilla.gecko.GeckoAppShell; + +public class MemoryController implements ComponentCallbacks2 { + private static final String LOGTAG = "MemoryController"; + private long mLastLowMemoryNotificationTime = 0; + + // Allowed elapsed time between full GCs while under constant memory pressure + private static final long LOW_MEMORY_ONGOING_RESET_TIME_MS = 10000; + + private static final int LOW = 0; + private static final int MODERATE = 1; + private static final int CRITICAL = 2; + + private int memoryLevelFromTrim(final int level) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE + || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + return CRITICAL; + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + return MODERATE; + } + return LOW; + } + + public void onTrimMemory(final int level) { + Log.i(LOGTAG, "onTrimMemory(" + level + ")"); + onMemoryNotification(memoryLevelFromTrim(level)); + } + + @Override + public void onConfigurationChanged(final @NonNull Configuration newConfig) {} + + public void onLowMemory() { + Log.i(LOGTAG, "onLowMemory"); + onMemoryNotification(CRITICAL); + } + + private void onMemoryNotification(final int level) { + if (level == LOW) { + // The trim level is too low to be actionable + return; + } + + // See nsIMemory.idl for descriptions of the various arguments to the "memory-pressure" + // observer. + final String observerArg; + + final long currentNotificationTime = System.currentTimeMillis(); + if (level == CRITICAL + || (currentNotificationTime - mLastLowMemoryNotificationTime) + >= LOW_MEMORY_ONGOING_RESET_TIME_MS) { + // We do a full "low-memory" notification for both new and last-ditch onTrimMemory requests. + observerArg = "low-memory"; + mLastLowMemoryNotificationTime = currentNotificationTime; + } else { + // If it has been less than ten seconds since the last time we sent a "low-memory" + // notification, we send a "low-memory-ongoing" notification instead. + // This prevents Gecko from re-doing full GC's repeatedly over and over in succession, + // as they are expensive and quickly result in diminishing returns. + observerArg = "low-memory-ongoing"; + } + + GeckoAppShell.notifyObservers("memory-pressure", observerArg); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java new file mode 100644 index 0000000000..8058d71601 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java @@ -0,0 +1,613 @@ +/* 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/. */ + +package org.mozilla.gecko.process; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; +import androidx.annotation.NonNull; +import java.security.SecureRandom; +import java.util.BitSet; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.util.XPCOMEventTarget; + +/* package */ final class ServiceAllocator { + private static final String LOGTAG = "ServiceAllocator"; + private static final int MAX_NUM_ISOLATED_CONTENT_SERVICES = + GeckoChildProcessServices.MAX_NUM_ISOLATED_CONTENT_SERVICES; + + private static boolean hasQApis() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + + /** + * Possible priority levels that are available to child services. Each one maps to a flag that is + * passed into Context.bindService(). + */ + @WrapForJNI + public static enum PriorityLevel { + FOREGROUND(Context.BIND_IMPORTANT), + BACKGROUND(0), + IDLE(Context.BIND_WAIVE_PRIORITY); + + private final int mAndroidFlag; + + private PriorityLevel(final int androidFlag) { + mAndroidFlag = androidFlag; + } + + public int getAndroidFlag() { + return mAndroidFlag; + } + } + + public static final class BindException extends RuntimeException { + public BindException(@NonNull final String msg) { + super(msg); + } + } + + private interface BindServiceDelegate { + boolean bindService(ServiceConnection binding, PriorityLevel priority); + + String getServiceName(); + } + + /** + * Abstract class that holds the essential per-service data that is required to work with + * ServiceAllocator. ServiceAllocator clients should extend this class when implementing their + * per-service connection objects. + */ + public abstract static class InstanceInfo { + private class Binding implements ServiceConnection { + /** + * This implementation of ServiceConnection.onServiceConnected simply bounces the connection + * notification over to the launcher thread (if it is not already on it). + */ + @Override + public final void onServiceConnected(final ComponentName name, final IBinder service) { + XPCOMEventTarget.runOnLauncherThread( + () -> { + onBinderConnectedInternal(service); + }); + } + + /** + * This implementation of ServiceConnection.onServiceDisconnected simply bounces the + * disconnection notification over to the launcher thread (if it is not already on it). + */ + @Override + public final void onServiceDisconnected(final ComponentName name) { + XPCOMEventTarget.runOnLauncherThread( + () -> { + onBinderConnectionLostInternal(); + }); + } + } + + private class DefaultBindDelegate implements BindServiceDelegate { + @Override + public boolean bindService( + @NonNull final ServiceConnection binding, @NonNull final PriorityLevel priority) { + final Context context = GeckoAppShell.getApplicationContext(); + final Intent intent = new Intent(); + intent.setClassName(context, getServiceName()); + return bindServiceDefault(context, intent, binding, getAndroidFlags(priority)); + } + + @Override + public String getServiceName() { + return getSvcClassNameDefault(InstanceInfo.this); + } + } + + private class IsolatedBindDelegate implements BindServiceDelegate { + @Override + public boolean bindService( + @NonNull final ServiceConnection binding, @NonNull final PriorityLevel priority) { + final Context context = GeckoAppShell.getApplicationContext(); + final Intent intent = new Intent(); + intent.setClassName(context, getServiceName()); + return bindServiceIsolated( + context, intent, getAndroidFlags(priority), getIdInternal(), binding); + } + + @Override + public String getServiceName() { + return ServiceUtils.buildIsolatedSvcName(getType()); + } + } + + private final ServiceAllocator mAllocator; + private final GeckoProcessType mType; + private final String mId; + private final EnumMap<PriorityLevel, Binding> mBindings; + private final BindServiceDelegate mBindDelegate; + + private boolean mCalledConnected = false; + private boolean mCalledConnectionLost = false; + private boolean mIsDefunct = false; + + private PriorityLevel mCurrentPriority; + private int mRelativeImportance = 0; + + protected InstanceInfo( + @NonNull final ServiceAllocator allocator, + @NonNull final GeckoProcessType type, + @NonNull final PriorityLevel initialPriority) { + mAllocator = allocator; + mType = type; + mId = mAllocator.allocate(type); + mBindings = new EnumMap<PriorityLevel, Binding>(PriorityLevel.class); + mBindDelegate = getBindServiceDelegate(); + + mCurrentPriority = initialPriority; + } + + private BindServiceDelegate getBindServiceDelegate() { + if (mType != GeckoProcessType.CONTENT) { + // Non-content services just use default binding + return this.new DefaultBindDelegate(); + } + + // Content services defer to the alloc policy + return mAllocator.mContentAllocPolicy.getBindServiceDelegate(this); + } + + public PriorityLevel getPriorityLevel() { + XPCOMEventTarget.assertOnLauncherThread(); + return mCurrentPriority; + } + + public boolean setPriorityLevel(@NonNull final PriorityLevel newPriority) { + return setPriorityLevel(newPriority, 0); + } + + public boolean setPriorityLevel( + @NonNull final PriorityLevel newPriority, final int relativeImportance) { + XPCOMEventTarget.assertOnLauncherThread(); + mCurrentPriority = newPriority; + mRelativeImportance = relativeImportance; + + // If we haven't bound yet then we can just return + if (mBindings.size() == 0) { + return true; + } + + // Otherwise we need to update our bindings + return updateBindings(); + } + + /** + * Only content services have unique IDs. This method throws if called for a non-content service + * type. + */ + public String getId() { + if (mId == null) { + throw new RuntimeException("This service does not have a unique id"); + } + + return mId; + } + + /** This method is infallible and returns an empty string for non-content services. */ + private String getIdInternal() { + return mId == null ? "" : mId; + } + + public boolean isContent() { + return mType == GeckoProcessType.CONTENT; + } + + public GeckoProcessType getType() { + return mType; + } + + protected boolean bindService() { + if (mIsDefunct) { + final String errorMsg = + "Attempt to bind a defunct InstanceInfo for " + mType + " child process"; + throw new BindException(errorMsg); + } + + return updateBindings(); + } + + /** + * Unbinds the service described by |this| and releases our unique ID. This method may safely be + * called multiple times even if we are already defunct. + */ + protected void unbindService() { + XPCOMEventTarget.assertOnLauncherThread(); + + // This could happen if a service death races with our attempt to shut it down. + if (mIsDefunct) { + return; + } + + final Context context = GeckoAppShell.getApplicationContext(); + + // Make a clone of mBindings to iterate over since we're going to mutate the original + final EnumMap<PriorityLevel, Binding> cloned = mBindings.clone(); + for (final Entry<PriorityLevel, Binding> entry : cloned.entrySet()) { + try { + context.unbindService(entry.getValue()); + } catch (final IllegalArgumentException e) { + // The binding was already dead. That's okay. + } + + mBindings.remove(entry.getKey()); + } + + if (mBindings.size() != 0) { + throw new IllegalStateException("Unable to release all bindings"); + } + + mIsDefunct = true; + mAllocator.release(this); + onReleaseResources(); + } + + private void onBinderConnectedInternal(@NonNull final IBinder service) { + XPCOMEventTarget.assertOnLauncherThread(); + // We only care about the first time this is called; subsequent bindings can be ignored. + if (mCalledConnected) { + return; + } + + mCalledConnected = true; + + onBinderConnected(service); + } + + private void onBinderConnectionLostInternal() { + XPCOMEventTarget.assertOnLauncherThread(); + // We only care about the first time this is called; subsequent connection errors can be + // ignored. + if (mCalledConnectionLost) { + return; + } + + mCalledConnectionLost = true; + + onBinderConnectionLost(); + } + + protected abstract void onBinderConnected(@NonNull final IBinder service); + + protected abstract void onReleaseResources(); + + // Optionally overridable by subclasses, but this is a sane default + protected void onBinderConnectionLost() { + // The binding has lost its connection, but the binding itself might still be active. + // Gecko itself will request a process restart, so here we attempt to unbind so that + // Android does not try to automatically restart and reconnect the service. + unbindService(); + } + + /** + * This function relies on the fact that the PriorityLevel enum is ordered from highest priority + * to lowest priority. We examine the ordinal of the current priority setting, and then iterate + * across all possible priority levels, adjusting as necessary. Any priority levels whose + * ordinals are less than then current priority level ordinal must be unbound, while all + * priority levels whose ordinals are greater than or equal to the current priority level + * ordinal must be bound. + */ + @TargetApi(29) + private boolean updateBindings() { + XPCOMEventTarget.assertOnLauncherThread(); + int numBindSuccesses = 0; + int numBindFailures = 0; + int numUnbindSuccesses = 0; + + final Context context = GeckoAppShell.getApplicationContext(); + + // This code assumes that the order of the PriorityLevel enum is highest to lowest + final int curPriorityOrdinal = mCurrentPriority.ordinal(); + final PriorityLevel[] levels = PriorityLevel.values(); + + for (int curLevelIdx = 0; curLevelIdx < levels.length; ++curLevelIdx) { + final PriorityLevel curLevel = levels[curLevelIdx]; + final Binding existingBinding = mBindings.get(curLevel); + final boolean hasExistingBinding = existingBinding != null; + + if (curLevelIdx < curPriorityOrdinal) { + // Remove if present + if (hasExistingBinding) { + try { + context.unbindService(existingBinding); + ++numUnbindSuccesses; + mBindings.remove(curLevel); + } catch (final IllegalArgumentException e) { + // The binding was already dead. That's okay. + ++numUnbindSuccesses; + mBindings.remove(curLevel); + } + } + } else { + // Normally we only need to do a bind if we do not yet have an existing binding + // for this priority level. + boolean bindNeeded = !hasExistingBinding; + + // We only update the service group when the binding for this level already + // exists and no binds have occurred yet during the current updateBindings call. + if (hasExistingBinding && hasQApis() && (numBindSuccesses + numBindFailures) == 0) { + // NB: Right now we're passing 0 as the |group| argument, indicating that + // the process is not grouped with any other processes. Once we support + // Fission we should re-evaluate this. + context.updateServiceGroup(existingBinding, 0, mRelativeImportance); + // Now we need to call bindService with the existing binding to make this + // change take effect. + bindNeeded = true; + } + + if (bindNeeded) { + final Binding useBinding = hasExistingBinding ? existingBinding : this.new Binding(); + if (mBindDelegate.bindService(useBinding, curLevel)) { + ++numBindSuccesses; + if (!hasExistingBinding) { + mBindings.put(curLevel, useBinding); + } + } else { + ++numBindFailures; + } + } + } + } + + final String svcName = mBindDelegate.getServiceName(); + final StringBuilder builder = new StringBuilder(svcName); + builder + .append(" updateBindings: ") + .append(mCurrentPriority) + .append(" priority, ") + .append(mRelativeImportance) + .append(" importance, ") + .append(numBindSuccesses) + .append(" successful binds, ") + .append(numBindFailures) + .append(" failed binds, ") + .append(numUnbindSuccesses) + .append(" successful unbinds"); + Log.d(LOGTAG, builder.toString()); + + return numBindFailures == 0; + } + } + + private interface ContentAllocationPolicy { + /** + * @return BindServiceDelegate that will be used for binding a new content service. + */ + BindServiceDelegate getBindServiceDelegate(InstanceInfo info); + + /** + * Allocate an unused service ID for use by the caller. + * + * @return The new service id. + */ + String allocate(); + + /** + * Release a previously used service ID. + * + * @param id The service id being released. + */ + void release(final String id); + } + + /** + * This policy is intended for Android versions < 10, as well as for content process services + * that are not defined as isolated processes. In this case, the number of possible content + * service IDs has a fixed upper bound, so we use a BitSet to manage their allocation. + */ + private static final class DefaultContentPolicy implements ContentAllocationPolicy { + private final int mMaxNumSvcs; + private final BitSet mAllocator; + private final SecureRandom mRandom; + + public DefaultContentPolicy() { + mMaxNumSvcs = getContentServiceCount(); + mAllocator = new BitSet(mMaxNumSvcs); + mRandom = new SecureRandom(); + } + + @Override + public BindServiceDelegate getBindServiceDelegate(@NonNull final InstanceInfo info) { + return info.new DefaultBindDelegate(); + } + + @Override + public String allocate() { + final int[] available = new int[mMaxNumSvcs]; + int size = 0; + for (int i = 0; i < mMaxNumSvcs; i++) { + if (!mAllocator.get(i)) { + available[size] = i; + size++; + } + } + + if (size == 0) { + throw new RuntimeException("No more content services available"); + } + + final int next = available[mRandom.nextInt(size)]; + mAllocator.set(next); + return Integer.toString(next); + } + + @Override + public void release(final String stringId) { + final int id = Integer.valueOf(stringId); + if (!mAllocator.get(id)) { + throw new IllegalStateException("Releasing an unallocated id=" + id); + } + + mAllocator.clear(id); + } + + /** + * @return The number of content services defined in our manifest. + */ + private static int getContentServiceCount() { + return ServiceUtils.getServiceCount( + GeckoAppShell.getApplicationContext(), GeckoProcessType.CONTENT); + } + } + + /** + * This policy is intended for Android versions >= 10 when our content process services are + * defined in our manifest as having isolated processes. Since isolated services share a single + * service definition, there is no longer an Android-induced hard limit on the number of content + * processes that may be started. We simply use a monotonically-increasing counter to generate + * unique instance IDs in this case. + */ + private static final class IsolatedContentPolicy implements ContentAllocationPolicy { + private final Set<String> mRunningServiceIds = new HashSet<>(); + + @Override + public BindServiceDelegate getBindServiceDelegate(@NonNull final InstanceInfo info) { + return info.new IsolatedBindDelegate(); + } + + /** + * We generate a new instance ID simply by incrementing a counter. We do track how many content + * services are currently active for the purposes of maintaining the configured limit on number + * of simultaneous content processes. + */ + @Override + public String allocate() { + if (mRunningServiceIds.size() >= MAX_NUM_ISOLATED_CONTENT_SERVICES) { + throw new RuntimeException("No more content services available"); + } + + final String newId = UUID.randomUUID().toString(); + mRunningServiceIds.add(newId); + return newId; + } + + /** Just drop the count of active services. */ + @Override + public void release(final String id) { + if (!mRunningServiceIds.remove(id)) { + throw new IllegalStateException("Releasing an unallocated id"); + } + } + } + + /** The policy used for allocating content processes. */ + private ContentAllocationPolicy mContentAllocPolicy = null; + + /** + * Allocate a service ID. + * + * @param type The type of service. + * @return Integer encapsulating the service ID, or null if no ID is necessary. + */ + private String allocate(@NonNull final GeckoProcessType type) { + XPCOMEventTarget.assertOnLauncherThread(); + if (type != GeckoProcessType.CONTENT) { + // No unique id necessary + return null; + } + + // Lazy initialization of mContentAllocPolicy to ensure that it is constructed on the + // launcher thread. + if (mContentAllocPolicy == null) { + if (canBindIsolated(GeckoProcessType.CONTENT)) { + mContentAllocPolicy = new IsolatedContentPolicy(); + } else { + mContentAllocPolicy = new DefaultContentPolicy(); + } + } + + return mContentAllocPolicy.allocate(); + } + + /** + * Free a defunct service's ID if necessary. + * + * @param info The InstanceInfo-derived object that contains essential information for tearing + * down the child service. + */ + private void release(@NonNull final InstanceInfo info) { + XPCOMEventTarget.assertOnLauncherThread(); + if (!info.isContent()) { + return; + } + + mContentAllocPolicy.release(info.getId()); + } + + /** + * Find out whether the desired service type is defined in our manifest as having an isolated + * process. + * + * @param type Service type to query + * @return true if this service type may use isolated binding, otherwise false. + */ + private static boolean canBindIsolated(@NonNull final GeckoProcessType type) { + if (!hasQApis()) { + return false; + } + + final Context context = GeckoAppShell.getApplicationContext(); + final int svcFlags = ServiceUtils.getServiceFlags(context, type); + return (svcFlags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0; + } + + /** Convert PriorityLevel into the flags argument to Context.bindService() et al */ + private static int getAndroidFlags(@NonNull final PriorityLevel priority) { + return Context.BIND_AUTO_CREATE | priority.getAndroidFlag(); + } + + /** Obtain the class name to use for service binding in the default (ie, non-isolated) case. */ + private static String getSvcClassNameDefault(@NonNull final InstanceInfo info) { + return ServiceUtils.buildSvcName(info.getType(), info.getIdInternal()); + } + + /** + * Wrapper for bindService() that utilizes the Context.bindService() overload that accepts an + * Executor argument, when available. Otherwise it falls back to the legacy overload. + */ + @TargetApi(29) + private static boolean bindServiceDefault( + @NonNull final Context context, + @NonNull final Intent intent, + @NonNull final ServiceConnection conn, + final int flags) { + if (hasQApis()) { + // We always specify the launcher thread as our Executor. + return context.bindService(intent, flags, XPCOMEventTarget.launcherThread(), conn); + } + + return context.bindService(intent, conn, flags); + } + + @TargetApi(29) + private static boolean bindServiceIsolated( + @NonNull final Context context, + @NonNull final Intent intent, + final int flags, + @NonNull final String instanceId, + @NonNull final ServiceConnection conn) { + // We always specify the launcher thread as our Executor. + return context.bindIsolatedService( + intent, flags, instanceId, XPCOMEventTarget.launcherThread(), conn); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java new file mode 100644 index 0000000000..695c69666b --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java @@ -0,0 +1,141 @@ +/* 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/. */ + +package org.mozilla.gecko.process; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import androidx.annotation.NonNull; + +/* package */ final class ServiceUtils { + private static final String DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX = "0"; + + private ServiceUtils() {} + + /** + * @return StringBuilder containing the name of a service class but not qualifed with any unique + * identifiers. + */ + private static StringBuilder startSvcName(@NonNull final GeckoProcessType type) { + final StringBuilder builder = new StringBuilder(GeckoChildProcessServices.class.getName()); + builder.append("$").append(type); + return builder; + } + + /** + * Given a service's GeckoProcessType, obtain the name of its class, including any qualifiers that + * are needed to uniquely identify its manifest definition. + */ + public static String buildSvcName( + @NonNull final GeckoProcessType type, final String... suffixes) { + final StringBuilder builder = startSvcName(type); + + for (final String suffix : suffixes) { + builder.append(suffix); + } + + return builder.toString(); + } + + /** + * Given a service's GeckoProcessType, obtain the name of its class to be used for the purpose of + * binding as an isolated service. + * + * <p>Content services are defined in the manifest as "tab0" through "tabN" for some value of N. + * For the purposes of binding to an isolated content service, we simply need to repeatedly re-use + * the definition of "tab0", the "0" being stored as the + * DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX constant. + */ + public static String buildIsolatedSvcName(@NonNull final GeckoProcessType type) { + if (type == GeckoProcessType.CONTENT) { + return buildSvcName(type, DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX); + } + + // Non-content services do not require any unique IDs + return buildSvcName(type); + } + + /** + * Given a service's GeckoProcessType, obtain the unqualified name of its class. + * + * @return The name of the class that hosts the implementation of the service corresponding to + * type, but without any unique identifiers that may be required to actually instantiate it. + */ + private static String buildSvcNamePrefix(@NonNull final GeckoProcessType type) { + return startSvcName(type).toString(); + } + + /** + * Extracts flags from the manifest definition of a service. + * + * @param context Context to use for extraction + * @param type Service type + * @return flags that are specified in the service's definition in our manifest. + * @see android.content.pm.ServiceInfo for explanation of the various flags. + */ + public static int getServiceFlags( + @NonNull final Context context, @NonNull final GeckoProcessType type) { + final ComponentName component = new ComponentName(context, buildIsolatedSvcName(type)); + final PackageManager pkgMgr = context.getPackageManager(); + + try { + final ServiceInfo svcInfo = pkgMgr.getServiceInfo(component, 0); + // svcInfo is never null + return svcInfo.flags; + } catch (final PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** Obtain the list of all services defined for |context|. */ + private static ServiceInfo[] getServiceList(@NonNull final Context context) { + final PackageInfo packageInfo; + try { + packageInfo = + context + .getPackageManager() + .getPackageInfo(context.getPackageName(), PackageManager.GET_SERVICES); + } catch (final PackageManager.NameNotFoundException e) { + throw new AssertionError("Should not happen: Can't get package info of own package"); + } + return packageInfo.services; + } + + /** + * Count the number of service definitions in our manifest that satisfy bindings for a particular + * service type. + * + * @param context Context object to use for extracting the service definitions + * @param type The type of service to count + * @return The number of available service definitions. + */ + public static int getServiceCount( + @NonNull final Context context, @NonNull final GeckoProcessType type) { + final ServiceInfo[] svcList = getServiceList(context); + final String serviceNamePrefix = buildSvcNamePrefix(type); + + int result = 0; + for (final ServiceInfo svc : svcList) { + final String svcName = svc.name; + // If svcName starts with serviceNamePrefix, then both strings must either be equal + // or else the first subsequent character in svcName must be a digit. + // This guards against any future GeckoProcessType whose string representation shares + // a common prefix with another GeckoProcessType value. + if (svcName.startsWith(serviceNamePrefix) + && (svcName.length() == serviceNamePrefix.length() + || Character.isDigit(svcName.codePointAt(serviceNamePrefix.length())))) { + ++result; + } + } + + if (result <= 0) { + throw new RuntimeException("Could not count " + serviceNamePrefix + " services in manifest"); + } + + return result; + } +} |