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/ServiceAllocator.java | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.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/ServiceAllocator.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java | 613 |
1 files changed, 613 insertions, 0 deletions
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); + } +} |