| // 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 'dart:core'; |
| import 'dart:ui'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:pedantic/pedantic.dart'; |
| import 'package:vm_service/vm_service.dart' hide Error; |
| |
| import 'analytics/analytics_stub.dart' |
| if (dart.library.html) 'analytics/analytics.dart' as ga; |
| import 'auto_dispose.dart'; |
| import 'config_specific/logger/logger.dart'; |
| import 'connected_app.dart'; |
| import 'core/message_bus.dart'; |
| import 'error_badge_manager.dart'; |
| import 'globals.dart'; |
| import 'logging/vm_service_logger.dart'; |
| import 'service_extensions.dart' as extensions; |
| import 'service_extensions.dart'; |
| import 'service_registrations.dart' as registrations; |
| import 'title.dart'; |
| import 'utils.dart'; |
| import 'version.dart'; |
| import 'vm_service_wrapper.dart'; |
| |
| // Note: don't check this in enabled. |
| /// Used to debug service protocol traffic. All requests to to the VM service |
| /// connection are logged to the Logging page, as well as all responses and |
| /// events from the service protocol device. |
| const debugLogServiceProtocolEvents = false; |
| |
| // TODO(kenz): add an offline service manager implementation. |
| |
| const defaultRefreshRate = 60.0; |
| |
| // TODO(jacobr): refactor all of these apis to be in terms of ValueListenable |
| // instead of Streams. |
| class ServiceConnectionManager { |
| ServiceConnectionManager() { |
| _isolateManager = IsolateManager(); |
| _serviceExtensionManager = |
| ServiceExtensionManager(_isolateManager.mainIsolate); |
| } |
| |
| final StreamController<VmServiceWrapper> _connectionAvailableController = |
| StreamController<VmServiceWrapper>.broadcast(); |
| |
| Completer<VmService> _serviceAvailable = Completer(); |
| |
| Future<VmService> get onServiceAvailable => _serviceAvailable.future; |
| |
| bool get isServiceAvailable => _serviceAvailable.isCompleted; |
| |
| VmServiceCapabilities _serviceCapabilities; |
| VmServiceTrafficLogger serviceTrafficLogger; |
| |
| Future<VmServiceCapabilities> get serviceCapabilities async { |
| if (_serviceCapabilities == null) { |
| await _serviceAvailable.future; |
| final version = await service.getVersion(); |
| _serviceCapabilities = VmServiceCapabilities(version); |
| } |
| return _serviceCapabilities; |
| } |
| |
| final _registeredServiceNotifiers = <String, ImmediateValueNotifier<bool>>{}; |
| |
| Map<String, List<String>> get registeredMethodsForService => |
| _registeredMethodsForService; |
| final Map<String, List<String>> _registeredMethodsForService = {}; |
| |
| VmFlagManager get vmFlagManager => _vmFlagManager; |
| final _vmFlagManager = VmFlagManager(); |
| |
| IsolateManager get isolateManager => _isolateManager; |
| IsolateManager _isolateManager; |
| |
| ErrorBadgeManager get errorBadgeManager => _errorBadgeManager; |
| final _errorBadgeManager = ErrorBadgeManager(); |
| |
| ServiceExtensionManager get serviceExtensionManager => |
| _serviceExtensionManager; |
| ServiceExtensionManager _serviceExtensionManager; |
| |
| ConnectedApp connectedApp; |
| |
| VmServiceWrapper service; |
| VM vm; |
| String sdkVersion; |
| |
| bool get hasConnection => |
| service != null && connectedApp != null && connectedApp.appTypeKnown; |
| |
| ValueListenable<ConnectedState> get connectedState => _connectedState; |
| |
| final ValueNotifier<ConnectedState> _connectedState = |
| ValueNotifier(const ConnectedState(false)); |
| |
| Stream<VmServiceWrapper> get onConnectionAvailable => |
| _connectionAvailableController.stream; |
| |
| Stream<void> get onConnectionClosed => _connectionClosedController.stream; |
| final _connectionClosedController = StreamController<void>.broadcast(); |
| |
| final ValueNotifier<bool> _deviceBusy = ValueNotifier<bool>(false); |
| |
| /// Whether the device is currently busy - performing a long-lived, blocking |
| /// operation. |
| ValueListenable<bool> get deviceBusy => _deviceBusy; |
| |
| /// Set whether the device is currently busy - performing a long-lived, |
| /// blocking operation. |
| void setDeviceBusy(bool isBusy) { |
| _deviceBusy.value = isBusy; |
| } |
| |
| /// Set the device as busy during the duration of the given async task. |
| Future<T> runDeviceBusyTask<T>(Future<T> task) async { |
| try { |
| setDeviceBusy(true); |
| return await task; |
| } finally { |
| setDeviceBusy(false); |
| } |
| } |
| |
| /// 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, |
| ); |
| } |
| |
| ValueListenable<bool> registeredServiceListenable(String name) { |
| final listenable = _registeredServiceNotifiers.putIfAbsent( |
| name, |
| () => ImmediateValueNotifier(false), |
| ); |
| return listenable; |
| } |
| |
| Future<void> vmServiceOpened( |
| VmServiceWrapper service, { |
| @required Future<void> onClosed, |
| }) async { |
| this.service = service; |
| await service.initServiceVersions(); |
| |
| connectedApp = ConnectedApp(); |
| // It is critical we call vmServiceOpened on each manager class before |
| // performing any async operations. Otherwise, we may get end up with |
| // race conditions where managers cannot listen for events soon enough. |
| isolateManager.vmServiceOpened(service); |
| serviceExtensionManager.vmServiceOpened(service, connectedApp); |
| await vmFlagManager.vmServiceOpened(service); |
| // This needs to be called last in the above group of `vmServiceOpened` |
| // calls. |
| errorBadgeManager.vmServiceOpened(service); |
| |
| if (debugLogServiceProtocolEvents) { |
| serviceTrafficLogger = VmServiceTrafficLogger(service); |
| } |
| |
| final serviceStreamName = await service.serviceStreamName; |
| |
| vm = await service.getVM(); |
| |
| sdkVersion = vm.version; |
| if (sdkVersion.contains(' ')) { |
| sdkVersion = sdkVersion.substring(0, sdkVersion.indexOf(' ')); |
| } |
| |
| _serviceAvailable.complete(service); |
| |
| setDeviceBusy(false); |
| |
| unawaited(onClosed.then((_) => vmServiceClosed())); |
| |
| void handleServiceEvent(Event e) { |
| if (e.kind == EventKind.kServiceRegistered) { |
| final serviceName = e.service; |
| _registeredMethodsForService |
| .putIfAbsent(serviceName, () => []) |
| .add(e.method); |
| final serviceNotifier = _registeredServiceNotifiers.putIfAbsent( |
| serviceName, |
| () => ImmediateValueNotifier(true), |
| ); |
| serviceNotifier.value = true; |
| } |
| |
| if (e.kind == EventKind.kServiceUnregistered) { |
| final serviceName = e.service; |
| _registeredMethodsForService.remove(serviceName); |
| final serviceNotifier = _registeredServiceNotifiers.putIfAbsent( |
| serviceName, |
| () => ImmediateValueNotifier(false), |
| ); |
| serviceNotifier.value = false; |
| } |
| } |
| |
| service.onEvent(serviceStreamName).listen(handleServiceEvent); |
| |
| _connectedState.value = const ConnectedState(true); |
| |
| final isolates = [ |
| ...vm.isolates, |
| if (preferences.vmDeveloperModeEnabled.value) ...vm.systemIsolates, |
| ]; |
| |
| await _isolateManager.init(isolates); |
| |
| final streamIds = [ |
| EventStreams.kDebug, |
| EventStreams.kExtension, |
| EventStreams.kGC, |
| EventStreams.kIsolate, |
| EventStreams.kLogging, |
| EventStreams.kStderr, |
| EventStreams.kStdout, |
| EventStreams.kTimeline, |
| EventStreams.kVM, |
| serviceStreamName, |
| ]; |
| |
| await Future.wait(streamIds.map((String id) async { |
| try { |
| await service.streamListen(id); |
| } catch (e) { |
| 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 { |
| log( |
| "Service client stream not supported: '$id'\n $e", |
| LogLevel.error, |
| ); |
| } |
| } |
| })); |
| |
| // This needs to be called before calling |
| // `ga.setupUserApplicationDimensions()`. |
| await connectedApp.initializeValues(); |
| |
| // Set up analytics dimensions for the connected app. |
| await ga.setupUserApplicationDimensions(); |
| |
| _connectionAvailableController.add(service); |
| } |
| |
| void manuallyDisconnect() { |
| vmServiceClosed( |
| connectionState: |
| const ConnectedState(false, userInitiatedConnectionState: true), |
| ); |
| } |
| |
| void vmServiceClosed({ |
| ConnectedState connectionState = const ConnectedState(false), |
| }) { |
| _serviceAvailable = Completer(); |
| |
| service = null; |
| vm = null; |
| sdkVersion = null; |
| connectedApp = null; |
| generateDevToolsTitle(); |
| |
| vmFlagManager.vmServiceClosed(); |
| serviceExtensionManager.vmServiceClosed(); |
| |
| serviceTrafficLogger?.dispose(); |
| |
| _isolateManager._handleVmServiceClosed(); |
| setDeviceBusy(false); |
| |
| _connectedState.value = connectionState; |
| _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, |
| ); |
| } |
| |
| Future<Response> get flutterVersion async { |
| return await callService( |
| registrations.flutterVersion.service, |
| isolateId: _isolateManager.selectedIsolate.id, |
| ); |
| } |
| |
| Future<Response> get adbMemoryInfo async { |
| return await callService( |
| registrations.flutterMemory.service, |
| isolateId: _isolateManager.selectedIsolate?.id, |
| ); |
| } |
| |
| /// @returns view id of selected isolate's 'FlutterView'. |
| /// @throws Exception if no 'FlutterView'. |
| Future<String> get flutterViewId async { |
| final flutterViewListResponse = await service.callServiceExtension( |
| registrations.flutterListViews, |
| isolateId: _isolateManager.selectedIsolate.id, |
| ); |
| final List<dynamic> views = |
| flutterViewListResponse.json['views'].cast<Map<String, dynamic>>(); |
| |
| // Each isolate should only have one FlutterView. |
| final flutterView = views.firstWhere( |
| (view) => view['type'] == 'FlutterView', |
| orElse: () => null, |
| ); |
| |
| if (flutterView == null) { |
| final message = |
| 'No Flutter Views to query: ${flutterViewListResponse.json}'; |
| log(message, LogLevel.error); |
| throw Exception(message); |
| } |
| |
| return flutterView['id']; |
| } |
| |
| /// Flutter engine returns estimate how much memory is used by layer/picture raster |
| /// cache entries in bytes. |
| /// |
| /// Call to returns JSON payload 'EstimateRasterCacheMemory' with two entries: |
| /// layerBytes - layer raster cache entries in bytes |
| /// pictureBytes - picture raster cache entries in bytes |
| Future<Response> get rasterCacheMetrics async { |
| if (connectedApp == null || !await connectedApp.isFlutterApp) { |
| return null; |
| } |
| |
| final viewId = await flutterViewId; |
| |
| return await service.callServiceExtension( |
| registrations.flutterEngineEstimateRasterCache, |
| args: <String, String>{ |
| 'viewId': viewId, |
| }, |
| isolateId: _isolateManager.selectedIsolate.id, |
| ); |
| } |
| |
| Future<double> get queryDisplayRefreshRate async { |
| if (connectedApp == null || !await connectedApp.isFlutterApp) { |
| return null; |
| } |
| |
| const unknownRefreshRate = 0.0; |
| |
| final viewId = await flutterViewId; |
| final displayRefreshRateResponse = await service.callServiceExtension( |
| registrations.displayRefreshRate, |
| isolateId: _isolateManager.selectedIsolate.id, |
| args: {'viewId': viewId}, |
| ); |
| final double fps = displayRefreshRateResponse.json['fps']; |
| |
| // The Flutter engine returns 0.0 if the refresh rate is unknown. Return |
| // [defaultRefreshRate] instead. |
| if (fps == unknownRefreshRate) { |
| return defaultRefreshRate; |
| } |
| |
| return fps.roundToDouble(); |
| } |
| |
| bool libraryUriAvailableNow(String uri) { |
| assert(_serviceAvailable.isCompleted); |
| assert(isolateManager.selectedIsolateAvailable.isCompleted); |
| return isolateManager.selectedIsolateLibraries |
| .map((ref) => ref.uri) |
| .toList() |
| .any((u) => u.startsWith(uri)); |
| } |
| |
| Future<bool> libraryUriAvailable(String uri) async { |
| assert(_serviceAvailable.isCompleted); |
| await isolateManager.selectedIsolateAvailable.future; |
| return libraryUriAvailableNow(uri); |
| } |
| } |
| |
| class IsolateManager extends Disposer { |
| Map<IsolateRef, Future<Isolate>> _isolates = {}; |
| IsolateRef _selectedIsolate; |
| VmServiceWrapper _service; |
| |
| 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>(); |
| |
| int _lastIsolateIndex = 0; |
| final Map<String, int> _isolateIndexMap = {}; |
| |
| List<LibraryRef> selectedIsolateLibraries; |
| |
| List<IsolateRef> get isolates => |
| List<IsolateRef>.unmodifiable(_isolates.keys); |
| |
| IsolateRef get selectedIsolate => _selectedIsolate; |
| |
| Stream<IsolateRef> get onIsolateCreated => _isolateCreatedController.stream; |
| |
| Stream<IsolateRef> get onSelectedIsolateChanged => |
| _selectedIsolateController.stream; |
| |
| Stream<IsolateRef> get onIsolateExited => _isolateExitedController.stream; |
| |
| final _mainIsolate = ValueNotifier<IsolateRef>(null); |
| ValueListenable<IsolateRef> get mainIsolate => _mainIsolate; |
| |
| Future<void> init(List<IsolateRef> isolates) async { |
| // Re-initialize isolates when VM developer mode is enabled/disabled to |
| // display/hide system isolates. |
| addAutoDisposeListener(preferences.vmDeveloperModeEnabled, () async { |
| final vmDeveloperModeEnabled = preferences.vmDeveloperModeEnabled.value; |
| final vm = await serviceManager.service.getVM(); |
| final isolates = [ |
| ...vm.isolates, |
| if (vmDeveloperModeEnabled) ...vm.systemIsolates, |
| ]; |
| if (selectedIsolate.isSystemIsolate && !vmDeveloperModeEnabled) { |
| selectIsolate(_isolates.keys.first.id); |
| } |
| await _initIsolates(isolates); |
| }); |
| await _initIsolates(isolates); |
| } |
| |
| /// Return a unique, monotonically increasing number for this Isolate. |
| int isolateIndex(IsolateRef isolateRef) { |
| if (!_isolateIndexMap.containsKey(isolateRef.id)) { |
| _isolateIndexMap[isolateRef.id] = ++_lastIsolateIndex; |
| } |
| return _isolateIndexMap[isolateRef.id]; |
| } |
| |
| void selectIsolate(String isolateRefId) { |
| final IsolateRef ref = _isolates.keys.firstWhere( |
| (IsolateRef ref) => ref.id == isolateRefId, |
| orElse: () => null); |
| _setSelectedIsolate(ref); |
| } |
| |
| Future<void> _initIsolates(List<IsolateRef> isolates) async { |
| _isolates = _buildIsolateMap(isolates); |
| _isolates.keys.forEach(isolateIndex); |
| |
| // It is critical that the _serviceExtensionManager is already listening |
| // for events indicating that new extension rpcs are registered before this |
| // call otherwise there is a race condition where service extensions are not |
| // described in the selectedIsolate or recieved as an event. It is ok if a |
| // service extension is included in both places as duplicate extensions are |
| // handled gracefully. |
| await _initSelectedIsolate(isolates); |
| |
| if (_selectedIsolate != null) { |
| _isolateCreatedController.add(_selectedIsolate); |
| _selectedIsolateController.add(_selectedIsolate); |
| } |
| } |
| |
| Future<void> _handleIsolateEvent(Event event) async { |
| _sendToMessageBus(event); |
| |
| if (event.kind == EventKind.kIsolateStart && |
| !event.isolate.isSystemIsolate) { |
| _isolates.putIfAbsent(event.isolate, () => null); |
| isolateIndex(event.isolate); |
| _isolateCreatedController.add(event.isolate); |
| // TODO(jacobr): we assume the first isolate started is the main isolate |
| // but that may not always be a safe assumption. |
| _mainIsolate.value ??= event.isolate; |
| |
| if (_selectedIsolate == null) { |
| await _setSelectedIsolate(event.isolate); |
| } |
| } else if (event.kind == EventKind.kServiceExtensionAdded) { |
| // Check to see if there is a new isolate. |
| if (_selectedIsolate == null && |
| extensions.isFlutterExtension(event.extensionRPC)) { |
| await _setSelectedIsolate(event.isolate); |
| } |
| } else if (event.kind == EventKind.kIsolateExit) { |
| unawaited(_isolates.remove(event.isolate)); |
| _isolateExitedController.add(event.isolate); |
| if (_mainIsolate.value == event.isolate) { |
| _mainIsolate.value = null; |
| } |
| if (_selectedIsolate == event.isolate) { |
| _selectedIsolate = _isolates.isEmpty ? null : _isolates.keys.first; |
| if (_selectedIsolate == null) { |
| selectedIsolateAvailable = Completer(); |
| } |
| _selectedIsolateController.add(_selectedIsolate); |
| } |
| } |
| } |
| |
| void _sendToMessageBus(Event event) { |
| messageBus?.addEvent(BusEvent( |
| 'debugger', |
| data: event, |
| )); |
| } |
| |
| Future<void> _initSelectedIsolate(List<IsolateRef> isolates) async { |
| if (isolates.isEmpty) { |
| return; |
| } |
| |
| _mainIsolate.value = await _computeMainIsolate(isolates); |
| await _setSelectedIsolate(_mainIsolate.value); |
| } |
| |
| Future<IsolateRef> _computeMainIsolate(List<IsolateRef> isolates) async { |
| if (isolates.isEmpty) return null; |
| |
| 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 (extensions.isFlutterExtension(extensionName)) { |
| return ref; |
| } |
| } |
| } |
| } |
| } |
| |
| final IsolateRef ref = isolates.firstWhere((IsolateRef ref) { |
| // 'foo.dart:main()' |
| return ref.name.contains(':main('); |
| }, orElse: () => null); |
| |
| return ref ?? isolates.first; |
| } |
| |
| Future<void> _setSelectedIsolate(IsolateRef ref) async { |
| if (_selectedIsolate == ref) { |
| return; |
| } |
| |
| _selectedIsolate = ref; |
| // Store the library uris for the selected isolate. |
| if (ref == null) { |
| selectedIsolateLibraries = []; |
| } else { |
| try { |
| final Isolate isolate = await _service.getIsolate(ref.id); |
| if (_selectedIsolate == ref) { |
| selectedIsolateLibraries = isolate.libraries; |
| } |
| } on SentinelException { |
| if (_selectedIsolate == ref) { |
| _selectedIsolate = null; |
| if (_isolates.isNotEmpty && _isolates.keys.first != ref) { |
| await _setSelectedIsolate(_isolates.keys.first); |
| } |
| } |
| return; |
| } |
| } |
| |
| 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); |
| } |
| |
| void _handleVmServiceClosed() { |
| cancel(); |
| _service = null; |
| _lastIsolateIndex = 0; |
| _setSelectedIsolate(null); |
| _isolateIndexMap.clear(); |
| _isolates.clear(); |
| _mainIsolate.value = null; |
| } |
| |
| void vmServiceOpened(VmServiceWrapper service) { |
| cancel(); |
| _service = service; |
| autoDispose(service.onIsolateEvent.listen(_handleIsolateEvent)); |
| // We don't yet known the main isolate. |
| _mainIsolate.value = null; |
| } |
| |
| Future<Isolate> getIsolateCached(IsolateRef isolateRef) { |
| return _isolates[isolateRef] ??= _service.getIsolate(isolateRef.id); |
| } |
| |
| Map<IsolateRef, Future<Isolate>> _buildIsolateMap(List<IsolateRef> isolates) { |
| final map = <IsolateRef, Future<Isolate>>{}; |
| for (var isolate in isolates) { |
| map[isolate] = null; |
| } |
| return map; |
| } |
| } |
| |
| /// Manager that handles tracking the service extension for the main isolate. |
| class ServiceExtensionManager extends Disposer { |
| ServiceExtensionManager(this._mainIsolate); |
| |
| VmServiceWrapper _service; |
| |
| bool _checkForFirstFrameStarted = false; |
| |
| final ValueListenable<IsolateRef> _mainIsolate; |
| |
| bool get _firstFrameEventReceived => _firstFrameReceived.isCompleted; |
| Completer<void> _firstFrameReceived = Completer(); |
| Future<void> get firstFrameReceived => _firstFrameReceived.future; |
| |
| final _serviceExtensionAvailable = <String, ValueNotifier<bool>>{}; |
| |
| final _serviceExtensionStateController = |
| <String, ValueNotifier<ServiceExtensionState>>{}; |
| |
| /// All available service extensions. |
| final _serviceExtensions = <String>{}; |
| |
| /// All service extensions that are currently enabled. |
| final _enabledServiceExtensions = <String, ServiceExtensionState>{}; |
| |
| /// Map from service extension name to [Completer] that completes when the |
| /// service extension is registered or the isolate shuts down. |
| final _maybeRegisteringServiceExtensions = <String, Completer<bool>>{}; |
| |
| /// Temporarily stores service extensions that we need to add. We should not |
| /// add extensions until the first frame event has been received |
| /// [_firstFrameEventReceived]. |
| final _pendingServiceExtensions = <String>{}; |
| |
| Map<String, List<AsyncCallback>> _callbacksOnIsolateResume = {}; |
| |
| ConnectedApp get connectedApp => _connectedApp; |
| ConnectedApp _connectedApp; |
| |
| Future<void> _handleIsolateEvent(Event event) async { |
| if (event.kind == EventKind.kServiceExtensionAdded) { |
| // On hot restart, service extensions are added from here. |
| await _maybeAddServiceExtension(event.extensionRPC); |
| } |
| } |
| |
| Future<void> _handleExtensionEvent(Event event) async { |
| switch (event.extensionKind) { |
| case 'Flutter.FirstFrame': |
| case 'Flutter.Frame': |
| await _onFrameEventReceived(); |
| break; |
| case 'Flutter.ServiceExtensionStateChanged': |
| final name = event.json['extensionData']['extension'].toString(); |
| final encodedValue = event.json['extensionData']['value'].toString(); |
| await _updateServiceExtensionForStateChange(name, encodedValue); |
| break; |
| case 'HttpTimelineLoggingStateChange': |
| final name = extensions.httpEnableTimelineLogging.extension; |
| final encodedValue = event.json['extensionData']['enabled'].toString(); |
| await _updateServiceExtensionForStateChange(name, encodedValue); |
| break; |
| case 'SocketProfilingStateChange': |
| final name = extensions.socketProfiling.extension; |
| final encodedValue = event.json['extensionData']['enabled'].toString(); |
| await _updateServiceExtensionForStateChange(name, encodedValue); |
| } |
| } |
| |
| Future<void> _handleDebugEvent(Event event) async { |
| if (event.kind == EventKind.kResume) { |
| final isolateId = event.isolate.id; |
| final callbacks = _callbacksOnIsolateResume[isolateId] ?? []; |
| _callbacksOnIsolateResume = {}; |
| for (final callback in callbacks) { |
| try { |
| await callback(); |
| } catch (e) { |
| log( |
| 'Error running isolate callback: $e', |
| LogLevel.error, |
| ); |
| } |
| } |
| } |
| } |
| |
| Future<void> _updateServiceExtensionForStateChange( |
| String name, |
| String encodedValue, |
| ) async { |
| final extension = extensions.serviceExtensionsAllowlist[name]; |
| if (extension != null) { |
| final dynamic extensionValue = _getExtensionValue(name, encodedValue); |
| final enabled = |
| extension is extensions.ToggleableServiceExtensionDescription |
| ? extensionValue == 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, |
| extensionValue, |
| callExtension: false, |
| ); |
| } |
| } |
| |
| dynamic _getExtensionValue(String name, String encodedValue) { |
| final expectedValueType = |
| extensions.serviceExtensionsAllowlist[name].values.first.runtimeType; |
| switch (expectedValueType) { |
| case bool: |
| return encodedValue == 'true'; |
| case int: |
| case double: |
| return num.parse(encodedValue); |
| default: |
| return encodedValue; |
| } |
| } |
| |
| Future<void> _onFrameEventReceived() async { |
| if (_firstFrameEventReceived) { |
| // The first frame event was already received. |
| return; |
| } |
| _firstFrameReceived.complete(); |
| |
| final extensionsToProcess = _pendingServiceExtensions.toList(); |
| _pendingServiceExtensions.clear(); |
| await Future.wait([ |
| for (String extension in extensionsToProcess) |
| _addServiceExtension(extension) |
| ]); |
| } |
| |
| Future<void> _onMainIsolateChanged() async { |
| if (_mainIsolate.value == null) { |
| _mainIsolateClosed(); |
| return; |
| } |
| _checkForFirstFrameStarted = false; |
| |
| final isolateRef = _mainIsolate.value; |
| final Isolate isolate = await _service.getIsolate(isolateRef.id); |
| if (isolateRef != _mainIsolate.value) { |
| // Isolate has changed again. |
| return; |
| } |
| if (isolate.extensionRPCs != null) { |
| if (await connectedApp.isFlutterApp) { |
| if (isolateRef != _mainIsolate.value) { |
| // Isolate has changed again. |
| return; |
| } |
| await Future.wait([ |
| for (String extension in isolate.extensionRPCs) |
| _maybeAddServiceExtension(extension) |
| ]); |
| } else { |
| await Future.wait([ |
| for (String extension in isolate.extensionRPCs) |
| _addServiceExtension(extension) |
| ]); |
| } |
| } |
| } |
| |
| Future<void> _maybeCheckForFirstFlutterFrame() async { |
| final _lastMainIsolate = _mainIsolate.value; |
| if (_checkForFirstFrameStarted || |
| _firstFrameEventReceived || |
| _lastMainIsolate == null) return; |
| if (!isServiceExtensionAvailable(extensions.didSendFirstFrameEvent)) { |
| return; |
| } |
| _checkForFirstFrameStarted = true; |
| |
| final value = await _service.callServiceExtension( |
| extensions.didSendFirstFrameEvent, |
| isolateId: _lastMainIsolate.id, |
| ); |
| if (_lastMainIsolate != _mainIsolate.value) { |
| // The active isolate has changed since we started querying the first |
| // frame. |
| return; |
| } |
| final didSendFirstFrameEvent = value?.json['enabled'] == 'true'; |
| |
| if (didSendFirstFrameEvent) { |
| await _onFrameEventReceived(); |
| } |
| } |
| |
| Future<void> _maybeAddServiceExtension(String name) async { |
| if (_firstFrameEventReceived || !isUnsafeBeforeFirstFlutterFrame(name)) { |
| await _addServiceExtension(name); |
| } else { |
| _pendingServiceExtensions.add(name); |
| } |
| } |
| |
| Future<void> _addServiceExtension(String name) async { |
| if (!_serviceExtensions.add(name)) { |
| // If the service extension was already added we do not need to add it |
| // again. This can happen depending on the timing between when extension |
| // added events were received and when we requested the list of all |
| // service extensions already defined for the isolate. |
| return; |
| } |
| _hasServiceExtension(name).value = 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. |
| return 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. |
| return await _restoreExtensionFromDevice(name); |
| } |
| } |
| |
| Future<void> _restoreExtensionFromDevice(String name) async { |
| final isolateRef = _mainIsolate.value; |
| if (isolateRef == null) return; |
| |
| if (!extensions.serviceExtensionsAllowlist.containsKey(name)) { |
| return; |
| } |
| final expectedValueType = |
| extensions.serviceExtensionsAllowlist[name].values.first.runtimeType; |
| |
| Future<void> restore() async { |
| // The restore request is obsolete if the isolate has changed. |
| if (isolateRef != _mainIsolate.value) return; |
| try { |
| final response = await _service.callServiceExtension( |
| name, |
| isolateId: isolateRef.id, |
| ); |
| |
| if (isolateRef != _mainIsolate.value) return; |
| |
| 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. |
| } |
| } |
| |
| if (isolateRef != _mainIsolate.value) return; |
| |
| final Isolate isolate = await _service.getIsolate(isolateRef.id); |
| if (isolateRef != _mainIsolate.value) return; |
| |
| // Do not try to restore Dart IO extensions for a paused isolate. |
| if (extensions.isDartIoExtension(name) && |
| isolate.pauseEvent.kind.contains('Pause')) { |
| _callbacksOnIsolateResume |
| .putIfAbsent(isolateRef.id, () => []) |
| .add(restore); |
| } else { |
| await restore(); |
| } |
| } |
| |
| Future<void> _maybeRestoreExtension(String name, dynamic value) async { |
| final extensionDescription = extensions.serviceExtensionsAllowlist[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; |
| } |
| |
| final mainIsolate = _mainIsolate.value; |
| Future<void> callExtension() async { |
| if (_mainIsolate.value != mainIsolate) return; |
| |
| assert(value != null); |
| if (value is bool) { |
| Future<void> call(String isolateId, bool value) async { |
| await _service.callServiceExtension( |
| name, |
| isolateId: isolateId, |
| args: {'enabled': value}, |
| ); |
| } |
| |
| if (extensions |
| .serviceExtensionsAllowlist[name].shouldCallOnAllIsolates) { |
| // TODO(jacobr): be more robust instead of just assuming that if the |
| // service extension is available on one isolate it is available on |
| // all. For example, some isolates may still be initializing so may |
| // not expose the service extension yet. |
| await _service.forEachIsolate((isolate) async { |
| // TODO(kenz): stop special casing http timeline logging once |
| // dart io version 1.4 hits stable (when vm_service 5.3.0 hits |
| // Flutter stable). |
| // See https://github.com/dart-lang/sdk/issues/43628. |
| if (name == extensions.httpEnableTimelineLogging.extension && |
| !(await _service.isDartIoVersionSupported( |
| supportedVersion: SemanticVersion(major: 1, minor: 4), |
| isolateId: isolate.id, |
| ))) { |
| await _service.httpEnableTimelineLogging(isolate.id, value); |
| } else { |
| await call(isolate.id, value); |
| } |
| }); |
| } else { |
| await call(mainIsolate.id, value); |
| } |
| } else if (value is String) { |
| await _service.callServiceExtension( |
| name, |
| isolateId: mainIsolate.id, |
| args: {'value': value}, |
| ); |
| } else if (value is double) { |
| await _service.callServiceExtension( |
| name, |
| isolateId: mainIsolate.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}, |
| ); |
| } |
| } |
| |
| if (mainIsolate == null) return; |
| final Isolate isolate = await _service.getIsolate(mainIsolate.id); |
| if (_mainIsolate.value != mainIsolate) return; |
| |
| // Do not try to call Dart IO extensions for a paused isolate. |
| if (extensions.isDartIoExtension(name) && |
| isolate.pauseEvent.kind.contains('Pause')) { |
| _callbacksOnIsolateResume |
| .putIfAbsent(mainIsolate.id, () => []) |
| .add(callExtension); |
| } else { |
| await callExtension(); |
| } |
| } |
| |
| void vmServiceClosed() { |
| cancel(); |
| _mainIsolateClosed(); |
| } |
| |
| void _mainIsolateClosed() { |
| _firstFrameReceived = Completer(); |
| _checkForFirstFrameStarted = false; |
| _pendingServiceExtensions.clear(); |
| _serviceExtensions.clear(); |
| |
| // If the isolate has closed, there is no need to wait any longer for |
| // service extensions that might be registered. |
| for (var completer in _maybeRegisteringServiceExtensions.values) { |
| if (!completer.isCompleted) { |
| completer.complete(false); |
| } |
| } |
| _maybeRegisteringServiceExtensions.clear(); |
| |
| for (var listenable in _serviceExtensionAvailable.values) { |
| listenable.value = 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 && _serviceExtensions.contains(name)) { |
| await _callServiceExtension(name, value); |
| } |
| |
| final state = ServiceExtensionState(enabled, value); |
| _serviceExtensionState(name).value = state; |
| |
| // Add or remove service extension from [enabledServiceExtensions]. |
| if (enabled) { |
| _enabledServiceExtensions[name] = state; |
| } else { |
| _enabledServiceExtensions.remove(name); |
| } |
| } |
| |
| bool isServiceExtensionAvailable(String name) { |
| return _serviceExtensions.contains(name) || |
| _pendingServiceExtensions.contains(name); |
| } |
| |
| Future<bool> waitForServiceExtensionAvailable(String name) { |
| if (isServiceExtensionAvailable(name)) return Future.value(true); |
| |
| Completer<bool> createCompleter() { |
| // Listen for when the service extension is added and use it. |
| final completer = Completer<bool>(); |
| final listenable = hasServiceExtension(name); |
| VoidCallback listener; |
| listener = () { |
| if (listenable.value || completer.isCompleted) { |
| listenable.removeListener(listener); |
| completer.complete(true); |
| } |
| }; |
| hasServiceExtension(name).addListener(listener); |
| return completer; |
| } |
| |
| _maybeRegisteringServiceExtensions[name] ??= createCompleter(); |
| return _maybeRegisteringServiceExtensions[name].future; |
| } |
| |
| ValueListenable<bool> hasServiceExtension(String name) { |
| return _hasServiceExtension(name); |
| } |
| |
| ValueNotifier<bool> _hasServiceExtension(String name) { |
| return _serviceExtensionAvailable.putIfAbsent( |
| name, |
| () => ValueNotifier(_serviceExtensions.contains(name)), |
| ); |
| } |
| |
| ValueListenable<ServiceExtensionState> getServiceExtensionState(String name) { |
| return _serviceExtensionState(name); |
| } |
| |
| ValueNotifier<ServiceExtensionState> _serviceExtensionState(String name) { |
| return _serviceExtensionStateController.putIfAbsent( |
| name, |
| () { |
| return ValueNotifier<ServiceExtensionState>( |
| _enabledServiceExtensions.containsKey(name) |
| ? _enabledServiceExtensions[name] |
| : ServiceExtensionState(false, null), |
| ); |
| }, |
| ); |
| } |
| |
| void vmServiceOpened(VmServiceWrapper service, ConnectedApp connectedApp) { |
| _checkForFirstFrameStarted = false; |
| cancel(); |
| _connectedApp = connectedApp; |
| _service = service; |
| // TODO(kenz): do we want to listen with event history here? |
| autoDispose(service.onExtensionEvent.listen(_handleExtensionEvent)); |
| addAutoDisposeListener( |
| hasServiceExtension(extensions.didSendFirstFrameEvent), |
| _maybeCheckForFirstFlutterFrame, |
| ); |
| addAutoDisposeListener(_mainIsolate, _onMainIsolateChanged); |
| autoDispose(service.onDebugEvent.listen(_handleDebugEvent)); |
| autoDispose(service.onIsolateEvent.listen(_handleIsolateEvent)); |
| if (_mainIsolate.value != null) { |
| _onMainIsolateChanged(); |
| } |
| } |
| } |
| |
| 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; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is ServiceExtensionState && |
| enabled == other.enabled && |
| value == other.value; |
| } |
| |
| @override |
| int get hashCode => hashValues( |
| enabled, |
| value, |
| ); |
| } |
| |
| class VmFlagManager extends Disposer { |
| VmServiceWrapper get service => _service; |
| VmServiceWrapper _service; |
| |
| ValueListenable get flags => _flags; |
| final _flags = ValueNotifier<FlagList>(null); |
| |
| final _flagNotifiers = <String, ValueNotifier<Flag>>{}; |
| |
| ValueNotifier<Flag> flag(String name) { |
| return _flagNotifiers.containsKey(name) ? _flagNotifiers[name] : null; |
| } |
| |
| Future<void> _initFlags() async { |
| final flagList = await service.getFlagList(); |
| _flags.value = flagList; |
| if (flagList == null) return; |
| |
| final flags = <String, Flag>{}; |
| for (var flag in flagList.flags) { |
| flags[flag.name] = flag; |
| _flagNotifiers[flag.name] = ValueNotifier<Flag>(flag); |
| } |
| } |
| |
| @visibleForTesting |
| void handleVmEvent(Event event) async { |
| if (event.kind == EventKind.kVMFlagUpdate) { |
| if (_flagNotifiers.containsKey(event.flag)) { |
| final currentFlag = _flagNotifiers[event.flag].value; |
| _flagNotifiers[event.flag].value = Flag.parse({ |
| 'name': currentFlag.name, |
| 'comment': currentFlag.comment, |
| 'modified': true, |
| 'valueAsString': event.newValue, |
| }); |
| _flags.value = await service.getFlagList(); |
| } |
| } |
| } |
| |
| Future<void> vmServiceOpened(VmServiceWrapper service) async { |
| cancel(); |
| _service = service; |
| // Upon setting the vm service, get initial values for vm flags. |
| await _initFlags(); |
| |
| autoDispose(service.onVMEvent.listen(handleVmEvent)); |
| } |
| |
| void vmServiceClosed() { |
| _flags.value = null; |
| } |
| } |
| |
| class VmServiceCapabilities { |
| VmServiceCapabilities(this.version); |
| |
| final Version version; |
| |
| bool get supportsGetScripts => |
| version.major > 3 || (version.major == 3 && version.minor >= 12); |
| } |
| |
| class ConnectedState { |
| const ConnectedState( |
| this.connected, { |
| this.userInitiatedConnectionState = false, |
| }); |
| |
| final bool connected; |
| |
| /// Whether this [ConnectedState] was manually initiated by the user. |
| final bool userInitiatedConnectionState; |
| } |