| // 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. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| |
| import 'package:file/file.dart'; |
| import 'package:json_rpc_2/error_code.dart' as rpc_error_code; |
| import 'package:json_rpc_2/json_rpc_2.dart' as rpc; |
| import 'package:meta/meta.dart' show required; |
| import 'package:stream_channel/stream_channel.dart'; |
| import 'package:web_socket_channel/io.dart'; |
| import 'package:web_socket_channel/web_socket_channel.dart'; |
| |
| import 'base/common.dart'; |
| import 'base/context.dart'; |
| import 'base/file_system.dart'; |
| import 'base/io.dart' as io; |
| import 'base/utils.dart'; |
| import 'convert.dart' show base64; |
| import 'globals.dart'; |
| import 'version.dart'; |
| import 'vmservice_record_replay.dart'; |
| |
| /// Override `WebSocketConnector` in [context] to use a different constructor |
| /// for [WebSocket]s (used by tests). |
| typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression}); |
| |
| /// A function that opens a two-way communication channel to the specified [uri]. |
| typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri, {io.CompressionOptions compression}); |
| |
| _OpenChannel _openChannel = _defaultOpenChannel; |
| |
| /// A function that reacts to the invocation of the 'reloadSources' service. |
| /// |
| /// The VM Service Protocol allows clients to register custom services that |
| /// can be invoked by other clients through the service protocol itself. |
| /// |
| /// Clients like Observatory use external 'reloadSources' services, |
| /// when available, instead of the VM internal one. This allows these clients to |
| /// invoke Flutter HotReload when connected to a Flutter Application started in |
| /// hot mode. |
| /// |
| /// See: https://github.com/dart-lang/sdk/issues/30023 |
| typedef ReloadSources = Future<void> Function( |
| String isolateId, { |
| bool force, |
| bool pause, |
| }); |
| |
| typedef Restart = Future<void> Function({ bool pause }); |
| |
| typedef CompileExpression = Future<String> Function( |
| String isolateId, |
| String expression, |
| List<String> definitions, |
| List<String> typeDefinitions, |
| String libraryUri, |
| String klass, |
| bool isStatic, |
| ); |
| |
| const String _kRecordingType = 'vmservice'; |
| |
| Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async { |
| Duration delay = const Duration(milliseconds: 100); |
| int attempts = 0; |
| io.WebSocket socket; |
| |
| Future<void> handleError(dynamic e) async { |
| printTrace('Exception attempting to connect to Observatory: $e'); |
| printTrace('This was attempt #$attempts. Will retry in $delay.'); |
| |
| if (attempts == 10) { |
| printStatus('This is taking longer than expected...'); |
| } |
| |
| // Delay next attempt. |
| await Future<void>.delayed(delay); |
| |
| // Back off exponentially, up to 1600ms per attempt. |
| if (delay < const Duration(seconds: 1)) { |
| delay *= 2; |
| } |
| } |
| |
| final WebSocketConnector constructor = context.get<WebSocketConnector>() ?? io.WebSocket.connect; |
| while (socket == null) { |
| attempts += 1; |
| try { |
| socket = await constructor(uri.toString(), compression: compression); |
| } on io.WebSocketException catch (e) { |
| await handleError(e); |
| } on io.SocketException catch (e) { |
| await handleError(e); |
| } |
| } |
| return IOWebSocketChannel(socket).cast<String>(); |
| } |
| |
| /// A connection to the Dart VM Service. |
| // TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031 |
| class VMService { |
| VMService( |
| this._peer, |
| this.httpAddress, |
| this.wsAddress, |
| ReloadSources reloadSources, |
| Restart restart, |
| CompileExpression compileExpression, |
| ) { |
| _vm = VM._empty(this); |
| _peer.listen().catchError(_connectionError.completeError); |
| |
| _peer.registerMethod('streamNotify', (rpc.Parameters event) { |
| _handleStreamNotify(event.asMap); |
| }); |
| |
| if (reloadSources != null) { |
| _peer.registerMethod('reloadSources', (rpc.Parameters params) async { |
| final String isolateId = params['isolateId'].value; |
| final bool force = params.asMap['force'] ?? false; |
| final bool pause = params.asMap['pause'] ?? false; |
| |
| if (isolateId is! String || isolateId.isEmpty) { |
| throw rpc.RpcException.invalidParams('Invalid \'isolateId\': $isolateId'); |
| } |
| if (force is! bool) { |
| throw rpc.RpcException.invalidParams('Invalid \'force\': $force'); |
| } |
| if (pause is! bool) { |
| throw rpc.RpcException.invalidParams('Invalid \'pause\': $pause'); |
| } |
| |
| try { |
| await reloadSources(isolateId, force: force, pause: pause); |
| return <String, String>{'type': 'Success'}; |
| } on rpc.RpcException { |
| rethrow; |
| } catch (e, st) { |
| throw rpc.RpcException(rpc_error_code.SERVER_ERROR, |
| 'Error during Sources Reload: $e\n$st'); |
| } |
| }); |
| |
| _peer.sendNotification('registerService', <String, String>{ |
| 'service': 'reloadSources', |
| 'alias': 'Flutter Tools', |
| }); |
| } |
| |
| if (restart != null) { |
| _peer.registerMethod('hotRestart', (rpc.Parameters params) async { |
| final bool pause = params.asMap['pause'] ?? false; |
| |
| if (pause is! bool) { |
| throw rpc.RpcException.invalidParams('Invalid \'pause\': $pause'); |
| } |
| |
| try { |
| await restart(pause: pause); |
| return <String, String>{'type': 'Success'}; |
| } on rpc.RpcException { |
| rethrow; |
| } catch (e, st) { |
| throw rpc.RpcException(rpc_error_code.SERVER_ERROR, |
| 'Error during Hot Restart: $e\n$st'); |
| } |
| }); |
| |
| _peer.sendNotification('registerService', <String, String>{ |
| 'service': 'hotRestart', |
| 'alias': 'Flutter Tools', |
| }); |
| } |
| |
| _peer.registerMethod('flutterVersion', (rpc.Parameters params) async { |
| final FlutterVersion version = FlutterVersion(); |
| final Map<String, Object> versionJson = version.toJson(); |
| versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; |
| versionJson['engineRevisionShort'] = version.engineRevisionShort; |
| return versionJson; |
| }); |
| |
| _peer.sendNotification('registerService', <String, String>{ |
| 'service': 'flutterVersion', |
| 'alias': 'Flutter Tools', |
| }); |
| |
| if (compileExpression != null) { |
| _peer.registerMethod('compileExpression', (rpc.Parameters params) async { |
| final String isolateId = params['isolateId'].asString; |
| if (isolateId is! String || isolateId.isEmpty) { |
| throw rpc.RpcException.invalidParams( |
| 'Invalid \'isolateId\': $isolateId'); |
| } |
| final String expression = params['expression'].asString; |
| if (expression is! String || expression.isEmpty) { |
| throw rpc.RpcException.invalidParams( |
| 'Invalid \'expression\': $expression'); |
| } |
| final List<String> definitions = |
| List<String>.from(params['definitions'].asList); |
| final List<String> typeDefinitions = |
| List<String>.from(params['typeDefinitions'].asList); |
| final String libraryUri = params['libraryUri'].asString; |
| final String klass = params['klass'].exists ? params['klass'].asString : null; |
| final bool isStatic = params['isStatic'].asBoolOr(false); |
| |
| try { |
| final String kernelBytesBase64 = await compileExpression(isolateId, |
| expression, definitions, typeDefinitions, libraryUri, klass, |
| isStatic); |
| return <String, dynamic>{'type': 'Success', |
| 'result': <String, dynamic> {'kernelBytes': kernelBytesBase64}}; |
| } on rpc.RpcException { |
| rethrow; |
| } catch (e, st) { |
| throw rpc.RpcException(rpc_error_code.SERVER_ERROR, |
| 'Error during expression compilation: $e\n$st'); |
| } |
| }); |
| |
| _peer.sendNotification('registerService', <String, String>{ |
| 'service': 'compileExpression', |
| 'alias': 'Flutter Tools', |
| }); |
| } |
| } |
| |
| /// Enables recording of VMService JSON-rpc activity to the specified base |
| /// recording [location]. |
| /// |
| /// Activity will be recorded in a subdirectory of [location] named |
| /// `"vmservice"`. It is permissible for [location] to represent an existing |
| /// non-empty directory as long as there is no collision with the |
| /// `"vmservice"` subdirectory. |
| static void enableRecordingConnection(String location) { |
| final Directory dir = getRecordingSink(location, _kRecordingType); |
| _openChannel = (Uri uri, {io.CompressionOptions compression}) async { |
| final StreamChannel<String> delegate = await _defaultOpenChannel(uri); |
| return RecordingVMServiceChannel(delegate, dir); |
| }; |
| } |
| |
| /// Enables VMService JSON-rpc replay mode. |
| /// |
| /// [location] must represent a directory to which VMService JSON-rpc |
| /// activity has been recorded (i.e. the result of having been previously |
| /// passed to [enableRecordingConnection]), or a [ToolExit] will be thrown. |
| static void enableReplayConnection(String location) { |
| final Directory dir = getReplaySource(location, _kRecordingType); |
| _openChannel = (Uri uri, {io.CompressionOptions compression}) async => ReplayVMServiceChannel(dir); |
| } |
| |
| static void _unhandledError(dynamic error, dynamic stack) { |
| logger.printTrace('Error in internal implementation of JSON RPC.\n$error\n$stack'); |
| assert(false); |
| } |
| |
| /// Connect to a Dart VM Service at [httpUri]. |
| /// |
| /// If the [reloadSources] parameter is not null, the 'reloadSources' service |
| /// will be registered. The VM Service Protocol allows clients to register |
| /// custom services that can be invoked by other clients through the service |
| /// protocol itself. |
| /// |
| /// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217 |
| static Future<VMService> connect( |
| Uri httpUri, { |
| ReloadSources reloadSources, |
| Restart restart, |
| CompileExpression compileExpression, |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
| }) async { |
| final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws')); |
| final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression); |
| final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel), onUnhandledError: _unhandledError); |
| final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression); |
| // This call is to ensure we are able to establish a connection instead of |
| // keeping on trucking and failing farther down the process. |
| await service._sendRequest('getVersion', const <String, dynamic>{}); |
| return service; |
| } |
| |
| final Uri httpAddress; |
| final Uri wsAddress; |
| final rpc.Peer _peer; |
| final Completer<Map<String, dynamic>> _connectionError = Completer<Map<String, dynamic>>(); |
| |
| VM _vm; |
| /// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects. |
| VM get vm => _vm; |
| |
| final Map<String, StreamController<ServiceEvent>> _eventControllers = |
| <String, StreamController<ServiceEvent>>{}; |
| |
| final Set<String> _listeningFor = <String>{}; |
| |
| /// Whether our connection to the VM service has been closed; |
| bool get isClosed => _peer.isClosed; |
| |
| Future<void> get done async { |
| await _peer.done; |
| } |
| |
| // Events |
| Future<Stream<ServiceEvent>> get onDebugEvent => onEvent('Debug'); |
| Future<Stream<ServiceEvent>> get onExtensionEvent => onEvent('Extension'); |
| // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded |
| Future<Stream<ServiceEvent>> get onIsolateEvent => onEvent('Isolate'); |
| Future<Stream<ServiceEvent>> get onTimelineEvent => onEvent('Timeline'); |
| // TODO(johnmccutchan): Add FlutterView events. |
| |
| /// Returns a stream of VM service events. |
| /// |
| /// This purposely returns a `Future<Stream<T>>` rather than a `Stream<T>` |
| /// because it first registers with the VM to receive events on the stream, |
| /// and only once the VM has acknowledged that the stream has started will |
| /// we return the associated stream. Any attempt to streamline this API into |
| /// returning `Stream<T>` should take that into account to avoid race |
| /// conditions. |
| Future<Stream<ServiceEvent>> onEvent(String streamId) async { |
| await _streamListen(streamId); |
| return _getEventController(streamId).stream; |
| } |
| |
| Future<Map<String, dynamic>> _sendRequest( |
| String method, |
| Map<String, dynamic> params, |
| ) { |
| return Future.any<Map<String, dynamic>>(<Future<Map<String, dynamic>>>[ |
| _peer.sendRequest(method, params).then<Map<String, dynamic>>(castStringKeyedMap), |
| _connectionError.future, |
| ]); |
| } |
| |
| StreamController<ServiceEvent> _getEventController(String eventName) { |
| StreamController<ServiceEvent> controller = _eventControllers[eventName]; |
| if (controller == null) { |
| controller = StreamController<ServiceEvent>.broadcast(); |
| _eventControllers[eventName] = controller; |
| } |
| return controller; |
| } |
| |
| void _handleStreamNotify(Map<String, dynamic> data) { |
| final String streamId = data['streamId']; |
| final Map<String, dynamic> eventData = data['event']; |
| final Map<String, dynamic> eventIsolate = eventData['isolate']; |
| |
| // Log event information. |
| printTrace('Notification from VM: $data'); |
| |
| ServiceEvent event; |
| if (eventIsolate != null) { |
| // getFromMap creates the Isolate if necessary. |
| final Isolate isolate = vm.getFromMap(eventIsolate); |
| event = ServiceObject._fromMap(isolate, eventData); |
| if (event.kind == ServiceEvent.kIsolateExit) { |
| vm._isolateCache.remove(isolate.id); |
| vm._buildIsolateList(); |
| } else if (event.kind == ServiceEvent.kIsolateRunnable) { |
| // Force reload once the isolate becomes runnable so that we |
| // update the root library. |
| isolate.reload(); |
| } |
| } else { |
| // The event doesn't have an isolate, so it is owned by the VM. |
| event = ServiceObject._fromMap(vm, eventData); |
| } |
| _getEventController(streamId).add(event); |
| } |
| |
| Future<void> _streamListen(String streamId) async { |
| if (!_listeningFor.contains(streamId)) { |
| _listeningFor.add(streamId); |
| await _sendRequest('streamListen', <String, dynamic>{'streamId': streamId}); |
| } |
| } |
| |
| /// Reloads the VM. |
| Future<void> getVM() async => await vm.reload(); |
| |
| Future<void> refreshViews({ bool waitForViews = false }) => vm.refreshViews(waitForViews: waitForViews); |
| } |
| |
| /// An error that is thrown when constructing/updating a service object. |
| class VMServiceObjectLoadError { |
| VMServiceObjectLoadError(this.message, this.map); |
| final String message; |
| final Map<String, dynamic> map; |
| } |
| |
| bool _isServiceMap(Map<String, dynamic> m) { |
| return (m != null) && (m['type'] != null); |
| } |
| bool _hasRef(String type) => (type != null) && type.startsWith('@'); |
| String _stripRef(String type) => _hasRef(type) ? type.substring(1) : type; |
| |
| /// Given a raw response from the service protocol and a [ServiceObjectOwner], |
| /// recursively walk the response and replace values that are service maps with |
| /// actual [ServiceObject]s. During the upgrade the owner is given a chance |
| /// to return a cached / canonicalized object. |
| void _upgradeCollection( |
| dynamic collection, |
| ServiceObjectOwner owner, |
| ) { |
| if (collection is ServiceMap) { |
| return; |
| } |
| if (collection is Map<String, dynamic>) { |
| _upgradeMap(collection, owner); |
| } else if (collection is List) { |
| _upgradeList(collection, owner); |
| } |
| } |
| |
| void _upgradeMap(Map<String, dynamic> map, ServiceObjectOwner owner) { |
| map.forEach((String k, Object v) { |
| if ((v is Map<String, dynamic>) && _isServiceMap(v)) { |
| map[k] = owner.getFromMap(v); |
| } else if (v is List) { |
| _upgradeList(v, owner); |
| } else if (v is Map<String, dynamic>) { |
| _upgradeMap(v, owner); |
| } |
| }); |
| } |
| |
| void _upgradeList(List<dynamic> list, ServiceObjectOwner owner) { |
| for (int i = 0; i < list.length; i += 1) { |
| final Object v = list[i]; |
| if ((v is Map<String, dynamic>) && _isServiceMap(v)) { |
| list[i] = owner.getFromMap(v); |
| } else if (v is List) { |
| _upgradeList(v, owner); |
| } else if (v is Map<String, dynamic>) { |
| _upgradeMap(v, owner); |
| } |
| } |
| } |
| |
| /// Base class of all objects received over the service protocol. |
| abstract class ServiceObject { |
| ServiceObject._empty(this._owner); |
| |
| /// Factory constructor given a [ServiceObjectOwner] and a service map, |
| /// upgrade the map into a proper [ServiceObject]. This function always |
| /// returns a new instance and does not interact with caches. |
| factory ServiceObject._fromMap( |
| ServiceObjectOwner owner, |
| Map<String, dynamic> map, |
| ) { |
| if (map == null) { |
| return null; |
| } |
| |
| if (!_isServiceMap(map)) { |
| throw VMServiceObjectLoadError('Expected a service map', map); |
| } |
| |
| final String type = _stripRef(map['type']); |
| |
| ServiceObject serviceObject; |
| switch (type) { |
| case 'Event': |
| serviceObject = ServiceEvent._empty(owner); |
| break; |
| case 'FlutterView': |
| serviceObject = FlutterView._empty(owner.vm); |
| break; |
| case 'Isolate': |
| serviceObject = Isolate._empty(owner.vm); |
| break; |
| } |
| // If we don't have a model object for this service object type, as a |
| // fallback return a ServiceMap object. |
| serviceObject ??= ServiceMap._empty(owner); |
| // We have now constructed an empty service object, call update to populate it. |
| serviceObject.updateFromMap(map); |
| return serviceObject; |
| } |
| |
| final ServiceObjectOwner _owner; |
| ServiceObjectOwner get owner => _owner; |
| |
| /// The id of this object. |
| String get id => _id; |
| String _id; |
| |
| /// The user-level type of this object. |
| String get type => _type; |
| String _type; |
| |
| /// The vm-level type of this object. Usually the same as [type]. |
| String get vmType => _vmType; |
| String _vmType; |
| |
| /// Is it safe to cache this object? |
| bool _canCache = false; |
| bool get canCache => _canCache; |
| |
| /// Has this object been fully loaded? |
| bool get loaded => _loaded; |
| bool _loaded = false; |
| |
| /// Is this object immutable after it is [loaded]? |
| bool get immutable => false; |
| |
| String get name => _name; |
| String _name; |
| |
| String get vmName => _vmName; |
| String _vmName; |
| |
| /// If this is not already loaded, load it. Otherwise reload. |
| Future<ServiceObject> load() async { |
| if (loaded) { |
| return this; |
| } |
| return reload(); |
| } |
| |
| /// Fetch this object from vmService and return the response directly. |
| Future<Map<String, dynamic>> _fetchDirect() { |
| final Map<String, dynamic> params = <String, dynamic>{ |
| 'objectId': id, |
| }; |
| return _owner.isolate.invokeRpcRaw('getObject', params: params); |
| } |
| |
| Future<ServiceObject> _inProgressReload; |
| /// Reload the service object (if possible). |
| Future<ServiceObject> reload() async { |
| final bool hasId = (id != null) && (id != ''); |
| final bool isVM = this is VM; |
| // We should always reload the VM. |
| // We can't reload objects without an id. |
| // We shouldn't reload an immutable and already loaded object. |
| if (!isVM && (!hasId || (immutable && loaded))) { |
| return this; |
| } |
| |
| if (_inProgressReload == null) { |
| final Completer<ServiceObject> completer = Completer<ServiceObject>(); |
| _inProgressReload = completer.future; |
| try { |
| final Map<String, dynamic> response = await _fetchDirect(); |
| if (_stripRef(response['type']) == 'Sentinel') { |
| // An object may have been collected. |
| completer.complete(ServiceObject._fromMap(owner, response)); |
| } else { |
| updateFromMap(response); |
| completer.complete(this); |
| } |
| } catch (e, st) { |
| completer.completeError(e, st); |
| } |
| _inProgressReload = null; |
| return await completer.future; |
| } |
| |
| return await _inProgressReload; |
| } |
| |
| /// Update [this] using [map] as a source. [map] can be a service reference. |
| void updateFromMap(Map<String, dynamic> map) { |
| // Don't allow the type to change on an object update. |
| final bool mapIsRef = _hasRef(map['type']); |
| final String mapType = _stripRef(map['type']); |
| |
| if ((_type != null) && (_type != mapType)) { |
| throw VMServiceObjectLoadError('ServiceObject types must not change', |
| map); |
| } |
| _type = mapType; |
| _vmType = map.containsKey('_vmType') ? _stripRef(map['_vmType']) : _type; |
| |
| _canCache = map['fixedId'] == true; |
| if ((_id != null) && (_id != map['id']) && _canCache) { |
| throw VMServiceObjectLoadError('ServiceObject id changed', map); |
| } |
| _id = map['id']; |
| |
| // Copy name properties. |
| _name = map['name']; |
| _vmName = map.containsKey('_vmName') ? map['_vmName'] : _name; |
| |
| // We have now updated all common properties, let the subclasses update |
| // their specific properties. |
| _update(map, mapIsRef); |
| } |
| |
| /// Implemented by subclasses to populate their model. |
| void _update(Map<String, dynamic> map, bool mapIsRef); |
| } |
| |
| class ServiceEvent extends ServiceObject { |
| ServiceEvent._empty(ServiceObjectOwner owner) : super._empty(owner); |
| |
| String _kind; |
| String get kind => _kind; |
| DateTime _timestamp; |
| DateTime get timestamp => _timestamp; |
| String _extensionKind; |
| String get extensionKind => _extensionKind; |
| Map<String, dynamic> _extensionData; |
| Map<String, dynamic> get extensionData => _extensionData; |
| List<Map<String, dynamic>> _timelineEvents; |
| List<Map<String, dynamic>> get timelineEvents => _timelineEvents; |
| |
| // The possible 'kind' values. |
| static const String kVMUpdate = 'VMUpdate'; |
| static const String kIsolateStart = 'IsolateStart'; |
| static const String kIsolateRunnable = 'IsolateRunnable'; |
| static const String kIsolateExit = 'IsolateExit'; |
| static const String kIsolateUpdate = 'IsolateUpdate'; |
| static const String kIsolateReload = 'IsolateReload'; |
| static const String kIsolateSpawn = 'IsolateSpawn'; |
| static const String kServiceExtensionAdded = 'ServiceExtensionAdded'; |
| static const String kPauseStart = 'PauseStart'; |
| static const String kPauseExit = 'PauseExit'; |
| static const String kPauseBreakpoint = 'PauseBreakpoint'; |
| static const String kPauseInterrupted = 'PauseInterrupted'; |
| static const String kPauseException = 'PauseException'; |
| static const String kPausePostRequest = 'PausePostRequest'; |
| static const String kNone = 'None'; |
| static const String kResume = 'Resume'; |
| static const String kBreakpointAdded = 'BreakpointAdded'; |
| static const String kBreakpointResolved = 'BreakpointResolved'; |
| static const String kBreakpointRemoved = 'BreakpointRemoved'; |
| static const String kGraph = '_Graph'; |
| static const String kGC = 'GC'; |
| static const String kInspect = 'Inspect'; |
| static const String kDebuggerSettingsUpdate = '_DebuggerSettingsUpdate'; |
| static const String kConnectionClosed = 'ConnectionClosed'; |
| static const String kLogging = '_Logging'; |
| static const String kExtension = 'Extension'; |
| |
| @override |
| void _update(Map<String, dynamic> map, bool mapIsRef) { |
| _loaded = true; |
| _upgradeCollection(map, owner); |
| _kind = map['kind']; |
| assert(map['isolate'] == null || owner == map['isolate']); |
| _timestamp = |
| DateTime.fromMillisecondsSinceEpoch(map['timestamp']); |
| if (map['extensionKind'] != null) { |
| _extensionKind = map['extensionKind']; |
| _extensionData = map['extensionData']; |
| } |
| // map['timelineEvents'] is List<dynamic> which can't be assigned to |
| // List<Map<String, dynamic>> directly. Unfortunately, we previously didn't |
| // catch this exception because json_rpc_2 is hiding all these exceptions |
| // on a Stream. |
| final List<dynamic> dynamicList = map['timelineEvents']; |
| _timelineEvents = dynamicList?.cast<Map<String, dynamic>>(); |
| } |
| |
| bool get isPauseEvent { |
| return kind == kPauseStart || |
| kind == kPauseExit || |
| kind == kPauseBreakpoint || |
| kind == kPauseInterrupted || |
| kind == kPauseException || |
| kind == kPausePostRequest || |
| kind == kNone; |
| } |
| } |
| |
| /// A ServiceObjectOwner is either a [VM] or an [Isolate]. Owners can cache |
| /// and/or canonicalize service objects received over the wire. |
| abstract class ServiceObjectOwner extends ServiceObject { |
| ServiceObjectOwner._empty(ServiceObjectOwner owner) : super._empty(owner); |
| |
| /// Returns the owning VM. |
| VM get vm => null; |
| |
| /// Returns the owning isolate (if any). |
| Isolate get isolate => null; |
| |
| /// Returns the vmService connection. |
| VMService get vmService => null; |
| |
| /// Builds a [ServiceObject] corresponding to the [id] from [map]. |
| /// The result may come from the cache. The result will not necessarily |
| /// be [loaded]. |
| ServiceObject getFromMap(Map<String, dynamic> map); |
| } |
| |
| /// There is only one instance of the VM class. The VM class owns [Isolate] |
| /// and [FlutterView] objects. |
| class VM extends ServiceObjectOwner { |
| VM._empty(this._vmService) : super._empty(null); |
| |
| /// Connection to the VMService. |
| final VMService _vmService; |
| @override |
| VMService get vmService => _vmService; |
| |
| @override |
| VM get vm => this; |
| |
| @override |
| Future<Map<String, dynamic>> _fetchDirect() => invokeRpcRaw('getVM'); |
| |
| @override |
| void _update(Map<String, dynamic> map, bool mapIsRef) { |
| if (mapIsRef) { |
| return; |
| } |
| |
| // Upgrade the collection. A side effect of this call is that any new |
| // isolates in the map are created and added to the isolate cache. |
| _upgradeCollection(map, this); |
| _loaded = true; |
| |
| _pid = map['pid']; |
| if (map['_heapAllocatedMemoryUsage'] != null) { |
| _heapAllocatedMemoryUsage = map['_heapAllocatedMemoryUsage']; |
| } |
| _maxRSS = map['_maxRSS']; |
| _embedder = map['_embedder']; |
| |
| // Remove any isolates which are now dead from the isolate cache. |
| _removeDeadIsolates(map['isolates'].cast<Isolate>()); |
| } |
| |
| final Map<String, ServiceObject> _cache = <String,ServiceObject>{}; |
| final Map<String,Isolate> _isolateCache = <String,Isolate>{}; |
| |
| /// The list of live isolates, ordered by isolate start time. |
| final List<Isolate> isolates = <Isolate>[]; |
| |
| /// The set of live views. |
| final Map<String, FlutterView> _viewCache = <String, FlutterView>{}; |
| |
| /// The pid of the VM's process. |
| int _pid; |
| int get pid => _pid; |
| |
| /// The number of bytes allocated (e.g. by malloc) in the native heap. |
| int _heapAllocatedMemoryUsage; |
| int get heapAllocatedMemoryUsage => _heapAllocatedMemoryUsage ?? 0; |
| |
| /// The peak resident set size for the process. |
| int _maxRSS; |
| int get maxRSS => _maxRSS ?? 0; |
| |
| // The embedder's name, Flutter or dart_runner. |
| String _embedder; |
| String get embedder => _embedder; |
| bool get isFlutterEngine => embedder == 'Flutter'; |
| bool get isDartRunner => embedder == 'dart_runner'; |
| |
| int _compareIsolates(Isolate a, Isolate b) { |
| final DateTime aStart = a.startTime; |
| final DateTime bStart = b.startTime; |
| if (aStart == null) { |
| if (bStart == null) { |
| return 0; |
| } else { |
| return 1; |
| } |
| } |
| if (bStart == null) { |
| return -1; |
| } |
| return aStart.compareTo(bStart); |
| } |
| |
| void _buildIsolateList() { |
| final List<Isolate> isolateList = _isolateCache.values.toList(); |
| isolateList.sort(_compareIsolates); |
| isolates.clear(); |
| isolates.addAll(isolateList); |
| } |
| |
| void _removeDeadIsolates(List<Isolate> newIsolates) { |
| // Build a set of new isolates. |
| final Set<String> newIsolateSet = <String>{}; |
| for (Isolate iso in newIsolates) { |
| newIsolateSet.add(iso.id); |
| } |
| |
| // Remove any old isolates which no longer exist. |
| final List<String> toRemove = <String>[]; |
| _isolateCache.forEach((String id, _) { |
| if (!newIsolateSet.contains(id)) { |
| toRemove.add(id); |
| } |
| }); |
| toRemove.forEach(_isolateCache.remove); |
| _buildIsolateList(); |
| } |
| |
| @override |
| ServiceObject getFromMap(Map<String, dynamic> map) { |
| if (map == null) { |
| return null; |
| } |
| final String type = _stripRef(map['type']); |
| if (type == 'VM') { |
| // Update this VM object. |
| updateFromMap(map); |
| return this; |
| } |
| |
| final String mapId = map['id']; |
| |
| switch (type) { |
| case 'Isolate': { |
| // Check cache. |
| Isolate isolate = _isolateCache[mapId]; |
| if (isolate == null) { |
| // Add new isolate to the cache. |
| isolate = ServiceObject._fromMap(this, map); |
| _isolateCache[mapId] = isolate; |
| _buildIsolateList(); |
| |
| // Eagerly load the isolate. |
| isolate.load().catchError((dynamic e, StackTrace stack) { |
| printTrace('Eagerly loading an isolate failed: $e\n$stack'); |
| }); |
| } else { |
| // Existing isolate, update data. |
| isolate.updateFromMap(map); |
| } |
| return isolate; |
| } |
| break; |
| case 'FlutterView': { |
| FlutterView view = _viewCache[mapId]; |
| if (view == null) { |
| // Add new view to the cache. |
| view = ServiceObject._fromMap(this, map); |
| _viewCache[mapId] = view; |
| } else { |
| view.updateFromMap(map); |
| } |
| return view; |
| } |
| break; |
| default: |
| throw VMServiceObjectLoadError( |
| 'VM.getFromMap called for something other than an isolate', map); |
| } |
| } |
| |
| // This function does not reload the isolate if it's found in the cache. |
| Future<Isolate> getIsolate(String isolateId) { |
| if (!loaded) { |
| // Trigger a VM load, then get the isolate. Ignore any errors. |
| return load().then<Isolate>((ServiceObject serviceObject) => getIsolate(isolateId)).catchError((dynamic error) => null); |
| } |
| return Future<Isolate>.value(_isolateCache[isolateId]); |
| } |
| |
| static String _truncate(String message, int width, String ellipsis) { |
| assert(ellipsis.length < width); |
| if (message.length <= width) { |
| return message; |
| } |
| return message.substring(0, width - ellipsis.length) + ellipsis; |
| } |
| |
| /// Invoke the RPC and return the raw response. |
| Future<Map<String, dynamic>> invokeRpcRaw( |
| String method, { |
| Map<String, dynamic> params = const <String, dynamic>{}, |
| bool truncateLogs = true, |
| }) async { |
| printTrace('Sending to VM service: $method($params)'); |
| assert(params != null); |
| try { |
| final Map<String, dynamic> result = await _vmService._sendRequest(method, params); |
| final String resultString = |
| truncateLogs ? _truncate(result.toString(), 250, '...') : result.toString(); |
| printTrace('Result: $resultString'); |
| return result; |
| } on WebSocketChannelException catch (error) { |
| throwToolExit('Error connecting to observatory: $error'); |
| return null; |
| } on rpc.RpcException catch (error) { |
| printError('Error ${error.code} received from application: ${error.message}'); |
| printTrace('${error.data}'); |
| rethrow; |
| } |
| } |
| |
| /// Invoke the RPC and return a [ServiceObject] response. |
| Future<T> invokeRpc<T extends ServiceObject>( |
| String method, { |
| Map<String, dynamic> params = const <String, dynamic>{}, |
| bool truncateLogs = true, |
| }) async { |
| final Map<String, dynamic> response = await invokeRpcRaw( |
| method, |
| params: params, |
| truncateLogs: truncateLogs, |
| ); |
| final ServiceObject serviceObject = ServiceObject._fromMap(this, response); |
| if ((serviceObject != null) && (serviceObject._canCache)) { |
| final String serviceObjectId = serviceObject.id; |
| _cache.putIfAbsent(serviceObjectId, () => serviceObject); |
| } |
| return serviceObject; |
| } |
| |
| /// Create a new development file system on the device. |
| Future<Map<String, dynamic>> createDevFS(String fsName) { |
| return invokeRpcRaw('_createDevFS', params: <String, dynamic>{'fsName': fsName}); |
| } |
| |
| /// List the development file system son the device. |
| Future<List<String>> listDevFS() async { |
| return (await invokeRpcRaw('_listDevFS'))['fsNames']; |
| } |
| |
| // Write one file into a file system. |
| Future<Map<String, dynamic>> writeDevFSFile( |
| String fsName, { |
| @required String path, |
| @required List<int> fileContents, |
| }) { |
| assert(path != null); |
| assert(fileContents != null); |
| return invokeRpcRaw( |
| '_writeDevFSFile', |
| params: <String, dynamic>{ |
| 'fsName': fsName, |
| 'path': path, |
| 'fileContents': base64.encode(fileContents), |
| }, |
| ); |
| } |
| |
| // Read one file from a file system. |
| Future<List<int>> readDevFSFile(String fsName, String path) async { |
| final Map<String, dynamic> response = await invokeRpcRaw( |
| '_readDevFSFile', |
| params: <String, dynamic>{ |
| 'fsName': fsName, |
| 'path': path, |
| }, |
| ); |
| return base64.decode(response['fileContents']); |
| } |
| |
| /// The complete list of a file system. |
| Future<List<String>> listDevFSFiles(String fsName) async { |
| return (await invokeRpcRaw('_listDevFSFiles', params: <String, dynamic>{'fsName': fsName}))['files']; |
| } |
| |
| /// Delete an existing file system. |
| Future<Map<String, dynamic>> deleteDevFS(String fsName) { |
| return invokeRpcRaw('_deleteDevFS', params: <String, dynamic>{'fsName': fsName}); |
| } |
| |
| Future<ServiceMap> runInView( |
| String viewId, |
| Uri main, |
| Uri packages, |
| Uri assetsDirectory, |
| ) { |
| return invokeRpc<ServiceMap>('_flutter.runInView', |
| params: <String, dynamic>{ |
| 'viewId': viewId, |
| 'mainScript': main.toString(), |
| 'packagesFile': packages.toString(), |
| 'assetDirectory': assetsDirectory.toString(), |
| }); |
| } |
| |
| Future<Map<String, dynamic>> clearVMTimeline() { |
| return invokeRpcRaw('clearVMTimeline'); |
| } |
| |
| Future<Map<String, dynamic>> setVMTimelineFlags(List<String> recordedStreams) { |
| assert(recordedStreams != null); |
| return invokeRpcRaw( |
| 'setVMTimelineFlags', |
| params: <String, dynamic>{ |
| 'recordedStreams': recordedStreams, |
| }, |
| ); |
| } |
| |
| Future<Map<String, dynamic>> getVMTimeline() { |
| return invokeRpcRaw('getVMTimeline'); |
| } |
| |
| Future<void> refreshViews({ bool waitForViews = false }) async { |
| assert(waitForViews != null); |
| assert(loaded); |
| if (!isFlutterEngine) { |
| return; |
| } |
| int failCount = 0; |
| while (true) { |
| _viewCache.clear(); |
| // When the future returned by invokeRpc() below returns, |
| // the _viewCache will have been updated. |
| // This message updates all the views of every isolate. |
| await vmService.vm.invokeRpc<ServiceObject>( |
| '_flutter.listViews', truncateLogs: false); |
| if (_viewCache.values.isNotEmpty || !waitForViews) { |
| return; |
| } |
| failCount += 1; |
| if (failCount == 5) { // waited 200ms |
| printStatus('Flutter is taking longer than expected to report its views. Still trying...'); |
| } |
| await Future<void>.delayed(const Duration(milliseconds: 50)); |
| await reload(); |
| } |
| } |
| |
| Iterable<FlutterView> get views => _viewCache.values; |
| |
| FlutterView get firstView { |
| return _viewCache.values.isEmpty ? null : _viewCache.values.first; |
| } |
| |
| List<FlutterView> allViewsWithName(String isolateFilter) { |
| if (_viewCache.values.isEmpty) { |
| return null; |
| } |
| return _viewCache.values.where( |
| (FlutterView v) => v.uiIsolate.name.contains(isolateFilter) |
| ).toList(); |
| } |
| } |
| |
| class HeapSpace extends ServiceObject { |
| HeapSpace._empty(ServiceObjectOwner owner) : super._empty(owner); |
| |
| int _used = 0; |
| int _capacity = 0; |
| int _external = 0; |
| int _collections = 0; |
| double _totalCollectionTimeInSeconds = 0.0; |
| double _averageCollectionPeriodInMillis = 0.0; |
| |
| int get used => _used; |
| int get capacity => _capacity; |
| int get external => _external; |
| |
| Duration get avgCollectionTime { |
| final double mcs = _totalCollectionTimeInSeconds * |
| Duration.microsecondsPerSecond / |
| math.max(_collections, 1); |
| return Duration(microseconds: mcs.ceil()); |
| } |
| |
| Duration get avgCollectionPeriod { |
| final double mcs = _averageCollectionPeriodInMillis * |
| Duration.microsecondsPerMillisecond; |
| return Duration(microseconds: mcs.ceil()); |
| } |
| |
| @override |
| void _update(Map<String, dynamic> map, bool mapIsRef) { |
| _used = map['used']; |
| _capacity = map['capacity']; |
| _external = map['external']; |
| _collections = map['collections']; |
| _totalCollectionTimeInSeconds = map['time']; |
| _averageCollectionPeriodInMillis = map['avgCollectionPeriodMillis']; |
| } |
| } |
| |
| /// An isolate running inside the VM. Instances of the Isolate class are always |
| /// canonicalized. |
| class Isolate extends ServiceObjectOwner { |
| Isolate._empty(ServiceObjectOwner owner) : super._empty(owner); |
| |
| @override |
| VM get vm => owner; |
| |
| @override |
| VMService get vmService => vm.vmService; |
| |
| @override |
| Isolate get isolate => this; |
| |
| DateTime startTime; |
| |
| /// The last pause event delivered to the isolate. If the isolate is running, |
| /// this will be a resume event. |
| ServiceEvent pauseEvent; |
| |
| final Map<String, ServiceObject> _cache = <String, ServiceObject>{}; |
| |
| HeapSpace _newSpace; |
| HeapSpace _oldSpace; |
| |
| HeapSpace get newSpace => _newSpace; |
| HeapSpace get oldSpace => _oldSpace; |
| |
| @override |
| ServiceObject getFromMap(Map<String, dynamic> map) { |
| if (map == null) { |
| return null; |
| } |
| final String mapType = _stripRef(map['type']); |
| if (mapType == 'Isolate') { |
| // There are sometimes isolate refs in ServiceEvents. |
| return vm.getFromMap(map); |
| } |
| |
| final String mapId = map['id']; |
| ServiceObject serviceObject = (mapId != null) ? _cache[mapId] : null; |
| if (serviceObject != null) { |
| serviceObject.updateFromMap(map); |
| return serviceObject; |
| } |
| // Build the object from the map directly. |
| serviceObject = ServiceObject._fromMap(this, map); |
| if ((serviceObject != null) && serviceObject.canCache) { |
| _cache[mapId] = serviceObject; |
| } |
| return serviceObject; |
| } |
| |
| @override |
| Future<Map<String, dynamic>> _fetchDirect() => invokeRpcRaw('getIsolate'); |
| |
| /// Invoke the RPC and return the raw response. |
| Future<Map<String, dynamic>> invokeRpcRaw( |
| String method, { |
| Map<String, dynamic> params, |
| }) { |
| // Inject the 'isolateId' parameter. |
| if (params == null) { |
| params = <String, dynamic>{ |
| 'isolateId': id, |
| }; |
| } else { |
| params['isolateId'] = id; |
| } |
| return vm.invokeRpcRaw(method, params: params); |
| } |
| |
| /// Invoke the RPC and return a ServiceObject response. |
| Future<ServiceObject> invokeRpc(String method, Map<String, dynamic> params) async { |
| return getFromMap(await invokeRpcRaw(method, params: params)); |
| } |
| |
| void _updateHeaps(Map<String, dynamic> map, bool mapIsRef) { |
| _newSpace ??= HeapSpace._empty(this); |
| _newSpace._update(map['new'], mapIsRef); |
| _oldSpace ??= HeapSpace._empty(this); |
| _oldSpace._update(map['old'], mapIsRef); |
| } |
| |
| @override |
| void _update(Map<String, dynamic> map, bool mapIsRef) { |
| if (mapIsRef) { |
| return; |
| } |
| _loaded = true; |
| |
| final int startTimeMillis = map['startTime']; |
| startTime = DateTime.fromMillisecondsSinceEpoch(startTimeMillis); |
| |
| _upgradeCollection(map, this); |
| |
| pauseEvent = map['pauseEvent']; |
| |
| _updateHeaps(map['_heaps'], mapIsRef); |
| } |
| |
| static const int kIsolateReloadBarred = 1005; |
| |
| Future<Map<String, dynamic>> reloadSources({ |
| bool pause = false, |
| Uri rootLibUri, |
| Uri packagesUri, |
| }) async { |
| try { |
| final Map<String, dynamic> arguments = <String, dynamic>{ |
| 'pause': pause, |
| }; |
| if (rootLibUri != null) { |
| arguments['rootLibUri'] = rootLibUri.toString(); |
| } |
| if (packagesUri != null) { |
| arguments['packagesUri'] = packagesUri.toString(); |
| } |
| final Map<String, dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments); |
| return response; |
| } on rpc.RpcException catch (e) { |
| return Future<Map<String, dynamic>>.value(<String, dynamic>{ |
| 'code': e.code, |
| 'message': e.message, |
| 'data': e.data, |
| }); |
| } |
| } |
| |
| Future<Map<String, dynamic>> getObject(Map<String, dynamic> objectRef) { |
| return invokeRpcRaw('getObject', |
| params: <String, dynamic>{'objectId': objectRef['id']}); |
| } |
| |
| /// Resumes the isolate. |
| Future<Map<String, dynamic>> resume() { |
| return invokeRpcRaw('resume'); |
| } |
| |
| // Flutter extension methods. |
| |
| // Invoke a flutter extension method, if the flutter extension is not |
| // available, returns null. |
| Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw( |
| String method, { |
| Map<String, dynamic> params, |
| }) async { |
| try { |
| return await invokeRpcRaw(method, params: params); |
| } on rpc.RpcException catch (e) { |
| // If an application is not using the framework |
| if (e.code == rpc_error_code.METHOD_NOT_FOUND) { |
| return null; |
| } |
| rethrow; |
| } |
| } |
| |
| Future<Map<String, dynamic>> flutterDebugDumpApp() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp'); |
| } |
| |
| Future<Map<String, dynamic>> flutterDebugDumpRenderTree() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree'); |
| } |
| |
| Future<Map<String, dynamic>> flutterDebugDumpLayerTree() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree'); |
| } |
| |
| Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder'); |
| } |
| |
| Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder'); |
| } |
| |
| Future<Map<String, dynamic>> _flutterToggle(String name) async { |
| Map<String, dynamic> state = await invokeFlutterExtensionRpcRaw('ext.flutter.$name'); |
| if (state != null && state.containsKey('enabled') && state['enabled'] is String) { |
| state = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.$name', |
| params: <String, dynamic>{'enabled': state['enabled'] == 'true' ? 'false' : 'true'}, |
| ); |
| } |
| return state; |
| } |
| |
| Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled() => _flutterToggle('debugPaint'); |
| |
| Future<Map<String, dynamic>> flutterToggleDebugCheckElevationsEnabled() => _flutterToggle('debugCheckElevationsEnabled'); |
| |
| Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay'); |
| |
| Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('inspector.show'); |
| |
| Future<Map<String, dynamic>> flutterToggleProfileWidgetBuilds() => _flutterToggle('profileWidgetBuilds'); |
| |
| Future<Map<String, dynamic>> flutterDebugAllowBanner(bool show) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.debugAllowBanner', |
| params: <String, dynamic>{'enabled': show ? 'true' : 'false'}, |
| ); |
| } |
| |
| Future<Map<String, dynamic>> flutterReassemble() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble'); |
| } |
| |
| Future<bool> flutterAlreadyPaintedFirstUsefulFrame() async { |
| final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw('ext.flutter.didSendFirstFrameRasterizedEvent'); |
| // result might be null when the service extension is not initialized |
| return result != null && result['enabled'] == 'true'; |
| } |
| |
| Future<Map<String, dynamic>> uiWindowScheduleFrame() { |
| return invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame'); |
| } |
| |
| Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) { |
| return invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.evict', |
| params: <String, dynamic>{ |
| 'value': assetPath, |
| }, |
| ); |
| } |
| |
| Future<List<int>> flutterDebugSaveCompilationTrace() async { |
| final Map<String, dynamic> result = |
| await invokeFlutterExtensionRpcRaw('ext.flutter.saveCompilationTrace'); |
| if (result != null && result['value'] is List<dynamic>) { |
| return result['value'].cast<int>(); |
| } |
| return null; |
| } |
| |
| // Application control extension methods. |
| Future<Map<String, dynamic>> flutterExit() { |
| return invokeFlutterExtensionRpcRaw('ext.flutter.exit'); |
| } |
| |
| Future<String> flutterPlatformOverride([ String platform ]) async { |
| final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw( |
| 'ext.flutter.platformOverride', |
| params: platform != null ? <String, dynamic>{'value': platform} : <String, String>{}, |
| ); |
| if (result != null && result['value'] is String) { |
| return result['value']; |
| } |
| return 'unknown'; |
| } |
| |
| @override |
| String toString() => 'Isolate $id'; |
| } |
| |
| class ServiceMap extends ServiceObject implements Map<String, dynamic> { |
| ServiceMap._empty(ServiceObjectOwner owner) : super._empty(owner); |
| |
| final Map<String, dynamic> _map = <String, dynamic>{}; |
| |
| @override |
| void _update(Map<String, dynamic> map, bool mapIsRef) { |
| _loaded = !mapIsRef; |
| _upgradeCollection(map, owner); |
| _map.clear(); |
| _map.addAll(map); |
| } |
| |
| // Forward Map interface calls. |
| @override |
| void addAll(Map<String, dynamic> other) => _map.addAll(other); |
| @override |
| void clear() => _map.clear(); |
| @override |
| bool containsValue(dynamic v) => _map.containsValue(v); |
| @override |
| bool containsKey(Object k) => _map.containsKey(k); |
| @override |
| void forEach(void f(String key, dynamic value)) => _map.forEach(f); |
| @override |
| dynamic putIfAbsent(String key, dynamic ifAbsent()) => _map.putIfAbsent(key, ifAbsent); |
| @override |
| void remove(Object key) => _map.remove(key); |
| @override |
| dynamic operator [](Object k) => _map[k]; |
| @override |
| void operator []=(String k, dynamic v) => _map[k] = v; |
| @override |
| bool get isEmpty => _map.isEmpty; |
| @override |
| bool get isNotEmpty => _map.isNotEmpty; |
| @override |
| Iterable<String> get keys => _map.keys; |
| @override |
| Iterable<dynamic> get values => _map.values; |
| @override |
| int get length => _map.length; |
| @override |
| String toString() => _map.toString(); |
| @override |
| void addEntries(Iterable<MapEntry<String, dynamic>> entries) => _map.addEntries(entries); |
| @override |
| Map<RK, RV> cast<RK, RV>() => _map.cast<RK, RV>(); |
| @override |
| void removeWhere(bool test(String key, dynamic value)) => _map.removeWhere(test); |
| @override |
| Map<K2, V2> map<K2, V2>(MapEntry<K2, V2> transform(String key, dynamic value)) => _map.map<K2, V2>(transform); |
| @override |
| Iterable<MapEntry<String, dynamic>> get entries => _map.entries; |
| @override |
| void updateAll(dynamic update(String key, dynamic value)) => _map.updateAll(update); |
| Map<RK, RV> retype<RK, RV>() => _map.cast<RK, RV>(); |
| @override |
| dynamic update(String key, dynamic update(dynamic value), { dynamic ifAbsent() }) => _map.update(key, update, ifAbsent: ifAbsent); |
| } |
| |
| /// Peered to an Android/iOS FlutterView widget on a device. |
| class FlutterView extends ServiceObject { |
| FlutterView._empty(ServiceObjectOwner owner) : super._empty(owner); |
| |
| Isolate _uiIsolate; |
| Isolate get uiIsolate => _uiIsolate; |
| |
| @override |
| void _update(Map<String, dynamic> map, bool mapIsRef) { |
| _loaded = !mapIsRef; |
| _upgradeCollection(map, owner); |
| _uiIsolate = map['isolate']; |
| } |
| |
| // TODO(johnmccutchan): Report errors when running failed. |
| Future<void> runFromSource( |
| Uri entryUri, |
| Uri packagesUri, |
| Uri assetsDirectoryUri, |
| ) async { |
| final String viewId = id; |
| // When this completer completes the isolate is running. |
| final Completer<void> completer = Completer<void>(); |
| final StreamSubscription<ServiceEvent> subscription = |
| (await owner.vm.vmService.onIsolateEvent).listen((ServiceEvent event) { |
| // TODO(johnmccutchan): Listen to the debug stream and catch initial |
| // launch errors. |
| if (event.kind == ServiceEvent.kIsolateRunnable) { |
| printTrace('Isolate is runnable.'); |
| if (!completer.isCompleted) { |
| completer.complete(); |
| } |
| } |
| }); |
| await owner.vm.runInView(viewId, |
| entryUri, |
| packagesUri, |
| assetsDirectoryUri); |
| await completer.future; |
| await owner.vm.refreshViews(waitForViews: true); |
| await subscription.cancel(); |
| } |
| |
| Future<void> setAssetDirectory(Uri assetsDirectory) async { |
| assert(assetsDirectory != null); |
| await owner.vmService.vm.invokeRpc<ServiceObject>('_flutter.setAssetBundlePath', |
| params: <String, dynamic>{ |
| 'isolateId': _uiIsolate.id, |
| 'viewId': id, |
| 'assetDirectory': assetsDirectory.toFilePath(windows: false), |
| }); |
| } |
| |
| Future<void> setSemanticsEnabled(bool enabled) async { |
| assert(enabled != null); |
| await owner.vmService.vm.invokeRpc<ServiceObject>('_flutter.setSemanticsEnabled', |
| params: <String, dynamic>{ |
| 'isolateId': _uiIsolate.id, |
| 'viewId': id, |
| 'enabled': enabled, |
| }); |
| } |
| |
| bool get hasIsolate => _uiIsolate != null; |
| |
| Future<void> flushUIThreadTasks() async { |
| await owner.vm.invokeRpcRaw('_flutter.flushUIThreadTasks', |
| params: <String, dynamic>{'isolateId': _uiIsolate.id}); |
| } |
| |
| @override |
| String toString() => id; |
| } |