[dds] Refactor some DAP classes in preperation for "dart test" adapter

Change-Id: Ice48b45c0ad11a1981e8e4115bb8de6d009d7b5a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/212062
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 5099eef..1343db2 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -1654,10 +1654,18 @@
   /// enabling debugging.
   final bool? noDebug;
 
+  /// The program/Dart script to be run.
   final String program;
+
+  /// Arguments to be passed to [program].
   final List<String>? args;
+
+  /// Arguments to be passed to the tool that will run [program] (for example,
+  /// the VM or Flutter tool).
+  final List<String>? toolArgs;
+
   final int? vmServicePort;
-  final List<String>? vmAdditionalArgs;
+
   final bool? enableAsserts;
 
   /// Which console to run the program in.
@@ -1678,7 +1686,7 @@
     required this.program,
     this.args,
     this.vmServicePort,
-    this.vmAdditionalArgs,
+    this.toolArgs,
     this.console,
     this.enableAsserts,
     String? name,
@@ -1705,7 +1713,7 @@
         noDebug = obj['noDebug'] as bool?,
         program = obj['program'] as String,
         args = (obj['args'] as List?)?.cast<String>(),
-        vmAdditionalArgs = (obj['vmAdditionalArgs'] as List?)?.cast<String>(),
+        toolArgs = (obj['toolArgs'] as List?)?.cast<String>(),
         vmServicePort = obj['vmServicePort'] as int?,
         console = obj['console'] as String?,
         enableAsserts = obj['enableAsserts'] as bool?,
@@ -1718,7 +1726,7 @@
         if (noDebug != null) 'noDebug': noDebug,
         'program': program,
         if (args != null) 'args': args,
-        if (vmAdditionalArgs != null) 'vmAdditionalArgs': vmAdditionalArgs,
+        if (toolArgs != null) 'toolArgs': toolArgs,
         if (vmServicePort != null) 'vmServicePort': vmServicePort,
         if (console != null) 'console': console,
         if (enableAsserts != null) 'enableAsserts': enableAsserts,
diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli.dart b/pkg/dds/lib/src/dap/adapters/dart_cli.dart
index bf0eab9..2a3f351 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli.dart
@@ -10,25 +10,18 @@
 import 'package:pedantic/pedantic.dart';
 import 'package:vm_service/vm_service.dart' as vm;
 
-import '../exceptions.dart';
 import '../logging.dart';
 import '../protocol_generated.dart';
 import '../protocol_stream.dart';
 import 'dart.dart';
+import 'mixins.dart';
 
 /// A DAP Debug Adapter for running and debugging Dart CLI scripts.
 class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
-    DartAttachRequestArguments> {
+        DartAttachRequestArguments>
+    with PidTracker, VmServiceInfoFileUtils, PackageConfigUtils {
   Process? _process;
 
-  /// 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;
 
@@ -75,45 +68,7 @@
   /// 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),
-    );
-  }
-
-  /// Waits for [vmServiceInfoFile] to exist and become valid before returning
-  /// the VM Service URI contained within.
-  Future<Uri> waitForVmServiceInfoFile(File vmServiceInfoFile) async {
-    final completer = Completer<Uri>();
-    late final StreamSubscription<FileSystemEvent> vmServiceInfoFileWatcher;
-
-    Uri? tryParseServiceInfoFile(FileSystemEvent event) {
-      final uri = _readVmServiceInfoFile(vmServiceInfoFile);
-      if (uri != null && !completer.isCompleted) {
-        vmServiceInfoFileWatcher.cancel();
-        completer.complete(uri);
-      }
-    }
-
-    vmServiceInfoFileWatcher = vmServiceInfoFile.parent
-        .watch(events: FileSystemEvent.all)
-        .where((event) => event.path == vmServiceInfoFile.path)
-        .listen(
-          tryParseServiceInfoFile,
-          onError: (e) => logger?.call('Ignoring exception from watcher: $e'),
-        );
-
-    // After setting up the watcher, also check if the file already exists to
-    // ensure we don't miss it if it was created right before we set the
-    // watched up.
-    final uri = _readVmServiceInfoFile(vmServiceInfoFile);
-    if (uri != null && !completer.isCompleted) {
-      unawaited(vmServiceInfoFileWatcher.cancel());
-      completer.complete(uri);
-    }
-
-    return completer.future;
+    terminatePids(ProcessSignal.sigkill);
   }
 
   /// Called by [launchRequest] to request that we actually start the app to be
@@ -128,25 +83,15 @@
 
     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);
