binding.dart 19.1 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:convert' show json;
7
import 'dart:developer' as developer;
8
import 'dart:io' show exit;
9
import 'dart:ui' show saveCompilationTrace;
10

11
import 'package:meta/meta.dart';
12 13 14

import 'assertions.dart';
import 'basic_types.dart';
15
import 'debug.dart';
16
import 'platform.dart';
17
import 'print.dart';
18 19 20 21 22 23 24 25

/// Signature for service extensions.
///
/// The returned map must not contain the keys "type" or "method", as
/// they will be replaced before the value is sent to the client. The
/// "type" key will be set to the string `_extensionType` to indicate
/// that this is a return value from a service extension, and the
/// "method" key will be set to the full name of the method.
26
typedef ServiceExtensionCallback = Future<Map<String, dynamic>> Function(Map<String, String> parameters);
27

28 29 30
/// Base class for mixins that provide singleton services (also known as
/// "bindings").
///
31
/// To use this class in an `on` clause of a mixin, inherit from it and implement
32 33 34
/// [initInstances()]. The mixin is guaranteed to only be constructed once in
/// the lifetime of the app (more precisely, it will assert if constructed twice
/// in checked mode).
35
///
36 37 38 39 40 41 42
/// The top-most layer used to write the application will have a concrete class
/// that inherits from [BindingBase] and uses all the various [BindingBase]
/// mixins (such as [ServicesBinding]). For example, the Widgets library in
/// Flutter introduces a binding called [WidgetsFlutterBinding]. The relevant
/// library defines how to create the binding. It could be implied (for example,
/// [WidgetsFlutterBinding] is automatically started from [runApp]), or the
/// application might be required to explicitly call the constructor.
43
abstract class BindingBase {
44 45 46 47 48 49
  /// Default abstract constructor for bindings.
  ///
  /// First calls [initInstances] to have bindings initialize their
  /// instance pointers and other state, then calls
  /// [initServiceExtensions] to have bindings initialize their
  /// observatory service extensions, if any.
50
  BindingBase() {
51 52
    developer.Timeline.startSync('Framework initialization');

53 54 55
    assert(!_debugInitialized);
    initInstances();
    assert(_debugInitialized);
56 57 58 59

    assert(!_debugServiceExtensionsRegistered);
    initServiceExtensions();
    assert(_debugServiceExtensionsRegistered);
60

61
    developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
62

63
    developer.Timeline.finishSync();
64 65 66
  }

  static bool _debugInitialized = false;
67
  static bool _debugServiceExtensionsRegistered = false;
68 69 70 71 72 73 74 75 76

  /// The initialization method. Subclasses override this method to hook into
  /// the platform and otherwise configure their services. Subclasses must call
  /// "super.initInstances()".
  ///
  /// By convention, if the service is to be provided as a singleton, it should
  /// be exposed as `MixinClassName.instance`, a static getter that returns
  /// `MixinClassName._instance`, a static field that is set by
  /// `initInstances()`.
77 78
  @protected
  @mustCallSuper
79
  void initInstances() {
80
    assert(!_debugInitialized);
81
    assert(() { _debugInitialized = true; return true; }());
82 83
  }

84 85 86 87 88 89 90 91 92 93 94 95 96
  /// Called when the binding is initialized, to register service
  /// extensions.
  ///
  /// Bindings that want to expose service extensions should overload
  /// this method to register them using calls to
  /// [registerSignalServiceExtension],
  /// [registerBoolServiceExtension],
  /// [registerNumericServiceExtension], and
  /// [registerServiceExtension] (in increasing order of complexity).
  ///
  /// Implementations of this method must call their superclass
  /// implementation.
  ///
97
  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
98 99 100 101
  ///
  /// See also:
  ///
  ///  * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses>
102 103
  @protected
  @mustCallSuper
104 105
  void initServiceExtensions() {
    assert(!_debugServiceExtensionsRegistered);
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120

    assert(() {
      registerSignalServiceExtension(
        name: 'reassemble',
        callback: reassembleApplication,
      );
      return true;
    }());

    const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
    if (!isReleaseMode) {
      registerSignalServiceExtension(
        name: 'exit',
        callback: _exitApplication,
      );
121
      registerServiceExtension(
122
        name: 'saveCompilationTrace',
123 124
        callback: (Map<String, String> parameters) async {
          return <String, dynamic> {
125
            'value': saveCompilationTrace(),
126 127 128
          };
        }
      );
129 130
    }

131
    assert(() {
132
      const String platformOverrideExtensionName = 'platformOverride';
133
      registerServiceExtension(
134
        name: platformOverrideExtensionName,
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
        callback: (Map<String, String> parameters) async {
          if (parameters.containsKey('value')) {
            switch (parameters['value']) {
              case 'android':
                debugDefaultTargetPlatformOverride = TargetPlatform.android;
                break;
              case 'iOS':
                debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
                break;
              case 'fuchsia':
                debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
                break;
              case 'default':
              default:
                debugDefaultTargetPlatformOverride = null;
            }
151 152 153 154
            _postExtensionStateChangedEvent(
              platformOverrideExtensionName,
              defaultTargetPlatform.toString().substring('$TargetPlatform.'.length),
            );
155 156
            await reassembleApplication();
          }
157
          return <String, dynamic>{
158 159 160 161 162 163 164
            'value': defaultTargetPlatform
                     .toString()
                     .substring('$TargetPlatform.'.length),
          };
        }
      );
      return true;
165 166
    }());
    assert(() { _debugServiceExtensionsRegistered = true; return true; }());
167 168
  }

169 170 171 172 173 174 175 176 177 178 179 180 181 182
  /// Whether [lockEvents] is currently locking events.
  ///
  /// Binding subclasses that fire events should check this first, and if it is
  /// set, queue events instead of firing them.
  ///
  /// Events should be flushed when [unlocked] is called.
  @protected
  bool get locked => _lockCount > 0;
  int _lockCount = 0;

