/* 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 enum PriorityLevel { FOREGROUND(Context.BIND_IMPORTANT), BACKGROUND(0), IDLE(Context.BIND_WAIVE_PRIORITY); private final int mAndroidFlag; 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 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.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 cloned = mBindings.clone(); for (final Entry 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 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); } }