-      unawaited(waitForVmServiceInfoFile(vmServiceInfoFile)
+      vmServiceInfoFile = generateVmServiceInfoFile();
+      unawaited(waitForVmServiceInfoFile(logger, vmServiceInfoFile)
           .then((uri) => connectDebugger(uri, resumeIfStarting: true)));
     }
 
     final vmArgs = <String>[
       if (debug) ...[
         '--enable-vm-service=${args.vmServicePort ?? 0}${ipv6 ? '/::1' : ''}',
-        '--pause_isolates_on_start=true',
+        '--pause_isolates_on_start',
         if (!enableAuthCodes) '--disable-service-auth-codes'
       ],
       '--disable-dart-dev',
@@ -156,10 +101,11 @@
       ],
       // Default to asserts on, this seems like the most useful behaviour for
       // editor-spawned debug sessions.
-      if (args.enableAsserts ?? true) '--enable-asserts'
+      if (args.enableAsserts ?? true) '--enable-asserts',
     ];
     final processArgs = [
       ...vmArgs,
+      ...?args.toolArgs,
       args.program,
       ...?args.args,
     ];
@@ -171,7 +117,7 @@
     var possibleRoot = path.isAbsolute(args.program)
         ? path.dirname(args.program)
         : path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
-    final packageConfig = _findPackageConfigFile(possibleRoot);
+    final packageConfig = findPackageConfigFile(possibleRoot);
     if (packageConfig != null) {
       this.usePackageConfigFile(packageConfig);
     }
@@ -209,9 +155,12 @@
     final vmServiceInfoFile = args.vmServiceInfoFile;
 
     if ((vmServiceUri == null) == (vmServiceInfoFile == null)) {
-      throw DebugAdapterException(
-        'To attach, provide exactly one of vmServiceUri/vmServiceInfoFile',
+      sendOutput(
+        'console',
+        '\nTo attach, provide exactly one of vmServiceUri/vmServiceInfoFile',
       );
+      handleSessionTerminate();
+      return;
     }
 
     // Find the package_config file for this script.
@@ -220,7 +169,7 @@
     //   necessary.
     final cwd = args.cwd;
     if (cwd != null) {
-      final packageConfig = _findPackageConfigFile(cwd);
+      final packageConfig = findPackageConfigFile(cwd);
       if (packageConfig != null) {
         this.usePackageConfigFile(packageConfig);
       }
@@ -228,7 +177,7 @@
 
     final uri = vmServiceUri != null
         ? Uri.parse(vmServiceUri)
-        : await waitForVmServiceInfoFile(File(vmServiceInfoFile!));
+        : await waitForVmServiceInfoFile(logger, File(vmServiceInfoFile!));
 
     unawaited(connectDebugger(uri, resumeIfStarting: false));
   }
@@ -297,42 +246,10 @@
     unawaited(process.exitCode.then(_handleExitCode));
   }
 
-  /// Find the `package_config.json` file for the program being launched.
-  ///
-  /// TODO(dantup): Remove this once
-  ///   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
-  ///   necessary.
-  File? _findPackageConfigFile(String possibleRoot) {
-    File? packageConfig;
-    while (true) {
-      packageConfig =
-          File(path.join(possibleRoot, '.dart_tool', 'package_config.json'));
-
-      // If this packageconfig exists, use it.
-      if (packageConfig.existsSync()) {
-        break;
-      }
-
-      final parent = path.dirname(possibleRoot);
-
-      // If we can't go up anymore, the search failed.
-      if (parent == possibleRoot) {
-        packageConfig = null;
-        break;
-      }
-
-      possibleRoot = parent;
-    }
-
-    return packageConfig;
-  }
-
   /// 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),
-    );
+    terminatePids(ProcessSignal.sigint);
     await _process?.exitCode;
   }
 
@@ -349,19 +266,4 @@
   void _handleStdout(List<int> data) {
     sendOutput('stdout', utf8.decode(data));
   }
-
-  /// Attempts to read VM Service info from a watcher event.
-  ///
-  /// If successful, returns the URI. Otherwise, returns null.
-  Uri? _readVmServiceInfoFile(File file) {
-    try {
-      final content = file.readAsStringSync();
-      final json = jsonDecode(content);
-      return Uri.parse(json['uri']);
-    } 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');
-    }
-  }
 }
