// 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 'package:dwds/src/config/tool_configuration.dart'; import 'package:dwds/src/debugging/chrome_inspector.dart'; import 'package:dwds/src/debugging/dart_scope.dart'; import 'package:dwds/src/debugging/frame_computer.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/location.dart'; import 'package:dwds/src/debugging/remote_debugger.dart'; import 'package:dwds/src/debugging/skip_list.dart'; import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/domain.dart'; import 'package:dwds/src/utilities/objects.dart' show Property; import 'package:dwds/src/utilities/server.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:dwds/src/utilities/synchronized.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace; /// Adds [event] to the stream with [streamId] if there is anybody listening /// on that stream. typedef StreamNotify = void Function(String streamId, Event event); /// Converts from ExceptionPauseMode strings to [PauseState] enums. /// /// Values defined in: /// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#method-setPauseOnExceptions const _pauseModePauseStates = { 'none': PauseState.none, 'all': PauseState.all, 'unhandled': PauseState.uncaught, }; /// Mapping from the path of a script in Chrome to the Runtime.ScriptId Chrome /// uses to reference it. /// /// See https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ScriptId /// /// e.g. 'packages/myapp/main.dart.lib.js' -> '12' final chromePathToRuntimeScriptId = {}; class Debugger { static final logger = Logger('Debugger'); final RemoteDebugger _remoteDebugger; final StreamNotify _streamNotify; final Locations _locations; final SkipLists _skipLists; late ChromeAppInspector inspector; int _frameErrorCount = 0; final StreamController parsedScriptsController = StreamController.broadcast(); Debugger._( this._remoteDebugger, this._streamNotify, this._locations, this._skipLists, root, ) : _breakpoints = _Breakpoints( locations: _locations, remoteDebugger: _remoteDebugger, root: root, ); /// The breakpoints we have set so far, indexable by either /// Dart or JS ID. final _Breakpoints _breakpoints; PauseState _pauseState = PauseState.none; // TODO(elliette): https://github.com/dart-lang/webdev/issues/1501 Re-enable // after checking with Chrome team if there is a way to check if the Chrome // DevTools is showing an overlay. Both cannot be shown at the same time: // bool _pausedOverlayVisible = false; String get pauseState => _pauseModePauseStates.entries .firstWhere((entry) => entry.value == _pauseState) .key; /// The JS frames at the current paused location. /// /// The most important thing here is that frames are identified by /// frameIndex in the Dart API, but by frame Id in Chrome, so we need /// to keep the JS frames and their Ids around. FrameComputer? stackComputer; bool _isStepping = false; DartLocation? _previousSteppingLocation; // If not null and not completed, the pause handler completes this instead of // sending a `PauseInterrupted` event to the event stream. // // In some cases e.g. a hot reload, DWDS pauses the execution itself in order // to handle debugging logic like breakpoints. In such cases, sending the // event to the event stream may trigger the client to believe that the user // sent the event. To avoid that and still signal that a pause is completed, // this completer is used. See https://github.com/dart-lang/sdk/issues/61560 // for more details. Completer? _internalPauseCompleter; void updateInspector(ChromeAppInspector appInspector) { inspector = appInspector; _breakpoints.inspector = appInspector; } Future pause({bool internalPause = false}) async { _isStepping = false; if (internalPause) _internalPauseCompleter = Completer(); final result = await _remoteDebugger.pause(); handleErrorIfPresent(result); await _internalPauseCompleter?.future; return Success(); } Future setExceptionPauseMode(String mode) async { mode = mode.toLowerCase(); final state = _pauseModePauseStates[mode]; if (state == null) { throwInvalidParam('setExceptionPauseMode', 'Unsupported mode: $mode'); } else { _pauseState = state; } await _remoteDebugger.setPauseOnExceptions(_pauseState); return Success(); } /// Resumes the debugger. /// /// Step parameter options: /// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#resume /// /// If the step parameter is not provided, the program will resume regular /// execution. /// /// If the step parameter is provided, it indicates what form of /// single-stepping to use. /// /// Note that stepping will automatically continue until Chrome is paused at /// a location for which we have source information. Future resume({String? step, int? frameIndex}) async { try { if (frameIndex != null) { throw ArgumentError('FrameIndex is currently unsupported.'); } WipResponse? result; if (step != null) { _isStepping = true; switch (step) { case 'Over': result = await _remoteDebugger.stepOver(); break; case 'Out': result = await _remoteDebugger.stepOut(); break; case 'Into': result = await _remoteDebugger.stepInto(); break; default: throwInvalidParam('resume', 'Unexpected value for step: $step'); } } else { _isStepping = false; _previousSteppingLocation = null; result = await _remoteDebugger.resume(); } handleErrorIfPresent(result); return Success(); } on WipError catch (e) { final errorMessage = e.message; if (errorMessage != null && errorMessage.contains('Can only perform operation while paused')) { throw RPCError( 'resume', RPCErrorKind.kIsolateMustBePaused.code, errorMessage, ); } rethrow; } } /// Returns the current Dart stack for the paused debugger. /// /// Throws RPCError if the debugger is not paused. /// /// The returned stack will contain up to [limit] frames if provided. Future getStack({int? limit}) async { if (stackComputer == null) { throw RPCError( 'getStack', RPCErrorKind.kInternalError.code, 'Cannot compute stack when application is not paused', ); } final frames = await stackComputer!.calculateFrames(limit: limit); return Stack( frames: frames, messages: [], truncated: limit != null && frames.length == limit, ); } static Future create( RemoteDebugger remoteDebugger, StreamNotify streamNotify, Locations locations, SkipLists skipLists, String root, ) async { final debugger = Debugger._( remoteDebugger, streamNotify, locations, skipLists, root, ); remoteDebugger.onReconnect = debugger._initialize; await debugger._initialize(); return debugger; } Future _initialize() async { // We must add a listener before enabling the debugger otherwise we will // miss events. // Allow a null debugger/connection for unit tests. runZonedGuarded( () { _remoteDebugger.onScriptParsed.listen(_scriptParsedHandler); _remoteDebugger.onPaused.listen(_pauseHandler); _remoteDebugger.onResumed.listen(_resumeHandler); _remoteDebugger.onTargetCrashed.listen(_crashHandler); }, (e, StackTrace s) { logger.warning('Error handling Chrome event', e, s); }, ); handleErrorIfPresent(await _remoteDebugger.enablePage()); await _remoteDebugger.enable(); // Enable collecting information about async frames when paused. handleErrorIfPresent( await _remoteDebugger.sendCommand( 'Debugger.setAsyncCallStackDepth', params: {'maxDepth': 128}, ), ); } /// Resumes the Isolate from start. /// /// The JS VM is technically not paused at the start of the Isolate so there /// will not be a corresponding [DebuggerResumedEvent]. void resumeFromStart() => _resumeHandler(null); /// Notify the debugger the [Isolate] is paused at the application start. void notifyPausedAtStart() { stackComputer = FrameComputer(this, []); } /// Add a breakpoint at the given position. /// /// Note that line and column are Dart source locations and are one-based. Future addBreakpoint( String scriptId, int line, { int? column, }) async { column ??= 0; final breakpoint = await _breakpoints.add(scriptId, line, column); _notifyBreakpoint(breakpoint); return breakpoint; } void _notifyBreakpoint(Breakpoint breakpoint) { final event = Event( kind: EventKind.kBreakpointAdded, timestamp: DateTime.now().millisecondsSinceEpoch, isolate: inspector.isolateRef, ); event.breakpoint = breakpoint; _streamNotify('Debug', event); } /// Remove a Dart breakpoint. Future removeBreakpoint(String breakpointId) async { if (_breakpoints.breakpointFor(breakpointId) == null) { throwInvalidParam( 'removeBreakpoint', 'invalid breakpoint id $breakpointId', ); } final jsId = _breakpoints.jsIdFor(breakpointId); if (jsId == null) { throw RPCError( 'removeBreakpoint', RPCErrorKind.kInternalError.code, 'invalid JS breakpoint id $jsId', ); } await _removeBreakpoint(jsId); final bp = await _breakpoints.remove(jsId: jsId, dartId: breakpointId); if (bp != null) { _streamNotify( 'Debug', Event( kind: EventKind.kBreakpointRemoved, timestamp: DateTime.now().millisecondsSinceEpoch, isolate: inspector.isolateRef, )..breakpoint = bp, ); } return Success(); } /// Call the Chrome protocol removeBreakpoint. Future _removeBreakpoint(String breakpointId) async { try { final response = await _remoteDebugger.removeBreakpoint(breakpointId); handleErrorIfPresent(response); } on WipError catch (e) { throw RPCError('removeBreakpoint', 102, '$e'); } } /// Returns Chrome script uri for Chrome script ID. String? urlForScriptId(String scriptId) => _remoteDebugger.scripts[scriptId]?.url; /// Returns source [Location] for the paused event. /// /// If we do not have [Location] data for the embedded JS location, null is /// returned. Future _sourceLocation(DebuggerPausedEvent e) async { final frame = e.params?['callFrames']?[0]; final location = frame?['location']; if (location == null) return null; final scriptId = location['scriptId'] as String?; final line = location['lineNumber'] as int?; if (scriptId == null || line == null) return null; final column = location['columnNumber'] as int?; final url = urlForScriptId(scriptId); if (url == null) return null; final loc = await _locations.locationForJs(url, line, column); if (loc == null || loc.dartLocation == _previousSteppingLocation) { return null; } _previousSteppingLocation = loc.dartLocation; return loc; } /// Returns script ID for the paused event. String? _frameScriptId(DebuggerPausedEvent e) { final frame = e.params?['callFrames']?[0]; return frame?['location']?['scriptId'] as String?; } /// The variables visible in a frame in Dart protocol [BoundVariable] form. Future> variablesFor(WipCallFrame frame) async { // TODO(alanknight): Can these be moved to dart_scope.dart? final properties = await visibleVariables( inspector: inspector, frame: frame, ); final boundVariables = await Future.wait(properties.map(_boundVariable)); // Filter out variables that do not come from dart code, such as native // JavaScript objects return boundVariables .where((bv) => inspector.isDisplayableObject(bv?.value)) .toList() .cast(); } Future _boundVariable(Property property) async { // TODO(annagrin): value might be null in the future for variables // optimized by V8. Return appropriate sentinel values for them. if (property.value != null) { final value = property.value!; // We return one level of properties from this object. Sub-properties are // another round trip. final instanceRef = await inspector.instanceRefFor(value); // Skip null instance refs, which we get for weird objects, e.g. // properties that are getter/setter pairs. // TODO(alanknight): Handle these properly. if (instanceRef == null) return null; return BoundVariable( name: property.name, value: instanceRef, // TODO(grouma) - Provide actual token positions. declarationTokenPos: -1, scopeStartTokenPos: -1, scopeEndTokenPos: -1, ); } return null; } // TODO(elliette): https://github.com/dart-lang/webdev/issues/1501 Re-enable // after checking with Chrome team if there is a way to check if the Chrome // DevTools is showing an overlay. Both cannot be shown at the same time: // Renders the paused at breakpoint overlay over the application. // void _showPausedOverlay() async { // if (_pausedOverlayVisible) return; // handleErrorIfPresent(await _remoteDebugger?.sendCommand('DOM.enable')); // handleErrorIfPresent(await _remoteDebugger?.sendCommand('Overlay.enable')); // handleErrorIfPresent(await _remoteDebugger // ?.sendCommand('Overlay.setPausedInDebuggerMessage', params: { // 'message': 'Paused', // })); // _pausedOverlayVisible = true; // } // Removes the paused at breakpoint overlay from the application. // void _hidePausedOverlay() async { // if (!_pausedOverlayVisible) return; // handleErrorIfPresent(await _remoteDebugger?.sendCommand('Overlay.disable')); // _pausedOverlayVisible = false; // } /// Returns a Dart [Frame] for a JS [frame]. Future calculateDartFrameFor( WipCallFrame frame, int frameIndex, { bool populateVariables = true, }) async { final location = frame.location; final line = location.lineNumber; final column = location.columnNumber; final url = urlForScriptId(location.scriptId); if (url == null) { logger.fine( 'Failed to create dart frame for ${frame.functionName}: ' 'cannot find url for script ${location.scriptId}', ); return null; } final bestLocation = await _locations.locationForJs(url, line, column ?? 0); if (bestLocation == null) return null; final script = await inspector.scriptRefFor( bestLocation.dartLocation.uri.serverPath, ); // We think we found a location, but for some reason we can't find the // script. Just drop the frame. // TODO(#700): Understand when this can happen and have a better fix. if (script == null) return null; final functionName = _prettifyMember(frame.functionName.split('.').last); final codeRefName = functionName.isEmpty ? '' : functionName; final dartFrame = Frame( index: frameIndex, code: CodeRef(name: codeRefName, kind: CodeKind.kDart, id: createId()), location: SourceLocation( line: bestLocation.dartLocation.line, column: bestLocation.dartLocation.column, tokenPos: bestLocation.tokenPos, script: script, ), kind: FrameKind.kRegular, ); // Don't populate variables for async frames. if (populateVariables) { try { dartFrame.vars = await variablesFor(frame); } catch (e) { _frameErrorCount++; } } return dartFrame; } /// Can be called after [calculateDartFrameFor] to log any errors. void logAnyFrameErrors({required String frameType}) { if (_frameErrorCount > 0) { logger.warning( 'Error calculating Dart variables for $_frameErrorCount $frameType frames.', ); } _frameErrorCount = 0; } void _scriptParsedHandler(ScriptParsedEvent e) { final scriptPath = _pathForChromeScript(e.script.url); if (scriptPath != null) { chromePathToRuntimeScriptId[scriptPath] = e.script.scriptId; if (parsedScriptsController.hasListener) { parsedScriptsController.sink.add(e.script.url); } } } String? _pathForChromeScript(String scriptUrl) { final scriptPathSegments = Uri.parse(scriptUrl).pathSegments; if (scriptPathSegments.isEmpty) { return null; } final isInternal = globalToolConfiguration.appMetadata.isInternalBuild; const packagesDir = 'packages'; if (isInternal && scriptUrl.contains(packagesDir)) { final packagesIdx = scriptPathSegments.indexOf(packagesDir); return p.joinAll(scriptPathSegments.sublist(packagesIdx)); } // Note: Replacing "\" with "/" is necessary because `joinAll` uses "\" if // the platform is Windows. However, only "/" is expected by the browser. return p.joinAll(scriptPathSegments).replaceAll('\\', '/'); } /// Handles pause events coming from the Chrome connection. Future _pauseHandler(DebuggerPausedEvent e) async { final isolate = inspector.isolate; Event event; final timestamp = DateTime.now().millisecondsSinceEpoch; final jsBreakpointIds = e.hitBreakpoints ?? []; if (jsBreakpointIds.isNotEmpty) { final breakpointIds = jsBreakpointIds .map((id) => _breakpoints._dartIdByJsId[id]) // In case the breakpoint was set in Chrome DevTools outside of // package:dwds. .where((entry) => entry != null) .toSet(); final pauseBreakpoints = isolate.breakpoints ?.where((bp) => breakpointIds.contains(bp.id)) .toList(); event = Event( kind: EventKind.kPauseBreakpoint, timestamp: timestamp, isolate: inspector.isolateRef, )..pauseBreakpoints = pauseBreakpoints; } else if (e.reason == 'exception' || e.reason == 'assert') { InstanceRef? exception; if (e.data is Map) { final map = e.data as Map; if (map['type'] == 'object') { final obj = RemoteObject(map); exception = await inspector.instanceRefFor(obj); if (exception != null && inspector.isNativeJsError(exception)) { if (obj.description != null) { // Create a string exception object. final description = await inspector.mapExceptionStackTrace( obj.description!, ); exception = await inspector.instanceRefFor(description); } else { exception = null; } } } } event = Event( kind: EventKind.kPauseException, timestamp: timestamp, isolate: inspector.isolateRef, exception: exception, ); } else { // Continue stepping until we hit a dart location, // avoiding stepping through library loading code. if (_isStepping) { final scriptId = _frameScriptId(e); if (scriptId == null) { logger.severe( 'Stepping failed: ' 'cannot find script id for event $e', ); throw StateError('Stepping failed on event $e'); } final url = urlForScriptId(scriptId); if (url == null) { logger.severe( 'Stepping failed: ' 'cannot find url for script $scriptId', ); throw StateError('Stepping failed in script $scriptId'); } if (url.contains( globalToolConfiguration.loadStrategy.loadLibrariesModule, )) { await _remoteDebugger.stepOut(); return; } else if ((await _sourceLocation(e)) == null) { // TODO(grouma) - In the future we should send all previously computed // skipLists. await _remoteDebugger.stepInto( params: { 'skipList': _skipLists.compute( scriptId, url, await _locations.locationsForUrl(url), ), }, ); return; } } event = Event( kind: EventKind.kPauseInterrupted, timestamp: timestamp, isolate: inspector.isolateRef, ); } // Calculate the frames (and handle any exceptions that may occur). stackComputer = FrameComputer( this, e.getCallFrames().toList(), asyncStackTrace: e.asyncStackTrace, ); try { final frames = await stackComputer!.calculateFrames(limit: 1); event.topFrame = frames.isNotEmpty ? frames.first : null; } catch (e, s) { // TODO: Return information about the error to the user. logger.warning('Error calculating Dart frames', e, s); } // TODO(elliette): https://github.com/dart-lang/webdev/issues/1501 Re-enable // after checking with Chrome team if there is a way to check if the Chrome // DevTools is showing an overlay. Both cannot be shown at the same time. // _showPausedOverlay(); isolate.pauseEvent = event; final internalPauseCompleter = _internalPauseCompleter; if (event.kind == EventKind.kPauseInterrupted && internalPauseCompleter != null && !internalPauseCompleter.isCompleted) { internalPauseCompleter.complete(); } else { _streamNotify('Debug', event); } } /// Handles resume events coming from the Chrome connection. void _resumeHandler(DebuggerResumedEvent? _) { // We can receive a resume event in the middle of a reload which will result // in a null isolate. final isolate = inspector.isolate; stackComputer = null; final event = Event( kind: EventKind.kResume, timestamp: DateTime.now().millisecondsSinceEpoch, isolate: inspector.isolateRef, ); // TODO(elliette): https://github.com/dart-lang/webdev/issues/1501 Re-enable // after checking with Chrome team if there is a way to check if the Chrome // DevTools is showing an overlay. Both cannot be shown at the same time. // _hidePausedOverlay(); isolate.pauseEvent = event; _streamNotify('Debug', event); } /// Handles targetCrashed events coming from the Chrome connection. void _crashHandler(TargetCrashedEvent _) { // We can receive a resume event in the middle of a reload which will result // in a null isolate. final isolate = inspector.isolate; stackComputer = null; final event = Event( kind: EventKind.kIsolateExit, timestamp: DateTime.now().millisecondsSinceEpoch, isolate: inspector.isolateRef, ); isolate.pauseEvent = event; _streamNotify('Isolate', event); logger.severe('Target crashed!'); } WipCallFrame? jsFrameForIndex(int frameIndex) { final computer = stackComputer; if (computer == null) { throw RPCError( 'evaluateInFrame', 106, 'Cannot evaluate on a call frame when the program is not paused', ); } return computer.jsFrameForIndex(frameIndex); } /// Evaluate [expression] by calling Chrome's Runtime.evaluateOnCallFrame on /// the call frame with index [frameIndex] in the currently saved stack. /// /// If the program is not paused, so there is no current stack, throws a /// [StateError]. Future evaluateJsOnCallFrameIndex( int frameIndex, String expression, ) { final index = jsFrameForIndex(frameIndex)?.callFrameId; if (index == null) { // This might happen on async frames. throw StateError('No frame for frame index: $index'); } return evaluateJsOnCallFrame(index, expression); } /// Evaluate [expression] by calling Chrome's Runtime.evaluateOnCallFrame on /// the call frame with id [callFrameId]. Future evaluateJsOnCallFrame( String callFrameId, String expression, ) async { // TODO(alanknight): Support a version with arguments if needed. try { return await _remoteDebugger.evaluateOnCallFrame(callFrameId, expression); } on ExceptionDetails catch (e) { throw ChromeDebugException(e.json, evalContents: expression); } } } Future sendCommandAndValidateResult( RemoteDebugger remoteDebugger, { required String method, required String resultField, Map? params, }) async { final response = await remoteDebugger.sendCommand(method, params: params); final result = response.result?[resultField]; if (result == null) { throw RPCError( method, RPCErrorKind.kInternalError.code, '$resultField not found in result from sendCommand', params, ); } return result; } /// Returns the breakpoint ID for the provided Dart script ID and Dart line /// number. String breakpointIdFor(String scriptId, int line, int column) => 'bp/$scriptId#$line:$column'; /// Keeps track of the Dart and JS breakpoint Ids that correspond. class _Breakpoints { final _logger = Logger('Breakpoints'); final _dartIdByJsId = {}; final _jsIdByDartId = {}; final _bpByDartId = >{}; final _queue = AtomicQueue(); final Locations locations; final RemoteDebugger remoteDebugger; late AppInspector inspector; /// The root URI from which the application is served. final String root; _Breakpoints({ required this.locations, required this.remoteDebugger, required this.root, }); Future _createBreakpoint( String id, String scriptId, int line, int column, ) async { final dartScript = inspector.scriptWithId(scriptId); final dartScriptUri = dartScript?.uri; Location? location; if (dartScriptUri != null) { final dartUri = DartUri(dartScriptUri, root); location = await locations.locationForDart(dartUri, line, column); } // TODO: Handle cases where a breakpoint can't be set exactly at that line. if (location == null) { _logger.fine( 'Failed to set breakpoint $id ' '($scriptId:$line:$column): ' 'cannot find Dart location.', ); throw RPCError( 'addBreakpoint', 102, 'The VM is unable to add a breakpoint $id ' 'at the specified line or function: ($scriptId:$line:$column): ' ' cannot find Dart location.', ); } try { final dartBreakpoint = _dartBreakpoint(dartScript!, location, id); final jsBreakpointId = await _setJsBreakpoint(location); if (jsBreakpointId == null) { _logger.fine( 'Failed to set breakpoint $id ' '($scriptId:$line:$column): ' 'cannot set JS breakpoint.', ); throw RPCError( 'addBreakpoint', 102, 'The VM is unable to add a breakpoint $id ' 'at the specified line or function: ($scriptId:$line:$column): ' 'cannot set JS breakpoint at $location', ); } _note(jsId: jsBreakpointId, bp: dartBreakpoint); return dartBreakpoint; } on WipError catch (wipError) { throw RPCError('addBreakpoint', 102, '$wipError'); } } /// Adds a breakpoint at [scriptId] and [line] or returns an existing one if /// present. Future add(String scriptId, int line, int column) { final id = breakpointIdFor(scriptId, line, column); return _bpByDartId.putIfAbsent( id, () => _createBreakpoint(id, scriptId, line, column), ); } /// Create a Dart breakpoint at [location] in [dartScript] with [id]. Breakpoint _dartBreakpoint( ScriptRef dartScript, Location location, String id, ) { final breakpoint = Breakpoint( id: id, breakpointNumber: int.parse(createId()), resolved: true, location: SourceLocation( script: dartScript, tokenPos: location.tokenPos, line: location.dartLocation.line, column: location.dartLocation.column, ), enabled: true, )..id = id; return breakpoint; } /// Calls the Chrome protocol setBreakpoint and returns the remote ID. Future _setJsBreakpoint(Location location) { // Prevent `Aww, snap!` errors when setting multiple breakpoints // simultaneously by serializing the requests. return _queue.run(() async { final scriptId = location.jsLocation.runtimeScriptId; if (scriptId != null) { return sendCommandAndValidateResult( remoteDebugger, method: 'Debugger.setBreakpoint', resultField: 'breakpointId', params: { 'location': { 'lineNumber': location.jsLocation.line, 'columnNumber': location.jsLocation.column, 'scriptId': scriptId, }, }, ); } else { _logger.fine('No runtime script ID for location $location'); return null; } }); } /// Records the internal Dart <=> JS breakpoint id mapping and adds the /// breakpoint to the current isolates list of breakpoints. void _note({required Breakpoint bp, required String jsId}) { final bpId = bp.id; if (bpId != null) { _dartIdByJsId[jsId] = bpId; _jsIdByDartId[bpId] = jsId; final isolate = inspector.isolate; isolate.breakpoints?.add(bp); } } Future remove({ required String jsId, required String dartId, }) async { final isolate = inspector.isolate; _dartIdByJsId.remove(jsId); _jsIdByDartId.remove(dartId); isolate.breakpoints?.removeWhere((b) => b.id == dartId); return await _bpByDartId.remove(dartId); } Future? breakpointFor(String dartId) => _bpByDartId[dartId]; String? jsIdFor(String dartId) => _jsIdByDartId[dartId]; } final escapedPipe = '\$124'; final escapedPound = '\$35'; /// Reformats a JS member name to make it look more Dart-like. /// /// Logic copied from build/build_web_compilers/web/stack_trace_mapper.dart. /// TODO(https://github.com/dart-lang/sdk/issues/38869): Remove this logic when /// DDC stack trace deobfuscation is overhauled. String _prettifyMember(String member) { member = member.replaceAll(escapedPipe, '|'); if (member.contains('|')) { return _prettifyExtension(member); } else { if (member.startsWith('[') && member.endsWith(']')) { member = member.substring(1, member.length - 1); } return member; } } /// Reformats a JS member name as an extension method invocation. String _prettifyExtension(String member) { var isSetter = false; final pipeIndex = member.indexOf('|'); final spaceIndex = member.indexOf(' '); final poundIndex = member.indexOf(escapedPound); if (spaceIndex >= 0) { // Here member is a static field or static getter/setter. isSetter = member.substring(0, spaceIndex) == 'set'; member = member.substring(spaceIndex + 1, member.length); } else if (poundIndex >= 0) { // Here member is a tear-off or local property getter/setter. isSetter = member.substring(pipeIndex + 1, poundIndex) == 'set'; member = member.replaceRange(pipeIndex + 1, poundIndex + 3, ''); } else { final body = member.substring(pipeIndex + 1, member.length); if (body.startsWith('unary') || body.startsWith('\$')) { // Here member's an operator, so it's safe to unescape everything lazily. member = _unescape(member); } } member = member.replaceAll('|', '.'); return isSetter ? '$member=' : member; } /// Un-escapes a DDC-escaped JS identifier name. /// /// Identifier names that contain illegal JS characters are escaped by DDC to a /// decimal representation of the symbol's UTF-16 value. /// Warning: this greedily escapes characters, so it can be unsafe in the event /// that an escaped sequence precedes a number literal in the JS name. String _unescape(String name) { return name.replaceAllMapped( RegExp(r'\$[0-9]+'), (m) => String.fromCharCode(int.parse(name.substring(m.start + 1, m.end))), ); }