blob: 1fa00fe1d14fdcfee2c6c33b72c443d34421ac86 [file] [log] [blame]
// Copyright (c) 2021, 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:io';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vm;
import 'adapters/dart.dart';
import 'isolate_manager.dart';
import 'protocol_generated.dart' as dap;
/// A helper that handlers converting to/from DAP and VM Service types and to
/// user-friendly display strings.
///
/// This class may call back to the VM Service to fetch additional information
/// when converting classes - for example when converting a stack frame it may
/// fetch scripts from the VM Service in order to map token positions back to
/// line/columns as required by DAP.
class ProtocolConverter {
/// The parent debug adapter, used to access arguments and the VM Service for
/// the debug session.
final DartDebugAdapter _adapter;
ProtocolConverter(this._adapter);
/// Converts an absolute path to one relative to the cwd used to launch the
/// application.
///
/// If [sourcePath] is outside of the cwd used for launching the application
/// then the full absolute path will be returned.
String convertToRelativePath(String sourcePath) {
final cwd = _adapter.args.cwd;
if (cwd == null) {
return sourcePath;
}
final rel = path.relative(sourcePath, from: cwd);
return !rel.startsWith('..') ? rel : sourcePath;
}
/// Converts a VM Service stack frame to a DAP stack frame.
Future<dap.StackFrame> convertVmToDapStackFrame(
ThreadInfo thread,
vm.Frame frame, {
required bool isTopFrame,
int? firstAsyncMarkerIndex,
}) async {
final frameId = thread.storeData(frame);
if (frame.kind == vm.FrameKind.kAsyncSuspensionMarker) {
return dap.StackFrame(
id: frameId,
name: '<asynchronous gap>',
presentationHint: 'label',
line: 0,
column: 0,
);
}
// The VM may supply frames with a prefix that we don't want to include in
// the frame for the user.
const unoptimizedPrefix = '[Unoptimized] ';
final codeName = frame.code?.name;
final frameName = codeName != null
? (codeName.startsWith(unoptimizedPrefix)
? codeName.substring(unoptimizedPrefix.length)
: codeName)
: '<unknown>';
// If there's no location, this isn't source a user can debug so use a
// subtle hint (which the editor may use to render the frame faded).
final location = frame.location;
if (location == null) {
return dap.StackFrame(
id: frameId,
name: frameName,
presentationHint: 'subtle',
line: 0,
column: 0,
);
}
final scriptRef = location.script;
final tokenPos = location.tokenPos;
final uri = scriptRef?.uri;
final sourcePath = uri != null ? await convertVmUriToSourcePath(uri) : null;
var canShowSource = sourcePath != null && File(sourcePath).existsSync();
// Download the source if from a "dart:" uri.
int? sourceReference;
if (uri != null &&
(uri.startsWith('dart:') || uri.startsWith('org-dartlang-app:')) &&
scriptRef != null) {
sourceReference = thread.storeData(scriptRef);
canShowSource = true;
}
var line = 0, col = 0;
if (scriptRef != null && tokenPos != null) {
try {
final script = await thread.getScript(scriptRef);
line = script.getLineNumberFromTokenPos(tokenPos) ?? 0;
col = script.getColumnNumberFromTokenPos(tokenPos) ?? 0;
} catch (e) {
_adapter.logger?.call('Failed to map frame location to line/col: $e');
}
}
final source = canShowSource
? dap.Source(
name: sourcePath != null ? convertToRelativePath(sourcePath) : uri,
path: sourcePath,
sourceReference: sourceReference,
origin: null,
adapterData: location.script)
: null;
// The VM only allows us to restart from frames that are not the top frame,
// but since we're also showing asyncCausalFrames any indexes past the first
// async boundary will not line up so we cap it there.
final canRestart = !isTopFrame &&
(firstAsyncMarkerIndex == null || frame.index! < firstAsyncMarkerIndex);
return dap.StackFrame(
id: frameId,
name: frameName,
source: source,
line: line,
column: col,
canRestart: canRestart,
);
}
/// Converts the source path from the VM to a file path.
///
/// This is required so that when the user stops (or navigates via a stack
/// frame) we open the same file on their local disk. If we downloaded the
/// source from the VM, they would end up seeing two copies of files (and they
/// would each have their own breakpoints) which can be confusing.
Future<String?> convertVmUriToSourcePath(String uri) async {
if (uri.startsWith('file://')) {
return Uri.parse(uri).toFilePath();
} else if (uri.startsWith('package:')) {
// TODO(dantup): Handle mapping package: uris ?
return null;
} else {
return null;
}
}
}