blob: ecc9beecad2b906cc15d5e24d40ea7d685ffc288 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.import 'dart:async';
import 'dart:async';
import 'package:vm_service_lib/vm_service_lib.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import 'chrome_proxy_service.dart';
import 'dart_uri.dart';
import 'helpers.dart';
import 'location.dart';
import 'sources.dart';
/// 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,
};
class Debugger {
WipConnection _tabConnection;
final AssetHandler _assetHandler;
final StreamNotify _streamNotify;
final AppInspectorProvider _appInspectorProvider;
/// The root URI from which the application is served.
final String _root;
Debugger._(
this._assetHandler,
this._tabConnection,
this._streamNotify,
this._appInspectorProvider,
// TODO(401) - Remove.
this._root,
) : _breakpoints = _Breakpoints(_appInspectorProvider);
WipDebugger get _chromeDebugger => _tabConnection.debugger;
/// The scripts and sourcemaps for the application, both JS and Dart.
Sources sources;
/// The breakpoints we have set so far, indexable by either
/// Dart or JS ID.
_Breakpoints _breakpoints;
/// Allocates Dart breakpoint IDs
int _nextBreakpointId = 1;
Stack _pausedStack;
bool _isStepping = false;
Future<Success> pause() async {
_isStepping = false;
var result = await _chromeDebugger.sendCommand('Debugger.pause');
handleErrorIfPresent(result);
return Success();
}
Future<Success> setExceptionPauseMode(String isolateId, String mode) async {
if (isolateId != _appInspectorProvider().isolate?.id) {
throw ArgumentError.value(
isolateId, 'isolateId', 'Unrecognized isolate id');
}
var pauseState = _pauseModePauseStates[mode.toLowerCase()] ??
(throw ArgumentError.value('mode', 'Unsupported mode `$mode`'));
await _chromeDebugger.setPauseOnExceptions(pauseState);
return Success();
}
/// Resumes the debugger.
///
/// Step parameter options:
/// https://github.com/dart-lang/sdk/blob/master/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<Success> resume(String isolateId,
{String step, int frameIndex}) async {
if (isolateId != _appInspectorProvider().isolate?.id) {
throw ArgumentError.value(
isolateId, 'isolateId', 'Unrecognized isolate id');
}
if (frameIndex != null) {
throw ArgumentError('FrameIndex is currently unsupported.');
}
WipResponse result;
if (step != null) {
_isStepping = true;
switch (step) {
case 'Over':
result = await _chromeDebugger.sendCommand('Debugger.stepOver');
break;
case 'Out':
result = await _chromeDebugger.sendCommand('Debugger.stepOut');
break;
case 'Into':
result = await _chromeDebugger.sendCommand('Debugger.stepInto');
break;
default:
throw ArgumentError('Unexpected value for step: $step');
}
} else {
_isStepping = false;
result = await _chromeDebugger.sendCommand('Debugger.resume');
}
handleErrorIfPresent(result);
return Success();
}
/// Returns the current Dart stack for the paused debugger.
///
/// Returns null if the debugger is not paused.
Future<Stack> getStack(String isolateId) async {
if (isolateId != _appInspectorProvider().isolate?.id) {
throw ArgumentError.value(
isolateId, 'isolateId', 'Unrecognized isolate id');
}
return _pausedStack;
}
static Future<Debugger> create(
AssetHandler assetHandler,
WipConnection tabConnection,
StreamNotify streamNotify,
AppInspectorProvider appInspectorProvider,
String root) async {
var debugger = Debugger._(
assetHandler,
tabConnection,
streamNotify,
appInspectorProvider,
// TODO(401) - Remove.
root,
);
await debugger._initialize();
return debugger;
}
Future<Null> _initialize() async {
sources = Sources(_assetHandler);
// We must add a listener before enabling the debugger otherwise we will
// miss events.
_chromeDebugger.onScriptParsed.listen(sources.scriptParsed);
_chromeDebugger.onPaused.listen(_pauseHandler);
_chromeDebugger.onResumed.listen(_resumeHandler);
handleErrorIfPresent(await _tabConnection.page.enable() as WipResponse);
handleErrorIfPresent(await _chromeDebugger.enable() as WipResponse);
}
/// Add a breakpoint at the given position.
///
/// Note that line and column are Dart source locations and one-based.
Future<Breakpoint> addBreakpoint(String isolateId, String scriptId, int line,
{int column}) async {
var inspector = _appInspectorProvider();
var isolate = inspector.isolate;
if (isolateId != isolate.id) {
throw ArgumentError.value(
isolateId, 'isolateId', 'Unrecognized isolate id');
}
var dartScript = await inspector.scriptWithId(scriptId);
// TODO(401): Remove the additional parameter.
var dartUri =
DartUri(dartScript.uri, '${Uri.parse(_root).path}/garbage.dart');
var location = _locationForDart(dartUri, line);
// TODO: Handle cases where a breakpoint can't be set exactly at that line.
if (location == null) return null;
var jsBreakpointId = await _setBreakpoint(location);
var dartBreakpoint = _dartBreakpoint(dartScript, location, isolate);
_breakpoints.noteBreakpoint(js: jsBreakpointId, bp: dartBreakpoint);
return dartBreakpoint;
}
/// Create a Dart breakpoint at [location] in [dartScript].
Breakpoint _dartBreakpoint(
ScriptRef dartScript, Location location, Isolate isolate) {
var breakpoint = Breakpoint()
..resolved = true
..id = '${_nextBreakpointId++}'
..location = (SourceLocation()
..script = dartScript
..tokenPos = location.tokenPos);
_streamNotify(
'Debug',
Event()
..kind = EventKind.kBreakpointAdded
..isolate = toIsolateRef(isolate)
..breakpoint = breakpoint);
return breakpoint;
}
/// Remove a Dart breakpoint.
Future<Success> removeBreakpoint(
String isolateId, String breakpointId) async {
var isolate = _appInspectorProvider().isolate;
if (isolateId != isolate.id) {
throw ArgumentError.value(
isolateId, 'isolateId', 'Unrecognized isolate id');
}
if (breakpointId == null) {
throw ArgumentError.notNull('breakpointId');
}
var jsId = _breakpoints.jsId(breakpointId);
var bp = _breakpoints.removeBreakpoint(js: jsId, dartId: breakpointId);
if (bp == null) {
throw ArgumentError.value(
breakpointId, 'Breakpoint not found with this id.');
}
_streamNotify(
'Debug',
Event()
..kind = EventKind.kBreakpointRemoved
..isolate = toIsolateRef(isolate)
..breakpoint = bp);
await _removeBreakpoint(jsId);
return Success();
}
/// Call the Chrome protocol setBreakpoint and return the breakpoint ID.
Future<String> _setBreakpoint(Location location) async {
// Location is 0 based according to:
// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#type-Location
var response =
await _chromeDebugger.sendCommand('Debugger.setBreakpoint', params: {
'location': {
'scriptId': location.jsLocation.scriptId,
'lineNumber': location.jsLocation.line - 1,
}
});
handleErrorIfPresent(response);
return response.result['breakpointId'] as String;
}
/// Call the Chrome protocol removeBreakpoint.
Future<void> _removeBreakpoint(String breakpointId) async {
var response = await _chromeDebugger.sendCommand(
'Debugger.removeBreakpoint',
params: {'breakpointId': breakpointId});
handleErrorIfPresent(response);
}
/// Find the [Location] for the given Dart source position.
///
/// The [line] number is 1-based.
Location _locationForDart(DartUri uri, int line) => sources
.locationsForDart(uri.serverPath)
.firstWhere((location) => location.dartLocation.line == line,
orElse: () => null);
/// Find the [Location] for the given JS source position.
///
/// The [line] and [column] are 1-based.
Location _locationForJs(String scriptId, int line, int column) =>
sources.locationsForJs(scriptId).firstWhere(
(location) =>
location.jsLocation.line == line &&
location.jsLocation.column == column,
orElse: () => null);
/// Returns source [Location] for the paused event.
///
/// If we do not have [Location] data for the embedded JS location, null is
/// returned.
Location _sourceLocation(DebuggerPausedEvent e) {
var frame = e.params['callFrames'][0];
var location = frame['location'];
var jsLocation = JsLocation.fromZeroBased(location['scriptId'] as String,
location['lineNumber'] as int, location['columnNumber'] as int);
return _locationForJs(
jsLocation.scriptId, jsLocation.line, jsLocation.column);
}
/// Translates Chrome callFrames contained in [DebuggerPausedEvent] into Dart
/// [Frame]s.
List<Frame> _dartFramesFor(DebuggerPausedEvent e) {
var dartFrames = <Frame>[];
var index = 0;
for (var frame in e.params['callFrames']) {
var location = frame['location'];
var functionName = frame['functionName'] as String ?? '';
functionName = functionName.split('.').last;
// Chrome is 0 based. Account for this.
var jsLocation = JsLocation.fromZeroBased(location['scriptId'] as String,
location['lineNumber'] as int, location['columnNumber'] as int);
var dartFrame = _frameFor(jsLocation);
if (dartFrame != null) {
dartFrame.code.name = functionName.isEmpty ? '<closure>' : functionName;
dartFrame.index = index++;
dartFrames.add(dartFrame);
}
}
return dartFrames;
}
/// Returns a Dart [Frame] for a [JsLocation].
Frame _frameFor(JsLocation jsLocation) {
// TODO(sdk/issues/37240) - ideally we look for an exact location instead
// of the closest location on a given line.
Location bestLocation;
for (var location in sources.locationsForJs(jsLocation.scriptId)) {
if (location.jsLocation.line == jsLocation.line) {
bestLocation ??= location;
if ((location.jsLocation.column - jsLocation.column).abs() <
(bestLocation.jsLocation.column - jsLocation.column).abs()) {
bestLocation = location;
}
}
}
if (bestLocation == null) return null;
var script = _appInspectorProvider()
?.scriptRefFor(bestLocation.dartLocation.uri.serverPath);
return Frame()
..code = (CodeRef()..kind = CodeKind.kDart)
..location = (SourceLocation()
..tokenPos = bestLocation.tokenPos
..script = script)
..kind = FrameKind.kRegular;
}
/// Handles pause events coming from the Chrome connection.
Future<void> _pauseHandler(DebuggerPausedEvent e) async {
var isolate = _appInspectorProvider().isolate;
if (isolate == null) return;
var event = Event()..isolate = toIsolateRef(isolate);
var params = e.params;
var breakpoints = params['hitBreakpoints'] as List;
if (breakpoints.isNotEmpty) {
event.kind = EventKind.kPauseBreakpoint;
} else if (e.reason == 'exception' || e.reason == 'assert') {
event.kind = EventKind.kPauseException;
} else {
// If we don't have source location continue stepping.
if (_isStepping && _sourceLocation(e) == null) {
await _chromeDebugger.sendCommand('Debugger.stepInto');
return;
}
event.kind = EventKind.kPauseInterrupted;
}
var frames = _dartFramesFor(e);
_pausedStack = Stack()
..frames = frames
..messages = [];
if (frames.isNotEmpty) event.topFrame = frames.first;
_streamNotify('Debug', event);
}
/// Handles resume events coming from the Chrome connection.
Future<void> _resumeHandler(DebuggerResumedEvent e) async {
// We can receive a resume event in the middle of a reload which will
// result in a null isolate.
var isolate = _appInspectorProvider()?.isolate;
if (isolate == null) return;
_pausedStack = null;
_streamNotify(
'Debug',
Event()
..kind = EventKind.kResume
..isolate = toIsolateRef(isolate));
}
}
/// Keeps track of the Dart and JS breakpoint Ids that correspond.
class _Breakpoints {
final Map<String, String> _byJsId = {};
final Map<String, String> _byDartId = {};
final AppInspectorProvider _appInspectorProvider;
_Breakpoints(this._appInspectorProvider);
/// Record the breakpoint.
///
/// Either [dartId] or the Dart breakpoint [bp] must be provided.
void noteBreakpoint({String js, String dartId, Breakpoint bp}) {
_byJsId[js] = dartId ?? bp?.id;
_byDartId[dartId ?? bp?.id] = js;
var isolate = _appInspectorProvider().isolate;
if (bp != null) {
isolate?.breakpoints?.add(bp);
}
}
Breakpoint removeBreakpoint({String js, String dartId, Breakpoint bp}) {
var isolate = _appInspectorProvider().isolate;
_byJsId.remove(js);
_byDartId.remove(dartId ?? bp?.id);
Breakpoint dartBp;
// TODO: Do something better than the default throw when it's not found.
dartBp = bp ??
isolate.breakpoints
.firstWhere((b) => b.id == dartId, orElse: () => null);
isolate?.breakpoints?.remove(dartBp);
return dartBp;
}
String dartId(String jsId) => _byJsId[jsId];
String jsId(String dartId) => _byDartId[dartId];
}