diff --git a/dwds/lib/src/debugging/chrome_inspector.dart b/dwds/lib/src/debugging/chrome_inspector.dart new file mode 100644 index 000000000..e3cac4848 --- /dev/null +++ b/dwds/lib/src/debugging/chrome_inspector.dart @@ -0,0 +1,679 @@ +// 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:math' as math; + +import 'package:dwds/src/config/tool_configuration.dart'; +import 'package:dwds/src/connections/app_connection.dart'; +import 'package:dwds/src/debugging/classes.dart'; +import 'package:dwds/src/debugging/debugger.dart'; +import 'package:dwds/src/debugging/execution_context.dart'; +import 'package:dwds/src/debugging/inspector.dart'; +import 'package:dwds/src/debugging/instance.dart'; +import 'package:dwds/src/debugging/libraries.dart'; +import 'package:dwds/src/debugging/location.dart'; +import 'package:dwds/src/debugging/metadata/provider.dart'; +import 'package:dwds/src/debugging/remote_debugger.dart'; +import 'package:dwds/src/loaders/ddc_library_bundle.dart'; +import 'package:dwds/src/readers/asset_reader.dart'; +import 'package:dwds/src/utilities/conversions.dart'; +import 'package:dwds/src/utilities/dart_uri.dart'; +import 'package:dwds/src/utilities/domain.dart'; +import 'package:dwds/src/utilities/objects.dart'; +import 'package:dwds/src/utilities/server.dart'; +import 'package:dwds/src/utilities/shared.dart'; +import 'package:logging/logging.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. +class ChromeAppInspector extends AppInspector { + final _logger = Logger('AppInspector'); + + final RemoteDebugger remoteDebugger; + final ExecutionContext _executionContext; + + @override + @protected + late final ChromeLibraryHelper libraryHelper = ChromeLibraryHelper(this); + + late ChromeAppClassHelper _classHelper; + late ChromeAppInstanceHelper _instanceHelper; + + final AssetReader _assetReader; + final Locations _locations; + + /// 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); + + /// Regex used to extract a stack trace line from the exception description. + static final stackTraceLineRegex = RegExp(r'^\s*at\s.*$', multiLine: true); + + ChromeAppInspector._( + super.appConnection, + super.isolate, + this.remoteDebugger, + this._assetReader, + this._locations, + super.root, + this._executionContext, + ); + + /// Reset all caches and recompute any mappings. + /// + /// Should be called across hot reloads with a valid [ModifiedModuleReport]. + @override + Future initialize({ModifiedModuleReport? modifiedModuleReport}) async { + await super.initialize(modifiedModuleReport: modifiedModuleReport); + + // TODO(srujzs): We can invalidate these in a smarter way instead of + // reinitializing when doing a hot reload, but these helpers recompute info + // on demand later and therefore are not in the critical path. + _classHelper = ChromeAppClassHelper(this); + _instanceHelper = ChromeAppInstanceHelper(this); + } + + static Future create( + AppConnection appConnection, + RemoteDebugger remoteDebugger, + AssetReader assetReader, + Locations locations, + String root, + Debugger debugger, + ExecutionContext executionContext, + ) async { + final id = createId(); + final time = DateTime.now().millisecondsSinceEpoch; + final name = 'main()'; + final isolate = Isolate( + id: id, + number: id, + name: name, + startTime: time, + runnable: true, + pauseOnExit: false, + pauseEvent: Event( + kind: EventKind.kPauseStart, + timestamp: time, + isolate: IsolateRef( + id: id, + name: name, + number: id, + isSystemIsolate: false, + ), + ), + livePorts: 0, + libraries: [], + breakpoints: [], + exceptionPauseMode: debugger.pauseState, + isSystemIsolate: false, + isolateFlags: [], + )..extensionRPCs = []; + final inspector = ChromeAppInspector._( + appConnection, + isolate, + remoteDebugger, + assetReader, + locations, + root, + executionContext, + ); + + debugger.updateInspector(inspector); + await inspector.initialize(); + return inspector; + } + + /// Returns the ID for the execution context or null if not found. + Future get contextId async { + try { + return await _executionContext.id; + } catch (e, s) { + _logger.severe('Missing execution context ID: ', e, s); + return null; + } + } + + /// Get the value of the field named [fieldName] from [receiver]. + Future loadField(RemoteObject receiver, String fieldName) { + final load = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getPropertyJsExpression(fieldName); + return jsCallFunctionOn(receiver, load, []); + } + + /// Call a method by name on [receiver], with arguments [positionalArgs] and + /// [namedArgs]. + Future _invokeMethod( + RemoteObject receiver, + String methodName, [ + List positionalArgs = const [], + Map namedArgs = const {}, + ]) async { + // TODO(alanknight): Support named arguments. + if (namedArgs.isNotEmpty) { + throw UnsupportedError('Named arguments are not yet supported'); + } + // We use the JS pseudo-variable 'arguments' to get the list of all arguments. + final send = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .callInstanceMethodJsExpression(methodName); + final remote = await jsCallFunctionOn(receiver, send, positionalArgs); + return remote; + } + + /// Calls Chrome's Runtime.callFunctionOn method. + /// + /// [evalExpression] should be a JS function definition that can accept + /// [arguments]. + Future jsCallFunctionOn( + RemoteObject receiver, + String evalExpression, + List arguments, { + bool returnByValue = false, + }) async { + final jsArguments = arguments.map(callArgumentFor).toList(); + final response = await remoteDebugger.sendCommand( + 'Runtime.callFunctionOn', + params: { + 'functionDeclaration': evalExpression, + 'arguments': jsArguments, + 'objectId': receiver.objectId, + 'returnByValue': returnByValue, + }, + ); + final result = getResultOrHandleError( + response, + evalContents: evalExpression, + ); + return RemoteObject(result); + } + + /// Calls Chrome's Runtime.callFunctionOn method with a global function. + /// + /// [evalExpression] should be a JS function definition that can accept + /// [arguments]. + Future _jsCallFunction( + String evalExpression, + List arguments, { + bool returnByValue = false, + }) async { + final jsArguments = arguments.map(callArgumentFor).toList(); + final response = await remoteDebugger.sendCommand( + 'Runtime.callFunctionOn', + params: { + 'functionDeclaration': evalExpression, + 'arguments': jsArguments, + 'executionContextId': await contextId, + 'returnByValue': returnByValue, + }, + ); + final result = getResultOrHandleError( + response, + evalContents: evalExpression, + ); + return RemoteObject(result); + } + + /// Invoke the function named [selector] on the object identified by + /// [targetId]. + /// + /// The [targetId] can be the URL of a Dart library, in which case this means + /// invoking a top-level function. The [arguments] are always strings that are + /// Dart object Ids (which can also be Chrome RemoteObject objectIds that are + /// for non-Dart JS objects.) + Future invoke( + String targetId, + String selector, [ + List arguments = const [], + ]) async { + final remoteArguments = arguments + .cast() + .map(remoteObjectFor) + .toList(); + // We special case the Dart library, where invokeMethod won't work because + // it's not really a Dart object. + if (isLibraryId(targetId)) { + final library = await getObject(targetId) as Library; + return await _invokeLibraryFunction(library, selector, remoteArguments); + } else { + return _invokeMethod( + remoteObjectFor(targetId), + selector, + remoteArguments, + ); + } + } + + /// Invoke the function named [selector] from [library] with [arguments]. + Future _invokeLibraryFunction( + Library library, + String selector, + List arguments, + ) { + final libraryUri = library.uri; + if (libraryUri == null) { + throwInvalidParam('invoke', 'library uri is null'); + } + return globalToolConfiguration.loadStrategy is DdcLibraryBundleStrategy + ? _evaluateLibraryMethodWithDdcLibraryBundle( + libraryUri, + selector, + arguments, + ) + : _evaluateInLibrary( + libraryUri, + 'function () { return this.$selector.apply(this, arguments); }', + arguments, + ); + } + + /// Evaluate [expression] by calling Chrome's Runtime.evaluate. + Future jsEvaluate( + String expression, { + bool returnByValue = false, + bool awaitPromise = false, + }) async { + // TODO(alanknight): Support a version with arguments if needed. + final response = await remoteDebugger.sendCommand( + 'Runtime.evaluate', + params: { + 'expression': expression, + 'returnByValue': returnByValue, + 'awaitPromise': awaitPromise, + 'contextId': await contextId, + }, + ); + final result = getResultOrHandleError(response, evalContents: expression); + return RemoteObject(result); + } + + /// Evaluate the JS function with source [jsFunction] in the context of + /// the library identified by [libraryUri] with [arguments]. + Future _evaluateInLibrary( + String libraryUri, + String jsFunction, + List arguments, + ) async { + final findLibraryJsExpression = globalToolConfiguration + .loadStrategy + .dartRuntimeDebugger + .callLibraryMethodJsExpression(libraryUri, jsFunction); + + final remoteLibrary = await jsEvaluate(findLibraryJsExpression); + return jsCallFunctionOn(remoteLibrary, jsFunction, arguments); + } + + /// Evaluates the specified top-level method [methodName] within the library + /// identified by [libraryUri] using the Dart Development Compiler (DDC) + /// library bundle strategy with the given optional [arguments]. + Future _evaluateLibraryMethodWithDdcLibraryBundle( + String libraryUri, + String methodName, [ + List arguments = const [], + ]) { + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .callLibraryMethodJsExpression(libraryUri, methodName); + return _jsCallFunction(expression, arguments); + } + + /// Call [function] with objects referred by [argumentIds] as arguments. + Future callFunction( + String function, + Iterable argumentIds, + ) { + final arguments = argumentIds.map(remoteObjectFor).toList(); + return _jsCallFunction(function, arguments); + } + + Future instanceRefFor(Object value) => + _instanceHelper.instanceRefFor(value); + + Future instanceFor(RemoteObject value) => + _instanceHelper.instanceFor(value); + + Future getLibrary(String objectId) async { + final libraryRef = await libraryRefFor(objectId); + if (libraryRef == null) return null; + return libraryHelper.libraryFor(libraryRef); + } + + Future getObject(String objectId, {int? offset, int? count}) async { + try { + final library = await getLibrary(objectId); + if (library != null) { + return library; + } + final clazz = await _classHelper.forObjectId(objectId); + if (clazz != null) { + return clazz; + } + final scriptRef = scriptRefsById[objectId]; + if (scriptRef != null) { + return _getScript(scriptRef); + } + final instance = await _instanceHelper.instanceFor( + remoteObjectFor(objectId), + offset: offset, + count: count, + ); + if (instance != null) { + return instance; + } + } catch (e, s) { + _logger.fine('getObject $objectId failed', e, s); + rethrow; + } + throw UnsupportedError( + 'Only libraries, instances, classes, and scripts ' + 'are supported for getObject', + ); + } + + Future