[dds/dap] Normalise Windows drive letters to avoid missing breakpoints

See https://github.com/dart-lang/sdk/issues/32222.
See https://github.com/Dart-Code/Dart-Code/issues/4149.

Change-Id: I6f975734839ff7cad4d086d5363c0ab03390b966
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/258900
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
This commit is contained in:
Danny Tuppeny 2022-09-13 13:24:47 +00:00 committed by Commit Bot
parent e4ae0cf2ce
commit 1b9adcb502
8 changed files with 136 additions and 22 deletions

View file

@ -1,6 +1,7 @@
# 2.3.0-dev # 2.3.0
- [DAP] Removed an unused parameter `resumeIfStarting` from `DartDebugAdapter.connectDebugger`. - [DAP] Removed an unused parameter `resumeIfStarting` from `DartDebugAdapter.connectDebugger`.
- [DAP] Fixed some issues where removing breakpoints could fail if an isolate exited during an update or multiple client breakpoints mapped to the same VM breakpoint. - [DAP] Fixed some issues where removing breakpoints could fail if an isolate exited during an update or multiple client breakpoints mapped to the same VM breakpoint.
- [DAP] Paths provided to DAP now always have Windows drive letters normalized to uppercase to avoid some issues where paths may be treated case sensitively.
# 2.2.6 # 2.2.6
- Fixed an issue where debug adapters would not automatically close after terminating/disconnecting from the debugee. - Fixed an issue where debug adapters would not automatically close after terminating/disconnecting from the debugee.

View file

