summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java
blob: 496fa9d2be3488f0b5a3a15ba12d42ac431e3888 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
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 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<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 &lt; 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 &gt;= 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);
  }
}