| // 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| import 'package:pedantic/pedantic.dart'; |
| import 'package:vm_service/vm_service.dart' as vm; |
| |
| import '../logging.dart'; |
| import '../protocol_generated.dart'; |
| import '../protocol_stream.dart'; |
| import 'dart.dart'; |
| |
| /// A DAP Debug Adapter for running and debugging Dart CLI scripts. |
| class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> { |
| Process? _process; |
| |
| /// The location of the vm-service-info file (if debugging). |
| /// |
| /// This may be provided by the user (eg. if attaching) or generated by the DA. |
| File? _vmServiceInfoFile; |
| |
| /// A watcher for [_vmServiceInfoFile] to detect when the VM writes the service |
| /// info file. |
| /// |
| /// Should be cancelled once the file has been successfully read. |
| StreamSubscription<FileSystemEvent>? _vmServiceInfoFileWatcher; |
| |
| /// Process IDs to terminate during shutdown. |
| /// |
| /// This may be populated with pids from the VM Service to ensure we clean up |
| /// properly where signals may not be passed through the shell to the |
| /// underlying VM process. |
| /// https://github.com/Dart-Code/Dart-Code/issues/907 |
| final pidsToTerminate = <int>{}; |
| |
| @override |
| final parseLaunchArgs = DartLaunchRequestArguments.fromJson; |
| |
| DartCliDebugAdapter( |
| ByteStreamServerChannel channel, { |
| bool ipv6 = false, |
| bool enableDds = true, |
| bool enableAuthCodes = true, |
| Logger? logger, |
| }) : super( |
| channel, |
| ipv6: ipv6, |
| enableDds: enableDds, |
| enableAuthCodes: enableAuthCodes, |
| logger: logger, |
| ); |
| |
| Future<void> debuggerConnected(vm.VM vmInfo) async { |
| if (!isAttach) { |
| // Capture the PID from the VM Service so that we can terminate it when |
| // cleaning up. Terminating the process might not be enough as it could be |
| // just a shell script (eg. pub on Windows) and may not pass the |
| // signal on correctly. |
| // See: https://github.com/Dart-Code/Dart-Code/issues/907 |
| final pid = vmInfo.pid; |
| if (pid != null) { |
| pidsToTerminate.add(pid); |
| } |
| } |
| } |
| |
| /// Called by [disconnectRequest] to request that we forcefully shut down the |
| /// app being run (or in the case of an attach, disconnect). |
| Future<void> disconnectImpl() async { |
| // TODO(dantup): In Dart-Code DAP, we first try again with sigint and wait |
| // for a few seconds before sending sigkill. |
| pidsToTerminate.forEach( |
| (pid) => Process.killPid(pid, ProcessSignal.sigkill), |
| ); |
| } |
| |
| /// Called by [launchRequest] to request that we actually start the app to be |
| /// run/debugged. |
| /// |
| /// For debugging, this should start paused, connect to the VM Service, set |
| /// breakpoints, and resume. |
| Future<void> launchImpl() async { |
| final vmPath = Platform.resolvedExecutable; |
| |
| final debug = !(args.noDebug ?? false); |
| if (debug) { |
| // Create a temp folder for the VM to write the service-info-file into. |
| // Using tmpDir.createTempory() is flakey on Windows+Linux (at least |
| // on GitHub Actions) complaining the file does not exist when creating a |
| // watcher. Creating/watching a folder and writing the file into it seems |
| // to be reliable. |
| final serviceInfoFilePath = path.join( |
| Directory.systemTemp.createTempSync('dart-vm-service').path, |
| 'vm.json', |
| ); |
| _vmServiceInfoFile = File(serviceInfoFilePath); |
| _vmServiceInfoFileWatcher = _vmServiceInfoFile?.parent |
| .watch(events: FileSystemEvent.all) |
| .where((event) => event.path == _vmServiceInfoFile?.path) |
| .listen( |
| _handleVmServiceInfoEvent, |
| onError: (e) => logger?.call('Ignoring exception from watcher: $e'), |
| ); |
| } |
| |
| final vmServiceInfoFile = _vmServiceInfoFile; |
| final vmArgs = <String>[ |
| if (debug) ...[ |
| '--enable-vm-service=${args.vmServicePort ?? 0}${ipv6 ? '/::1' : ''}', |
| '--pause_isolates_on_start=true', |
| if (!enableAuthCodes) '--disable-service-auth-codes' |
| ], |
| '--disable-dart-dev', |
| if (debug && vmServiceInfoFile != null) ...[ |
| '-DSILENT_OBSERVATORY=true', |
| '--write-service-info=${Uri.file(vmServiceInfoFile.path)}' |
| ], |
| // Default to asserts on, this seems like the most useful behaviour for |
| // editor-spawned debug sessions. |
| if (args.enableAsserts ?? true) '--enable-asserts' |
| ]; |
| final processArgs = [ |
| ...vmArgs, |
| args.program, |
| ...?args.args, |
| ]; |
| |
| logger?.call('Spawning $vmPath with $processArgs in ${args.cwd}'); |
| final process = await Process.start( |
| vmPath, |
| processArgs, |
| workingDirectory: args.cwd, |
| ); |
| _process = process; |
| pidsToTerminate.add(process.pid); |
| |
| process.stdout.listen(_handleStdout); |
| process.stderr.listen(_handleStderr); |
| unawaited(process.exitCode.then(_handleExitCode)); |
| } |
| |
| /// Called by [terminateRequest] to request that we gracefully shut down the |
| /// app being run (or in the case of an attach, disconnect). |
| Future<void> terminateImpl() async { |
| pidsToTerminate.forEach( |
| (pid) => Process.killPid(pid, ProcessSignal.sigint), |
| ); |
| await _process?.exitCode; |
| } |
| |
| void _handleExitCode(int code) { |
| final codeSuffix = code == 0 ? '' : ' ($code)'; |
| logger?.call('Process exited ($code)'); |
| // Always add a leading newline since the last written text might not have |
| // had one. |
| sendOutput('console', '\nExited$codeSuffix.'); |
| sendEvent(TerminatedEventBody()); |
| } |
| |
| void _handleStderr(List<int> data) { |
| sendOutput('stderr', utf8.decode(data)); |
| } |
| |
| void _handleStdout(List<int> data) { |
| sendOutput('stdout', utf8.decode(data)); |
| } |
| |
| /// Handles file watcher events for the vm-service-info file and connects the |
| /// debugger. |
| /// |
| /// The vm-service-info file is written by the VM when we start the app/script |
| /// to debug and contains the VM Service URI. This allows us to access the |
| /// auth token without needing to have the URI printed to/scraped from stdout. |
| void _handleVmServiceInfoEvent(FileSystemEvent event) { |
| try { |
| final content = _vmServiceInfoFile!.readAsStringSync(); |
| final json = jsonDecode(content); |
| final uri = Uri.parse(json['uri']); |
| unawaited(connectDebugger(uri)); |
| _vmServiceInfoFileWatcher?.cancel(); |
| } catch (e) { |
| // It's possible we tried to read the file before it was completely |
| // written so ignore and try again on the next event. |
| logger?.call('Ignoring error parsing vm-service-info file: $e'); |
| } |
| } |
| } |