  /// Locks the dispatching of asynchronous events and callbacks until the
  /// callback's future completes.
  ///
  /// This causes input lag and should therefore be avoided when possible. It is
183 184 185
  /// primarily intended for use during non-user-interactive time such as to
  /// allow [reassembleApplication] to block input while it walks the tree
  /// (which it partially does asynchronously).
186 187 188
  ///
  /// The [Future] returned by the `callback` argument is returned by [lockEvents].
  @protected
189
  Future<void> lockEvents(Future<void> callback()) {
190 191
    developer.Timeline.startSync('Lock events');

192 193
    assert(callback != null);
    _lockCount += 1;
194
    final Future<void> future = callback();
195
    assert(future != null, 'The lockEvents() callback returned null; it should return a Future<void> that completes when the lock is to expire.');
196 197
    future.whenComplete(() {
      _lockCount -= 1;
198 199
      if (!locked) {
        developer.Timeline.finishSync();
200
        unlocked();
201
      }
202 203 204 205 206 207 208 209
    });
    return future;
  }

  /// Called by [lockEvents] when events get unlocked.
  ///
  /// This should flush any events that were queued while [locked] was true.
  @protected
210
  @mustCallSuper
211 212 213 214
  void unlocked() {
    assert(!locked);
  }

215
  /// Cause the entire application to redraw, e.g. after a hot reload.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
  ///
  /// This is used by development tools when the application code has changed,
  /// to cause the application to pick up any changed code. It can be triggered
  /// manually by sending the `ext.flutter.reassemble` service extension signal.
  ///
  /// This method is very computationally expensive and should not be used in
  /// production code. There is never a valid reason to cause the entire
  /// application to repaint in production. All aspects of the Flutter framework
  /// know how to redraw when necessary. It is only necessary in development
  /// when the code is literally changed on the fly (e.g. in hot reload) or when
  /// debug flags are being toggled.
  ///
  /// While this method runs, events are locked (e.g. pointer events are not
  /// dispatched).
  ///
  /// Subclasses (binding classes) should override [performReassemble] to react
  /// to this method being called. This method itself should not be overridden.
233
  Future<void> reassembleApplication() {
234 235 236 237
    return lockEvents(performReassemble);
  }

