| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:collection'; |
| |
| import 'package:async/async.dart'; |
| import 'package:dwds/src/config/tool_configuration.dart'; |
| import 'package:dwds/src/connections/app_connection.dart'; |
| import 'package:dwds/src/debugging/libraries.dart'; |
| import 'package:dwds/src/debugging/metadata/provider.dart'; |
| import 'package:dwds/src/utilities/dart_uri.dart'; |
| import 'package:dwds/src/utilities/shared.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:vm_service/vm_service.dart'; |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; |
| |
| /// An inspector for a running Dart application contained in the |
| /// [WipConnection]. |
| /// |
| /// Provides information about currently loaded scripts and objects and support |
| /// for eval. |
| abstract class AppInspector { |
| var _scriptCacheMemoizer = AsyncMemoizer<List<ScriptRef>>(); |
| |
| Future<List<ScriptRef>> getScriptRefs({ |
| ModifiedModuleReport? modifiedModuleReport, |
| }) => _populateScriptCaches(modifiedModuleReport: modifiedModuleReport); |
| |
| /// Map of scriptRef ID to [ScriptRef]. |
| UnmodifiableMapView<String, ScriptRef> get scriptRefsById => |
| UnmodifiableMapView(_scriptRefsById); |
| final _scriptRefsById = <String, ScriptRef>{}; |
| |
| /// Map of Dart server path to [ScriptRef]. |
| final _serverPathToScriptRef = <String, ScriptRef>{}; |
| |
| /// Map of [ScriptRef] id to containing [LibraryRef] id. |
| UnmodifiableMapView<String, String> get scriptIdToLibraryId => |
| UnmodifiableMapView(_scriptIdToLibraryId); |
| final _scriptIdToLibraryId = <String, String>{}; |
| |
| /// Map of [Library] id to included [ScriptRef]s. |
| final _libraryIdToScriptRefs = <String, List<ScriptRef>>{}; |
| |
| final Isolate isolate; |
| |
| final IsolateRef isolateRef; |
| |
| final AppConnection appConnection; |
| |
| LibraryHelper get libraryHelper; |
| |
| /// The root URI from which the application is served. |
| final String root; |
| |
| /// JavaScript expression that evaluates to the Dart stack trace mapper. |
| static const stackTraceMapperExpression = '\$dartStackTraceUtility.mapper'; |
| |
| /// Regex used to extract the message from an exception description. |
| static final exceptionMessageRegex = RegExp(r'^.*$', multiLine: true); |
| |
| /// Flutter widget inspector library. |
| Future<LibraryRef?> get flutterWidgetInspectorLibrary => libraryHelper |
| .libraryRefFor('package:flutter/src/widgets/widget_inspector.dart'); |
| |
| /// Regex used to extract a stack trace line from the exception description. |
| static final stackTraceLineRegex = RegExp(r'^\s*at\s.*$', multiLine: true); |
| |
| AppInspector(this.appConnection, this.isolate, this.root) |
| : isolateRef = _toIsolateRef(isolate); |
| |
| /// Reset all caches and recompute any mappings. |
| /// |
| /// Should be called across hot reloads with a valid [ModifiedModuleReport]. |
| @protected |
| @mustCallSuper |
| Future<void> initialize({ModifiedModuleReport? modifiedModuleReport}) async { |
| _scriptCacheMemoizer = AsyncMemoizer<List<ScriptRef>>(); |
| |
| // Invalidate `_libraryHelper` as we use it populate any script caches. |
| libraryHelper.initialize(modifiedModuleReport: modifiedModuleReport); |
| if (modifiedModuleReport == null) { |
| _scriptRefsById.clear(); |
| _serverPathToScriptRef.clear(); |
| _scriptIdToLibraryId.clear(); |
| _libraryIdToScriptRefs.clear(); |
| } |
| |
| final libraries = await libraryHelper.libraryRefs; |
| isolate.rootLib = await libraryHelper.rootLib; |
| isolate.libraries?.clear(); |
| isolate.libraries?.addAll(libraries); |
| |
| final scripts = await getScriptRefs( |
| modifiedModuleReport: modifiedModuleReport, |
| ); |
| |
| await DartUri.initialize(); |
| DartUri.recordAbsoluteUris(libraries.map((lib) => lib.uri).nonNulls); |
| DartUri.recordAbsoluteUris(scripts.map((script) => script.uri).nonNulls); |
| } |
| |
| static IsolateRef _toIsolateRef(Isolate isolate) => IsolateRef( |
| id: isolate.id, |
| name: isolate.name, |
| number: isolate.number, |
| isSystemIsolate: isolate.isSystemIsolate, |
| ); |
| |
| Future<LibraryRef?> libraryRefFor(String objectId) => |
| libraryHelper.libraryRefFor(objectId); |
| |
| /// Returns the [ScriptRef] for the provided Dart server path [uri]. |
| Future<ScriptRef?> scriptRefFor(String uri) async { |
| await _populateScriptCaches(); |
| return _serverPathToScriptRef[uri]; |
| } |
| |
| /// Returns the [ScriptRef]s in the library with [libraryId]. |
| Future<List<ScriptRef>> scriptRefsForLibrary(String libraryId) async { |
| await _populateScriptCaches(); |
| return _libraryIdToScriptRefs[libraryId] ?? []; |
| } |
| |
| /// All the scripts in the isolate. |
| Future<ScriptList> getScripts() async { |
| return ScriptList(scripts: await getScriptRefs()); |
| } |
| |
| /// Request and cache `<ScriptRef>`s for all the scripts in the application. |
| /// |
| /// This populates [_scriptRefsById], [_scriptIdToLibraryId], |
| /// [_libraryIdToScriptRefs] and [_serverPathToScriptRef]. |
| /// |
| /// This will get repopulated on restarts and reloads. |
| /// |
| /// If [modifiedModuleReport] is provided, only invalidates and |
| /// recalculates caches for the modified libraries. |
| /// |
| /// Returns the list of scripts refs cached. |
| Future<List<ScriptRef>> _populateScriptCaches({ |
| ModifiedModuleReport? modifiedModuleReport, |
| }) { |
| return _scriptCacheMemoizer.runOnce(() async { |
| final scripts = await globalToolConfiguration.loadStrategy |
| .metadataProviderFor(appConnection.request.entrypointPath) |
| .scripts; |
| if (modifiedModuleReport != null) { |
| // Invalidate any script caches that were computed for the now invalid |
| // libraries. They will get repopulated below. |
| for (final libraryUri in modifiedModuleReport.modifiedLibraries) { |
| final libraryRef = await libraryHelper.libraryRefFor(libraryUri); |
| final libraryId = libraryRef?.id; |
| // If this was not a pre-existing library, nothing to invalidate. |
| if (libraryId == null) continue; |
| final scriptRefs = _libraryIdToScriptRefs.remove(libraryId); |
| if (scriptRefs == null) continue; |
| for (final scriptRef in scriptRefs) { |
| final scriptId = scriptRef.id; |
| final scriptUri = scriptRef.uri; |
| if (scriptId != null && scriptUri != null) { |
| _scriptRefsById.remove(scriptId); |
| _scriptIdToLibraryId.remove(scriptId); |
| _serverPathToScriptRef.remove( |
| DartUri(scriptUri, root).serverPath, |
| ); |
| } |
| } |
| } |
| } |
| // For all the non-dart: libraries, find their parts and create scriptRefs |
| // for them. |
| final userLibraries = _userLibraryUris( |
| isolate.libraries ?? <LibraryRef>[], |
| ); |
| for (final uri in userLibraries) { |
| if (modifiedModuleReport?.modifiedLibraries.contains(uri) == false) { |
| continue; |
| } |
| final parts = scripts[uri]; |
| final scriptRefs = [ |
| ScriptRef(uri: uri, id: createId()), |
| for (final part in parts ?? []) ScriptRef(uri: part, id: createId()), |
| ]; |
| final libraryRef = await libraryHelper.libraryRefFor(uri); |
| final libraryId = libraryRef?.id; |
| if (libraryId != null) { |
| final libraryIdToScriptRefs = _libraryIdToScriptRefs.putIfAbsent( |
| libraryId, |
| () => <ScriptRef>[], |
| ); |
| for (final scriptRef in scriptRefs) { |
| final scriptId = scriptRef.id; |
| final scriptUri = scriptRef.uri; |
| if (scriptId != null && scriptUri != null) { |
| _scriptRefsById[scriptId] = scriptRef; |
| _scriptIdToLibraryId[scriptId] = libraryId; |
| _serverPathToScriptRef[DartUri(scriptUri, root).serverPath] = |
| scriptRef; |
| libraryIdToScriptRefs.add(scriptRef); |
| } |
| } |
| } |
| } |
| return _scriptRefsById.values.toList(); |
| }); |
| } |
| |
| Iterable<String> _userLibraryUris(Iterable<LibraryRef> libraries) { |
| return libraries |
| .map((library) => library.uri ?? '') |
| .where((uri) => uri.isNotEmpty && !uri.startsWith('dart:')); |
| } |
| |
| /// Look up the script by id in an isolate. |
| ScriptRef? scriptWithId(String? scriptId) => |
| scriptId == null ? null : _scriptRefsById[scriptId]; |
| |
| /// Runs an eval on the page to compute all existing registered extensions. |
| /// |
| /// Combines this with the RPCs registered in the [isolate]. Use this over |
| /// [Isolate.extensionRPCs] as this computes a live set. |
| /// |
| /// Updates [Isolate.extensionRPCs] to this set. |
| Future<Set<String>> getExtensionRpcs(); |
| } |