@ -22,6 +22,7 @@ import '../protocol_converter.dart';
import '../protocol_generated.dart'; import '../protocol_generated.dart';
import '../protocol_stream.dart'; import '../protocol_stream.dart';
import '../utils.dart'; import '../utils.dart';
import 'mixins.dart';
/// The mime type to send with source responses to the client. /// The mime type to send with source responses to the client.
/// ///
@ -285,7 +286,8 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
/// (for example when the server sends a `StoppedEvent` it may cause the client /// (for example when the server sends a `StoppedEvent` it may cause the client
/// to then send a `stackTraceRequest` or `scopesRequest` to get variables). /// to then send a `stackTraceRequest` or `scopesRequest` to get variables).
abstract class DartDebugAdapter<TL extends LaunchRequestArguments, abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
TA extends AttachRequestArguments> extends BaseDebugAdapter<TL, TA> { TA extends AttachRequestArguments> extends BaseDebugAdapter<TL, TA>
with FileUtils {
late final DartCommonLaunchAttachRequestArguments args; late final DartCommonLaunchAttachRequestArguments args;
final _debuggerInitializedCompleter = Completer<void>(); final _debuggerInitializedCompleter = Completer<void>();
final _configurationDoneCompleter = Completer<void>(); final _configurationDoneCompleter = Completer<void>();
@ -706,7 +708,6 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
void Function(Object?) sendResponse, void Function(Object?) sendResponse,
) async { ) async {
switch (request.command) { switch (request.command) {
// Used by tests to validate available protocols (e.g. DDS). There may be // Used by tests to validate available protocols (e.g. DDS). There may be
// value in making this available to clients in future, but for now it's // value in making this available to clients in future, but for now it's
// internal. // internal.
@ -1186,7 +1187,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
final path = args.source.path; final path = args.source.path;
final name = args.source.name; final name = args.source.name;
final uri = path != null ? Uri.file(path).toString() : name!; final uri = path != null ? Uri.file(normalizePath(path)).toString() : name!;
await _isolateManager.setBreakpoints(uri, breakpoints); await _isolateManager.setBreakpoints(uri, breakpoints);

View file

@ -124,7 +124,9 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
} }
// Handle customTool and deletion of any arguments for it. // Handle customTool and deletion of any arguments for it.
final executable = args.customTool ?? Platform.resolvedExecutable; final executable = normalizePath(
args.customTool ?? Platform.resolvedExecutable,
);
final removeArgs = args.customToolReplacesArgs; final removeArgs = args.customToolReplacesArgs;
if (args.customTool != null && removeArgs != null) { if (args.customTool != null && removeArgs != null) {
vmArgs.removeRange(0, math.min(removeArgs, vmArgs.length)); vmArgs.removeRange(0, math.min(removeArgs, vmArgs.length));
@ -133,7 +135,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
final processArgs = [ final processArgs = [
...vmArgs, ...vmArgs,
...toolArgs, ...toolArgs,
args.program, normalizePath(args.program),
...?args.args, ...?args.args,
]; ];
@ -153,20 +155,25 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
: null : null
: null; : null;
var cwd = args.cwd;
if (cwd != null) {
cwd = normalizePath(cwd);
}
if (terminalKind != null) { if (terminalKind != null) {
await launchInEditorTerminal( await launchInEditorTerminal(
debug, debug,
terminalKind, terminalKind,
executable, executable,
processArgs, processArgs,
workingDirectory: args.cwd, workingDirectory: cwd,
env: args.env, env: args.env,
); );
} else { } else {
await launchAsProcess( await launchAsProcess(
executable, executable,
processArgs, processArgs,
workingDirectory: args.cwd, workingDirectory: cwd,
env: args.env, env: args.env,
); );
} }
@ -216,7 +223,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
// we can detect with the normal watching code. // we can detect with the normal watching code.
final requestArgs = RunInTerminalRequestArguments( final requestArgs = RunInTerminalRequestArguments(
args: [executable, ...processArgs], args: [executable, ...processArgs],
cwd: workingDirectory ?? path.dirname(args.program), cwd: workingDirectory ?? normalizePath(path.dirname(args.program)),
env: env, env: env,
kind: terminalKind, kind: terminalKind,
title: args.name ?? 'Dart', title: args.name ?? 'Dart',

View file

@ -111,7 +111,9 @@ class DartTestDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
]; ];
// Handle customTool and deletion of any arguments for it. // Handle customTool and deletion of any arguments for it.
final executable = args.customTool ?? Platform.resolvedExecutable; final executable = normalizePath(
args.customTool ?? Platform.resolvedExecutable,
);
final removeArgs = args.customToolReplacesArgs; final removeArgs = args.customToolReplacesArgs;
if (args.customTool != null && removeArgs != null) { if (args.customTool != null && removeArgs != null) {
vmArgs.removeRange(0, math.min(removeArgs, vmArgs.length)); vmArgs.removeRange(0, math.min(removeArgs, vmArgs.length));
@ -120,14 +122,19 @@ class DartTestDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
final processArgs = [ final processArgs = [
...vmArgs, ...vmArgs,
...?args.toolArgs, ...?args.toolArgs,
args.program, normalizePath(args.program),
...?args.args, ...?args.args,
]; ];
var cwd = args.cwd;
if (cwd != null) {
cwd = normalizePath(cwd);
}
await launchAsProcess( await launchAsProcess(
executable, executable,
processArgs, processArgs,
workingDirectory: args.cwd, workingDirectory: cwd,
env: args.env, env: args.env,
); );
} }

View file

@ -134,7 +134,7 @@ mixin TestAdapter {
/// A mixin providing some utility functions for working with vm-service-info /// A mixin providing some utility functions for working with vm-service-info
/// files such as ensuring a temp folder exists to create them in, and waiting /// files such as ensuring a temp folder exists to create them in, and waiting
/// for the file to become valid parsable JSON. /// for the file to become valid parsable JSON.
mixin VmServiceInfoFileUtils { mixin VmServiceInfoFileUtils on FileUtils {
/// Creates a temp folder for the VM to write the service-info-file into and /// Creates a temp folder for the VM to write the service-info-file into and
/// returns the [File] to use. /// returns the [File] to use.
File generateVmServiceInfoFile() { File generateVmServiceInfoFile() {
@ -143,7 +143,8 @@ mixin VmServiceInfoFileUtils {
// watcher. Creating/watching a folder and writing the file into it seems // watcher. Creating/watching a folder and writing the file into it seems
// to be reliable. // to be reliable.
final serviceInfoFilePath = path.join( final serviceInfoFilePath = path.join(
Directory.systemTemp.createTempSync('dart-vm-service').path, normalizePath(
Directory.systemTemp.createTempSync('dart-vm-service').path),
'vm.json', 'vm.json',
); );
@ -203,3 +204,24 @@ mixin VmServiceInfoFileUtils {
} }
} }
} }
mixin FileUtils {
/// Normalizes [filePath] to avoid issues with different casing of drive
/// letters on Windows.
///
/// Some clients like VS Code do their own normalization and may provide drive
/// letters case different in some requests (such as breakpoints) to drive
/// letters computed elsewhere (such in `Platform.resolvedExecutable`). When
/// these do not match, breakpoints may not be hit.
///
/// This is the case for the whole path, but drive letters are most commonly
/// mismatched due to VS Code's explicit normalization.
///
/// https://github.com/dart-lang/sdk/issues/32222
String normalizePath(String filePath) {
if (!Platform.isWindows || filePath.isEmpty || path.isRelative(filePath)) {
return filePath;
}
return filePath.substring(0, 1).toUpperCase() + filePath.substring(1);
}
}

View file

@ -1,5 +1,5 @@
name: dds name: dds
version: 2.2.6 version: 2.3.0
description: >- description: >-
A library used to spawn the Dart Developer Service, used to communicate with A library used to spawn the Dart Developer Service, used to communicate with
a Dart VM Service instance. a Dart VM Service instance.

View file

@ -107,7 +107,8 @@ void main(List<String> args) async {
await client.hitBreakpoint(sdkFile, breakpointLine, entryFile: testFile); await client.hitBreakpoint(sdkFile, breakpointLine, entryFile: testFile);
}); });
test('stops at a line breakpoint and can be resumed', () async { /// Tests hitting a simple breakpoint and resuming.
Future<void> _testHitBreakpointAndResume() async {
final client = dap.client; final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram); final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker); final breakpointLine = lineWith(testFile, breakpointMarker);
@ -120,8 +121,32 @@ void main(List<String> args) async {
client.event('terminated'), client.event('terminated'),
client.continue_(stop.threadId!), client.continue_(stop.threadId!),
], eagerError: true); ], eagerError: true);
}
test('stops at a line breakpoint and can be resumed', () async {
await _testHitBreakpointAndResume();
}); });
test(
'stops at a line breakpoint and can be resumed '
'when breakpoint requests have lowercase drive letters '
'and program/VM have uppercase drive letters', () async {
final client = dap.client;
client.forceDriveLetterCasingUpper = true;
client.forceBreakpointDriveLetterCasingLower = true;
await _testHitBreakpointAndResume();
}, skip: !Platform.isWindows);
test(
'stops at a line breakpoint and can be resumed '
'when breakpoint requests have uppercase drive letters '
'and program/VM have lowercase drive letters', () async {
final client = dap.client;
client.forceDriveLetterCasingLower = true;
client.forceBreakpointDriveLetterCasingUpper = true;
await _testHitBreakpointAndResume();
}, skip: !Platform.isWindows);
test('stops at a line breakpoint and can step over (next)', () async { test('stops at a line breakpoint and can step over (next)', () async {
final testFile = dap.createTestFile(''' final testFile = dap.createTestFile('''
void main(List<String> args) async { void main(List<String> args) async {

View file

@ -38,6 +38,20 @@ class DapTestClient {
late final Future<Uri?> vmServiceUri; late final Future<Uri?> vmServiceUri;
/// Used to control drive letter casing on Windows for testing.
bool? forceDriveLetterCasingUpper;
/// Used to control drive letter casing on Windows for testing.
bool? forceDriveLetterCasingLower;
/// Used to control drive letter casing for breakpoint requests on Windows for
/// testing.
bool? forceBreakpointDriveLetterCasingUpper;
/// Used to control drive letter casing for breakpoint requests on Windows for
/// testing.
bool? forceBreakpointDriveLetterCasingLower;
DapTestClient._( DapTestClient._(
this._channel, this._channel,
this._logger, { this._logger, {
@ -238,11 +252,12 @@ class DapTestClient {
return sendRequest( return sendRequest(
DartLaunchRequestArguments( DartLaunchRequestArguments(
noDebug: noDebug, noDebug: noDebug,
program: program, program: _normalizePath(program),
cwd: cwd, cwd: cwd != null ? _normalizePath(cwd) : null,
args: args, args: args,
toolArgs: toolArgs, toolArgs: toolArgs,
additionalProjectPaths: additionalProjectPaths, additionalProjectPaths:
additionalProjectPaths?.map(_normalizePath).toList(),
console: console, console: console,
debugSdkLibraries: debugSdkLibraries, debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries, debugExternalPackageLibraries: debugExternalPackageLibraries,
@ -522,7 +537,7 @@ extension DapTestClientExtension on DapTestClient {
Future<void> setBreakpoint(File file, int line, {String? condition}) async { Future<void> setBreakpoint(File file, int line, {String? condition}) async {
await sendRequest( await sendRequest(
SetBreakpointsArguments( SetBreakpointsArguments(
source: Source(path: file.path), source: Source(path: _normalizeBreakpointPath(file.path)),
breakpoints: [SourceBreakpoint(line: line, condition: condition)], breakpoints: [SourceBreakpoint(line: line, condition: condition)],
), ),
); );
@ -532,12 +547,48 @@ extension DapTestClientExtension on DapTestClient {
Future<void> setBreakpoints(File file, List<int> lines) async { Future<void> setBreakpoints(File file, List<int> lines) async {
await sendRequest( await sendRequest(
SetBreakpointsArguments( SetBreakpointsArguments(
source: Source(path: file.path), source: Source(path: _normalizeBreakpointPath(file.path)),
breakpoints: lines.map((line) => SourceBreakpoint(line: line)).toList(), breakpoints: lines.map((line) => SourceBreakpoint(line: line)).toList(),
), ),
); );
} }
/// Normalizes a non-breakpoint path being sent to the debug adapter based on
/// the values of [forceDriveLetterCasingUpper] and
/// [forceDriveLetterCasingLower].
String _normalizePath(String path) {
return _forceDriveLetterCasing(
path,
upper: forceDriveLetterCasingUpper,
lower: forceDriveLetterCasingLower,
);
}
/// Normalizes a non-breakpoint path being sent to the debug adapter based on
/// the values of [forceBreakpointDriveLetterCasingUpper] and
/// [forceBreakpointDriveLetterCasingLower].
String _normalizeBreakpointPath(String path) {
return _forceDriveLetterCasing(
path,
upper: forceBreakpointDriveLetterCasingUpper,
lower: forceBreakpointDriveLetterCasingLower,
);
}
String _forceDriveLetterCasing(String path, {bool? upper, bool? lower}) {
assert(upper != true || lower != true);
if (!Platform.isWindows || path.isEmpty) {
return path;
}
if (upper ?? false) {
return path.substring(0, 1).toUpperCase() + path.substring(1);
} else if (lower ?? false) {
return path.substring(0, 1).toLowerCase() + path.substring(1);
} else {
return path;
}
}
/// Sets the exception pause mode to [pauseMode] and expects to pause after /// Sets the exception pause mode to [pauseMode] and expects to pause after
/// running the script. /// running the script.
/// ///
@ -585,7 +636,7 @@ extension DapTestClientExtension on DapTestClient {
initialize(), initialize(),
sendRequest( sendRequest(
SetBreakpointsArguments( SetBreakpointsArguments(
source: Source(path: file.path), source: Source(path: _normalizeBreakpointPath(file.path)),
breakpoints: [ breakpoints: [
SourceBreakpoint( SourceBreakpoint(
line: line, line: line,