  /// This method is called by [reassembleApplication] to actually cause the
238
  /// application to reassemble, e.g. after a hot reload.
239 240 241 242 243 244 245 246 247 248
  ///
  /// Bindings are expected to use this method to reregister anything that uses
  /// closures, so that they do not keep pointing to old code, and to flush any
  /// caches of previously computed values, in case the new code would compute
  /// them differently. For example, the rendering layer triggers the entire
  /// application to repaint when this is called.
  ///
  /// Do not call this method directly. Instead, use [reassembleApplication].
  @mustCallSuper
  @protected
249
  Future<void> performReassemble() {
250
    FlutterError.resetErrorCount();
251
    return Future<void>.value();
252
  }
253 254 255 256 257

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes no arguments and returns
  /// no value.
  ///
258
  /// Calls the `callback` callback when the service extension is called.
259 260
  ///
  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
261
  @protected
262
  void registerSignalServiceExtension({
263
    @required String name,
264
    @required AsyncCallback callback
265 266 267 268 269 270
  }) {
    assert(name != null);
    assert(callback != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
271
        await callback();
272
        return <String, dynamic>{};
273 274 275 276 277 278 279 280 281 282 283
      }
    );
  }

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes a single argument
  /// "enabled" which can have the value "true" or the value "false"
  /// or can be omitted to read the current value. (Any value other
  /// than "true" is considered equivalent to "false". Other arguments
  /// are ignored.)
  ///
284 285
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
286
  ///
287 288
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
289 290
  ///
  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
291
  @protected
292
  void registerBoolServiceExtension({
293
    @required String name,
294 295
    @required AsyncValueGetter<bool> getter,
    @required AsyncValueSetter<bool> setter
296 297 298 299 300
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
301
      name: name,
302
      callback: (Map<String, String> parameters) async {
303
        if (parameters.containsKey('enabled')) {
304
          await setter(parameters['enabled'] == 'true');
305 306
          _postExtensionStateChangedEvent(name, await getter() ? 'true' : 'false');
        }
307
        return <String, dynamic>{ 'enabled': await getter() ? 'true' : 'false' };
308 309 310 311 312 313 314 315 316 317
      }
    );
  }

  /// Registers a service extension method with the given name (full
  /// name "ext.flutter.name"), which takes a single argument with the
  /// same name as the method which, if present, must have a value
  /// that can be parsed by [double.parse], and can be omitted to read
  /// the current value. (Other arguments are ignored.)
  ///
318 319
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
320
  ///
321 322
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
323 324
  ///
  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
325
  @protected