diff --git a/pkg/dds/lib/src/dap/adapters/mixins.dart b/pkg/dds/lib/src/dap/adapters/mixins.dart
new file mode 100644
index 0000000..8c73716
--- /dev/null
+++ b/pkg/dds/lib/src/dap/adapters/mixins.dart
@@ -0,0 +1,139 @@
+// 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 '../logging.dart';
+
+/// A mixin providing some utility functions for locating/working with
+/// package_config.json files.
+mixin PackageConfigUtils {
+  /// Find the `package_config.json` file for the program being launched.
+  ///
+  /// TODO(dantup): Remove this once
+  ///   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
+  ///   necessary.
+  File? findPackageConfigFile(String possibleRoot) {
+    File? packageConfig;
+    while (true) {
+      packageConfig =
+          File(path.join(possibleRoot, '.dart_tool', 'package_config.json'));
+
+      // If this packageconfig exists, use it.
+      if (packageConfig.existsSync()) {
+        break;
+      }
+
+      final parent = path.dirname(possibleRoot);
+
+      // If we can't go up anymore, the search failed.
+      if (parent == possibleRoot) {
+        packageConfig = null;
+        break;
+      }
+
+      possibleRoot = parent;
+    }
+
+    return packageConfig;
+  }
+}
+
+/// A mixin for tracking additional PIDs that can be shut down at the end of a
+/// debug session.
+mixin PidTracker {
+  /// 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>{};
+
+  /// Terminates all processes with the PIDs registered in [pidsToTerminate].
+  void terminatePids(ProcessSignal signal) {
+    // 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, signal),
+    );
+  }
+}
+
+/// 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
+/// for the file to become valid parsable JSON.
+mixin VmServiceInfoFileUtils {
+  /// Creates a temp folder for the VM to write the service-info-file into and
+  /// returns the [File] to use.
+  File generateVmServiceInfoFile() {
+    // 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',
+    );
+
+    return File(serviceInfoFilePath);
+  }
+
+  /// Waits for [vmServiceInfoFile] to exist and become valid before returning
+  /// the VM Service URI contained within.
+  Future<Uri> waitForVmServiceInfoFile(
+    Logger? logger,
+    File vmServiceInfoFile,
+  ) async {
+    final completer = Completer<Uri>();
+    late final StreamSubscription<FileSystemEvent> vmServiceInfoFileWatcher;
+
+    Uri? tryParseServiceInfoFile(FileSystemEvent event) {
+      final uri = _readVmServiceInfoFile(logger, vmServiceInfoFile);
+      if (uri != null && !completer.isCompleted) {
+        vmServiceInfoFileWatcher.cancel();
+        completer.complete(uri);
+      }
+    }
+
+    vmServiceInfoFileWatcher = vmServiceInfoFile.parent
+        .watch(events: FileSystemEvent.all)
+        .where((event) => event.path == vmServiceInfoFile.path)
+        .listen(
+          tryParseServiceInfoFile,
+          onError: (e) => logger?.call('Ignoring exception from watcher: $e'),
+        );
+
+    // After setting up the watcher, also check if the file already exists to
+    // ensure we don't miss it if it was created right before we set the
+    // watched up.
+    final uri = _readVmServiceInfoFile(logger, vmServiceInfoFile);
+    if (uri != null && !completer.isCompleted) {
+      unawaited(vmServiceInfoFileWatcher.cancel());
+      completer.complete(uri);
+    }
+
+    return completer.future;
+  }
+
+  /// Attempts to read VM Service info from a watcher event.
+  ///
+  /// If successful, returns the URI. Otherwise, returns null.
+  Uri? _readVmServiceInfoFile(Logger? logger, File file) {
+    try {
+      final content = file.readAsStringSync();
+      final json = jsonDecode(content);
+      return Uri.parse(json['uri']);
+    } 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');
+    }
+  }
+}
diff --git a/pkg/dds/lib/src/dap/protocol_generated.dart b/pkg/dds/lib/src/dap/protocol_generated.dart
index 30620e4..a39f56f 100644
--- a/pkg/dds/lib/src/dap/protocol_generated.dart
+++ b/pkg/dds/lib/src/dap/protocol_generated.dart
@@ -4,8 +4,8 @@
 
 // This code was auto-generated by tool/dap/generate_all.dart - do not hand-edit!
 
-import 'package:dds/src/dap/protocol_common.dart';
-import 'package:dds/src/dap/protocol_special.dart';
+import 'protocol_common.dart';
+import 'protocol_special.dart';
 
 /// Arguments for 'attach' request. Additional attributes are implementation
 /// specific.
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index 4c804c6..a8cbd0f 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -88,7 +88,7 @@
   vmArgs ??= [];
   vmArgs.addAll([
     '--enable-vm-service=0',
-    '--pause_isolates_on_start=true',
+    '--pause_isolates_on_start',
   ]);
   final processArgs = [
     ...vmArgs,