vmservice redux (#5437)
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index c94c817..0ad84a0 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -143,7 +143,7 @@ return 1; } } else { - if (argResults['control-pipe']) { + if (argResults['control-pipe'] != null) { printError('--control-pipe requires --hot'); return 1; }
diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart index 2632c7f..2fd1dc9 100644 --- a/packages/flutter_tools/lib/src/commands/trace.dart +++ b/packages/flutter_tools/lib/src/commands/trace.dart
@@ -95,40 +95,39 @@ } class Tracing { - Tracing(this.observatory); + Tracing(this.vmService); static Future<Tracing> connect(int port) { return VMService.connect(port).then((VMService observatory) => new Tracing(observatory)); } - final VMService observatory; + final VMService vmService; Future<Null> startTracing() async { - await observatory.setVMTimelineFlags(<String>['Compiler', 'Dart', 'Embedder', 'GC']); - await observatory.clearVMTimeline(); + await vmService.vm.setVMTimelineFlags(<String>['Compiler', 'Dart', 'Embedder', 'GC']); + await vmService.vm.clearVMTimeline(); } /// Stops tracing; optionally wait for first frame. Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({ bool waitForFirstFrame: false }) async { - Response timeline; + Map<String, dynamic> timeline; if (!waitForFirstFrame) { // Stop tracing immediately and get the timeline - await observatory.setVMTimelineFlags(<String>[]); - timeline = await observatory.getVMTimeline(); + await vmService.vm.setVMTimelineFlags(<String>[]); + timeline = await vmService.vm.getVMTimeline(); } else { Completer<Null> whenFirstFrameRendered = new Completer<Null>(); - observatory.onTimelineEvent.listen((Event timelineEvent) { - List<Map<String, dynamic>> events = timelineEvent['timelineEvents']; + vmService.onTimelineEvent.listen((ServiceEvent timelineEvent) { + List<Map<String, dynamic>> events = timelineEvent.timelineEvents; for (Map<String, dynamic> event in events) { if (event['name'] == kFirstUsefulFrameEventName) whenFirstFrameRendered.complete(); } }); - await observatory.streamListen('Timeline'); await whenFirstFrameRendered.future.timeout( const Duration(seconds: 10), @@ -142,12 +141,12 @@ } ); - timeline = await observatory.getVMTimeline(); + timeline = await vmService.vm.getVMTimeline(); - await observatory.setVMTimelineFlags(<String>[]); + await vmService.vm.setVMTimelineFlags(<String>[]); } - return timeline.response; + return timeline; } }
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index fdc5ec3..80a40f2 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -106,22 +106,22 @@ } /// An implementation of [DevFSOperations] that speaks to the -/// service protocol. +/// vm service. class ServiceProtocolDevFSOperations implements DevFSOperations { - final VMService serviceProtocol; + final VMService vmService; - ServiceProtocolDevFSOperations(this.serviceProtocol); + ServiceProtocolDevFSOperations(this.vmService); @override Future<Uri> create(String fsName) async { - Response response = await serviceProtocol.createDevFS(fsName); + Map<String, dynamic> response = await vmService.vm.createDevFS(fsName); return Uri.parse(response['uri']); } @override Future<dynamic> destroy(String fsName) async { - await serviceProtocol.sendRequest('_deleteDevFS', - <String, dynamic> { 'fsName': fsName }); + await vmService.vm.invokeRpcRaw('_deleteDevFS', + <String, dynamic> { 'fsName': fsName }); } @override @@ -134,12 +134,12 @@ } String fileContents = BASE64.encode(bytes); try { - return await serviceProtocol.sendRequest('_writeDevFSFile', - <String, dynamic> { - 'fsName': fsName, - 'path': entry.devicePath, - 'fileContents': fileContents - }); + return await vmService.vm.invokeRpcRaw('_writeDevFSFile', + <String, dynamic> { + 'fsName': fsName, + 'path': entry.devicePath, + 'fileContents': fileContents + }); } catch (e) { printTrace('DevFS: Failed to write ${entry.devicePath}: $e'); } @@ -155,12 +155,12 @@ String devicePath, String contents) async { String fileContents = BASE64.encode(UTF8.encode(contents)); - return await serviceProtocol.sendRequest('_writeDevFSFile', - <String, dynamic> { - 'fsName': fsName, - 'path': devicePath, - 'fileContents': fileContents - }); + return await vmService.vm.invokeRpcRaw('_writeDevFSFile', + <String, dynamic> { + 'fsName': fsName, + 'path': devicePath, + 'fileContents': fileContents + }); } } @@ -251,7 +251,8 @@ final Set<DevFSEntry> _deletedEntries = new Set<DevFSEntry>(); final Set<DevFSEntry> dirtyAssetEntries = new Set<DevFSEntry>(); - final List<Future<Response>> _pendingOperations = new List<Future<Response>>(); + final List<Future<Map<String, dynamic>>> _pendingOperations = + new List<Future<Map<String, dynamic>>>(); int _bytes = 0; int get bytes => _bytes; @@ -358,7 +359,8 @@ if (_deletedEntries.length > 0) { status = logger.startProgress('Removing deleted files...'); for (DevFSEntry entry in _deletedEntries) { - Future<Response> operation = _operations.deleteFile(fsName, entry); + Future<Map<String, dynamic>> operation = + _operations.deleteFile(fsName, entry); if (operation != null) _pendingOperations.add(operation); } @@ -382,7 +384,8 @@ } else { // Make service protocol requests for each. for (DevFSEntry entry in _dirtyEntries) { - Future<Response> operation = _operations.writeFile(fsName, entry); + Future<Map<String, dynamic>> operation = + _operations.writeFile(fsName, entry); if (operation != null) _pendingOperations.add(operation); }
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart index 2e1cad5..80d98a2 100644 --- a/packages/flutter_tools/lib/src/hot.dart +++ b/packages/flutter_tools/lib/src/hot.dart
@@ -23,7 +23,6 @@ import 'vmservice.dart'; import 'resident_runner.dart'; import 'toolchain.dart'; -import 'view.dart'; String getDevFSLoaderScript() { return path.absolute(path.join(Cache.flutterRoot, @@ -80,18 +79,18 @@ class FirstFrameTimer { - FirstFrameTimer(this.serviceProtocol); + FirstFrameTimer(this.vmService); void start() { stopwatch.reset(); stopwatch.start(); - _subscription = serviceProtocol.onExtensionEvent.listen(_onExtensionEvent); + _subscription = vmService.onExtensionEvent.listen(_onExtensionEvent); } /// Returns a Future which completes after the first frame event is received. Future<Null> firstFrame() => _completer.future; - void _onExtensionEvent(Event event) { + void _onExtensionEvent(ServiceEvent event) { if (event.extensionKind == 'Flutter.FirstFrame') _stop(); } @@ -108,10 +107,10 @@ return stopwatch.elapsed; } - final VMService serviceProtocol; + final VMService vmService; final Stopwatch stopwatch = new Stopwatch(); final Completer<Null> _completer = new Completer<Null>(); - StreamSubscription<Event> _subscription; + StreamSubscription<ServiceEvent> _subscription; } class HotRunner extends ResidentRunner { @@ -294,8 +293,8 @@ return 3; } - await viewManager.refresh(); - printStatus('Connected to view \'${viewManager.mainView}\'.'); + await vmService.vm.refreshViews(); + printStatus('Connected to view \'${vmService.vm.mainView}\'.'); printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...'); _loaderShowMessage('Launching...'); @@ -332,13 +331,13 @@ } void _loaderShowMessage(String message, { int progress, int max }) { - serviceProtocol.flutterLoaderShowMessage(serviceProtocol.firstIsolateId, message); + currentView.uiIsolate.flutterLoaderShowMessage(message); if (progress != null) { - serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, progress.toDouble()); - serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, max?.toDouble() ?? 0.0); + currentView.uiIsolate.flutterLoaderSetProgress(progress.toDouble()); + currentView.uiIsolate.flutterLoaderSetProgressMax(max?.toDouble() ?? 0.0); } else { - serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, 0.0); - serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, -1.0); + currentView.uiIsolate.flutterLoaderSetProgress(0.0); + currentView.uiIsolate.flutterLoaderSetProgressMax(-1.0); } } @@ -346,7 +345,7 @@ Future<Uri> _initDevFS() { String fsName = path.basename(_projectRootPath); - _devFS = new DevFS(serviceProtocol, + _devFS = new DevFS(vmService, fsName, new Directory(_projectRootPath)); return _devFS.create(); @@ -377,11 +376,10 @@ Future<Null> _evictDirtyAssets() async { if (_devFS.dirtyAssetEntries.length == 0) return; - if (serviceProtocol.firstIsolateId == null) + if (currentView.uiIsolate == null) throw 'Application isolate not found'; for (DevFSEntry entry in _devFS.dirtyAssetEntries) { - await serviceProtocol.flutterEvictAsset(serviceProtocol.firstIsolateId, - entry.assetPath); + await currentView.uiIsolate.flutterEvictAsset(entry.assetPath); } } @@ -400,7 +398,7 @@ Future<Null> _launchInView(String entryPath, String packagesPath, String assetsDirectoryPath) async { - FlutterView view = viewManager.mainView; + FlutterView view = vmService.vm.mainView; return view.runFromSource(entryPath, packagesPath, assetsDirectoryPath); } @@ -419,7 +417,7 @@ } Future<Null> _restartFromSources() async { - FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol); + FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService); firstFrameTimer.start(); await _updateDevFS(); await _launchFromDevFS(_package, _mainPath); @@ -459,16 +457,16 @@ } Future<bool> _reloadSources() async { - if (serviceProtocol.firstIsolateId == null) + if (currentView.uiIsolate == null) throw 'Application isolate not found'; - FirstFrameTimer firstFrameTimer = new FirstFrameTimer(serviceProtocol); + FirstFrameTimer firstFrameTimer = new FirstFrameTimer(vmService); firstFrameTimer.start(); if (_devFS != null) await _updateDevFS(); Status reloadStatus = logger.startProgress('Performing hot reload...'); try { Map<String, dynamic> reloadReport = - await serviceProtocol.reloadSources(serviceProtocol.firstIsolateId); + await currentView.uiIsolate.reloadSources(); reloadStatus.stop(showElapsedTime: true); if (!_printReloadReport(reloadReport)) { // Reload failed. @@ -477,16 +475,16 @@ } else { flutterUsage.sendEvent('hot', 'reload'); } - } catch (errorMessage) { + } catch (errorMessage, st) { reloadStatus.stop(showElapsedTime: true); - printError('Hot reload failed:\n$errorMessage'); + printError('Hot reload failed:\n$errorMessage\n$st'); return false; } await _evictDirtyAssets(); Status reassembleStatus = logger.startProgress('Reassembling application...'); try { - await serviceProtocol.flutterReassemble(serviceProtocol.firstIsolateId); + await currentView.uiIsolate.flutterReassemble(); } catch (_) { reassembleStatus.stop(showElapsedTime: true); printError('Reassembling application failed.');
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 470d721..166f729 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -12,7 +12,6 @@ import 'device.dart'; import 'globals.dart'; import 'vmservice.dart'; -import 'view.dart'; // Shared code between different resident application runners. abstract class ResidentRunner { @@ -28,8 +27,8 @@ final bool usesTerminalUI; final Completer<int> _finished = new Completer<int>(); - VMService serviceProtocol; - ViewManager viewManager; + VMService vmService; + FlutterView currentView; StreamSubscription<String> _loggingSubscription; /// Start the app and keep the process running during its lifetime. @@ -48,11 +47,11 @@ } Future<Null> _debugDumpApp() async { - await serviceProtocol.flutterDebugDumpApp(serviceProtocol.firstIsolateId); + await currentView.uiIsolate.flutterDebugDumpApp(); } Future<Null> _debugDumpRenderTree() async { - await serviceProtocol.flutterDebugDumpRenderTree(serviceProtocol.firstIsolateId); + await currentView.uiIsolate.flutterDebugDumpRenderTree(); } void registerSignalHandlers() { @@ -90,22 +89,23 @@ if (!debuggingOptions.debuggingEnabled) { return new Future<Null>.error('Error the service protocol is not enabled.'); } - serviceProtocol = await VMService.connect(port); + vmService = await VMService.connect(port); printTrace('Connected to service protocol on port $port'); - serviceProtocol.populateIsolateInfo(); - serviceProtocol.onExtensionEvent.listen((Event event) { + await vmService.getVM(); + vmService.onExtensionEvent.listen((ServiceEvent event) { printTrace(event.toString()); }); - serviceProtocol.onIsolateEvent.listen((Event event) { + vmService.onIsolateEvent.listen((ServiceEvent event) { printTrace(event.toString()); }); - // Setup view manager and refresh the view list. - viewManager = new ViewManager(serviceProtocol); - await viewManager.refresh(); + // Refresh the view list. + await vmService.vm.refreshViews(); + currentView = vmService.vm.mainView; + assert(currentView != null); // Listen for service protocol connection to close. - serviceProtocol.done.whenComplete(() { + vmService.done.whenComplete(() { appFinished(); }); } @@ -175,9 +175,10 @@ Future<Null> preStop() async { } Future<Null> stopApp() async { - if (serviceProtocol != null && !serviceProtocol.isClosed) { - if (serviceProtocol.isolates.isNotEmpty) { - serviceProtocol.flutterExit(serviceProtocol.firstIsolateId); + if (vmService != null && !vmService.isClosed) { + if ((currentView != null) && (currentView.uiIsolate != null)) { + // TODO(johnmccutchan): Wait for the exit command to complete. + currentView.uiIsolate.flutterExit(); await new Future<Null>.delayed(new Duration(milliseconds: 100)); } }
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart index a3a5ca9..9b0a35e 100644 --- a/packages/flutter_tools/lib/src/run.dart +++ b/packages/flutter_tools/lib/src/run.dart
@@ -59,17 +59,17 @@ @override Future<bool> restart({ bool fullRestart: false }) async { - if (serviceProtocol == null) { + if (vmService == null) { printError('Debugging is not enabled.'); return false; } else { Status status = logger.startProgress('Re-starting application...'); - Future<Event> extensionAddedEvent; + Future<ServiceEvent> extensionAddedEvent; if (device.restartSendsFrameworkInitEvent) { - extensionAddedEvent = serviceProtocol.onExtensionEvent - .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization') + extensionAddedEvent = vmService.onExtensionEvent + .where((ServiceEvent event) => event.extensionKind == 'Flutter.FrameworkInitialization') .first; } @@ -77,7 +77,7 @@ _package, _result, mainPath: _mainPath, - observatory: serviceProtocol + observatory: vmService ); status.stop(showElapsedTime: true); @@ -178,16 +178,20 @@ if (debuggingOptions.debuggingEnabled) { await connectToServiceProtocol(_result.observatoryPort); - if (benchmark) - await serviceProtocol.waitFirstIsolate; + if (benchmark) { + await vmService.getVM(); + } } printStatus('Application running.'); - if (serviceProtocol != null && traceStartup) { + await vmService.vm.refreshViews(); + printStatus('Connected to view \'${vmService.vm.mainView}\'.'); + + if (vmService != null && traceStartup) { printStatus('Downloading startup trace info...'); try { - await downloadStartupTrace(serviceProtocol); + await downloadStartupTrace(vmService); } catch(error) { printError(error); return 2;
diff --git a/packages/flutter_tools/lib/src/view.dart b/packages/flutter_tools/lib/src/view.dart deleted file mode 100644 index f425fb8..0000000 --- a/packages/flutter_tools/lib/src/view.dart +++ /dev/null
@@ -1,89 +0,0 @@ -// 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 'globals.dart'; -import 'vmservice.dart'; - -/// Peered to a Android/iOS FlutterView widget on a device. -class FlutterView { - FlutterView(this.viewId, this.viewManager); - - final String viewId; - final ViewManager viewManager; - String _uiIsolateId; - String get uiIsolateId => _uiIsolateId; - - Future<Null> runFromSource(String entryPath, - String packagesPath, - String assetsDirectoryPath) async { - return viewManager._runFromSource(this, - entryPath, - packagesPath, - assetsDirectoryPath); - } - - @override - String toString() => viewId; - - @override - bool operator ==(FlutterView other) { - return other.viewId == viewId; - } - - @override - int get hashCode => viewId.hashCode; -} - -/// Manager of FlutterViews. -class ViewManager { - ViewManager(this.serviceProtocol); - - final VMService serviceProtocol; - - Future<Null> refresh() async { - List<Map<String, String>> viewList = await serviceProtocol.getViewList(); - for (Map<String, String> viewDescription in viewList) { - FlutterView view = new FlutterView(viewDescription['id'], this); - if (!views.contains(view)) { - // Canonicalize views against the view set. - views.add(view); - } - } - } - - // TODO(johnmccutchan): Report errors when running failed. - Future<Null> _runFromSource(FlutterView view, - String entryPath, - String packagesPath, - String assetsDirectoryPath) async { - final String viewId = await serviceProtocol.getFirstViewId(); - // When this completer completes the isolate is running. - final Completer<Null> completer = new Completer<Null>(); - final StreamSubscription<Event> subscription = - serviceProtocol.onIsolateEvent.listen((Event event) { - // TODO(johnmccutchan): Listen to the debug stream and catch initial - // launch errors. - if (event.kind == 'IsolateRunnable') { - printTrace('Isolate is runnable.'); - completer.complete(null); - } - }); - await serviceProtocol.runInView(viewId, - entryPath, - packagesPath, - assetsDirectoryPath); - await completer.future; - await subscription.cancel(); - } - - // TODO(johnmccutchan): Remove this accessor and make the runner multi-view - // aware. - FlutterView get mainView { - return views.first; - } - - final Set<FlutterView> views = new Set<FlutterView>(); -}
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; }