blob: 5827a77a3cf99a890be8179b0d2d1b11af40b398 [file] [log] [blame]
// 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();
}