
I got tired of drive-by spelling fixes, so I figured I'd just take care of them all at once. This only corrects errors in the dartdocs, not regular comments, and I skipped any sample code in the dartdocs. It doesn't touch any identifiers in the dartdocs either. No code changes, just comments.
1330 lines
44 KiB
Dart
1330 lines
44 KiB
Dart
// 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:convert' show BASE64;
|
|
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/file_system.dart';
|
|
import 'globals.dart';
|
|
import 'vmservice_record_replay.dart';
|
|
|
|
/// A function that opens a two-way communication channel to the specified [uri].
|
|
typedef StreamChannel<String> _OpenChannel(Uri uri);
|
|
|
|
_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 Future<Null> ReloadSources(
|
|
String isolateId, {
|
|
bool force,
|
|
bool pause,
|
|
});
|
|
|
|
const String _kRecordingType = 'vmservice';
|
|
|
|
StreamChannel<String> _defaultOpenChannel(Uri uri) =>
|
|
new IOWebSocketChannel.connect(uri.toString()).cast();
|
|
|
|
/// The default VM service request timeout.
|
|
const Duration kDefaultRequestTimeout = const Duration(seconds: 30);
|
|
|
|
/// Used for RPC requests that may take a long time.
|
|
const Duration kLongRequestTimeout = const Duration(minutes: 1);
|
|
|
|
/// Used for RPC requests that should never take a long time.
|
|
const Duration kShortRequestTimeout = const Duration(seconds: 5);
|
|
|
|
/// A connection to the Dart VM Service.
|
|
class VMService {
|
|
VMService._(
|
|
this._peer,
|
|
this.httpAddress,
|
|
this.wsAddress,
|
|
this._requestTimeout,
|
|
ReloadSources reloadSources,
|
|
) {
|
|
_vm = new 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 new rpc.RpcException.invalidParams('Invalid \'isolateId\': $isolateId');
|
|
if (force is! bool)
|
|
throw new rpc.RpcException.invalidParams('Invalid \'force\': $force');
|
|
if (pause is! bool)
|
|
throw new 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 new rpc.RpcException(rpc_error_code.SERVER_ERROR,
|
|
'Error during Sources Reload: $e\n$st');
|
|
}
|
|
});
|
|
|
|
// If the Flutter Engine doesn't support service registration this will
|
|
// have no effect
|
|
_peer.sendNotification('_registerService', <String, String>{
|
|
'service': 'reloadSources',
|
|
'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) {
|
|
final StreamChannel<String> delegate = _defaultOpenChannel(uri);
|
|
return new 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) => new ReplayVMServiceChannel(dir);
|
|
}
|
|
|
|
/// Connect to a Dart VM Service at [httpUri].
|
|
///
|
|
/// Requests made via the returned [VMService] time out after [requestTimeout]
|
|
/// amount of time, which is [kDefaultRequestTimeout] by default.
|
|
///
|
|
/// 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, {
|
|
Duration requestTimeout: kDefaultRequestTimeout,
|
|
ReloadSources reloadSources,
|
|
}) async {
|
|
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
|
|
final StreamChannel<String> channel = _openChannel(wsUri);
|
|
final rpc.Peer peer = new rpc.Peer.withoutJson(jsonDocument.bind(channel));
|
|
final VMService service = new VMService._(peer, httpUri, wsUri, requestTimeout, reloadSources);
|
|
// 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 Duration _requestTimeout;
|
|
final Completer<Map<String, dynamic>> _connectionError = new 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 = new Set<String>();
|
|
|
|
/// Whether our connection to the VM service has been closed;
|
|
bool get isClosed => _peer.isClosed;
|
|
Future<Null> get done => _peer.done;
|
|
|
|
// Events
|
|
Stream<ServiceEvent> get onDebugEvent => onEvent('Debug');
|
|
Stream<ServiceEvent> get onExtensionEvent => onEvent('Extension');
|
|
// IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded
|
|
Stream<ServiceEvent> get onIsolateEvent => onEvent('Isolate');
|
|
Stream<ServiceEvent> get onTimelineEvent => onEvent('Timeline');
|
|
// TODO(johnmccutchan): Add FlutterView events.
|
|
|
|
// Listen for a specific event name.
|
|
Stream<ServiceEvent> onEvent(String streamId) {
|
|
_streamListen(streamId);
|
|
return _getEventController(streamId).stream;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _sendRequest(
|
|
String method,
|
|
Map<String, dynamic> params,
|
|
) {
|
|
return Future.any(<Future<Map<String, dynamic>>>[
|
|
_peer.sendRequest(method, params),
|
|
_connectionError.future,
|
|
]);
|
|
}
|
|
|
|
StreamController<ServiceEvent> _getEventController(String eventName) {
|
|
StreamController<ServiceEvent> controller = _eventControllers[eventName];
|
|
if (controller == null) {
|
|
controller = new 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(data.toString());
|
|
|
|
ServiceEvent event;
|
|
if (eventIsolate != null) {
|
|
// getFromMap creates the Isolate if necessary.
|
|
final 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> _streamListen(String streamId) async {
|
|
if (!_listeningFor.contains(streamId)) {
|
|
_listeningFor.add(streamId);
|
|
await _sendRequest('streamListen', <String, dynamic>{ 'streamId': streamId });
|
|
}
|
|
}
|
|
|
|
/// Reloads the VM.
|
|
Future<VM> getVM() {
|
|
return _vm.reload();
|
|
}
|
|
|
|
Future<Null> waitForViews({int attempts = 5, int attemptSeconds = 1}) async {
|
|
await vm.refreshViews();
|
|
for (int i = 0; (vm.firstView == null) && (i < attempts); i++) {
|
|
// If the VM doesn't yet have a view, wait for one to show up.
|
|
printTrace('Waiting for Flutter view');
|
|
await new Future<Null>.delayed(new Duration(seconds: attemptSeconds));
|
|
await vm.refreshViews();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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, dynamic 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++) {
|
|
final dynamic 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 new VMServiceObjectLoadError('Expected a service map', map);
|
|
|
|
final 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;
|
|
}
|
|
// 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 empty service object, call update to populate it.
|
|
serviceObject.update(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.
|
|
final bool skipLoad = !isVM && (!hasId || (immutable && loaded));
|
|
if (skipLoad) {
|
|
return this;
|
|
}
|
|
|
|
if (_inProgressReload == null) {
|
|
final Completer<ServiceObject> completer = new 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(new ServiceObject._fromMap(owner, response));
|
|
} else {
|
|
update(response);
|
|
completer.complete(this);
|
|
}
|
|
} catch (e, st) {
|
|
completer.completeError(e, st);
|
|
}
|
|
_inProgressReload = null;
|
|
}
|
|
|
|
return await _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 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';
|
|
|
|
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 == 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() async {
|
|
return 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;
|
|
|
|
// TODO(johnmccutchan): Extract any properties we care about here.
|
|
_pid = map['pid'];
|
|
if (map['_heapAllocatedMemoryUsage'] != null) {
|
|
_heapAllocatedMemoryUsage = map['_heapAllocatedMemoryUsage'];
|
|
}
|
|
_maxRSS = map['_maxRSS'];
|
|
|
|
// Remove any isolates which are now dead from the isolate cache.
|
|
_removeDeadIsolates(map['isolates']);
|
|
}
|
|
|
|
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 {
|
|
return _heapAllocatedMemoryUsage == null ? 0 : _heapAllocatedMemoryUsage;
|
|
}
|
|
|
|
/// The peak resident set size for the process.
|
|
int _maxRSS;
|
|
int get maxRSS => _maxRSS == null ? 0 : _maxRSS;
|
|
|
|
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 = new Set<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.
|
|
update(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 = 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);
|
|
}
|
|
}
|
|
|
|
// 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 new Future<Isolate>.value(_isolateCache[isolateId]);
|
|
}
|
|
|
|
/// Invoke the RPC and return the raw response.
|
|
///
|
|
/// If `timeoutFatal` is false, then a timeout will result in a null return
|
|
/// value. Otherwise, it results in an exception.
|
|
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
|
|
Map<String, dynamic> params: const <String, dynamic>{},
|
|
Duration timeout,
|
|
bool timeoutFatal: true,
|
|
}) async {
|
|
printTrace('$method: $params');
|
|
|
|
assert(params != null);
|
|
timeout ??= _vmService._requestTimeout;
|
|
try {
|
|
final Map<String, dynamic> result = await _vmService
|
|
._sendRequest(method, params)
|
|
.timeout(timeout);
|
|
return result;
|
|
} on TimeoutException {
|
|
printTrace('Request to Dart VM Service timed out: $method($params)');
|
|
if (timeoutFatal)
|
|
throw new TimeoutException('Request to Dart VM Service timed out: $method($params)');
|
|
return null;
|
|
} 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}');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Invoke the RPC and return a [ServiceObject] response.
|
|
Future<ServiceObject> invokeRpc(String method, {
|
|
Map<String, dynamic> params: const <String, dynamic>{},
|
|
Duration timeout,
|
|
}) async {
|
|
final Map<String, dynamic> response = await invokeRpcRaw(
|
|
method,
|
|
params: params,
|
|
timeout: timeout,
|
|
);
|
|
final ServiceObject serviceObject = new 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) {
|
|
// TODO(goderbauer): Transfer Uri (instead of file path) when remote end supports it.
|
|
return invokeRpc('_flutter.runInView',
|
|
params: <String, dynamic> {
|
|
'viewId': viewId,
|
|
'mainScript': main.toFilePath(windows: false),
|
|
'packagesFile': packages.toFilePath(windows: false),
|
|
'assetDirectory': assetsDirectory.toFilePath(windows: false)
|
|
});
|
|
}
|
|
|
|
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', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
Future<Null> refreshViews() async {
|
|
_viewCache.clear();
|
|
await vmService.vm.invokeRpc('_flutter.listViews', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
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.MICROSECONDS_PER_SECOND /
|
|
math.max(_collections, 1);
|
|
return new Duration(microseconds: mcs.ceil());
|
|
}
|
|
|
|
Duration get avgCollectionPeriod {
|
|
final double mcs = _averageCollectionPeriodInMillis *
|
|
Duration.MICROSECONDS_PER_MILLISECOND;
|
|
return new 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'];
|
|
}
|
|
}
|
|
|
|
/// A function, field or class along with its source location.
|
|
class ProgramElement {
|
|
ProgramElement(this.qualifiedName, this.uri, [this.line, this.column]);
|
|
|
|
final String qualifiedName;
|
|
final Uri uri;
|
|
final int line;
|
|
final int column;
|
|
|
|
@override
|
|
String toString() {
|
|
if (line == null)
|
|
return '$qualifiedName ($uri)';
|
|
else
|
|
return '$qualifiedName ($uri:$line)';
|
|
}
|
|
}
|
|
|
|
/// 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.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');
|
|
}
|
|
|
|
/// Invoke the RPC and return the raw response.
|
|
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
|
|
Map<String, dynamic> params,
|
|
Duration timeout,
|
|
bool timeoutFatal: true,
|
|
}) {
|
|
// Inject the 'isolateId' parameter.
|
|
if (params == null) {
|
|
params = <String, dynamic>{
|
|
'isolateId': id
|
|
};
|
|
} else {
|
|
params['isolateId'] = id;
|
|
}
|
|
return vm.invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
|
|
}
|
|
|
|
/// 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 ??= new HeapSpace._empty(this);
|
|
_newSpace._update(map['new'], mapIsRef);
|
|
_oldSpace ??= new 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 = new DateTime.fromMillisecondsSinceEpoch(startTimeMillis);
|
|
|
|
_upgradeCollection(map, this);
|
|
|
|
pauseEvent = map['pauseEvent'];
|
|
|
|
_updateHeaps(map['_heaps'], mapIsRef);
|
|
}
|
|
|
|
static final 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
|
|
};
|
|
// TODO(goderbauer): Transfer Uri (instead of file path) when remote end supports it.
|
|
// Note: Despite the name, `rootLibUri` and `packagesUri` expect file paths.
|
|
if (rootLibUri != null) {
|
|
arguments['rootLibUri'] = rootLibUri.toFilePath(windows: false);
|
|
}
|
|
if (packagesUri != null) {
|
|
arguments['packagesUri'] = packagesUri.toFilePath(windows: false);
|
|
}
|
|
final Map<String, dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments);
|
|
return response;
|
|
} on rpc.RpcException catch(e) {
|
|
return new Future<Map<String, dynamic>>.error(<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']});
|
|
}
|
|
|
|
Future<ProgramElement> _describeElement(Map<String, dynamic> elementRef) async {
|
|
String name = elementRef['name'];
|
|
Map<String, dynamic> owner = elementRef['owner'];
|
|
while (owner != null) {
|
|
final String ownerType = owner['type'];
|
|
if (ownerType == 'Library' || ownerType == '@Library')
|
|
break;
|
|
final String ownerName = owner['name'];
|
|
name = '$ownerName.$name';
|
|
owner = owner['owner'];
|
|
}
|
|
|
|
final Map<String, dynamic> fullElement = await getObject(elementRef);
|
|
final Map<String, dynamic> location = fullElement['location'];
|
|
final int tokenPos = location['tokenPos'];
|
|
final Map<String, dynamic> script = await getObject(location['script']);
|
|
|
|
// The engine's tag handler doesn't seem to create proper URIs.
|
|
Uri uri = Uri.parse(script['uri']);
|
|
if (uri.scheme == '')
|
|
uri = uri.replace(scheme: 'file');
|
|
|
|
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
|
|
for (List<int> lineTuple in script['tokenPosTable']) {
|
|
final int line = lineTuple[0];
|
|
for (int i = 1; i < lineTuple.length; i += 2) {
|
|
if (lineTuple[i] == tokenPos) {
|
|
final int column = lineTuple[i + 1];
|
|
return new ProgramElement(name, uri, line, column);
|
|
}
|
|
}
|
|
}
|
|
return new ProgramElement(name, uri);
|
|
}
|
|
|
|
// Lists program elements changed in the most recent reload that have not
|
|
// since executed.
|
|
Future<List<ProgramElement>> getUnusedChangesInLastReload() async {
|
|
final Map<String, dynamic> response =
|
|
await invokeRpcRaw('_getUnusedChangesInLastReload');
|
|
final List<Future<ProgramElement>> unusedElements =
|
|
<Future<ProgramElement>>[];
|
|
for (Map<String, dynamic> element in response['unused'])
|
|
unusedElements.add(_describeElement(element));
|
|
return Future.wait(unusedElements);
|
|
}
|
|
|
|
/// 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,
|
|
Duration timeout,
|
|
bool timeoutFatal: true,
|
|
}
|
|
) async {
|
|
try {
|
|
return await invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
// Debug dump extension methods.
|
|
|
|
Future<Map<String, dynamic>> flutterDebugDumpApp() {
|
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> flutterDebugDumpRenderTree() {
|
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> flutterDebugDumpLayerTree() {
|
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() {
|
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() {
|
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', timeout: kLongRequestTimeout);
|
|
}
|
|
|
|
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' },
|
|
timeout: const Duration(milliseconds: 150),
|
|
timeoutFatal: false,
|
|
);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled() => _flutterToggle('debugPaint');
|
|
|
|
Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay');
|
|
|
|
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('debugWidgetInspector');
|
|
|
|
Future<Null> flutterDebugAllowBanner(bool show) async {
|
|
await invokeFlutterExtensionRpcRaw(
|
|
'ext.flutter.debugAllowBanner',
|
|
params: <String, dynamic>{ 'enabled': show ? 'true' : 'false' },
|
|
timeout: const Duration(milliseconds: 150),
|
|
timeoutFatal: false,
|
|
);
|
|
}
|
|
|
|
// Reload related extension methods.
|
|
Future<Map<String, dynamic>> flutterReassemble() async {
|
|
return await invokeFlutterExtensionRpcRaw(
|
|
'ext.flutter.reassemble',
|
|
timeout: kShortRequestTimeout,
|
|
timeoutFatal: true,
|
|
);
|
|
}
|
|
|
|
Future<bool> flutterFrameworkPresent() async {
|
|
return (await invokeFlutterExtensionRpcRaw('ext.flutter.frameworkPresent') != null);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> uiWindowScheduleFrame() async {
|
|
return await invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame');
|
|
}
|
|
|
|
Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) async {
|
|
return await invokeFlutterExtensionRpcRaw('ext.flutter.evict',
|
|
params: <String, dynamic>{
|
|
'value': assetPath,
|
|
}
|
|
);
|
|
}
|
|
|
|
// Application control extension methods.
|
|
Future<Map<String, dynamic>> flutterExit() async {
|
|
return await invokeFlutterExtensionRpcRaw(
|
|
'ext.flutter.exit',
|
|
timeout: const Duration(seconds: 2),
|
|
timeoutFatal: false,
|
|
);
|
|
}
|
|
|
|
Future<String> flutterPlatformOverride([String platform]) async {
|
|
final Map<String, String> result = await invokeFlutterExtensionRpcRaw(
|
|
'ext.flutter.platformOverride',
|
|
params: platform != null ? <String, dynamic>{ 'value': platform } : <String, String>{},
|
|
timeout: const Duration(seconds: 5),
|
|
timeoutFatal: false,
|
|
);
|
|
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();
|
|
}
|
|
|
|
/// 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(Uri entryUri,
|
|
Uri packagesUri,
|
|
Uri assetsDirectoryUri) 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.');
|
|
if (!completer.isCompleted)
|
|
completer.complete(null);
|
|
}
|
|
});
|
|
await owner.vm.runInView(viewId,
|
|
entryUri,
|
|
packagesUri,
|
|
assetsDirectoryUri);
|
|
await completer.future;
|
|
await owner.vm.refreshViews();
|
|
await subscription.cancel();
|
|
}
|
|
|
|
bool get hasIsolate => _uiIsolate != null;
|
|
|
|
Future<Null> flushUIThreadTasks() async {
|
|
await owner.vm.invokeRpcRaw('_flutter.flushUIThreadTasks');
|
|
}
|
|
|
|
@override
|
|
String toString() => id;
|
|
}
|