| // 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:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:dwds/data/debug_event.dart'; |
| import 'package:dwds/data/register_event.dart'; |
| import 'package:dwds/src/config/tool_configuration.dart'; |
| import 'package:dwds/src/connections/app_connection.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/location.dart'; |
| import 'package:dwds/src/debugging/metadata/provider.dart'; |
| import 'package:dwds/src/debugging/modules.dart'; |
| import 'package:dwds/src/debugging/remote_debugger.dart'; |
| import 'package:dwds/src/debugging/skip_list.dart'; |
| import 'package:dwds/src/events.dart'; |
| import 'package:dwds/src/readers/asset_reader.dart'; |
| import 'package:dwds/src/services/batched_expression_evaluator.dart'; |
| import 'package:dwds/src/services/debug_service.dart'; |
| import 'package:dwds/src/services/expression_compiler.dart'; |
| import 'package:dwds/src/services/expression_evaluator.dart'; |
| import 'package:dwds/src/services/proxy_service.dart'; |
| import 'package:dwds/src/utilities/dart_uri.dart'; |
| import 'package:dwds/src/utilities/shared.dart'; |
| import 'package:logging/logging.dart' hide LogRecord; |
| import 'package:vm_service/vm_service.dart' hide vmServiceVersion; |
| import 'package:vm_service_interface/vm_service_interface.dart'; |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; |
| |
| // This event is identical to the one sent by the VM service from |
| // sdk/lib/vmservice/vmservice.dart before existing VM service clients are |
| // disconnected. |
| final class DartDevelopmentServiceConnectedEvent extends Event { |
| DartDevelopmentServiceConnectedEvent({ |
| required super.timestamp, |
| required this.uri, |
| }) : message = |
| 'A Dart Developer Service instance has connected and this direct ' |
| 'connection to the VM service will now be closed. Please reconnect to ' |
| 'the Dart Development Service at $uri.', |
| super(kind: 'DartDevelopmentServiceConnected'); |
| |
| final String message; |
| final String uri; |
| |
| @override |
| Map<String, Object?> toJson() => { |
| ...super.toJson(), |
| 'uri': uri, |
| 'message': message, |
| }; |
| } |
| |
| final class DisconnectNonDartDevelopmentServiceClients extends RPCError { |
| DisconnectNonDartDevelopmentServiceClients() |
| : super('_yieldControlToDDS', kErrorCode); |
| |
| // Arbitrary error code that's unlikely to be used elsewhere. |
| static const kErrorCode = -199328; |
| } |
| |
| /// A proxy from the chrome debug protocol to the dart vm service protocol. |
| class ChromeProxyService extends ProxyService { |
| /// Signals when isolate starts. |
| Future<void> get isStarted => _startedCompleter.future; |
| Completer<void> _startedCompleter = Completer<void>(); |
| |
| /// Signals when expression compiler is ready to evaluate. |
| Future<void> get isCompilerInitialized => _compilerCompleter.future; |
| Completer<void> _compilerCompleter = Completer<void>(); |
| |
| /// The root at which we're serving. |
| final String root; |
| |
| final RemoteDebugger remoteDebugger; |
| final ExecutionContext executionContext; |
| |
| final AssetReader _assetReader; |
| |
| final Locations _locations; |
| |
| final SkipLists _skipLists; |
| |
| final Modules _modules; |
| |
| /// Provides debugger-related functionality. |
| Future<Debugger> get debuggerFuture => _debuggerCompleter.future; |
| final _debuggerCompleter = Completer<Debugger>(); |
| |
| /// Provides variable inspection functionality. |
| AppInspector get inspector { |
| if (_inspector == null) { |
| throw StateError('No running isolate (inspector is not set).'); |
| } |
| return _inspector!; |
| } |
| |
| AppInspector? _inspector; |
| |
| /// Determines if there an isolate running currently. |
| /// |
| /// [_inspector] is `null` iff the isolate is not running, |
| /// for example, before the first isolate starts or during |
| /// a hot restart. |
| bool get _isIsolateRunning => _inspector != null; |
| |
| StreamSubscription<ConsoleAPIEvent>? _consoleSubscription; |
| |
| /// If non-null, a resume event should await the result of this after resuming |
| /// execution. |
| /// |
| /// This is used to complete a hot reload. |
| Future<void> Function()? _finishHotReloadOnResume; |
| |
| final _logger = Logger('ChromeProxyService'); |
| |
| final ExpressionCompiler? _compiler; |
| ExpressionEvaluator? _expressionEvaluator; |
| |
| /// Isolate creation should wait until this completer is complete to prevent |
| /// computing metadata in an invalid state. |
| /// |
| /// Starts out completed and is reinitialized and completed when needed. |
| Completer<void> allowedToCreateIsolate = Completer<void>()..complete(); |
| |
| bool terminatingIsolates = false; |
| |
| ChromeProxyService._( |
| super.vm, |
| this.root, |
| this._assetReader, |
| this.remoteDebugger, |
| this._modules, |
| this._locations, |
| this._skipLists, |
| this.executionContext, |
| this._compiler, |
| ) { |
| final debugger = Debugger.create( |
| remoteDebugger, |
| streamNotify, |
| _locations, |
| _skipLists, |
| root, |
| ); |
| debugger.then(_debuggerCompleter.complete); |
| } |
| |
| static Future<ChromeProxyService> create( |
| RemoteDebugger remoteDebugger, |
| String root, |
| AssetReader assetReader, |
| AppConnection appConnection, |
| ExecutionContext executionContext, |
| ExpressionCompiler? expressionCompiler, |
| ) async { |
| final vm = VM( |
| name: 'ChromeDebugProxy', |
| operatingSystem: Platform.operatingSystem, |
| startTime: DateTime.now().millisecondsSinceEpoch, |
| version: Platform.version, |
| isolates: [], |
| isolateGroups: [], |
| systemIsolates: [], |
| systemIsolateGroups: [], |
| targetCPU: 'Web', |
| hostCPU: 'DWDS', |
| architectureBits: -1, |
| pid: -1, |
| ); |
| |
| final modules = Modules(root); |
| final locations = Locations(assetReader, modules, root); |
| final skipLists = SkipLists(root); |
| final service = ChromeProxyService._( |
| vm, |
| root, |
| assetReader, |
| remoteDebugger, |
| modules, |
| locations, |
| skipLists, |
| executionContext, |
| expressionCompiler, |
| ); |
| safeUnawaited(service.createIsolate(appConnection, newConnection: true)); |
| return service; |
| } |
| |
| /// Reinitializes any caches so that they can be recomputed across hot reload. |
| /// |
| /// We use the [ModifiedModuleReport] to more efficiently invalidate caches. |
| Future<void> _reinitializeForHotReload( |
| Map<String, List> reloadedModules, |
| ) async { |
| final entrypoint = inspector.appConnection.request.entrypointPath; |
| final modifiedModuleReport = await globalToolConfiguration.loadStrategy |
| .reinitializeProviderAfterHotReload(entrypoint, reloadedModules); |
| await _initializeEntrypoint( |
| entrypoint, |
| modifiedModuleReport: modifiedModuleReport, |
| ); |
| await inspector.initialize(modifiedModuleReport: modifiedModuleReport); |
| } |
| |
| /// Initializes metadata in [Locations], [Modules], and [ExpressionCompiler]. |
| /// |
| /// If [modifiedModuleReport] is not null, only removes and reinitializes |
| /// modified metadata. |
| Future<void> _initializeEntrypoint( |
| String entrypoint, { |
| ModifiedModuleReport? modifiedModuleReport, |
| }) async { |
| await _modules.initialize( |
| entrypoint, |
| modifiedModuleReport: modifiedModuleReport, |
| ); |
| await _locations.initialize( |
| entrypoint, |
| modifiedModuleReport: modifiedModuleReport, |
| ); |
| await _skipLists.initialize( |
| entrypoint, |
| modifiedModuleReport: modifiedModuleReport, |
| ); |
| // We do not need to wait for compiler dependencies to be updated as the |
| // [ExpressionEvaluator] is robust to evaluation requests during updates. |
| safeUnawaited(_updateCompilerDependencies(entrypoint)); |
| } |
| |
| Future<void> _updateCompilerDependencies(String entrypoint) async { |
| final loadStrategy = globalToolConfiguration.loadStrategy; |
| final moduleFormat = loadStrategy.moduleFormat; |
| final canaryFeatures = loadStrategy.buildSettings.canaryFeatures; |
| final experiments = loadStrategy.buildSettings.experiments; |
| |
| _logger.info('Initializing expression compiler for $entrypoint'); |
| |
| final compilerOptions = CompilerOptions( |
| moduleFormat: ModuleFormat.values.byName(moduleFormat), |
| canaryFeatures: canaryFeatures, |
| experiments: experiments, |
| ); |
| |
| final compiler = _compiler; |
| if (compiler != null) { |
| await compiler.initialize(compilerOptions); |
| final dependencies = await loadStrategy.moduleInfoForEntrypoint( |
| entrypoint, |
| ); |
| await captureElapsedTime(() async { |
| final result = await compiler.updateDependencies(dependencies); |
| // Expression evaluation is ready after dependencies are updated. |
| if (!_compilerCompleter.isCompleted) _compilerCompleter.complete(); |
| return result; |
| }, (result) => DwdsEvent.compilerUpdateDependencies(entrypoint)); |
| } |
| } |
| |
| Future<void> _prewarmExpressionCompilerCache() async { |
| // Exit early if the expression evaluation is not enabled. |
| if (_compiler == null || _expressionEvaluator == null) { |
| return; |
| } |
| // Wait until the inspector is ready. |
| await isInitialized; |
| |
| // Pre-warm the flutter framework module cache in the compiler. |
| // |
| // Flutter inspector relies on evaluations in widget_inspector |
| // library, which is a part of the flutter framework module, to |
| // produce widget trees, draw the layout explorer, show hover |
| // cards etc. |
| // Pre-warming the cache while DevTools is still loading helps |
| // Flutter Inspector start faster. |
| final libraryToCache = await inspector.flutterWidgetInspectorLibrary; |
| if (libraryToCache != null) { |
| final isolateId = inspector.isolateRef.id; |
| final libraryId = libraryToCache.id; |
| if (isolateId != null && libraryId != null) { |
| _logger.finest( |
| 'Caching ${libraryToCache.uri} in expression compiler worker', |
| ); |
| await evaluate(isolateId, libraryId, 'true'); |
| } |
| } |
| } |
| |
| /// Creates a new isolate. |
| /// |
| /// Only one isolate at a time is supported, but they should be cleaned up |
| /// with [destroyIsolate] and recreated with this method there is a hot |
| /// restart or full page refresh. |
| /// |
| /// If [newConnection] is true, this method does not recompute metadata |
| /// information as the metadata couldn't have changed. |
| @override |
| Future<void> createIsolate( |
| AppConnection appConnection, { |
| bool newConnection = false, |
| }) async { |
| // Inspector is null if the previous isolate is destroyed. |
| if (_isIsolateRunning) { |
| throw UnsupportedError( |
| 'Cannot create multiple isolates for the same app', |
| ); |
| } |
| // Wait until we're allowed to create the isolate. This is needed in hot |
| // restart as scripts may not be parsed yet. |
| if (!allowedToCreateIsolate.isCompleted) { |
| _logger.info( |
| 'Waiting until hot restart is completed before creating ' |
| 'isolate', |
| ); |
| await allowedToCreateIsolate.future; |
| } |
| // Waiting for the debugger to be ready before initializing the entrypoint. |
| // |
| // Note: moving `await debugger` after the `_initializeEntryPoint` call |
| // causes `getcwd` system calls to fail. Since that system call is used |
| // in first `Uri.base` call in the expression compiler service isolate, |
| // the expression compiler service will fail to start. |
| // Issue: https://github.com/dart-lang/webdev/issues/1282 |
| final debugger = await debuggerFuture; |
| final entrypoint = appConnection.request.entrypointPath; |
| if (!newConnection) { |
| await globalToolConfiguration.loadStrategy.trackEntrypoint(entrypoint); |
| } |
| await _initializeEntrypoint(entrypoint); |
| |
| debugger.notifyPausedAtStart(); |
| _inspector = await AppInspector.create( |
| appConnection, |
| remoteDebugger, |
| _assetReader, |
| _locations, |
| root, |
| debugger, |
| executionContext, |
| ); |
| |
| final compiler = _compiler; |
| _expressionEvaluator = |
| compiler == null |
| ? null |
| : BatchedExpressionEvaluator( |
| entrypoint, |
| inspector, |
| debugger, |
| _locations, |
| _modules, |
| compiler, |
| ); |
| |
| safeUnawaited(_prewarmExpressionCompilerCache()); |
| |
| safeUnawaited( |
| appConnection.onStart.then((_) { |
| debugger.resumeFromStart(); |
| _startedCompleter.complete(); |
| }), |
| ); |
| |
| safeUnawaited(appConnection.onDone.then((_) => destroyIsolate())); |
| |
| final isolateRef = inspector.isolateRef; |
| final timestamp = DateTime.now().millisecondsSinceEpoch; |
| |
| // Listen for `registerExtension` and `postEvent` calls. |
| _setUpChromeConsoleListeners(isolateRef); |
| |
| vm.isolates?.add(isolateRef); |
| |
| streamNotify( |
| 'Isolate', |
| Event( |
| kind: EventKind.kIsolateStart, |
| timestamp: timestamp, |
| isolate: isolateRef, |
| ), |
| ); |
| streamNotify( |
| 'Isolate', |
| Event( |
| kind: EventKind.kIsolateRunnable, |
| timestamp: timestamp, |
| isolate: isolateRef, |
| ), |
| ); |
| |
| // TODO: We shouldn't need to fire these events since they exist on the |
| // isolate, but devtools doesn't recognize extensions after a page refresh |
| // otherwise. |
| for (final extensionRpc in await inspector.getExtensionRpcs()) { |
| streamNotify( |
| 'Isolate', |
| Event( |
| kind: EventKind.kServiceExtensionAdded, |
| timestamp: timestamp, |
| isolate: isolateRef, |
| )..extensionRPC = extensionRpc, |
| ); |
| } |
| |
| // If the new isolate was created as part of a restart, send a |
| // kPausePostRequest event to notify client that the app is paused so that |
| // it can resume: |
| if (hasPendingRestart) { |
| streamNotify( |
| 'Debug', |
| Event( |
| kind: EventKind.kPausePostRequest, |
| timestamp: timestamp, |
| isolate: isolateRef, |
| ), |
| ); |
| } |
| |
| // The service is considered initialized when the first isolate is created. |
| if (!initializedCompleter.isCompleted) initializedCompleter.complete(); |
| } |
| |
| /// Should be called when there is a hot restart or full page refresh. |
| /// |
| /// Clears out the [_inspector] and all related cached information. |
| @override |
| void destroyIsolate() { |
| _logger.fine('Destroying isolate'); |
| if (!_isIsolateRunning) return; |
| |
| final isolate = inspector.isolate; |
| final isolateRef = inspector.isolateRef; |
| |
| initializedCompleter = Completer<void>(); |
| _startedCompleter = Completer<void>(); |
| _compilerCompleter = Completer<void>(); |
| streamNotify( |
| 'Isolate', |
| Event( |
| kind: EventKind.kIsolateExit, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| ), |
| ); |
| vm.isolates?.removeWhere((ref) => ref.id == isolate.id); |
| _inspector = null; |
| _expressionEvaluator?.close(); |
| _consoleSubscription?.cancel(); |
| _consoleSubscription = null; |
| } |
| |
| /// Removes the breakpoints in the running isolate. |
| Future<void> disableBreakpoints() async { |
| if (!_isIsolateRunning) return; |
| final isolate = inspector.isolate; |
| |
| final debugger = await debuggerFuture; |
| await Future.wait([ |
| for (final breakpoint in isolate.breakpoints ?? <Breakpoint>[]) |
| debugger.removeBreakpoint(breakpoint.id!), |
| ]); |
| } |
| |
| @override |
| Future<Breakpoint> addBreakpoint( |
| String isolateId, |
| String scriptId, |
| int line, { |
| int? column, |
| }) { |
| return wrapInErrorHandlerAsync( |
| 'addBreakpoint', |
| () => _addBreakpoint(isolateId, scriptId, line), |
| ); |
| } |
| |
| Future<Breakpoint> _addBreakpoint( |
| String isolateId, |
| String scriptId, |
| int line, { |
| int? column, |
| }) async { |
| await isInitialized; |
| _checkIsolate('addBreakpoint', isolateId); |
| return (await debuggerFuture).addBreakpoint(scriptId, line, column: column); |
| } |
| |
| @override |
| Future<Breakpoint> addBreakpointAtEntry(String isolateId, String functionId) { |
| return rpcNotSupportedFuture('addBreakpointAtEntry'); |
| } |
| |
| @override |
| Future<Breakpoint> addBreakpointWithScriptUri( |
| String isolateId, |
| String scriptUri, |
| int line, { |
| int? column, |
| }) => wrapInErrorHandlerAsync( |
| 'addBreakpointWithScriptUri', |
| () => |
| _addBreakpointWithScriptUri(isolateId, scriptUri, line, column: column), |
| ); |
| |
| Future<Breakpoint> _addBreakpointWithScriptUri( |
| String isolateId, |
| String scriptUri, |
| int line, { |
| int? column, |
| }) async { |
| await isInitialized; |
| _checkIsolate('addBreakpointWithScriptUri', isolateId); |
| if (Uri.parse(scriptUri).scheme == 'dart') { |
| // TODO(annagrin): Support setting breakpoints in dart SDK locations. |
| // Issue: https://github.com/dart-lang/webdev/issues/1584 |
| throw RPCError( |
| 'addBreakpoint', |
| 102, |
| 'The VM is unable to add a breakpoint ' |
| 'at the specified line or function: $scriptUri:$line:$column: ' |
| 'breakpoints in dart SDK locations are not supported yet.', |
| ); |
| } |
| final dartUri = DartUri(scriptUri, root); |
| final scriptRef = await inspector.scriptRefFor(dartUri.serverPath); |
| final scriptId = scriptRef?.id; |
| if (scriptId == null) { |
| throw RPCError( |
| 'addBreakpoint', |
| 102, |
| 'The VM is unable to add a breakpoint ' |
| 'at the specified line or function: $scriptUri:$line:$column: ' |
| 'cannot find script ID for ${dartUri.serverPath}', |
| ); |
| } |
| return (await debuggerFuture).addBreakpoint(scriptId, line, column: column); |
| } |
| |
| @override |
| Future<Response> callServiceExtension( |
| String method, { |
| String? isolateId, |
| Map? args, |
| }) => wrapInErrorHandlerAsync( |
| 'callServiceExtension', |
| () => _callServiceExtension(method, isolateId: isolateId, args: args), |
| ); |
| |
| Future<Response> _callServiceExtension( |
| String method, { |
| String? isolateId, |
| Map? args, |
| }) async { |
| await isInitialized; |
| isolateId ??= _inspector?.isolate.id; |
| _checkIsolate('callServiceExtension', isolateId); |
| args ??= <String, String>{}; |
| final stringArgs = args.map( |
| (k, v) => MapEntry( |
| k is String ? k : jsonEncode(k), |
| v is String ? v : jsonEncode(v), |
| ), |
| ); |
| if (!(await inspector.getExtensionRpcs()).contains(method)) { |
| throw RPCError( |
| method, |
| RPCErrorKind.kMethodNotFound.code, |
| 'Unknown service method: $method', |
| ); |
| } |
| final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger |
| .invokeExtensionJsExpression(method, jsonEncode(stringArgs)); |
| final result = await inspector.jsEvaluate(expression, awaitPromise: true); |
| final decodedResponse = |
| jsonDecode(result.value as String) as Map<String, dynamic>; |
| if (decodedResponse.containsKey('code') && |
| decodedResponse.containsKey('message') && |
| decodedResponse.containsKey('data')) { |
| // ignore: only_throw_errors |
| throw RPCError( |
| method, |
| decodedResponse['code'] as int, |
| // ignore: avoid-unnecessary-type-casts |
| decodedResponse['message'] as String, |
| decodedResponse['data'] as Map, |
| ); |
| } else { |
| return Response()..json = decodedResponse; |
| } |
| } |
| |
| Future<Response> _getEvaluationResult( |
| String isolateId, |
| Future<RemoteObject> Function() evaluation, |
| String expression, |
| ) async { |
| try { |
| final result = await evaluation(); |
| if (!_isIsolateRunning || isolateId != inspector.isolate.id) { |
| _logger.fine( |
| 'Cannot get evaluation result for isolate $isolateId: ' |
| ' isolate exited.', |
| ); |
| return ErrorRef( |
| kind: 'error', |
| message: 'Isolate exited', |
| id: createId(), |
| ); |
| } |
| |
| // Handle compilation errors, internal errors, |
| // and reference errors from JavaScript evaluation in chrome. |
| if (_hasEvaluationError(result.type)) { |
| if (_hasReportableEvaluationError(result.type)) { |
| _logger.warning( |
| 'Failed to evaluate expression \'$expression\': ' |
| '${result.type}: ${result.value}.', |
| ); |
| |
| _logger.info( |
| 'Please follow instructions at ' |
| 'https://github.com/dart-lang/webdev/issues/956 ' |
| 'to file a bug.', |
| ); |
| } |
| return ErrorRef( |
| kind: 'error', |
| message: '${result.type}: ${result.value}', |
| id: createId(), |
| ); |
| } |
| return await _instanceRef(result); |
| } on RPCError catch (_) { |
| rethrow; |
| } catch (e, s) { |
| // Handle errors that throw exceptions, such as invalid JavaScript |
| // generated by the expression evaluator. |
| _logger.warning('Failed to evaluate expression \'$expression\'. '); |
| _logger.info( |
| 'Please follow instructions at ' |
| 'https://github.com/dart-lang/webdev/issues/956 ' |
| 'to file a bug.', |
| ); |
| _logger.info('$e:$s'); |
| return ErrorRef(kind: 'error', message: '<unknown>', id: createId()); |
| } |
| } |
| |
| bool _hasEvaluationError(String type) => type.contains('Error'); |
| |
| // Decides if the error is serious enough to be shown to the user |
| // to encourage bug reporting. |
| bool _hasReportableEvaluationError(String type) { |
| if (!_hasEvaluationError(type)) return false; |
| |
| if (type == EvaluationErrorKind.compilation || |
| type == EvaluationErrorKind.asyncFrame) { |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Future<Response> evaluate( |
| String isolateId, |
| String targetId, |
| String expression, { |
| Map<String, String>? scope, |
| // TODO(798) - respect disableBreakpoints. |
| bool? disableBreakpoints, |
| |
| /// Note that `idZoneId` arguments will be ignored. This parameter is only |
| /// here to make this method is a valid override of |
| /// [VmServiceInterface.evaluate]. |
| String? idZoneId, |
| }) => wrapInErrorHandlerAsync( |
| 'evaluate', |
| () => _evaluate(isolateId, targetId, expression, scope: scope), |
| ); |
| |
| Future<Response> _evaluate( |
| String isolateId, |
| String targetId, |
| String expression, { |
| Map<String, String>? scope, |
| }) { |
| // TODO(798) - respect disableBreakpoints. |
| return captureElapsedTime(() async { |
| await isInitialized; |
| final evaluator = _expressionEvaluator; |
| if (evaluator != null) { |
| await isCompilerInitialized; |
| _checkIsolate('evaluate', isolateId); |
| |
| late Obj object; |
| try { |
| object = await inspector.getObject(targetId); |
| } catch (_) { |
| return ErrorRef( |
| kind: 'error', |
| message: |
| 'Evaluate is called on an unsupported target:' |
| '$targetId', |
| id: createId(), |
| ); |
| } |
| |
| final library = object is Library ? object : inspector.isolate.rootLib; |
| |
| if (object is Instance) { |
| // Evaluate is called on a target - convert this to a dart |
| // expression and scope by adding a target variable to the |
| // expression and the scope, for example: |
| // |
| // Library: 'package:hello_world/main.dart' |
| // Expression: 'hashCode' => 'x.hashCode' |
| // Scope: {} => { 'x' : targetId } |
| |
| final target = _newVariableForScope(scope); |
| expression = '$target.$expression'; |
| scope = (scope ?? {})..addAll({target: targetId}); |
| } |
| |
| return await _getEvaluationResult( |
| isolateId, |
| () => evaluator.evaluateExpression( |
| isolateId, |
| library?.uri, |
| expression, |
| scope, |
| ), |
| expression, |
| ); |
| } |
| throw RPCError( |
| 'evaluate', |
| RPCErrorKind.kInvalidRequest.code, |
| 'Expression evaluation is not supported for this configuration.', |
| ); |
| }, (result) => DwdsEvent.evaluate(expression, result)); |
| } |
| |
| String _newVariableForScope(Map<String, String>? scope) { |
| // Find a new variable not in scope. |
| var candidate = 'x'; |
| while (scope?.containsKey(candidate) ?? false) { |
| candidate += '\$1'; |
| } |
| return candidate; |
| } |
| |
| @override |
| Future<Response> evaluateInFrame( |
| String isolateId, |
| int frameIndex, |
| String expression, { |
| Map<String, String>? scope, |
| // TODO(798) - respect disableBreakpoints. |
| bool? disableBreakpoints, |
| |
| /// Note that `idZoneId` arguments will be ignored. This parameter is only |
| /// here to make this method is a valid override of |
| /// [VmServiceInterface.evaluateInFrame]. |
| String? idZoneId, |
| }) => wrapInErrorHandlerAsync( |
| 'evaluateInFrame', |
| () => _evaluateInFrame(isolateId, frameIndex, expression, scope: scope), |
| ); |
| |
| Future<Response> _evaluateInFrame( |
| String isolateId, |
| int frameIndex, |
| String expression, { |
| Map<String, String>? scope, |
| }) { |
| // TODO(798) - respect disableBreakpoints. |
| |
| return captureElapsedTime(() async { |
| await isInitialized; |
| final evaluator = _expressionEvaluator; |
| if (evaluator != null) { |
| await isCompilerInitialized; |
| _checkIsolate('evaluateInFrame', isolateId); |
| |
| return await _getEvaluationResult( |
| isolateId, |
| () => evaluator.evaluateExpressionInFrame( |
| isolateId, |
| frameIndex, |
| expression, |
| scope, |
| ), |
| expression, |
| ); |
| } |
| throw RPCError( |
| 'evaluateInFrame', |
| RPCErrorKind.kInvalidRequest.code, |
| 'Expression evaluation is not supported for this configuration.', |
| ); |
| }, (result) => DwdsEvent.evaluateInFrame(expression, result)); |
| } |
| |
| @override |
| Future<Isolate> getIsolate(String isolateId) => |
| wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId)); |
| |
| Future<Isolate> _getIsolate(String isolateId) { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| _checkIsolate('getIsolate', isolateId); |
| return inspector.isolate; |
| }, (result) => DwdsEvent.getIsolate()); |
| } |
| |
| @override |
| Future<MemoryUsage> getMemoryUsage(String isolateId) => |
| wrapInErrorHandlerAsync( |
| 'getMemoryUsage', |
| () => _getMemoryUsage(isolateId), |
| ); |
| |
| Future<MemoryUsage> _getMemoryUsage(String isolateId) async { |
| await isInitialized; |
| _checkIsolate('getMemoryUsage', isolateId); |
| return inspector.getMemoryUsage(); |
| } |
| |
| @override |
| Future<Obj> getObject( |
| String isolateId, |
| String objectId, { |
| int? offset, |
| int? count, |
| |
| /// Note that `idZoneId` arguments will be ignored. This parameter is only |
| /// here to make this method is a valid override of |
| /// [VmServiceInterface.getObject]. |
| String? idZoneId, |
| }) => wrapInErrorHandlerAsync( |
| 'getObject', |
| () => _getObject(isolateId, objectId, offset: offset, count: count), |
| ); |
| |
| Future<Obj> _getObject( |
| String isolateId, |
| String objectId, { |
| int? offset, |
| int? count, |
| }) async { |
| await isInitialized; |
| _checkIsolate('getObject', isolateId); |
| return inspector.getObject(objectId, offset: offset, count: count); |
| } |
| |
| @override |
| Future<ScriptList> getScripts(String isolateId) => |
| wrapInErrorHandlerAsync('getScripts', () => _getScripts(isolateId)); |
| |
| Future<ScriptList> _getScripts(String isolateId) { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| _checkIsolate('getScripts', isolateId); |
| return inspector.getScripts(); |
| }, (result) => DwdsEvent.getScripts()); |
| } |
| |
| @override |
| Future<SourceReport> getSourceReport( |
| String isolateId, |
| List<String> reports, { |
| String? scriptId, |
| int? tokenPos, |
| int? endTokenPos, |
| bool? forceCompile, |
| bool? reportLines, |
| List<String>? libraryFilters, |
| // Note: Ignore the optional librariesAlreadyCompiled parameter. It is here |
| // to match the VM service interface. |
| List<String>? librariesAlreadyCompiled, |
| }) => wrapInErrorHandlerAsync( |
| 'getSourceReport', |
| () => _getSourceReport( |
| isolateId, |
| reports, |
| scriptId: scriptId, |
| tokenPos: tokenPos, |
| endTokenPos: endTokenPos, |
| forceCompile: forceCompile, |
| reportLines: reportLines, |
| libraryFilters: libraryFilters, |
| ), |
| ); |
| |
| Future<SourceReport> _getSourceReport( |
| String isolateId, |
| List<String> reports, { |
| String? scriptId, |
| int? tokenPos, |
| int? endTokenPos, |
| bool? forceCompile, |
| bool? reportLines, |
| List<String>? libraryFilters, |
| }) { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| _checkIsolate('getSourceReport', isolateId); |
| return await inspector.getSourceReport( |
| reports, |
| scriptId: scriptId, |
| tokenPos: tokenPos, |
| endTokenPos: endTokenPos, |
| forceCompile: forceCompile, |
| reportLines: reportLines, |
| libraryFilters: libraryFilters, |
| ); |
| }, (result) => DwdsEvent.getSourceReport()); |
| } |
| |
| /// Returns the current stack. |
| /// |
| /// Throws RPCError the corresponding isolate is not paused. |
| /// |
| /// The returned stack will contain up to [limit] frames if provided. |
| @override |
| Future<Stack> getStack( |
| String isolateId, { |
| int? limit, |
| |
| /// Note that `idZoneId` arguments will be ignored. This parameter is only |
| /// here to make this method is a valid override of |
| /// [VmServiceInterface.getStack]. |
| String? idZoneId, |
| }) => wrapInErrorHandlerAsync( |
| 'getStack', |
| () => _getStack(isolateId, limit: limit), |
| ); |
| |
| Future<Stack> _getStack(String isolateId, {int? limit}) async { |
| await isInitialized; |
| await isStarted; |
| _checkIsolate('getStack', isolateId); |
| return (await debuggerFuture).getStack(limit: limit); |
| } |
| |
| @override |
| Future<VM> getVM() => wrapInErrorHandlerAsync('getVM', _getVM); |
| |
| Future<VM> _getVM() { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| return vm; |
| }, (result) => DwdsEvent.getVM()); |
| } |
| |
| @override |
| Future<Response> invoke( |
| String isolateId, |
| String targetId, |
| String selector, |
| List argumentIds, { |
| // TODO(798) - respect disableBreakpoints. |
| bool? disableBreakpoints, |
| |
| /// Note that `idZoneId` arguments will be ignored. This parameter is only |
| /// here to make this method is a valid override of |
| /// [VmServiceInterface.invoke]. |
| String? idZoneId, |
| }) => wrapInErrorHandlerAsync( |
| 'invoke', |
| () => _invoke(isolateId, targetId, selector, argumentIds), |
| ); |
| |
| Future<Response> _invoke( |
| String isolateId, |
| String targetId, |
| String selector, |
| List argumentIds, |
| ) async { |
| await isInitialized; |
| _checkIsolate('invoke', isolateId); |
| final remote = await inspector.invoke(targetId, selector, argumentIds); |
| return _instanceRef(remote); |
| } |
| |
| @override |
| Stream<Event> onEvent(String streamId) { |
| return streamControllers.putIfAbsent(streamId, () { |
| switch (streamId) { |
| case EventStreams.kExtension: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kIsolate: |
| // TODO: right now we only support the `ServiceExtensionAdded` event |
| // for the Isolate stream. |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kVM: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kGC: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kTimeline: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kService: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kDebug: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kLogging: |
| return StreamController<Event>.broadcast(); |
| case EventStreams.kStdout: |
| return _chromeConsoleStreamController( |
| (e) => _stdoutTypes.contains(e.type), |
| ); |
| case EventStreams.kStderr: |
| return _chromeConsoleStreamController( |
| (e) => _stderrTypes.contains(e.type), |
| includeExceptions: true, |
| ); |
| default: |
| throw RPCError( |
| 'streamListen', |
| RPCErrorKind.kInvalidParams.code, |
| 'The stream `$streamId` is not supported on web devices', |
| ); |
| } |
| }).stream; |
| } |
| |
| @override |
| Future<Success> pause(String isolateId) => |
| wrapInErrorHandlerAsync('pause', () => _pause(isolateId)); |
| |
| Future<Success> _pause(String isolateId) async { |
| await isInitialized; |
| _checkIsolate('pause', isolateId); |
| return (await debuggerFuture).pause(); |
| } |
| |
| // Note: Ignore the optional local parameter, when it is set to `true` the |
| // request is intercepted and handled by DDS. |
| @override |
| Future<UriList> lookupResolvedPackageUris( |
| String isolateId, |
| List<String> uris, { |
| bool? local, |
| }) => wrapInErrorHandlerAsync( |
| 'lookupResolvedPackageUris', |
| () => _lookupResolvedPackageUris(isolateId, uris), |
| ); |
| |
| Future<UriList> _lookupResolvedPackageUris( |
| String isolateId, |
| List<String> uris, |
| ) async { |
| await isInitialized; |
| _checkIsolate('lookupResolvedPackageUris', isolateId); |
| return UriList(uris: uris.map(DartUri.toResolvedUri).toList()); |
| } |
| |
| @override |
| Future<UriList> lookupPackageUris(String isolateId, List<String> uris) => |
| wrapInErrorHandlerAsync( |
| 'lookupPackageUris', |
| () => _lookupPackageUris(isolateId, uris), |
| ); |
| |
| Future<UriList> _lookupPackageUris( |
| String isolateId, |
| List<String> uris, |
| ) async { |
| await isInitialized; |
| _checkIsolate('lookupPackageUris', isolateId); |
| return UriList(uris: uris.map(DartUri.toPackageUri).toList()); |
| } |
| |
| @override |
| Future<Success> registerService(String service, String alias) { |
| return rpcNotSupportedFuture('registerService'); |
| } |
| |
| @override |
| Future<ReloadReport> reloadSources( |
| String isolateId, { |
| bool? force, |
| bool? pause, |
| String? rootLibUri, |
| String? packagesUri, |
| }) async { |
| await isInitialized; |
| _checkIsolate('reloadSources', isolateId); |
| |
| ReloadReport getFailedReloadReport(String error) => |
| _ReloadReportWithMetadata(success: false) |
| ..json = { |
| 'notices': [ |
| {'message': error}, |
| ], |
| }; |
| try { |
| await _performClientSideHotReload(isolateId); |
| } catch (e) { |
| _logger.info('Hot reload failed: $e'); |
| return getFailedReloadReport(e.toString()); |
| } |
| _logger.info('Successful hot reload'); |
| return _ReloadReportWithMetadata(success: true); |
| } |
| |
| /// Performs a client-side hot reload by fetching libraries, handling |
| /// PausePostRequests, and invoking the reload. |
| Future<void> _performClientSideHotReload(String isolateId) async { |
| _logger.info('Attempting a hot reload'); |
| |
| final debugger = await debuggerFuture; |
| final reloadedSrcs = <String>{}; |
| final computedReloadedSrcs = Completer<void>(); |
| final parsedAllReloadedSrcs = Completer<void>(); |
| // Wait until all the reloaded scripts are parsed before we reinitialize |
| // metadata below. |
| final parsedScriptsSubscription = debugger.parsedScriptsController.stream |
| .listen((url) { |
| computedReloadedSrcs.future.then((_) { |
| reloadedSrcs.remove(Uri.parse(url).normalizePath().path); |
| if (reloadedSrcs.isEmpty && !parsedAllReloadedSrcs.isCompleted) { |
| parsedAllReloadedSrcs.complete(); |
| } |
| }); |
| }); |
| |
| // Initiate a hot reload. |
| _logger.info('Issuing \$dartHotReloadStartDwds request'); |
| final remoteObject = await inspector.jsEvaluate( |
| '\$dartHotReloadStartDwds();', |
| awaitPromise: true, |
| returnByValue: true, |
| ); |
| final reloadedSrcModuleLibraries = (remoteObject.value as List).cast<Map>(); |
| final reloadedModulesToLibraries = <String, List<String>>{}; |
| for (final srcModuleLibrary in reloadedSrcModuleLibraries) { |
| final srcModuleLibraryCast = srcModuleLibrary.cast<String, Object>(); |
| reloadedSrcs.add( |
| Uri.parse(srcModuleLibraryCast['src'] as String).normalizePath().path, |
| ); |
| reloadedModulesToLibraries[srcModuleLibraryCast['module'] as String] = |
| (srcModuleLibraryCast['libraries'] as List).cast<String>(); |
| } |
| computedReloadedSrcs.complete(); |
| if (reloadedSrcs.isNotEmpty) await parsedAllReloadedSrcs.future; |
| await parsedScriptsSubscription.cancel(); |
| |
| if (!pauseIsolatesOnStart) { |
| // Finish hot reload immediately. |
| _logger.info('Issuing \$dartHotReloadEndDwds request'); |
| await inspector.jsEvaluate( |
| '\$dartHotReloadEndDwds();', |
| awaitPromise: true, |
| ); |
| _logger.info('\$dartHotReloadEndDwds request complete.'); |
| // TODO(srujzs): Supposedly Dart DevTools uses a kIsolateReload event |
| // for breakpoints? We should confirm and add tests before sending the |
| // event. |
| return; |
| } |
| // If `pause_isolates_on_start` is enabled, pause and then the reload |
| // should finish later after the client removes breakpoints, reregisters |
| // breakpoints, and resumes. |
| _finishHotReloadOnResume = () async { |
| // Client finished setting breakpoints, called resume, and now the |
| // execution has resumed. Finish the hot reload so we start executing |
| // the new code instead. |
| _logger.info('Issuing \$dartHotReloadEndDwds request'); |
| await inspector.jsEvaluate( |
| '\$dartHotReloadEndDwds();', |
| awaitPromise: true, |
| ); |
| _logger.info('\$dartHotReloadEndDwds request complete.'); |
| }; |
| |
| // Pause and wait for the pause to occur before managing breakpoints. |
| final pausedEvent = _firstStreamEvent('Debug', EventKind.kPauseInterrupted); |
| await pause(isolateId); |
| await pausedEvent; |
| |
| await _reinitializeForHotReload(reloadedModulesToLibraries); |
| |
| // This lets the client know that we're ready for breakpoint management |
| // and a resume. |
| streamNotify( |
| 'Debug', |
| Event( |
| kind: EventKind.kPausePostRequest, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: inspector.isolateRef, |
| ), |
| ); |
| } |
| |
| @override |
| Future<Success> removeBreakpoint(String isolateId, String breakpointId) => |
| wrapInErrorHandlerAsync( |
| 'removeBreakpoint', |
| () => _removeBreakpoint(isolateId, breakpointId), |
| ); |
| |
| Future<Success> _removeBreakpoint( |
| String isolateId, |
| String breakpointId, |
| ) async { |
| await isInitialized; |
| _checkIsolate('removeBreakpoint', isolateId); |
| return (await debuggerFuture).removeBreakpoint(breakpointId); |
| } |
| |
| @override |
| Future<Success> resume(String isolateId, {String? step, int? frameIndex}) => |
| wrapInErrorHandlerAsync( |
| 'resume', |
| () => _resume(isolateId, step: step, frameIndex: frameIndex), |
| ); |
| |
| Future<Success> _resume( |
| String isolateId, { |
| String? step, |
| int? frameIndex, |
| }) async { |
| // If there is a subscriber listening for a resume event after hot-restart, |
| // then add the event to the stream and skip processing it. |
| if (resumeAfterRestartEventsController.hasListener) { |
| resumeAfterRestartEventsController.add(isolateId); |
| return Success(); |
| } |
| |
| if (inspector.appConnection.isStarted) { |
| await captureElapsedTime(() async { |
| await isInitialized; |
| await isStarted; |
| _checkIsolate('resume', isolateId); |
| final debugger = await debuggerFuture; |
| return await debugger.resume(step: step, frameIndex: frameIndex); |
| }, (result) => DwdsEvent.resume(step)); |
| } else { |
| inspector.appConnection.runMain(); |
| } |
| // Finish the hot reload if needed. |
| if (_finishHotReloadOnResume != null) { |
| await _finishHotReloadOnResume!(); |
| _finishHotReloadOnResume = null; |
| } |
| return Success(); |
| } |
| |
| /// This method is deprecated in vm_service package. |
| /// |
| /// TODO(annagrin): remove after dart-code and IntelliJ stop using this API. |
| /// Issue: https://github.com/dart-lang/webdev/issues/1868 |
| /// |
| // ignore: annotate_overrides |
| Future<Success> setExceptionPauseMode( |
| String isolateId, |
| /*ExceptionPauseMode*/ |
| String mode, |
| ) => setIsolatePauseMode(isolateId, exceptionPauseMode: mode); |
| |
| @override |
| Future<Success> setIsolatePauseMode( |
| String isolateId, { |
| String? exceptionPauseMode, |
| // TODO(elliette): Is there a way to respect the shouldPauseOnExit parameter |
| // in Chrome? |
| bool? shouldPauseOnExit, |
| }) => wrapInErrorHandlerAsync( |
| 'setIsolatePauseMode', |
| () => |
| _setIsolatePauseMode(isolateId, exceptionPauseMode: exceptionPauseMode), |
| ); |
| |
| Future<Success> _setIsolatePauseMode( |
| String isolateId, { |
| String? exceptionPauseMode, |
| }) async { |
| await isInitialized; |
| _checkIsolate('setIsolatePauseMode', isolateId); |
| return (await debuggerFuture).setExceptionPauseMode( |
| exceptionPauseMode ?? ExceptionPauseMode.kNone, |
| ); |
| } |
| |
| @override |
| Future<Success> setFlag(String name, String value) => |
| wrapInErrorHandlerAsync('setFlag', () => _setFlag(name, value)); |
| |
| Future<Success> _setFlag(String name, String value) async { |
| if (!currentVmServiceFlags.containsKey(name)) { |
| return rpcNotSupportedFuture('setFlag'); |
| } |
| |
| assert(value == 'true' || value == 'false'); |
| currentVmServiceFlags[name] = value == 'true'; |
| |
| return Success(); |
| } |
| |
| @override |
| Future<Success> setLibraryDebuggable( |
| String isolateId, |
| String libraryId, |
| bool isDebuggable, |
| ) { |
| return rpcNotSupportedFuture('setLibraryDebuggable'); |
| } |
| |
| @override |
| Future<Success> setName(String isolateId, String name) => |
| wrapInErrorHandlerAsync('setName', () => _setName(isolateId, name)); |
| |
| Future<Success> _setName(String isolateId, String name) async { |
| await isInitialized; |
| _checkIsolate('setName', isolateId); |
| inspector.isolate.name = name; |
| return Success(); |
| } |
| |
| @override |
| Future<Success> setVMName(String name) => |
| wrapInErrorHandlerAsync('setVMName', () => _setVMName(name)); |
| |
| Future<Success> _setVMName(String name) async { |
| vm.name = name; |
| streamNotify( |
| 'VM', |
| Event( |
| kind: EventKind.kVMUpdate, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| // We are not guaranteed to have an isolate at this point in time. |
| isolate: null, |
| )..vm = toVMRef(vm), |
| ); |
| return Success(); |
| } |
| |
| @override |
| Future<Success> streamListen(String streamId) => |
| wrapInErrorHandlerAsync('streamListen', () => _streamListen(streamId)); |
| |
| Future<Success> _streamListen(String streamId) async { |
| // TODO: This should return an error if the stream is already being listened |
| // to. |
| onEvent(streamId); |
| return Success(); |
| } |
| |
| /// Returns a streamController that listens for console logs from chrome and |
| /// adds all events passing [filter] to the stream. |
| StreamController<Event> _chromeConsoleStreamController( |
| bool Function(ConsoleAPIEvent) filter, { |
| bool includeExceptions = false, |
| }) { |
| late StreamController<Event> controller; |
| StreamSubscription? chromeConsoleSubscription; |
| StreamSubscription? exceptionsSubscription; |
| |
| // This is an edge case for this lint apparently |
| // |
| // ignore: join_return_with_assignment |
| controller = StreamController<Event>.broadcast( |
| onCancel: () { |
| chromeConsoleSubscription?.cancel(); |
| exceptionsSubscription?.cancel(); |
| }, |
| onListen: () { |
| chromeConsoleSubscription = remoteDebugger.onConsoleAPICalled.listen(( |
| e, |
| ) { |
| if (!_isIsolateRunning) return; |
| final isolateRef = inspector.isolateRef; |
| if (!filter(e)) return; |
| final args = e.params?['args'] as List?; |
| final item = args?[0] as Map?; |
| final value = '${item?["value"]}\n'; |
| controller.add( |
| Event( |
| kind: EventKind.kWriteEvent, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| ) |
| ..bytes = base64.encode(utf8.encode(value)) |
| ..timestamp = e.timestamp.toInt(), |
| ); |
| }); |
| if (includeExceptions) { |
| exceptionsSubscription = remoteDebugger.onExceptionThrown.listen(( |
| e, |
| ) async { |
| if (!_isIsolateRunning) return; |
| final isolateRef = inspector.isolateRef; |
| var description = e.exceptionDetails.exception?.description; |
| if (description != null) { |
| description = await inspector.mapExceptionStackTrace(description); |
| } |
| controller.add( |
| Event( |
| kind: EventKind.kWriteEvent, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| )..bytes = base64.encode(utf8.encode(description ?? '')), |
| ); |
| }); |
| } |
| }, |
| ); |
| return controller; |
| } |
| |
| /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service |
| /// protocol [Event]s. |
| @override |
| void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) { |
| for (final debugEvent in debugEvents.events) { |
| parseDebugEvent(debugEvent); |
| } |
| } |
| |
| /// Parses the [DebugEvent] and emits a corresponding Dart VM Service |
| /// protocol [Event]. |
| @override |
| void parseDebugEvent(DebugEvent debugEvent) { |
| if (terminatingIsolates) return; |
| if (!_isIsolateRunning) return; |
| final isolateRef = inspector.isolateRef; |
| |
| streamNotify( |
| EventStreams.kExtension, |
| Event( |
| kind: EventKind.kExtension, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| ) |
| ..extensionKind = debugEvent.kind |
| ..extensionData = ExtensionData.parse( |
| jsonDecode(debugEvent.eventData) as Map<String, dynamic>, |
| ), |
| ); |
| } |
| |
| /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service |
| /// protocol [Event]. |
| @override |
| void parseRegisterEvent(RegisterEvent registerEvent) { |
| if (terminatingIsolates) return; |
| if (!_isIsolateRunning) return; |
| |
| final isolate = inspector.isolate; |
| final isolateRef = inspector.isolateRef; |
| final service = registerEvent.eventData; |
| isolate.extensionRPCs?.add(service); |
| |
| streamNotify( |
| EventStreams.kIsolate, |
| Event( |
| kind: EventKind.kServiceExtensionAdded, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| )..extensionRPC = service, |
| ); |
| } |
| |
| /// Listens for chrome console events and handles the ones we care about. |
| void _setUpChromeConsoleListeners(IsolateRef isolateRef) { |
| _consoleSubscription = remoteDebugger.onConsoleAPICalled.listen(( |
| event, |
| ) async { |
| if (terminatingIsolates) return; |
| if (event.type != 'debug') return; |
| if (!_isIsolateRunning) return; |
| |
| final isolate = inspector.isolate; |
| if (isolateRef.id != isolate.id) return; |
| |
| final args = event.args; |
| final firstArgValue = (args.isNotEmpty ? args[0].value : null) as String?; |
| switch (firstArgValue) { |
| case 'dart.developer.inspect': |
| // All inspected objects should be real objects. |
| if (event.args[1].type != 'object') break; |
| |
| final inspectee = await _instanceRef(event.args[1]); |
| streamNotify( |
| EventStreams.kDebug, |
| Event( |
| kind: EventKind.kInspect, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| ) |
| ..inspectee = inspectee |
| ..timestamp = event.timestamp.toInt(), |
| ); |
| break; |
| case 'dart.developer.log': |
| await _handleDeveloperLog(isolateRef, event).catchError( |
| (error, stackTrace) => _logger.warning( |
| 'Error handling developer log:', |
| error, |
| stackTrace, |
| ), |
| ); |
| break; |
| default: |
| break; |
| } |
| }); |
| } |
| |
| Future<void> _firstStreamEvent(String streamId, String eventKind) { |
| final controller = streamControllers[streamId]!; |
| return controller.stream.firstWhere((event) => event.kind == eventKind); |
| } |
| |
| Future<void> _handleDeveloperLog( |
| IsolateRef isolateRef, |
| ConsoleAPIEvent event, |
| ) async { |
| final logObject = event.params?['args'][1] as Map?; |
| final objectId = logObject?['objectId']; |
| // Always attempt to fetch the full properties instead of relying on |
| // `RemoteObject.preview` which only has truncated log messages: |
| // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject |
| final logParams = |
| objectId != null |
| ? await _fetchFullLogParams(objectId, logObject: logObject) |
| : _fetchAbbreviatedLogParams(logObject); |
| |
| final logRecord = LogRecord( |
| message: await _instanceRef(logParams['message']), |
| loggerName: await _instanceRef(logParams['name']), |
| level: |
| logParams['level'] != null |
| ? int.tryParse(logParams['level']!.value.toString()) |
| : 0, |
| error: await _instanceRef(logParams['error']), |
| time: event.timestamp.toInt(), |
| sequenceNumber: |
| logParams['sequenceNumber'] != null |
| ? int.tryParse(logParams['sequenceNumber']!.value.toString()) |
| : 0, |
| stackTrace: await _instanceRef(logParams['stackTrace']), |
| zone: await _instanceRef(logParams['zone']), |
| ); |
| |
| streamNotify( |
| EventStreams.kLogging, |
| Event( |
| kind: EventKind.kLogging, |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| isolate: isolateRef, |
| ) |
| ..logRecord = logRecord |
| ..timestamp = event.timestamp.toInt(), |
| ); |
| } |
| |
| Future<Map<String, RemoteObject>> _fetchFullLogParams( |
| String objectId, { |
| required Map? logObject, |
| }) async { |
| final logParams = <String, RemoteObject>{}; |
| for (final property in await inspector.getProperties(objectId)) { |
| final name = property.name; |
| final value = property.value; |
| if (name != null && value != null) { |
| logParams[name] = value; |
| } |
| } |
| |
| // If for some reason we don't get the full log params, then return the |
| // abbreviated version instead: |
| if (logParams.isEmpty) { |
| return _fetchAbbreviatedLogParams(logObject); |
| } |
| return logParams; |
| } |
| |
| Map<String, RemoteObject> _fetchAbbreviatedLogParams(Map? logObject) { |
| final logParams = <String, RemoteObject>{}; |
| for (final dynamic property in logObject?['preview']?['properties'] ?? []) { |
| if (property is Map<String, dynamic> && property['name'] != null) { |
| logParams[property['name'] as String] = RemoteObject(property); |
| } |
| } |
| return logParams; |
| } |
| |
| @override |
| Future<void> yieldControlToDDS(String uri) async { |
| // This will throw an RPCError if there's already an existing DDS instance. |
| ChromeDebugService.yieldControlToDDS(uri); |
| |
| // Notify existing clients that DDS has connected and they're about to be |
| // disconnected. |
| final event = DartDevelopmentServiceConnectedEvent( |
| timestamp: DateTime.now().millisecondsSinceEpoch, |
| uri: uri, |
| ); |
| streamNotify(EventStreams.kService, event); |
| |
| // We throw since we have no other way to control what the response content |
| // is for this RPC. The debug service will check for this particular |
| // exception as a signal to close connections to all other clients. |
| throw DisconnectNonDartDevelopmentServiceClients(); |
| } |
| |
| Future<InstanceRef> _instanceRef(RemoteObject? obj) async { |
| final instance = obj == null ? null : await inspector.instanceRefFor(obj); |
| return instance ?? InstanceHelper.kNullInstanceRef; |
| } |
| |
| /// Validate that isolateId matches the current isolate we're connected to and |
| /// return that isolate. |
| /// |
| /// This is useful to call at the beginning of API methods that are passed an |
| /// isolate id. |
| Isolate _checkIsolate(String methodName, String? isolateId) { |
| final currentIsolateId = inspector.isolate.id; |
| if (currentIsolateId == null) { |
| throw StateError('No running isolate ID'); |
| } |
| if (isolateId != currentIsolateId) { |
| _throwSentinel( |
| methodName, |
| SentinelKind.kCollected, |
| 'Unrecognized isolateId: $isolateId', |
| ); |
| } |
| return inspector.isolate; |
| } |
| |
| static Never _throwSentinel(String method, String kind, String message) { |
| final data = <String, String>{'kind': kind, 'valueAsString': message}; |
| throw SentinelException.parse(method, data); |
| } |
| } |
| |
| // The default `ReloadReport`'s `toJson` only emits the type and success of the |
| // operation. Override it to expose additional possible metadata in the `json`. |
| // This then gets called in the VM service code that handles request and |
| // responses. |
| class _ReloadReportWithMetadata extends ReloadReport { |
| _ReloadReportWithMetadata({super.success}); |
| |
| @override |
| Map<String, dynamic> toJson() { |
| final jsonified = <String, Object?>{ |
| 'type': type, |
| 'success': success ?? false, |
| }; |
| // Include any other metadata we may have included in `json`, potentially |
| // even overriding the above defaults. |
| for (final jsonKey in super.json?.keys ?? <String>[]) { |
| jsonified[jsonKey] = super.json![jsonKey]; |
| } |
| return jsonified; |
| } |
| } |
| |
| /// The `type`s of [ConsoleAPIEvent]s that are treated as `stderr` logs. |
| const _stderrTypes = ['error']; |
| |
| /// The `type`s of [ConsoleAPIEvent]s that are treated as `stdout` logs. |
| const _stdoutTypes = ['log', 'info', 'warning']; |