vmservice redux (#5437)
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 569a35e..de019fc 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -11,23 +11,17 @@ import 'globals.dart'; -// TODO(johnmccutchan): Rename this class to ServiceProtocol or VmService. +/// A connection to the Dart VM Service. class VMService { VMService._(this.peer, this.port, this.httpAddress) { + _vm = new VM._empty(this); + peer.registerMethod('streamNotify', (rpc.Parameters event) { _handleStreamNotify(event.asMap); }); - - onIsolateEvent.listen((Event event) { - if (event.kind == 'IsolateStart') { - _addIsolate(event.isolate); - } else if (event.kind == 'IsolateExit') { - String removedId = event.isolate.id; - isolates.removeWhere((IsolateRef ref) => ref.id == removedId); - } - }); } + /// Connect to '127.0.0.1' at [port]. static Future<VMService> connect(int port) async { Uri uri = new Uri(scheme: 'ws', host: '127.0.0.1', port: port, path: 'ws'); WebSocket ws = await WebSocket.connect(uri.toString()); @@ -37,158 +31,572 @@ return new VMService._(peer, port, httpAddress); } final Uri httpAddress; - final rpc.Peer peer; final int port; + final rpc.Peer peer; - List<IsolateRef> isolates = <IsolateRef>[]; - Completer<IsolateRef> _waitFirstIsolateCompleter; + VM _vm; + /// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects. + VM get vm => _vm; - Map<String, StreamController<Event>> _eventControllers = <String, StreamController<Event>>{}; + final Map<String, StreamController<ServiceEvent>> _eventControllers = + <String, StreamController<ServiceEvent>>{}; Set<String> _listeningFor = new Set<String>(); bool get isClosed => peer.isClosed; Future<Null> get done => peer.done; - String get firstIsolateId => isolates.isEmpty ? null : isolates.first.id; - // Events - - Stream<Event> get onExtensionEvent => onEvent('Extension'); + Stream<ServiceEvent> get onDebugEvent => onEvent('Debug'); + Stream<ServiceEvent> get onExtensionEvent => onEvent('Extension'); // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded - Stream<Event> get onIsolateEvent => onEvent('Isolate'); - Stream<Event> get onTimelineEvent => onEvent('Timeline'); + Stream<ServiceEvent> get onIsolateEvent => onEvent('Isolate'); + Stream<ServiceEvent> get onTimelineEvent => onEvent('Timeline'); + // TODO(johnmccutchan): Add FlutterView events. // Listen for a specific event name. - Stream<Event> onEvent(String streamId) { - streamListen(streamId); + Stream<ServiceEvent> onEvent(String streamId) { + _streamListen(streamId); return _getEventController(streamId).stream; } - StreamController<Event> _getEventController(String eventName) { - StreamController<Event> controller = _eventControllers[eventName]; + StreamController<ServiceEvent> _getEventController(String eventName) { + StreamController<ServiceEvent> controller = _eventControllers[eventName]; if (controller == null) { - controller = new StreamController<Event>.broadcast(); + controller = new StreamController<ServiceEvent>.broadcast(); _eventControllers[eventName] = controller; } return controller; } void _handleStreamNotify(Map<String, dynamic> data) { - Event event = new Event(data['event']); - _getEventController(data['streamId']).add(event); + final String streamId = data['streamId']; + final Map<String, dynamic> eventData = data['event']; + final Map<String, dynamic> eventIsolate = eventData['isolate']; + ServiceEvent event; + if (eventIsolate != null) { + // getFromMap creates the Isolate if necessary. + Isolate isolate = vm.getFromMap(eventIsolate); + event = new 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 = new ServiceObject._fromMap(vm, eventData); + } + _getEventController(streamId).add(event); } - Future<Null> populateIsolateInfo() async { - // Calling this has the side effect of populating the isolate information. - await waitFirstIsolate; - } - - Future<IsolateRef> get waitFirstIsolate async { - if (isolates.isNotEmpty) - return isolates.first; - - _waitFirstIsolateCompleter ??= new Completer<IsolateRef>(); - - getVM().then((VM vm) { - for (IsolateRef isolate in vm.isolates) - _addIsolate(isolate); - }); - - return _waitFirstIsolateCompleter.future; - } - - // Requests - - Future<Response> sendRequest(String method, [Map<String, dynamic> args]) { - return peer.sendRequest(method, args).then((dynamic result) => new Response(result)); - } - - Future<Null> streamListen(String streamId) async { + Future<Null> _streamListen(String streamId) async { if (!_listeningFor.contains(streamId)) { _listeningFor.add(streamId); - sendRequest('streamListen', <String, dynamic>{ 'streamId': streamId }); + await peer.sendRequest('streamListen', + <String, dynamic>{ 'streamId': streamId }); } } + /// Reloads the VM. Future<VM> getVM() { - return peer.sendRequest('getVM').then((dynamic result) { - return new VM(result); - }); + return _vm.reload(); + } +} + +/// 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) { + _upgradeMap(collection, owner); + } else if (collection is List) { + _upgradeList(collection, owner); + } +} + +void _upgradeMap(Map<String, dynamic> map, ServiceObjectOwner owner) { + map.forEach((String k, dynamic v) { + if ((v is Map) && _isServiceMap(v)) { + map[k] = owner.getFromMap(v); + } else if (v is List) { + _upgradeList(v, owner); + } else if (v is Map) { + _upgradeMap(v, owner); + } + }); +} + +void _upgradeList(List<dynamic> list, ServiceObjectOwner owner) { + for (int i = 0; i < list.length; i++) { + dynamic v = list[i]; + if ((v is Map) && _isServiceMap(v)) { + list[i] = owner.getFromMap(v); + } else if (v is List) { + _upgradeList(v, owner); + } else if (v is Map) { + _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 new VMServiceObjectLoadError("Expected a service map", map); + + String type = _stripRef(map['type']); + + ServiceObject serviceObject; + switch (type) { + case 'Event': + serviceObject = new ServiceEvent._empty(owner); + break; + case 'FlutterView': + serviceObject = new FlutterView._empty(owner.vm); + break; + case 'Isolate': + serviceObject = new Isolate._empty(owner.vm); + break; + default: + printTrace("Unsupported service object type: $type"); + } + if (serviceObject == null) { + // If we don't have a model object for this service object type, as a + // fallback return a ServiceMap object. + serviceObject = new ServiceMap._empty(owner); + } + // We have now constructed an emtpy service object, call update to + // populate it. + serviceObject.update(map); + return serviceObject; } - Future<Map<String, dynamic>> reloadSources(String isolateId) async { - try { - Response response = - await sendRequest('_reloadSources', - <String, dynamic>{ 'isolateId': isolateId }); - return response.response; - } catch (e) { - return new Future<Map<String, dynamic>>.error(e.data['details']); + 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() { + Map<String, dynamic> params = <String, dynamic>{ + 'objectId': id, + }; + return _owner.isolate.invokeRpcRaw('getObject', params); + } + + Future<ServiceObject> _inProgressReload; + /// Reload the service object (if possible). + Future<ServiceObject> reload() async { + bool hasId = (id != null) && (id != ''); + 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. + bool skipLoad = !isVM && (!hasId || (immutable && loaded)); + if (skipLoad) { + return this; + } + + if (_inProgressReload == null) { + Completer<ServiceObject> completer = new Completer<ServiceObject>(); + _inProgressReload = completer.future; + + try { + Map<String, dynamic> response = await _fetchDirect(); + if (_stripRef(response['type']) == 'Sentinel') { + // An object may have been collected. + completer.complete(new ServiceObject._fromMap(owner, response)); + } else { + update(response); + completer.complete(this); + } + } catch (e, st) { + completer.completeError(e, st); + } + _inProgressReload = null; + } + + return _inProgressReload; + } + + /// Update [this] using [map] as a source. [map] can be a service reference. + void update(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 new 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 new 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 { + /// 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 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'; + + ServiceEvent._empty(ServiceObjectOwner owner) : super._empty(owner); + + String _kind; + String get kind => _kind; + DateTime _timestamp; + DateTime get timestmap => _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; + + @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 = + new DateTime.fromMillisecondsSinceEpoch(map['timestamp']); + if (map['extensionKind'] != null) { + _extensionKind = map['extensionKind']; + _extensionData = map['extensionData']; + } + _timelineEvents = map['timelineEvents']; + } + + bool get isPauseEvent { + return (kind == kPauseStart || + kind == kPauseExit || + kind == kPauseBreakpoint || + kind == kPauseInterrupted || + kind == kPauseException || + kind == kNone); + } +} + +/// A ServiceObjectOwner is either a [VM] or an [Isolate]. Owners can cache +/// and/or canonicalize service objets 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() async { + return invokeRpcRaw('getVM', <String, dynamic> {}); + } + + @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; + + // TODO(johnmccutchan): Extract any properties we care about here. + + // Remove any isolates which are now dead from the isolate cache. + _removeDeadIsolates(map['isolates']); + } + + final Map<String, ServiceObject> _cache = new Map<String,ServiceObject>(); + final Map<String,Isolate> _isolateCache = new Map<String,Isolate>(); + + /// The list of live isolates, ordered by isolate start time. + final List<Isolate> isolates = new List<Isolate>(); + + /// The set of live views. + final Map<String, FlutterView> _viewCache = new Map<String, FlutterView>(); + + int _compareIsolates(Isolate a, Isolate b) { + DateTime aStart = a.startTime; + 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() { + 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. + Set<String> newIsolateSet = new Set<String>(); + newIsolates.forEach((Isolate iso) => newIsolateSet.add(iso.id)); + + // Remove any old isolates which no longer exist. + List<String> toRemove = <String>[]; + _isolateCache.forEach((String id, _) { + if (!newIsolateSet.contains(id)) { + toRemove.add(id); + } + }); + toRemove.forEach((String id) => _isolateCache.remove(id)); + _buildIsolateList(); + } + + @override + ServiceObject getFromMap(Map<String, dynamic> map) { + if (map == null) { + return null; + } + String type = _stripRef(map['type']); + if (type == 'VM') { + // Update this VM object. + update(map); + return this; + } + + String mapId = map['id']; + + switch (type) { + case 'Isolate': { + // Check cache. + Isolate isolate = _isolateCache[mapId]; + if (isolate == null) { + // Add new isolate to the cache. + isolate = new 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.update(map); + } + return isolate; + } + break; + case 'FlutterView': { + FlutterView view = _viewCache[mapId]; + if (view == null) { + // Add new view to the cache. + view = new ServiceObject._fromMap(this, map); + _viewCache[mapId] = view; + } else { + view.update(map); + } + return view; + } + break; + default: + throw new VMServiceObjectLoadError( + 'VM.getFromMap called for something other than an isolate', map); } } - Future<List<Map<String, String>>> getViewList() async { - Map<String, dynamic> response = await peer.sendRequest('_flutter.listViews'); - List<Map<String, String>> views = response['views']; - return views; + // Note that this function does not reload the isolate if it 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((_) => getIsolate(isolateId)).catchError((_) => null); + } + return new Future<Isolate>.value(_isolateCache[isolateId]); } - Future<String> getFirstViewId() async { - List<Map<String, String>> views = await getViewList(); - return views[0]['id']; + /// Invoke the RPC and return the raw response. + Future<Map<String, dynamic>> invokeRpcRaw( + String method, [Map<String, dynamic> params]) async { + if (params == null) { + params = <String, dynamic>{}; + } + Map<String, dynamic> result = + await _vmService.peer.sendRequest(method, params); + return result; } - Future<Null> runInView(String viewId, - String main, - String packages, - String assetsDirectory) async { - await peer.sendRequest('_flutter.runInView', - <String, dynamic> { - 'viewId': viewId, - 'mainScript': main, - 'packagesFile': packages, - 'assetDirectory': assetsDirectory - }); - return null; + /// Invoke the RPC and return a ServiceObject response. + Future<ServiceObject> invokeRpc( + String method, [Map<String, dynamic> params]) async { + Map<String, dynamic> response = await invokeRpcRaw(method, params); + ServiceObject serviceObject = new ServiceObject._fromMap(this, response); + if ((serviceObject != null) && (serviceObject._canCache)) { + String serviceObjectId = serviceObject.id; + _cache.putIfAbsent(serviceObjectId, () => serviceObject); + } + return serviceObject; } - Future<Response> clearVMTimeline() => sendRequest('_clearVMTimeline'); - - Future<Response> setVMTimelineFlags(List<String> recordedStreams) { - assert(recordedStreams != null); - - return sendRequest('_setVMTimelineFlags', <String, dynamic> { - 'recordedStreams': recordedStreams - }); + /// Create a new development file system on the device. + Future<Map<String, dynamic>> createDevFS(String fsName) async { + Map<String, dynamic> response = + await invokeRpcRaw('_createDevFS', <String, dynamic> { + 'fsName': fsName + }); + return response; } - Future<Response> getVMTimeline() => sendRequest('_getVMTimeline'); - - // DevFS / VM virtual file system methods - - /// Create a new file system. - Future<CreateDevFSResponse> createDevFS(String fsName) async { - Response response = await sendRequest('_createDevFS', <String, dynamic> { 'fsName': fsName }); - return new CreateDevFSResponse(response.response); - } - - /// List the available file systems. - Future<List<String>> listDevFS() { - return sendRequest('_listDevFS').then((Response response) { - return response.response['fsNames']; - }); + /// List the development file system son the device. + Future<List<String>> listDevFS() async { + Map<String, dynamic> response = + await invokeRpcRaw('_listDevFS', <String, dynamic>{}); + return response['fsNames']; } // Write one file into a file system. - Future<Response> writeDevFSFile(String fsName, { + Future<Map<String, dynamic>> writeDevFSFile(String fsName, { String path, List<int> fileContents }) { assert(path != null); assert(fileContents != null); - return sendRequest('_writeDevFSFile', <String, dynamic> { + return invokeRpcRaw('_writeDevFSFile', <String, dynamic> { 'fsName': fsName, 'path': path, 'fileContents': BASE64.encode(fileContents) @@ -197,159 +605,292 @@ // Read one file from a file system. Future<List<int>> readDevFSFile(String fsName, String path) { - return sendRequest('_readDevFSFile', <String, dynamic> { + return invokeRpcRaw('_readDevFSFile', <String, dynamic> { 'fsName': fsName, 'path': path - }).then((Response response) { - return BASE64.decode(response.response['fileContents']); + }).then((Map<String, dynamic> response) { + return BASE64.decode(response['fileContents']); }); } /// The complete list of a file system. Future<List<String>> listDevFSFiles(String fsName) { - return sendRequest('_listDevFSFiles', <String, dynamic> { + return invokeRpcRaw('_listDevFSFiles', <String, dynamic> { 'fsName': fsName - }).then((Response response) { - return response.response['files']; + }).then((Map<String, dynamic> response) { + return response['files']; }); } /// Delete an existing file system. - Future<Response> deleteDevFS(String fsName) { - return sendRequest('_deleteDevFS', <String, dynamic> { 'fsName': fsName }); + Future<Map<String, dynamic>> deleteDevFS(String fsName) { + return invokeRpcRaw('_deleteDevFS', <String, dynamic> { 'fsName': fsName }); + } + + Future<ServiceMap> runInView(String viewId, + String main, + String packages, + String assetsDirectory) { + return invokeRpc('_flutter.runInView', + <String, dynamic> { + 'viewId': viewId, + 'mainScript': main, + 'packagesFile': packages, + 'assetDirectory': assetsDirectory + }); + } + + Future<Map<String, dynamic>> clearVMTimeline() { + return invokeRpcRaw('_clearVMTimeline', <String, dynamic>{}); + } + + Future<Map<String, dynamic>> setVMTimelineFlags( + List<String> recordedStreams) { + assert(recordedStreams != null); + + return invokeRpcRaw('_setVMTimelineFlags', <String, dynamic> { + 'recordedStreams': recordedStreams + }); + } + + Future<Map<String, dynamic>> getVMTimeline() { + return invokeRpcRaw('_getVMTimeline', <String, dynamic> {}); + } + + Future<Null> refreshViews() async { + await vmService.vm.invokeRpc('_flutter.listViews'); + } + + FlutterView get mainView { + return _viewCache.values.first; + } +} + +/// 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; + + final Map<String, ServiceObject> _cache = new Map<String, ServiceObject>(); + + @override + ServiceObject getFromMap(Map<String, dynamic> map) { + if (map == null) { + return null; + } + String mapType = _stripRef(map['type']); + if (mapType == 'Isolate') { + // There are sometimes isolate refs in ServiceEvents. + return vm.getFromMap(map); + } + + String mapId = map['id']; + ServiceObject serviceObject = (mapId != null) ? _cache[mapId] : null; + if (serviceObject != null) { + serviceObject.update(map); + return serviceObject; + } + // Build the object from the map directly. + serviceObject = new ServiceObject._fromMap(this, map); + if ((serviceObject != null) && serviceObject.canCache) { + _cache[mapId] = serviceObject; + } + return serviceObject; + } + + @override + Future<Map<String, dynamic>> _fetchDirect() { + return invokeRpcRaw('getIsolate', <String, dynamic>{}); + } + + /// 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); + } + + /// Invoke the RPC and return a ServiceObject response. + Future<ServiceObject> invokeRpc( + String method, Map<String, dynamic> params) async { + Map<String, dynamic> response = await invokeRpcRaw(method, params); + return getFromMap(response); + } + + @override + void _update(Map<String, dynamic> map, bool mapIsRef) { + if (mapIsRef) { + return; + } + _loaded = true; + + int startTimeMillis = map['startTime']; + startTime = new DateTime.fromMillisecondsSinceEpoch(startTimeMillis); + + // TODO(johnmccutchan): Extract any properties we care about here. + _upgradeCollection(map, this); + } + + Future<Map<String, dynamic>> reloadSources() async { + try { + Map<String, dynamic> response = await invokeRpcRaw('_reloadSources'); + return response; + } catch (e) { + return new Future<Map<String, dynamic>>.error(e.data['details']); + } } // Flutter extension methods. - Future<Response> flutterDebugDumpApp(String isolateId) { - return peer.sendRequest('ext.flutter.debugDumpApp', <String, dynamic>{ - 'isolateId': isolateId - }).then((dynamic result) => new Response(result)); + Future<Map<String, dynamic>> flutterDebugDumpApp() { + return invokeRpcRaw('ext.flutter.debugDumpApp'); } - Future<Response> flutterDebugDumpRenderTree(String isolateId) { - return peer.sendRequest('ext.flutter.debugDumpRenderTree', <String, dynamic>{ - 'isolateId': isolateId - }).then((dynamic result) => new Response(result)); + Future<Map<String, dynamic>> flutterDebugDumpRenderTree() { + return invokeRpcRaw('ext.flutter.debugDumpRenderTree'); } // Loader page extension methods. - Future<Response> flutterLoaderShowMessage(String isolateId, String message) { - return peer.sendRequest('ext.flutter.loaderShowMessage', <String, dynamic>{ - 'isolateId': isolateId, + Future<Map<String, dynamic>> flutterLoaderShowMessage(String message) { + return invokeRpcRaw('ext.flutter.loaderShowMessage', <String, dynamic> { 'value': message - }).then( - (dynamic result) => new Response(result), - onError: (dynamic exception) { printTrace('ext.flutter.loaderShowMessage: $exception'); } - ); + }); } - Future<Response> flutterLoaderSetProgress(String isolateId, double progress) { - return peer.sendRequest('ext.flutter.loaderSetProgress', <String, dynamic>{ - 'isolateId': isolateId, + Future<Map<String, dynamic>> flutterLoaderSetProgress(double progress) { + return invokeRpcRaw('ext.flutter.loaderSetProgress', <String, dynamic>{ 'loaderSetProgress': progress - }).then( - (dynamic result) => new Response(result), - onError: (dynamic exception) { printTrace('ext.flutter.loaderSetProgress: $exception'); } - ); + }); } - Future<Response> flutterLoaderSetProgressMax(String isolateId, double max) { - return peer.sendRequest('ext.flutter.loaderSetProgressMax', <String, dynamic>{ - 'isolateId': isolateId, + Future<Map<String, dynamic>> flutterLoaderSetProgressMax(double max) { + return invokeRpcRaw('ext.flutter.loaderSetProgressMax', <String, dynamic>{ 'loaderSetProgressMax': max - }).then( - (dynamic result) => new Response(result), - onError: (dynamic exception) { printTrace('ext.flutter.loaderSetProgressMax: $exception'); } - ); + }); } /// Causes the application to pick up any changed code. - Future<Response> flutterReassemble(String isolateId) { - return peer.sendRequest('ext.flutter.reassemble', <String, dynamic>{ - 'isolateId': isolateId - }).then((dynamic result) => new Response(result)); + Future<Map<String, dynamic>> flutterReassemble() { + return invokeRpcRaw('ext.flutter.reassemble'); } - Future<Response> flutterEvictAsset(String isolateId, String assetPath) { - return peer.sendRequest('ext.flutter.evict', <String, dynamic>{ - 'isolateId': isolateId, + Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) { + return invokeRpcRaw('ext.flutter.evict', <String, dynamic>{ 'value': assetPath - }).then((dynamic result) => new Response(result)); + }); } - Future<Response> flutterExit(String isolateId) { - return peer - .sendRequest('ext.flutter.exit', <String, dynamic>{ 'isolateId': isolateId }) - .then((dynamic result) => new Response(result)) - .timeout(new Duration(seconds: 2), onTimeout: () => null); + Future<Map<String, dynamic>> flutterExit() { + return invokeRpcRaw('ext.flutter.exit').timeout( + const Duration(seconds: 2), onTimeout: () => null); + } +} + +class ServiceMap extends ServiceObject implements Map<String, dynamic> { + ServiceMap._empty(ServiceObjectOwner owner) : super._empty(owner); + + final Map<String, dynamic> _map = new Map<String, dynamic>(); + + @override + void _update(Map<String, dynamic> map, bool mapIsRef) { + _loaded = !mapIsRef; + _upgradeCollection(map, owner); + _map.clear(); + _map.addAll(map); } - void _addIsolate(IsolateRef isolate) { - if (!isolates.contains(isolate)) { - isolates.add(isolate); + // 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(String k) => _map.containsKey(k); + @override + void forEach(Function f) => _map.forEach(f); + @override + dynamic putIfAbsent(String key, Function ifAbsent) => _map.putIfAbsent(key, ifAbsent); + @override + void remove(String key) => _map.remove(key); + @override + dynamic operator [](String 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(); +} - if (_waitFirstIsolateCompleter != null) { - _waitFirstIsolateCompleter.complete(isolate); - _waitFirstIsolateCompleter = null; +/// Peered to a 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<Null> runFromSource(String entryPath, + String packagesPath, + String assetsDirectoryPath) async { + final String viewId = id; + // When this completer completes the isolate is running. + final Completer<Null> completer = new Completer<Null>(); + final StreamSubscription<ServiceEvent> subscription = + 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.'); + completer.complete(null); } - } - } -} - -class Response { - Response(this.response); - - final Map<String, dynamic> response; - - String get type => response['type']; - - dynamic operator[](String key) => response[key]; - - @override - String toString() => response.toString(); -} - -class CreateDevFSResponse extends Response { - CreateDevFSResponse(Map<String, dynamic> response) : super(response); - - String get name => response['name']; - String get uri => response['uri']; -} - -class VM extends Response { - VM(Map<String, dynamic> response) : super(response); - - List<IsolateRef> get isolates => response['isolates'].map((dynamic ref) => new IsolateRef(ref)).toList(); -} - -class Event extends Response { - Event(Map<String, dynamic> response) : super(response); - - String get kind => response['kind']; - IsolateRef get isolate => new IsolateRef.from(response['isolate']); - - /// Only valid for [kind] == `Extension`. - String get extensionKind => response['extensionKind']; -} - -class IsolateRef extends Response { - IsolateRef(Map<String, dynamic> response) : super(response); - factory IsolateRef.from(dynamic ref) => ref == null ? null : new IsolateRef(ref); - - String get id => response['id']; - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) - return true; - if (other is! IsolateRef) - return false; - final IsolateRef typedOther = other; - return id == typedOther.id; + }); + await owner.vm.runInView(viewId, + entryPath, + packagesPath, + assetsDirectoryPath); + await completer.future; + await subscription.cancel(); } + bool get hasIsolate => _uiIsolate != null; + @override - int get hashCode => id.hashCode; + String toString() => id; }