mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 10:49:00 +00:00
[dds/dap] Add some basic global evaluation support
This provides support for basic global evaluation matching the legacy DAPs. The first available thread is used (because there's currently no way for the user to select a thread) and we look up a library from a file URI provided in the `context` field. In future I hope there's a standard DAP way of getting a file from the client (see https://github.com/microsoft/vscode/issues/134452). See https://github.com/dart-lang/sdk/issues/52574 See https://github.com/Dart-Code/Dart-Code/issues/4636 Change-Id: I7bfa466001142e7e39ebb270ce65f4746a9affcd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/312980 Commit-Queue: Ben Konyi <bkonyi@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com>
This commit is contained in:
parent
9a909ac320
commit
80a6670d5d
7 changed files with 200 additions and 10 deletions
|
@ -1,5 +1,6 @@
|
|||
# 2.9.3
|
||||
- [DAP] `threadId`s generated by the debug adapter now match the Isolate numbers of the underlying isolates.
|
||||
- [DAP] Global evaluation (evaluation without a `frameId`) is now available for top-levels if a `file://` URI for a script is provided as the `context` for an `evaluate` request.
|
||||
|
||||
# 2.9.2
|
||||
- [DAP] Fixed an issue that could cause breakpoints to become unresolved when there are multiple isolates (such as during a test run).
|
||||
|
|
|
@ -969,11 +969,18 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
|
|||
}
|
||||
}
|
||||
|
||||
if (thread == null || frameIndex == null) {
|
||||
// TODO(dantup): Dart-Code evaluates these in the context of the rootLib
|
||||
// rather than just not supporting it. Consider something similar (or
|
||||
// better here).
|
||||
throw UnimplementedError('Global evaluation not currently supported');
|
||||
// To support global evaluation, we allow passing a file:/// URI in the
|
||||
// context argument.
|
||||
final context = args.context;
|
||||
final targetScriptFileUri = context != null &&
|
||||
context.startsWith('file://') &&
|
||||
context.endsWith('.dart')
|
||||
? Uri.tryParse(context)
|
||||
: null;
|
||||
|
||||
if ((thread == null || frameIndex == null) && targetScriptFileUri == null) {
|
||||
throw UnimplementedError(
|
||||
'Global evaluation not currently supported without a Dart script context');
|
||||
}
|
||||
|
||||
// Parse the expression for trailing format specifiers.
|
||||
|
@ -991,7 +998,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
|
|||
// the arguments.
|
||||
VariableFormat.fromDapValueFormat(args.format);
|
||||
|
||||
final exceptionReference = thread.exceptionReference;
|
||||
final exceptionReference = thread?.exceptionReference;
|
||||
// The value in the constant `frameExceptionExpression` is used as a special
|
||||
// expression that evaluates to the exception on the current thread. This
|
||||
// allows us to construct evaluateNames that evaluate to the fields down the
|
||||
|
@ -1002,19 +1009,38 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
|
|||
|
||||
vm.Response? result;
|
||||
try {
|
||||
if (exceptionReference != null && isExceptionExpression) {
|
||||
if (thread != null &&
|
||||
exceptionReference != null &&
|
||||
isExceptionExpression) {
|
||||
result = await _evaluateExceptionExpression(
|
||||
exceptionReference,
|
||||
expression,
|
||||
thread,
|
||||
);
|
||||
} else {
|
||||
} else if (thread != null && frameIndex != null) {
|
||||
result = await vmService?.evaluateInFrame(
|
||||
thread.isolate.id!,
|
||||
frameIndex,
|
||||
expression,
|
||||
disableBreakpoints: true,
|
||||
);
|
||||
} else if (targetScriptFileUri != null &&
|
||||
// Since we can't currently get a thread, we assume the first thread is
|
||||
// a reasonable target for global evaluation.
|
||||
(thread = isolateManager.threads.firstOrNull) != null &&
|
||||
thread != null) {
|
||||
final library = await thread.getLibraryForFileUri(targetScriptFileUri);
|
||||
if (library == null) {
|
||||
// Wrapped in DebugAdapterException in the catch below.
|
||||
throw 'Unable to find the library for $targetScriptFileUri';
|
||||
}
|
||||
|
||||
result = await vmService?.evaluate(
|
||||
thread.isolate.id!,
|
||||
library.id!,
|
||||
expression,
|
||||
disableBreakpoints: true,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
final rawMessage = '$e';
|
||||
|
@ -1039,7 +1065,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
|
|||
throw DebugAdapterException(result.message ?? '<error ref>');
|
||||
} else if (result is vm.Sentinel) {
|
||||
throw DebugAdapterException(result.valueAsString ?? '<collected>');
|
||||
} else if (result is vm.InstanceRef) {
|
||||
} else if (result is vm.InstanceRef && thread != null) {
|
||||
final resultString = await _converter.convertVmInstanceRefToDisplayString(
|
||||
thread,
|
||||
result,
|
||||
|
|
|
@ -233,4 +233,12 @@ mixin FileUtils {
|
|||
}
|
||||
return filePath.substring(0, 1).toUpperCase() + filePath.substring(1);
|
||||
}
|
||||
|
||||
/// Normalizes a file [Uri] via [normalizePath].
|
||||
Uri normalizeUri(Uri fileUri) {
|
||||
if (!fileUri.isScheme('file')) {
|
||||
return fileUri;
|
||||
}
|
||||
return Uri.file(normalizePath(fileUri.toFilePath()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:vm_service/vm_service.dart' as vm;
|
|||
|
||||
import '../rpc_error_codes.dart';
|
||||
import 'adapters/dart.dart';
|
||||
import 'adapters/mixins.dart';
|
||||
import 'utils.dart';
|
||||
import 'variables.dart';
|
||||
|
||||
|
@ -175,6 +176,10 @@ class IsolateManager {
|
|||
return res as T;
|
||||
}
|
||||
|
||||
Future<vm.ScriptList> getScripts(vm.IsolateRef isolate) async {
|
||||
return (await _adapter.vmService?.getScripts(isolate.id!)) as vm.ScriptList;
|
||||
}
|
||||
|
||||
/// Retrieves some basic data indexed by an integer for use in "reference"
|
||||
/// fields that are round-tripped to the client.
|
||||
StoredData? getStoredData(int id) {
|
||||
|
@ -984,7 +989,7 @@ class IsolateManager {
|
|||
}
|
||||
|
||||
/// Holds state for a single Isolate/Thread.
|
||||
class ThreadInfo {
|
||||
class ThreadInfo with FileUtils {
|
||||
final IsolateManager _manager;
|
||||
final vm.IsolateRef isolate;
|
||||
final int isolateNumber;
|
||||
|
@ -1048,6 +1053,11 @@ class ThreadInfo {
|
|||
return _scripts.putIfAbsent(script.id!, () => getObject<vm.Script>(script));
|
||||
}
|
||||
|
||||
/// Fetches scripts for a given isolate.
|
||||
Future<vm.ScriptList> getScripts() {
|
||||
return _manager.getScripts(isolate);
|
||||
}
|
||||
|
||||
/// Resolves a source file path into a URI for the VM.
|
||||
///
|
||||
/// sdk-path/lib/core/print.dart -> dart:core/print.dart
|
||||
|
@ -1291,6 +1301,33 @@ class ThreadInfo {
|
|||
void clearStoredData() {
|
||||
_manager.clearStoredData(this);
|
||||
}
|
||||
|
||||
/// Attempts to get a [vm.LibraryRef] for the given [scriptFileUri].
|
||||
///
|
||||
/// This involves fetching all scripts for this isolate and looking for a
|
||||
/// match and then returning the relevant library reference.
|
||||
Future<vm.LibraryRef?> getLibraryForFileUri(Uri scriptFileUri) async {
|
||||
// We start with a file URI and need to find the Library (via the script).
|
||||
//
|
||||
// We need to handle msimatched drive letters, and also file vs package
|
||||
// URIs.
|
||||
final scriptResolvedUri =
|
||||
await resolvePathToUri(scriptFileUri.toFilePath());
|
||||
final candidateUris = {
|
||||
scriptFileUri.toString(),
|
||||
normalizeUri(scriptFileUri).toString(),
|
||||
if (scriptResolvedUri != null) scriptResolvedUri.toString(),
|
||||
if (scriptResolvedUri != null) normalizeUri(scriptResolvedUri).toString(),
|
||||
};
|
||||
|
||||
// Find the matching script/library.
|
||||
final scriptRefs = (await getScripts()).scripts ?? const [];
|
||||
final scriptRef = scriptRefs
|
||||
.singleWhereOrNull((script) => candidateUris.contains(script.uri));
|
||||
final script = scriptRef != null ? await getScript(scriptRef) : null;
|
||||
|
||||
return script?.library;
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper over the client-provided [SourceBreakpoint] with a unique ID.
|
||||
|
|
|
@ -216,6 +216,86 @@ void foo() {
|
|||
);
|
||||
});
|
||||
|
||||
group('global evaluation', () {
|
||||
test('can evaluate when not paused given a script URI', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(globalEvaluationProgram);
|
||||
|
||||
await Future.wait([
|
||||
client.initialize(),
|
||||
client.launch(testFile.path),
|
||||
], eagerError: true);
|
||||
|
||||
// Wait for a '.' to be printed to know the script is up and running.
|
||||
await dap.client.outputEvents
|
||||
.firstWhere((event) => event.output.trim() == '.');
|
||||
|
||||
await client.expectGlobalEvalResult(
|
||||
'myGlobal',
|
||||
'"Hello, world!"',
|
||||
context: Uri.file(testFile.path).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns a suitable error with no context', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(globalEvaluationProgram);
|
||||
|
||||
await Future.wait([
|
||||
client.initialize(),
|
||||
client.launch(testFile.path),
|
||||
], eagerError: true);
|
||||
|
||||
// Wait for a '.' to be printed to know the script is up and running.
|
||||
await dap.client.outputEvents
|
||||
.firstWhere((event) => event.output.trim() == '.');
|
||||
|
||||
final response = await client.sendRequest(
|
||||
EvaluateArguments(
|
||||
expression: 'myGlobal',
|
||||
),
|
||||
allowFailure: true,
|
||||
);
|
||||
expect(response.success, isFalse);
|
||||
expect(
|
||||
response.message,
|
||||
contains(
|
||||
'Global evaluation not currently supported without a Dart script context',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns a suitable error with an unknown script context', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(globalEvaluationProgram);
|
||||
|
||||
await Future.wait([
|
||||
client.initialize(),
|
||||
client.launch(testFile.path),
|
||||
], eagerError: true);
|
||||
|
||||
// Wait for a '.' to be printed to know the script is up and running.
|
||||
await dap.client.outputEvents
|
||||
.firstWhere((event) => event.output.trim() == '.');
|
||||
|
||||
final context =
|
||||
Uri.file(testFile.path.replaceAll('.dart', '_invalid.dart'))
|
||||
.toString();
|
||||
final response = await client.sendRequest(
|
||||
EvaluateArguments(
|
||||
expression: 'myGlobal',
|
||||
context: context,
|
||||
),
|
||||
allowFailure: true,
|
||||
);
|
||||
expect(response.success, isFalse);
|
||||
expect(
|
||||
response.message,
|
||||
contains('Unable to find the library for file:'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('format specifiers', () {
|
||||
test('",nq" suppresses quotes on strings', () async {
|
||||
final client = dap.client;
|
||||
|
|
|
@ -1176,4 +1176,27 @@ extension DapTestClientExtension on DapTestClient {
|
|||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// Evaluates [expression] without a frame and expects a specific
|
||||
/// [expectedResult].
|
||||
Future<EvaluateResponseBody> expectGlobalEvalResult(
|
||||
String expression,
|
||||
String expectedResult, {
|
||||
String? context,
|
||||
ValueFormat? format,
|
||||
}) async {
|
||||
final response = await evaluate(
|
||||
expression,
|
||||
context: context,
|
||||
format: format,
|
||||
);
|
||||
expect(response.success, isTrue);
|
||||
expect(response.command, equals('evaluate'));
|
||||
final body =
|
||||
EvaluateResponseBody.fromJson(response.body as Map<String, Object?>);
|
||||
|
||||
expect(body.result, equals(expectedResult));
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,6 +106,21 @@ const infiniteRunningProgram = '''
|
|||
}
|
||||
''';
|
||||
|
||||
/// A Dart script that loops forever sleeping for 1 second each
|
||||
/// iteration.
|
||||
///
|
||||
/// A top-level String variable `myGlobal` is available with the value
|
||||
/// `"Hello, world!"`.
|
||||
const globalEvaluationProgram = '''
|
||||
var myGlobal = 'Hello, world!';
|
||||
void main(List<String> args) async {
|
||||
while (true) {
|
||||
print('.');
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
/// A simple async Dart script that when stopped at the line of '// BREAKPOINT'
|
||||
/// will contain multiple stack frames across some async boundaries.
|
||||
const simpleAsyncProgram = '''
|
||||
|
|
Loading…
Reference in a new issue