| // 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:logging/logging.dart' hide LogRecord; |
| import 'package:pub_semver/pub_semver.dart' as semver; |
| import 'package:vm_service/vm_service.dart'; |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; |
| |
| import '../../data/debug_event.dart'; |
| import '../../data/register_event.dart'; |
| import '../connections/app_connection.dart'; |
| import '../debugging/debugger.dart'; |
| import '../debugging/execution_context.dart'; |
| import '../debugging/inspector.dart'; |
| import '../debugging/instance.dart'; |
| import '../debugging/location.dart'; |
| import '../debugging/modules.dart'; |
| import '../debugging/remote_debugger.dart'; |
| import '../debugging/skip_list.dart'; |
| import '../events.dart'; |
| import '../loaders/strategy.dart'; |
| import '../readers/asset_reader.dart'; |
| import '../services/expression_compiler.dart'; |
| import '../utilities/dart_uri.dart'; |
| import '../utilities/sdk_configuration.dart'; |
| import '../utilities/shared.dart'; |
| import 'expression_evaluator.dart'; |
| import 'batched_expression_evaluator.dart'; |
| |
| /// A proxy from the chrome debug protocol to the dart vm service protocol. |
| class ChromeProxyService implements VmServiceInterface { |
| /// Cache of all existing StreamControllers. |
| /// |
| /// These are all created through [onEvent]. |
| final _streamControllers = <String, StreamController<Event>>{}; |
| |
| /// The root `VM` instance. There can only be one of these, but its isolates |
| /// are dynamic and roughly map to chrome tabs. |
| final VM _vm; |
| |
| /// Signals when isolate is initialized. |
| Future<void> get isInitialized => _initializedCompleter.future; |
| Completer<void> _initializedCompleter = Completer<void>(); |
| |
| /// 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; |
| |
| final _disabledBreakpoints = <Breakpoint>{}; |
| final _previousBreakpoints = <Breakpoint>{}; |
| |
| final _logger = Logger('ChromeProxyService'); |
| |
| final ExpressionCompiler? _compiler; |
| ExpressionEvaluator? _expressionEvaluator; |
| |
| final SdkConfigurationProvider _sdkConfigurationProvider; |
| |
| bool terminatingIsolates = false; |
| |
| ChromeProxyService._( |
| this._vm, |
| this.root, |
| this._assetReader, |
| this.remoteDebugger, |
| this._modules, |
| this._locations, |
| this._skipLists, |
| this.executionContext, |
| this._compiler, |
| this._sdkConfigurationProvider, |
| ) { |
| final debugger = Debugger.create( |
| remoteDebugger, |
| _streamNotify, |
| _locations, |
| _skipLists, |
| root, |
| ); |
| debugger.then((value) => _debuggerCompleter.complete(value)); |
| } |
| |
| static Future<ChromeProxyService> create( |
| RemoteDebugger remoteDebugger, |
| String root, |
| AssetReader assetReader, |
| LoadStrategy loadStrategy, |
| AppConnection appConnection, |
| ExecutionContext executionContext, |
| ExpressionCompiler? expressionCompiler, |
| SdkConfigurationProvider sdkConfigurationProvider, |
| ) 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(); |
| final service = ChromeProxyService._( |
| vm, |
| root, |
| assetReader, |
| remoteDebugger, |
| modules, |
| locations, |
| skipLists, |
| executionContext, |
| expressionCompiler, |
| sdkConfigurationProvider, |
| ); |
| unawaited(service.createIsolate(appConnection)); |
| return service; |
| } |
| |
| /// Initializes metadata in [Locations], [Modules], and [ExpressionCompiler]. |
| Future<void> _initializeEntrypoint(String entrypoint) async { |
| _locations.initialize(entrypoint); |
| _modules.initialize(entrypoint); |
| _skipLists.initialize(); |
| // We do not need to wait for compiler dependencies to be updated as the |
| // [ExpressionEvaluator] is robust to evaluation requests during updates. |
| unawaited(_updateCompilerDependencies(entrypoint)); |
| } |
| |
| Future<void> _updateCompilerDependencies(String entrypoint) async { |
| final metadataProvider = globalLoadStrategy.metadataProviderFor(entrypoint); |
| final moduleFormat = globalLoadStrategy.moduleFormat; |
| final soundNullSafety = await metadataProvider.soundNullSafety; |
| |
| _logger.info('Initializing expression compiler for $entrypoint ' |
| 'with sound null safety: $soundNullSafety'); |
| |
| final compiler = _compiler; |
| if (compiler != null) { |
| await compiler.initialize( |
| moduleFormat: moduleFormat, soundNullSafety: soundNullSafety); |
| final dependencies = |
| await globalLoadStrategy.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. |
| Future<void> createIsolate(AppConnection appConnection) async { |
| // Inspector is null if the previous isolate is destroyed. |
| if (_isIsolateRunning) { |
| throw UnsupportedError( |
| 'Cannot create multiple isolates for the same app'); |
| } |
| // 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; |
| await _initializeEntrypoint(entrypoint); |
| final sdkConfiguration = await _sdkConfigurationProvider.configuration; |
| |
| debugger.notifyPausedAtStart(); |
| _inspector = await AppInspector.create( |
| appConnection, |
| remoteDebugger, |
| _assetReader, |
| _locations, |
| root, |
| debugger, |
| executionContext, |
| sdkConfiguration, |
| ); |
| debugger.updateInspector(inspector); |
| |
| final compiler = _compiler; |
| _expressionEvaluator = compiler == null |
| ? null |
| : BatchedExpressionEvaluator( |
| entrypoint, |
| inspector, |
| debugger, |
| _locations, |
| _modules, |
| compiler, |
| ); |
| |
| unawaited(_prewarmExpressionCompilerCache()); |
| |
| await debugger.reestablishBreakpoints( |
| _previousBreakpoints, _disabledBreakpoints); |
| _disabledBreakpoints.clear(); |
| |
| unawaited(appConnection.onStart.then((_) async { |
| await debugger.resumeFromStart(); |
| _startedCompleter.complete(); |
| })); |
| |
| unawaited(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 (var extensionRpc in inspector.isolate.extensionRPCs ?? []) { |
| _streamNotify( |
| 'Isolate', |
| Event( |
| kind: EventKind.kServiceExtensionAdded, |
| timestamp: timestamp, |
| isolate: isolateRef) |
| ..extensionRPC = extensionRpc); |
| } |
| |
| // 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. |
| 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; |
| _previousBreakpoints.clear(); |
| _previousBreakpoints.addAll(isolate.breakpoints ?? []); |
| _expressionEvaluator?.close(); |
| _consoleSubscription?.cancel(); |
| _consoleSubscription = null; |
| } |
| |
| Future<void> disableBreakpoints() async { |
| _disabledBreakpoints.clear(); |
| if (!_isIsolateRunning) return; |
| final isolate = inspector.isolate; |
| |
| _disabledBreakpoints.addAll(isolate.breakpoints ?? []); |
| for (var breakpoint in isolate.breakpoints?.toList() ?? []) { |
| await (await debuggerFuture).removeBreakpoint(breakpoint.id); |
| } |
| } |
| |
| @override |
| 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}) 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}) 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))); |
| final expression = ''' |
| ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( |
| "$method", JSON.stringify(${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, |
| decodedResponse['message'] as String, decodedResponse['data'] as Map); |
| } else { |
| return Response()..json = decodedResponse; |
| } |
| } |
| |
| @override |
| Future<Success> clearVMTimeline() { |
| return _rpcNotSupportedFuture('clearVMTimeline'); |
| } |
| |
| 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 (result.type.contains('Error')) { |
| if (!result.type.startsWith('CompilationError')) { |
| _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()); |
| } |
| } |
| |
| @override |
| Future<Response> evaluate( |
| String isolateId, |
| String targetId, |
| String expression, { |
| Map<String, String>? scope, |
| bool? disableBreakpoints, |
| }) async { |
| // TODO(798) - respect disableBreakpoints. |
| return captureElapsedTime(() async { |
| await isInitialized; |
| final evaluator = _expressionEvaluator; |
| if (evaluator != null) { |
| await isCompilerInitialized; |
| _checkIsolate('evaluate', isolateId); |
| |
| final library = await inspector.getLibrary(targetId); |
| return await _getEvaluationResult( |
| isolateId, |
| () => evaluator.evaluateExpression( |
| isolateId, library?.uri, expression, scope), |
| expression); |
| } |
| throw RPCError('evaluateInFrame', RPCError.kInvalidRequest, |
| 'Expression evaluation is not supported for this configuration.'); |
| }, (result) => DwdsEvent.evaluate(expression, result)); |
| } |
| |
| @override |
| Future<Response> evaluateInFrame( |
| String isolateId, int frameIndex, String expression, |
| {Map<String, String>? scope, bool? disableBreakpoints}) async { |
| // 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', RPCError.kInvalidRequest, |
| 'Expression evaluation is not supported for this configuration.'); |
| }, (result) => DwdsEvent.evaluateInFrame(expression, result)); |
| } |
| |
| @override |
| Future<AllocationProfile> getAllocationProfile(String isolateId, |
| {bool? gc, bool? reset}) { |
| return _rpcNotSupportedFuture('getAllocationProfile'); |
| } |
| |
| @override |
| Future<ClassList> getClassList(String isolateId) { |
| // See dart-lang/webdev/issues/971. |
| return _rpcNotSupportedFuture('getClassList'); |
| } |
| |
| @override |
| Future<FlagList> getFlagList() async { |
| // VM flags do not apply to web apps. |
| return FlagList(flags: []); |
| } |
| |
| @override |
| Future<InstanceSet> getInstances( |
| String isolateId, |
| String classId, |
| int limit, { |
| bool? includeImplementers, |
| bool? includeSubclasses, |
| }) { |
| return _rpcNotSupportedFuture('getInstances'); |
| } |
| |
| @override |
| Future<Isolate> getIsolate(String isolateId) async { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| _checkIsolate('getIsolate', isolateId); |
| return inspector.isolate; |
| }, (result) => DwdsEvent.getIsolate()); |
| } |
| |
| @override |
| 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}) async { |
| await isInitialized; |
| _checkIsolate('getObject', isolateId); |
| return inspector.getObject(objectId, offset: offset, count: count); |
| } |
| |
| @override |
| Future<ScriptList> getScripts(String isolateId) async { |
| return await 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, |
| }) async { |
| return await 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}) async { |
| await isInitialized; |
| await isStarted; |
| _checkIsolate('getStack', isolateId); |
| return (await debuggerFuture).getStack(limit: limit); |
| } |
| |
| @override |
| Future<VM> getVM() async { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| return _vm; |
| }, (result) => DwdsEvent.getVM()); |
| } |
| |
| @override |
| Future<Timeline> getVMTimeline( |
| {int? timeOriginMicros, int? timeExtentMicros}) { |
| return _rpcNotSupportedFuture('getVMTimeline'); |
| } |
| |
| @override |
| Future<TimelineFlags> getVMTimelineFlags() { |
| return _rpcNotSupportedFuture('getVMTimelineFlags'); |
| } |
| |
| @override |
| Future<Version> getVersion() async { |
| final version = semver.Version.parse(vmServiceVersion); |
| return Version(major: version.major, minor: version.minor); |
| } |
| |
| @override |
| Future<Response> invoke( |
| String isolateId, String targetId, String selector, List argumentIds, |
| {bool? disableBreakpoints}) async { |
| await isInitialized; |
| _checkIsolate('invoke', isolateId); |
| // TODO(798) - respect disableBreakpoints. |
| final remote = await inspector.invoke(targetId, selector, argumentIds); |
| return _instanceRef(remote); |
| } |
| |
| @override |
| Future<Success> kill(String isolateId) { |
| return _rpcNotSupportedFuture('kill'); |
| } |
| |
| @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', |
| RPCError.kMethodNotFound, |
| 'The stream `$streamId` is not supported on web devices', |
| ); |
| } |
| }).stream; |
| } |
| |
| @override |
| Future<Success> pause(String isolateId) async { |
| await isInitialized; |
| _checkIsolate('pause', isolateId); |
| return (await debuggerFuture).pause(); |
| } |
| |
| // Note: Ignore the optional local parameter, it is there to keep the method |
| // signature consistent with the VM service interface. |
| @override |
| Future<UriList> lookupResolvedPackageUris(String isolateId, List<String> uris, |
| {bool? local}) async { |
| await isInitialized; |
| _checkIsolate('lookupResolvedPackageUris', isolateId); |
| return UriList(uris: uris.map(DartUri.toResolvedUri).toList()); |
| } |
| |
| @override |
| 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) async { |
| return _rpcNotSupportedFuture('registerService'); |
| } |
| |
| @override |
| Future<ReloadReport> reloadSources(String isolateId, |
| {bool? force, bool? pause, String? rootLibUri, String? packagesUri}) { |
| return Future.error(RPCError( |
| 'reloadSources', |
| RPCError.kMethodNotFound, |
| 'Hot reload not supported on web devices', |
| )); |
| } |
| |
| @override |
| Future<Success> removeBreakpoint( |
| String isolateId, String breakpointId) async { |
| await isInitialized; |
| _checkIsolate('removeBreakpoint', isolateId); |
| _disabledBreakpoints |
| .removeWhere((breakpoint) => breakpoint.id == breakpointId); |
| return (await debuggerFuture).removeBreakpoint(breakpointId); |
| } |
| |
| @override |
| Future<Success> resume(String isolateId, |
| {String? step, int? frameIndex}) async { |
| if (inspector.appConnection.isStarted) { |
| return captureElapsedTime(() async { |
| await isInitialized; |
| await isStarted; |
| _checkIsolate('resume', isolateId); |
| return await (await debuggerFuture) |
| .resume(step: step, frameIndex: frameIndex); |
| }, (result) => DwdsEvent.resume(step)); |
| } else { |
| inspector.appConnection.runMain(); |
| 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, bool? shouldPauseOnExit}) async { |
| // TODO(elliette): Is there a way to respect the shouldPauseOnExit parameter |
| // in Chrome? |
| await isInitialized; |
| _checkIsolate('setIsolatePauseMode', isolateId); |
| return (await debuggerFuture) |
| .setExceptionPauseMode(exceptionPauseMode ?? ExceptionPauseMode.kNone); |
| } |
| |
| @override |
| Future<Success> setFlag(String name, String value) { |
| return _rpcNotSupportedFuture('setFlag'); |
| } |
| |
| @override |
| Future<Success> setLibraryDebuggable( |
| String isolateId, String libraryId, bool isDebuggable) { |
| return _rpcNotSupportedFuture('setLibraryDebuggable'); |
| } |
| |
| @override |
| 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) 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 an time. |
| isolate: null) |
| ..vm = toVMRef(_vm)); |
| return Success(); |
| } |
| |
| @override |
| Future<Success> setVMTimelineFlags(List<String> recordedStreams) { |
| return _rpcNotSupportedFuture('setVMTimelineFlags'); |
| } |
| |
| @override |
| Future<Success> streamCancel(String streamId) { |
| // TODO: We should implement this (as we've already implemented |
| // streamListen). |
| return _rpcNotSupportedFuture('streamCancel'); |
| } |
| |
| @override |
| Future<Success> streamListen(String streamId) async { |
| // TODO: This should return an error if the stream is already being listened |
| // to. |
| onEvent(streamId); |
| return Success(); |
| } |
| |
| @override |
| Future<Success> clearCpuSamples(String isolateId) { |
| return _rpcNotSupportedFuture('clearCpuSamples'); |
| } |
| |
| @override |
| Future<CpuSamples> getCpuSamples( |
| String isolateId, int timeOriginMicros, int timeExtentMicros) { |
| return _rpcNotSupportedFuture('getCpuSamples'); |
| } |
| |
| /// 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. |
| Future<void> parseBatchedDebugEvents(BatchedDebugEvents debugEvents) async { |
| for (var debugEvent in debugEvents.events) { |
| await parseDebugEvent(debugEvent); |
| } |
| } |
| |
| /// Parses the [DebugEvent] and emits a corresponding Dart VM Service |
| /// protocol [Event]. |
| Future<void> parseDebugEvent(DebugEvent debugEvent) async { |
| 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]. |
| Future<void> parseRegisterEvent(RegisterEvent registerEvent) async { |
| 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 firstArgValue = event.args[0].value as String; |
| // TODO(nshahan) - Migrate 'inspect' and 'log' events to the injected |
| // client communication approach as well? |
| 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': |
| _handleDeveloperLog(isolateRef, event); |
| break; |
| default: |
| break; |
| } |
| }); |
| } |
| |
| void _streamNotify(String streamId, Event event) { |
| final controller = _streamControllers[streamId]; |
| if (controller == null) return; |
| controller.add(event); |
| } |
| |
| void _handleDeveloperLog(IsolateRef isolateRef, ConsoleAPIEvent event) async { |
| final logObject = event.params?['args'][1] as Map?; |
| final logParams = <String, RemoteObject>{}; |
| for (dynamic obj in logObject?['preview']?['properties'] ?? {}) { |
| if (obj['name'] != null && obj is Map<String, dynamic>) { |
| logParams[obj['name'] as String] = RemoteObject(obj); |
| } |
| } |
| |
| 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(), |
| ); |
| } |
| |
| @override |
| Future<Timestamp> getVMTimelineMicros() { |
| return _rpcNotSupportedFuture('getVMTimelineMicros'); |
| } |
| |
| @override |
| Future<InboundReferences> getInboundReferences( |
| String isolateId, String targetId, int limit) { |
| return _rpcNotSupportedFuture('getInboundReferences'); |
| } |
| |
| @override |
| Future<RetainingPath> getRetainingPath( |
| String isolateId, String targetId, int limit) { |
| return _rpcNotSupportedFuture('getRetainingPath'); |
| } |
| |
| @override |
| Future<Success> requestHeapSnapshot(String isolateId) { |
| return _rpcNotSupportedFuture('requestHeapSnapshot'); |
| } |
| |
| @override |
| Future<IsolateGroup> getIsolateGroup(String isolateGroupId) { |
| return _rpcNotSupportedFuture('getIsolateGroup'); |
| } |
| |
| @override |
| Future<MemoryUsage> getIsolateGroupMemoryUsage(String isolateGroupId) { |
| return _rpcNotSupportedFuture('getIsolateGroupMemoryUsage'); |
| } |
| |
| @override |
| Future<ProtocolList> getSupportedProtocols() async { |
| final version = semver.Version.parse(vmServiceVersion); |
| return ProtocolList(protocols: [ |
| Protocol( |
| protocolName: 'VM Service', |
| major: version.major, |
| minor: version.minor, |
| ) |
| ]); |
| } |
| |
| Future<InstanceRef> _instanceRef(RemoteObject? obj) async { |
| final instance = obj == null ? null : await inspector.instanceRefFor(obj); |
| return instance ?? InstanceHelper.kNullInstanceRef; |
| } |
| |
| static RPCError _rpcNotSupported(String method) { |
| return RPCError(method, RPCError.kMethodNotFound, |
| '$method: Not supported on web devices'); |
| } |
| |
| static Future<T> _rpcNotSupportedFuture<T>(String method) { |
| return Future.error(_rpcNotSupported(method)); |
| } |
| |
| @override |
| Future<ProcessMemoryUsage> getProcessMemoryUsage() => |
| _rpcNotSupportedFuture('getProcessMemoryUsage'); |
| |
| @override |
| Future<PortList> getPorts(String isolateId) => throw UnimplementedError(); |
| |
| @override |
| Future<CpuSamples> getAllocationTraces(String isolateId, |
| {int? timeOriginMicros, int? timeExtentMicros, String? classId}) => |
| throw UnimplementedError(); |
| |
| @override |
| Future<Success> setTraceClassAllocation( |
| String isolateId, String classId, bool enable) => |
| throw UnimplementedError(); |
| |
| @override |
| Future<Breakpoint> setBreakpointState( |
| String isolateId, String breakpointId, bool enable) => |
| throw UnimplementedError(); |
| |
| @override |
| Future<Success> streamCpuSamplesWithUserTag(List<String> userTags) => |
| throw UnimplementedError(); |
| |
| /// Prevent DWDS from blocking Dart SDK rolls if changes in package:vm_service |
| /// are unimplemented in DWDS. |
| @override |
| dynamic noSuchMethod(Invocation invocation) { |
| return super.noSuchMethod(invocation); |
| } |
| |
| /// 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 `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']; |