blob: 9a9b907ba4605274bb8de504d7c2b2c63ca606de [file] [log] [blame]
// Copyright 2018 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 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart';
import 'package:vm_service/vm_service.dart';
import 'connected_app.dart';
import 'eval_on_dart_library.dart';
import 'service_extensions.dart' as extensions;
import 'service_registrations.dart' as registrations;
import 'vm_service_wrapper.dart';
// TODO(kenzie): add an offline service manager implementation.
class ServiceConnectionManager {
ServiceConnectionManager() {
final isolateManager = IsolateManager();
final serviceExtensionManager = ServiceExtensionManager();
isolateManager._serviceExtensionManager = serviceExtensionManager;
serviceExtensionManager._isolateManager = isolateManager;
_isolateManager = isolateManager;
_serviceExtensionManager = serviceExtensionManager;
}
final StreamController<VmServiceWrapper> _connectionAvailableController =
StreamController<VmServiceWrapper>.broadcast();
final serviceAvailable = Completer<void>();
VmServiceCapabilities _serviceCapabilities;
Future<VmServiceCapabilities> get serviceCapabilities async {
if (_serviceCapabilities == null) {
await serviceAvailable.future;
final version = await service.getVersion();
_serviceCapabilities = new VmServiceCapabilities(version);
}
return _serviceCapabilities;
}
final Map<String, StreamController<bool>> _serviceRegistrationController =
<String, StreamController<bool>>{};
final Map<String, List<String>> _registeredMethodsForService = {};
Map<String, List<String>> get registeredMethodsForService =>
_registeredMethodsForService;
IsolateManager _isolateManager;
ServiceExtensionManager _serviceExtensionManager;
IsolateManager get isolateManager => _isolateManager;
ServiceExtensionManager get serviceExtensionManager =>
_serviceExtensionManager;
ConnectedApp connectedApp;
VmServiceWrapper service;
VM vm;
String sdkVersion;
bool get hasConnection => service != null;
Stream<void> get onStateChange => _stateController.stream;
final _stateController = StreamController<void>.broadcast();
Stream<VmServiceWrapper> get onConnectionAvailable =>
_connectionAvailableController.stream;
Stream<void> get onConnectionClosed => _connectionClosedController.stream;
final _connectionClosedController = StreamController<void>.broadcast();
/// Call a service that is registered by exactly one client.
Future<Response> callService(
String name, {
String isolateId,
Map args,
}) async {
final registered = _registeredMethodsForService[name] ?? const [];
if (registered.isEmpty) {
throw Exception('There are no registered methods for service "$name"');
}
return service.callMethod(
registered.first,
isolateId: isolateId,
args: args,
);
}
StreamSubscription<bool> hasRegisteredService(
String name,
void onData(bool value),
) {
if (_registeredMethodsForService.containsKey(name) && onData != null) {
onData(true);
}
final StreamController<bool> streamController =
_getServiceRegistrationController(name);
return streamController.stream.listen(onData);
}
StreamController<bool> _getServiceRegistrationController(String name) {
return _getStreamController(
name,
_serviceRegistrationController,
onFirstListenerSubscribed: () {
_serviceRegistrationController[name]
.add(_registeredMethodsForService.containsKey(name));
},
);
}
Future<void> vmServiceOpened(
VmServiceWrapper service, {
@required Future<void> onClosed,
}) async {
final serviceStreamName = await service.serviceStreamName;
final vm = await service.getVM();
this.vm = vm;
sdkVersion = vm.version;
if (sdkVersion.contains(' ')) {
sdkVersion = sdkVersion.substring(0, sdkVersion.indexOf(' '));
}
this.service = service;
serviceAvailable.complete();
connectedApp = ConnectedApp();
void handleServiceEvent(Event e) {
if (e.kind == EventKind.kServiceRegistered) {
if (!_registeredMethodsForService.containsKey(e.service)) {
_registeredMethodsForService[e.service] = [e.method];
final StreamController<bool> streamController =
_getServiceRegistrationController(e.service);
streamController.add(true);
} else {
_registeredMethodsForService[e.service].add(e.method);
}
}
}
service.onEvent(serviceStreamName).listen(handleServiceEvent);
_isolateManager._service = service;
_serviceExtensionManager._service = service;
_stateController.add(null);
_connectionAvailableController.add(service);
await _isolateManager._initIsolates(vm.isolates);
service.onIsolateEvent.listen(_isolateManager._handleIsolateEvent);
service.onExtensionEvent
.listen(_serviceExtensionManager._handleExtensionEvent);
unawaited(onClosed.then((_) => vmServiceClosed()));
final streamIds = [
EventStreams.kStdout,
EventStreams.kStderr,
EventStreams.kVM,
EventStreams.kIsolate,
EventStreams.kDebug,
EventStreams.kGC,
EventStreams.kTimeline,
EventStreams.kExtension,
serviceStreamName
];
// The following streams are not yet supported by Flutter Web / Dart web
// apps.
if (!await connectedApp.isDartWebApp) {
streamIds.addAll(['_Graph', '_Logging', EventStreams.kLogging]);
}
await Future.wait(streamIds.map((String id) async {
try {
await service.streamListen(id);
} catch (e) {
// TODO(devoncarew): Remove this check on or after approx. Oct 1 2019.
if (id.endsWith('Logging')) {
// Don't complain about '_Logging' or 'Logging' events (new VMs don't
// have the private names, and older ones don't have the public ones).
} else {
print("Service client stream not supported: '$id'\n $e");
}
}
}));
}
void vmServiceClosed() {
service = null;
vm = null;
sdkVersion = null;
connectedApp = null;
_stateController.add(null);
_connectionClosedController.add(null);
}
/// This can throw an [RPCError].
Future<void> performHotReload() async {
await callService(
registrations.hotReload.service,
isolateId: _isolateManager.selectedIsolate.id,
);
}
/// This can throw an [RPCError].
Future<void> performHotRestart() async {
await callService(
registrations.hotRestart.service,
isolateId: _isolateManager.selectedIsolate.id,
);
}
}
class IsolateManager {
List<IsolateRef> _isolates = <IsolateRef>[];
IsolateRef _selectedIsolate;
VmServiceWrapper _service;
ServiceExtensionManager _serviceExtensionManager;
final StreamController<IsolateRef> _isolateCreatedController =
StreamController<IsolateRef>.broadcast();
final StreamController<IsolateRef> _isolateExitedController =
StreamController<IsolateRef>.broadcast();
final StreamController<IsolateRef> _selectedIsolateController =
StreamController<IsolateRef>.broadcast();
var selectedIsolateAvailable = Completer<void>();
List<LibraryRef> selectedIsolateLibraries;
List<IsolateRef> get isolates => List<IsolateRef>.unmodifiable(_isolates);
IsolateRef get selectedIsolate => _selectedIsolate;
Stream<IsolateRef> get onIsolateCreated => _isolateCreatedController.stream;
Stream<IsolateRef> get onSelectedIsolateChanged =>
_selectedIsolateController.stream;
Stream<IsolateRef> get onIsolateExited => _isolateExitedController.stream;
void selectIsolate(String isolateRefId) {
final IsolateRef ref = _isolates.firstWhere(
(IsolateRef ref) => ref.id == isolateRefId,
orElse: () => null);
_setSelectedIsolate(ref);
}
Future<void> _initIsolates(List<IsolateRef> isolates) async {
_isolates = isolates;
await _initSelectedIsolate(isolates);
if (_selectedIsolate != null) {
_isolateCreatedController.add(_selectedIsolate);
_selectedIsolateController.add(_selectedIsolate);
// On initial connection to running app, service extensions are added from
// here.
await _serviceExtensionManager
._addRegisteredExtensionRPCs(_selectedIsolate);
}
}
Future<void> _handleIsolateEvent(Event event) async {
if (event.kind == 'IsolateStart') {
_isolates.add(event.isolate);
_isolateCreatedController.add(event.isolate);
if (_selectedIsolate == null) {
await _setSelectedIsolate(event.isolate);
}
} else if (event.kind == 'ServiceExtensionAdded') {
// On hot restart, service extensions are added from here.
await _serviceExtensionManager
._maybeAddServiceExtension(event.extensionRPC);
// Check to see if there is a new isolate.
if (_selectedIsolate == null && _isFlutterExtension(event.extensionRPC)) {
await _setSelectedIsolate(event.isolate);
}
} else if (event.kind == 'IsolateExit') {
_isolates.remove(event.isolate);
_isolateExitedController.add(event.isolate);
if (_selectedIsolate == event.isolate) {
_selectedIsolate = _isolates.isEmpty ? null : _isolates.first;
if (_selectedIsolate == null) {
selectedIsolateAvailable = Completer();
}
_selectedIsolateController.add(_selectedIsolate);
_serviceExtensionManager.resetAvailableExtensions();
}
}
}
bool _isFlutterExtension(String extensionName) {
return extensionName.startsWith('ext.flutter.');
}
Future<void> _initSelectedIsolate(List<IsolateRef> isolates) async {
if (isolates.isEmpty) {
return;
}
for (IsolateRef ref in isolates) {
if (_selectedIsolate == null) {
final Isolate isolate = await _service.getIsolate(ref.id);
if (isolate.extensionRPCs != null) {
for (String extensionName in isolate.extensionRPCs) {
if (_isFlutterExtension(extensionName)) {
await _setSelectedIsolate(ref);
return;
}
}
}
}
}
final IsolateRef ref = isolates.firstWhere((IsolateRef ref) {
// 'foo.dart:main()'
return ref.name.contains(':main(');
}, orElse: () => null);
await _setSelectedIsolate(ref ?? isolates.first);
}
Future<void> _setSelectedIsolate(IsolateRef ref) async {
if (_selectedIsolate == ref) {
return;
}
// Store the library uris for the selected isolate.
final Isolate isolate = await _service.getIsolate(ref.id);
selectedIsolateLibraries = isolate.libraries;
_selectedIsolate = ref;
if (!selectedIsolateAvailable.isCompleted) {
selectedIsolateAvailable.complete();
}
_selectedIsolateController.add(ref);
}
StreamSubscription<IsolateRef> getSelectedIsolate(
void onData(IsolateRef ref)) {
if (_selectedIsolate != null) {
onData(_selectedIsolate);
}
return _selectedIsolateController.stream.listen(onData);
}
}
class ServiceExtensionManager {
VmServiceWrapper _service;
IsolateManager _isolateManager;
bool _firstFrameEventReceived = false;
final Map<String, StreamController<bool>> _serviceExtensionController =
<String, StreamController<bool>>{};
final Map<String, StreamController<ServiceExtensionState>>
_serviceExtensionStateController =
<String, StreamController<ServiceExtensionState>>{};
/// All available service extensions.
// ignore: prefer_collection_literals
final Set<String> _serviceExtensions = Set<String>();
/// All service extensions that are currently enabled.
final Map<String, ServiceExtensionState> _enabledServiceExtensions =
<String, ServiceExtensionState>{};
/// Temporarily stores service extensions that we need to add. We should not
/// add extensions until the first frame event has been received
/// [_firstFrameEventReceived].
// ignore: prefer_collection_literals
final Set<String> _pendingServiceExtensions = Set<String>();
var extensionStatesUpdated = Completer<void>();
Future<void> _handleExtensionEvent(Event event) async {
switch (event.extensionKind) {
case 'Flutter.FirstFrame':
case 'Flutter.Frame':
await _onFrameEventReceived();
break;
case 'Flutter.ServiceExtensionStateChanged':
final String name = event.json['extensionData']['extension'].toString();
final String valueFromJson =
event.json['extensionData']['value'].toString();
final extension = extensions.serviceExtensionsWhitelist[name];
if (extension != null) {
final dynamic value = _getExtensionValueFromJson(name, valueFromJson);
final enabled =
extension is extensions.ToggleableServiceExtensionDescription
? value == extension.enabledValue
// For extensions that have more than two states
// (enabled / disabled), we will always consider them to be
// enabled with the current value.
: true;
await setServiceExtensionState(
name,
enabled,
value,
callExtension: false,
);
}
}
}
dynamic _getExtensionValueFromJson(String name, String valueFromJson) {
final expectedValueType =
extensions.serviceExtensionsWhitelist[name].values.first.runtimeType;
switch (expectedValueType) {
case bool:
return valueFromJson == 'true' ? true : false;
case int:
case double:
return num.parse(valueFromJson);
default:
return valueFromJson;
}
}
Future<void> _onFrameEventReceived() async {
if (_firstFrameEventReceived) {
// The first frame event was already received.
return;
}
_firstFrameEventReceived = true;
for (String extension in _pendingServiceExtensions) {
await _addServiceExtension(extension);
}
extensionStatesUpdated.complete();
_pendingServiceExtensions.clear();
}
Future<void> _addRegisteredExtensionRPCs(IsolateRef isolateRef) async {
if (_service == null) {
return;
}
final Isolate isolate = await _service.getIsolate(isolateRef.id);
if (isolate.extensionRPCs != null) {
for (String extension in isolate.extensionRPCs) {
await _maybeAddServiceExtension(extension);
}
if (_pendingServiceExtensions.isEmpty) {
extensionStatesUpdated.complete();
}
if (!_firstFrameEventReceived) {
bool didSendFirstFrameEvent = false;
if (isServiceExtensionAvailable(extensions.didSendFirstFrameEvent)) {
final value = await _service.callServiceExtension(
extensions.didSendFirstFrameEvent,
isolateId: _isolateManager.selectedIsolate.id,
);
didSendFirstFrameEvent =
value != null && value.json['enabled'] == 'true';
} else {
final EvalOnDartLibrary flutterLibrary = EvalOnDartLibrary(
[
'package:flutter/src/widgets/binding.dart',
'package:flutter_web/src/widgets/binding.dart',
],
_service,
);
final InstanceRef value = await flutterLibrary.eval(
'WidgetsBinding.instance.debugDidSendFirstFrameEvent',
isAlive: null,
);
didSendFirstFrameEvent =
value != null && value.valueAsString == 'true';
}
if (didSendFirstFrameEvent) {
await _onFrameEventReceived();
}
}
}
}
Future<void> _maybeAddServiceExtension(String name) async {
if (_firstFrameEventReceived) {
assert(_pendingServiceExtensions.isEmpty);
await _addServiceExtension(name);
} else {
_pendingServiceExtensions.add(name);
}
}
Future<void> _addServiceExtension(String name) async {
final StreamController<bool> streamController =
_getServiceExtensionController(name);
_serviceExtensions.add(name);
streamController.add(true);
if (_enabledServiceExtensions.containsKey(name)) {
// Restore any previously enabled states by calling their service
// extension. This will restore extension states on the device after a hot
// restart. [_enabledServiceExtensions] will be empty on page refresh or
// initial start.
await _callServiceExtension(name, _enabledServiceExtensions[name].value);
} else {
// Set any extensions that are already enabled on the device. This will
// enable extension states in DevTools on page refresh or initial start.
await _restoreExtensionFromDevice(name);
}
}
Future<void> _restoreExtensionFromDevice(String name) async {
if (!extensions.serviceExtensionsWhitelist.containsKey(name)) {
return;
}
final expectedValueType =
extensions.serviceExtensionsWhitelist[name].values.first.runtimeType;
try {
final response = await _service.callServiceExtension(
name,
isolateId: _isolateManager.selectedIsolate.id,
);
switch (expectedValueType) {
case bool:
final bool enabled =
response.json['enabled'] == 'true' ? true : false;
await _maybeRestoreExtension(name, enabled);
return;
case String:
final String value = response.json['value'];
await _maybeRestoreExtension(name, value);
return;
case int:
case double:
final num value = num.parse(
response.json[name.substring(name.lastIndexOf('.') + 1)]);
await _maybeRestoreExtension(name, value);
return;
default:
return;
}
} catch (e) {
// Do not report an error if the VMService has gone away or the
// selectedIsolate has been closed probably due to a hot restart.
// There is no need
// TODO(jacobr): validate that the exception is one of a short list
// of allowed network related exceptions rather than ignoring all
// exceptions.
}
}
Future<void> _maybeRestoreExtension(String name, dynamic value) async {
final extensionDescription = extensions.serviceExtensionsWhitelist[name];
if (extensionDescription
is extensions.ToggleableServiceExtensionDescription) {
if (value == extensionDescription.enabledValue) {
await setServiceExtensionState(name, true, value, callExtension: false);
}
} else {
await setServiceExtensionState(name, true, value, callExtension: false);
}
}
Future<void> _callServiceExtension(String name, dynamic value) async {
if (_service == null) {
return;
}
assert(value != null);
if (value is bool) {
await _service.callServiceExtension(
name,
isolateId: _isolateManager.selectedIsolate.id,
args: {'enabled': value},
);
} else if (value is String) {
await _service.callServiceExtension(
name,
isolateId: _isolateManager.selectedIsolate.id,
args: {'value': value},
);
} else if (value is double) {
await _service.callServiceExtension(
name,
isolateId: _isolateManager.selectedIsolate.id,
// The param name for a numeric service extension will be the last part
// of the extension name (ext.flutter.extensionName => extensionName).
args: {name.substring(name.lastIndexOf('.') + 1): value},
);
}
}
void resetAvailableExtensions() {
extensionStatesUpdated = Completer();
_firstFrameEventReceived = false;
_pendingServiceExtensions.clear();
_serviceExtensions.clear();
_serviceExtensionController
.forEach((String name, StreamController<bool> stream) {
stream.add(false);
});
}
/// Sets the state for a service extension and makes the call to the VMService.
Future<void> setServiceExtensionState(
String name,
bool enabled,
dynamic value, {
bool callExtension = true,
}) async {
if (callExtension) {
await _callServiceExtension(name, value);
}
final StreamController<ServiceExtensionState> streamController =
_getServiceExtensionStateController(name);
streamController.add(ServiceExtensionState(enabled, value));
// Add or remove service extension from [enabledServiceExtensions].
if (enabled) {
_enabledServiceExtensions[name] = ServiceExtensionState(enabled, value);
} else {
_enabledServiceExtensions.remove(name);
}
}
bool isServiceExtensionAvailable(String name) {
return _serviceExtensions.contains(name) ||
_pendingServiceExtensions.contains(name);
}
StreamSubscription<bool> hasServiceExtension(
String name,
void onData(bool value),
) {
if (_serviceExtensions.contains(name) && onData != null) {
onData(true);
}
final StreamController<bool> streamController =
_getServiceExtensionController(name);
return streamController.stream.listen(onData);
}
StreamSubscription<ServiceExtensionState> getServiceExtensionState(
String name,
void onData(ServiceExtensionState state),
) {
if (_enabledServiceExtensions.containsKey(name) && onData != null) {
onData(_enabledServiceExtensions[name]);
}
final StreamController<ServiceExtensionState> streamController =
_getServiceExtensionStateController(name);
return streamController.stream.listen(onData);
}
StreamController<bool> _getServiceExtensionController(String name) {
return _getStreamController(
name,
_serviceExtensionController,
onFirstListenerSubscribed: () {
// If the service extension is in [_serviceExtensions], then we have been
// waiting for a listener to add the initial true event. Otherwise, the
// service extension is not available, so we should add a false event.
_serviceExtensionController[name]
.add(_serviceExtensions.contains(name));
},
);
}
StreamController<ServiceExtensionState> _getServiceExtensionStateController(
String name) {
return _getStreamController(
name,
_serviceExtensionStateController,
onFirstListenerSubscribed: () {
// If the service extension is enabled, add the current state as the first
// event. Otherwise, add a disabled state as the first event.
if (_enabledServiceExtensions.containsKey(name)) {
assert(_enabledServiceExtensions[name].enabled);
_serviceExtensionStateController[name]
.add(_enabledServiceExtensions[name]);
} else {
_serviceExtensionStateController[name]
.add(ServiceExtensionState(false, null));
}
},
);
}
}
/// Given a map of Strings to StreamControllers [streamControllers], get the
/// stream controller for the given name. If it does not exist, initialize a
/// generic stream controller and map it to the name.
StreamController<T> _getStreamController<T>(
String name, Map<String, StreamController<T>> streamControllers,
{@required void onFirstListenerSubscribed()}) {
streamControllers.putIfAbsent(
name,
() => StreamController<T>.broadcast(onListen: onFirstListenerSubscribed),
);
return streamControllers[name];
}
class ServiceExtensionState {
ServiceExtensionState(this.enabled, this.value) {
if (value is bool) {
assert(enabled == value);
}
}
// For boolean service extensions, [enabled] should equal [value].
final bool enabled;
final dynamic value;
}
class VmServiceCapabilities {
VmServiceCapabilities(this.version);
final Version version;
bool get supportsGetScripts =>
version.major > 3 || (version.major == 3 && version.minor >= 12);
}