326
  void registerNumericServiceExtension({
327
    @required String name,
328 329
    @required AsyncValueGetter<double> getter,
    @required AsyncValueSetter<double> setter
330 331 332 333 334 335 336
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
337
        if (parameters.containsKey(name)) {
338
          await setter(double.parse(parameters[name]));
339 340
          _postExtensionStateChangedEvent(name, (await getter()).toString());
        }
341
        return <String, dynamic>{ name: (await getter()).toString() };
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
  /// Sends an event when a service extension's state is changed.
  ///
  /// Clients should listen for this event to stay aware of the current service
  /// extension state. Any service extension that manages a state should call
  /// this method on state change.
  ///
  /// `value` reflects the newly updated service extension value.
  ///
  /// This will be called automatically for service extensions registered via
  /// [registerBoolServiceExtension], [registerNumericServiceExtension], or
  /// [registerStringServiceExtension].
  void _postExtensionStateChangedEvent(String name, dynamic value) {
    postEvent(
      'Flutter.ServiceExtensionStateChanged',
      <String, dynamic>{
        'extension': 'ext.flutter.$name',
        'value': value,
      },
    );
  }

  /// All events dispatched by a [BindingBase] use this method instead of
  /// calling [developer.postEvent] directly so that tests for [BindingBase]
  /// can track which events were dispatched by overriding this method.
  @protected
  void postEvent(String eventKind, Map<String, dynamic> eventData) {
    developer.postEvent(eventKind, eventData);
  }

375 376 377 378 379 380 381 382 383 384
  /// Registers a service extension method with the given name (full name
  /// "ext.flutter.name"), which optionally takes a single argument with the
  /// name "value". If the argument is omitted, the value is to be read,
  /// otherwise it is to be set. Returns the current value.
  ///
  /// Calls the `getter` callback to obtain the value when
  /// responding to the service extension method being called.
  ///
  /// Calls the `setter` callback with the new value when the
  /// service extension method is called with a new value.
385 386
  ///
  /// {@macro flutter.foundation.bindingBase.registerServiceExtension}
387
  @protected
388 389
  void registerStringServiceExtension({
    @required String name,
390 391
    @required AsyncValueGetter<String> getter,
    @required AsyncValueSetter<String> setter
392 393 394 395 396 397 398
  }) {
    assert(name != null);
    assert(getter != null);
    assert(setter != null);
    registerServiceExtension(
      name: name,
      callback: (Map<String, String> parameters) async {
399
        if (parameters.containsKey('value')) {
400
          await setter(parameters['value']);
401 402
          _postExtensionStateChangedEvent(name, await getter());
        }
403
        return <String, dynamic>{ 'value': await getter() };
404 405 406 407
      }
    );
  }

408 409 410 411 412 413 414 415 416
  /// Registers a service extension method with the given name (full name
  /// "ext.flutter.name").
  ///
  /// The given callback is called when the extension method is called. The
  /// callback must return a [Future] that either eventually completes to a
  /// return value in the form of a name/value map where the values can all be
  /// converted to JSON using `json.encode()` (see [JsonEncoder]), or fails. In
  /// case of failure, the failure is reported to the remote caller and is
  /// dumped to the logs.
417 418
  ///
  /// The returned map will be mutated.
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
  ///
  /// {@template flutter.foundation.bindingBase.registerServiceExtension}
  /// A registered service extension can only be activated if the vm-service
  /// is included in the build, which only happens in debug and profile mode.
  /// Although a service extension cannot be used in release mode its code may
  /// still be included in the Dart snapshot and blow up binary size if it is
  /// not wrapped in a guard that allows the tree shaker to remove it (see
  /// sample code below).
  ///
  /// ## Sample Code
  ///
  /// The following code registers a service extension that is only included in
  /// debug builds:
  ///
  /// ```dart
  /// assert(() {
  ///   // Register your service extension here.
  ///   return true;
  /// }());
  ///
  /// ```
  ///
  /// A service extension registered with the following code snippet is
  /// available in debug and profile mode:
  ///
  /// ```dart
  /// if (!const bool.fromEnvironment('dart.vm.product')) {
  //   // Register your service extension here.
  // }
  /// ```
  ///
  /// Both guards ensure that Dart's tree shaker can remove the code for the
  /// service extension in release builds.
452
  /// {@endtemplate}
453
  @protected
454
  void registerServiceExtension({
455 456 457 458 459 460 461 462
    @required String name,
    @required ServiceExtensionCallback callback
  }) {
    assert(name != null);
    assert(callback != null);
    final String methodName = 'ext.flutter.$name';
    developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
      assert(method == methodName);
463 464 465 466 467
      assert(() {
        if (debugInstrumentationEnabled)
          debugPrint('service extension method received: $method($parameters)');
        return true;
      }());
468 469 470 471 472 473 474 475 476 477 478

      // VM service extensions are handled as "out of band" messages by the VM,
      // which means they are handled at various times, generally ASAP.
      // Notably, this includes being handled in the middle of microtask loops.
      // While this makes sense for some service extensions (e.g. "dump current
      // stack trace", which explicitly doesn't want to wait for a loop to
      // complete), Flutter extensions need not be handled with such high
      // priority. Further, handling them with such high priority exposes us to
      // the possibility that they're handled in the middle of a frame, which
      // breaks many assertions. As such, we ensure they we run the callbacks
      // on the outer event loop here.
479
      await debugInstrumentAction<void>('Wait for outer event loop', () {
480
        return Future<void>.delayed(Duration.zero);
481
      });
482

483 484
      dynamic caughtException;
      StackTrace caughtStack;
485
      Map<String, dynamic> result;
486 487 488 489 490 491 492 493 494
      try {
        result = await callback(parameters);
      } catch (exception, stack) {
        caughtException = exception;
        caughtStack = stack;
      }
      if (caughtException == null) {
        result['type'] = '_extensionType';
        result['method'] = method;
495
        return developer.ServiceExtensionResponse.result(json.encode(result));
496
      } else {
497
        FlutterError.reportError(FlutterErrorDetails(
498 499 500 501
          exception: caughtException,
          stack: caughtStack,
          context: 'during a service extension callback for "$method"'
        ));
502
        return developer.ServiceExtensionResponse.error(
503
          developer.ServiceExtensionResponse.extensionError,
504
          json.encode(<String, String>{
505 506
            'exception': caughtException.toString(),
            'stack': caughtStack.toString(),
507
            'method': method,
508 509 510 511 512 513
          })
        );
      }
    });
  }

514 515 516
  @override
  String toString() => '<$runtimeType>';
}
517 518

/// Terminate the Flutter application.
519
Future<void> _exitApplication() async {
520 521
  exit(0);
}