// Copyright 2014 The Flutter Authors. 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:dds/dap.dart' hide PidTracker; import 'package:vm_service/vm_service.dart' as vm; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/platform.dart'; import '../cache.dart'; import 'flutter_adapter_args.dart'; import 'mixins.dart'; /// A base DAP Debug Adapter for Flutter applications and tests. abstract class FlutterBaseDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments, FlutterAttachRequestArguments> with PidTracker { FlutterBaseDebugAdapter( super.channel, { required this.fileSystem, required this.platform, super.ipv6, this.enableFlutterDds = true, super.enableAuthCodes, super.logger, super.onError, }) : flutterSdkRoot = Cache.flutterRoot!, // Always disable in the DAP layer as it's handled in the spawned // 'flutter' process. super(enableDds: false) { configureOrgDartlangSdkMappings(); } FileSystem fileSystem; Platform platform; Process? process; final String flutterSdkRoot; /// Whether DDS should be enabled in the Flutter process. /// /// We never enable DDS in the DAP process for Flutter, so this value is not /// the same as what is passed to the base class, which is always provided 'false'. final bool enableFlutterDds; @override final FlutterLaunchRequestArguments Function(Map<String, Object?> obj) parseLaunchArgs = FlutterLaunchRequestArguments.fromJson; @override final FlutterAttachRequestArguments Function(Map<String, Object?> obj) parseAttachArgs = FlutterAttachRequestArguments.fromJson; /// Whether the VM Service closing should be used as a signal to terminate the debug session. /// /// Since we always have a process for Flutter (whether run or attach) we'll /// always use its termination instead, so this is always false. @override bool get terminateOnVmServiceClose => false; /// Whether or not the user requested debugging be enabled. /// /// For debugging to be enabled, the user must have chosen "Debug" (and not /// "Run") in the editor (which maps to the DAP `noDebug` field). bool get enableDebugger { final DartCommonLaunchAttachRequestArguments args = this.args; if (args is FlutterLaunchRequestArguments) { // Invert DAP's noDebug flag, treating it as false (so _do_ debug) if not // provided. return !(args.noDebug ?? false); } // Otherwise (attach), always debug. return true; } void configureOrgDartlangSdkMappings() { /// When a user navigates into 'dart:xxx' sources in their editor (via the /// analysis server) they will land in flutter_sdk/bin/cache/pkg/sky_engine. /// /// The running VM knows nothing about these paths and will resolve these /// libraries to 'org-dartlang-sdk://' URIs. We need to map between these /// to ensure that if a user puts a breakpoint inside sky_engine the VM can /// apply it to the correct place and once hit, we can navigate the user /// back to the correct file on their disk. /// /// The mapping is handled by the base adapter but we need to override the /// paths to match the layout used by Flutter. /// /// In future this might become unnecessary if /// https://github.com/dart-lang/sdk/issues/48435 is implemented. Until /// then, providing these mappings improves the debugging experience. // Clear original Dart SDK mappings because they're not valid here. orgDartlangSdkMappings.clear(); // 'dart:ui' maps to /flutter/lib/ui final String flutterRoot = fileSystem.path.join(flutterSdkRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui'); orgDartlangSdkMappings[flutterRoot] = Uri.parse('org-dartlang-sdk:///flutter/lib/ui'); // The rest of the Dart SDK maps to /third_party/dart/sdk final String dartRoot = fileSystem.path.join(flutterSdkRoot, 'bin', 'cache', 'pkg', 'sky_engine'); orgDartlangSdkMappings[dartRoot] = Uri.parse('org-dartlang-sdk:///third_party/dart/sdk'); } @override Future<void> debuggerConnected(vm.VM vmInfo) async { // Usually we'd capture the pid from the VM here and record it for // terminating, however for Flutter apps it may be running on a remote // device so it's not valid to terminate a process with that pid locally. // For attach, pids should never be collected as terminateRequest() should // not terminate the debugee. } /// Called by [disconnectRequest] to request that we forcefully shut down the app being run (or in the case of an attach, disconnect). /// /// Client IDEs/editors should send a terminateRequest before a /// disconnectRequest to allow a graceful shutdown. This method must terminate /// quickly and therefore may leave orphaned processes. @override Future<void> disconnectImpl() async { if (isAttach) { await handleDetach(); } terminatePids(ProcessSignal.sigkill); } Future<void> launchAsProcess({ required String executable, required List<String> processArgs, required Map<String, String>? env, }) async { final Process process = await ( String executable, List<String> processArgs, { required Map<String, String>? env, }) async { logger?.call('Spawning $executable with $processArgs in ${args.cwd}'); final Process process = await Process.start( executable, processArgs, workingDirectory: args.cwd, environment: env, ); pidsToTerminate.add(process.pid); return process; }(executable, processArgs, env: env); this.process = process; process.stdout.transform(ByteToLineTransformer()).listen(handleStdout); process.stderr.listen(handleStderr); unawaited(process.exitCode.then(handleExitCode)); } void handleExitCode(int code); void handleStderr(List<int> data); void handleStdout(String data); }