diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart index f101d7bf275..8730cb1ec39 100644 --- a/pkg/dds/lib/src/dap/adapters/dart.dart +++ b/pkg/dds/lib/src/dap/adapters/dart.dart @@ -6,8 +6,10 @@ import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; import 'package:vm_service/vm_service.dart' as vm; +import '../../../dds.dart'; import '../base_debug_adapter.dart'; import '../exceptions.dart'; import '../isolate_manager.dart'; @@ -97,13 +99,32 @@ abstract class DartDebugAdapter /// yet been made. vm.VmServiceInterface? vmService; + /// The DDS instance that was started and that [vmService] is connected to. + /// + /// `null` if the session is running in noDebug mode of the connection has not + /// yet been made. + DartDevelopmentService? _dds; + + /// Whether to enable DDS for launched applications. + final bool enableDds; + + /// Whether to enable authentication codes for the VM Service/DDS. + final bool enableAuthCodes; + + /// A logger for printing diagnostic information. + final Logger? logger; + /// Whether the current debug session is an attach request (as opposed to a /// launch request). Not available until after launchRequest or attachRequest /// have been called. late final bool isAttach; - DartDebugAdapter(ByteStreamServerChannel channel, Logger? logger) - : super(channel, logger) { + DartDebugAdapter( + ByteStreamServerChannel channel, { + this.enableDds = true, + this.enableAuthCodes = true, + this.logger, + }) : super(channel) { _isolateManager = IsolateManager(this); _converter = ProtocolConverter(this); } @@ -166,10 +187,21 @@ abstract class DartDebugAdapter /// The caller should handle any other normalisation (such as adding /ws to /// the end if required). Future connectDebugger(Uri uri) async { - // The VM Service library always expects the WebSockets URI so fix the - // scheme (http -> ws, https -> wss). - final isSecure = uri.isScheme('https') || uri.isScheme('wss'); - uri = uri.replace(scheme: isSecure ? 'wss' : 'ws'); + // Start up a DDS instance for this VM. + if (enableDds) { + // TODO(dantup): Do we need to worry about there already being one connected + // if this URL came from another service that may have started one? + logger?.call('Starting a DDS instance for $uri'); + final dds = await DartDevelopmentService.startDartDevelopmentService( + uri, + // TODO(dantup): Allow this to be disabled? + enableAuthCodes: true, + ); + _dds = dds; + uri = dds.wsUri!; + } else { + uri = _cleanVmServiceUri(uri); + } logger?.call('Connecting to debugger at $uri'); sendOutput('console', 'Connecting to VM Service at $uri\n'); @@ -311,6 +343,7 @@ abstract class DartDebugAdapter void Function() sendResponse, ) async { await disconnectImpl(); + await shutdown(); sendResponse(); } @@ -597,6 +630,18 @@ abstract class DartDebugAdapter sendResponse(SetExceptionBreakpointsResponseBody()); } + /// Shuts down and cleans up. + /// + /// This is called by [disconnectRequest] and [terminateRequest] but may also + /// be called if the client just disconnects from the server without calling + /// either. + /// + /// This method must tolerate being called multiple times. + @mustCallSuper + Future shutdown() async { + await _dds?.shutdown(); + } + /// Handles a request from the client for the call stack for [args.threadId]. /// /// This is usually called after we sent a [StoppedEvent] to the client @@ -734,6 +779,7 @@ abstract class DartDebugAdapter void Function() sendResponse, ) async { await terminateImpl(); + await shutdown(); sendResponse(); } @@ -838,6 +884,25 @@ abstract class DartDebugAdapter sendResponse(VariablesResponseBody(variables: variables)); } + /// Fixes up an Observatory [uri] to a WebSocket URI with a trailing /ws + /// for connecting when not using DDS. + /// + /// DDS does its own cleaning up of the URI. + Uri _cleanVmServiceUri(Uri uri) { + // The VM Service library always expects the WebSockets URI so fix the + // scheme (http -> ws, https -> wss). + final isSecure = uri.isScheme('https') || uri.isScheme('wss'); + uri = uri.replace(scheme: isSecure ? 'wss' : 'ws'); + + if (uri.path.endsWith('/ws') || uri.path.endsWith('/ws/')) { + return uri; + } + + final append = uri.path.endsWith('/') ? 'ws' : '/ws'; + final newPath = '${uri.path}$append'; + return uri.replace(path: newPath); + } + /// Handles evaluation of an expression that is (or begins with) /// `threadExceptionExpression` which corresponds to the exception at the top /// of [thread]. @@ -870,12 +935,22 @@ abstract class DartDebugAdapter ); } - void _handleDebugEvent(vm.Event event) { - _isolateManager.handleEvent(event); + Future _handleDebugEvent(vm.Event event) async { + // Delay processing any events until the debugger initialization has + // finished running, as events may arrive (for ex. IsolateRunnable) while + // it's doing is own initialization that this may interfere with. + await debuggerInitialized; + + await _isolateManager.handleEvent(event); } - void _handleIsolateEvent(vm.Event event) { - _isolateManager.handleEvent(event); + Future _handleIsolateEvent(vm.Event event) async { + // Delay processing any events until the debugger initialization has + // finished running, as events may arrive (for ex. IsolateRunnable) while + // it's doing is own initialization that this may interfere with. + await debuggerInitialized; + + await _isolateManager.handleEvent(event); } /// Handles a dart:developer log() event, sending output to the client. diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli.dart b/pkg/dds/lib/src/dap/adapters/dart_cli.dart index 007943f9bab..6a01c8cd331 100644 --- a/pkg/dds/lib/src/dap/adapters/dart_cli.dart +++ b/pkg/dds/lib/src/dap/adapters/dart_cli.dart @@ -41,8 +41,17 @@ class DartCliDebugAdapter extends DartDebugAdapter { @override final parseLaunchArgs = DartLaunchRequestArguments.fromJson; - DartCliDebugAdapter(ByteStreamServerChannel channel, [Logger? logger]) - : super(channel, logger); + DartCliDebugAdapter( + ByteStreamServerChannel channel, { + bool enableDds = true, + bool enableAuthCodes = true, + Logger? logger, + }) : super( + channel, + enableDds: enableDds, + enableAuthCodes: enableAuthCodes, + logger: logger, + ); Future debuggerConnected(vm.VM vmInfo) async { if (!isAttach) { @@ -97,16 +106,14 @@ class DartCliDebugAdapter extends DartDebugAdapter { ); } - // TODO(dantup): Currently this just spawns the new VM and completely - // ignores DDS. Figure out how this will ultimately work - will we just wrap - // the call to initDebugger() with something that starts DDS? final vmServiceInfoFile = _vmServiceInfoFile; final vmArgs = [ - '--no-serve-devtools', if (debug) ...[ '--enable-vm-service=${args.vmServicePort ?? 0}', '--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)}' diff --git a/pkg/dds/lib/src/dap/base_debug_adapter.dart b/pkg/dds/lib/src/dap/base_debug_adapter.dart index 4c09eb8d275..cd2b99a4e6c 100644 --- a/pkg/dds/lib/src/dap/base_debug_adapter.dart +++ b/pkg/dds/lib/src/dap/base_debug_adapter.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'exceptions.dart'; -import 'logging.dart'; import 'protocol_common.dart'; import 'protocol_generated.dart'; import 'protocol_stream.dart'; @@ -30,9 +29,8 @@ typedef _VoidNoArgRequestHandler = Future Function( abstract class BaseDebugAdapter { int _sequence = 1; final ByteStreamServerChannel _channel; - final Logger? logger; - BaseDebugAdapter(this._channel, this.logger) { + BaseDebugAdapter(this._channel) { _channel.listen(_handleIncomingMessage); } diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart index 672f4a1dd16..99ee743b996 100644 --- a/pkg/dds/lib/src/dap/isolate_manager.dart +++ b/pkg/dds/lib/src/dap/isolate_manager.dart @@ -93,11 +93,6 @@ class IsolateManager { return; } - // Delay processing any events until the debugger initialization has - // finished running, as events may arrive (for ex. IsolateRunnable) while - // it's doing is own initialization that this may interfere with. - await _adapter.debuggerInitialized; - final eventKind = event.kind; if (eventKind == vm.EventKind.kIsolateStart || eventKind == vm.EventKind.kIsolateRunnable) { @@ -315,7 +310,12 @@ class IsolateManager { await _configureIsolate(isolate); await resumeThread(thread.threadId); } else if (eventKind == vm.EventKind.kPauseStart) { - await resumeThread(thread.threadId); + // Don't resume from a PauseStart if this has already happened (see + // comments on [thread.hasBeenStarted]). + if (!thread.hasBeenStarted) { + thread.hasBeenStarted = true; + await resumeThread(thread.threadId); + } } else { // PauseExit, PauseBreakpoint, PauseInterrupted, PauseException var reason = 'pause'; @@ -467,6 +467,17 @@ class ThreadInfo { int? exceptionReference; var paused = false; + /// Tracks whether an isolate has been started from its PauseStart state. + /// + /// This is used to prevent trying to resume a thread twice if a PauseStart + /// event arrives around the same time that are our initialization code (which + /// automatically resumes threads that are in the PauseStart state when we + /// connect). + /// + /// If we send a duplicate resume, it could trigger an unwanted resume for a + /// breakpoint or exception that occur early on. + bool hasBeenStarted = false; + // The most recent pauseEvent for this isolate. vm.Event? pauseEvent; diff --git a/pkg/dds/lib/src/dap/server.dart b/pkg/dds/lib/src/dap/server.dart index 14de592405c..20f205e8602 100644 --- a/pkg/dds/lib/src/dap/server.dart +++ b/pkg/dds/lib/src/dap/server.dart @@ -18,11 +18,18 @@ class DapServer { static const defaultPort = 9200; final ServerSocket _socket; - final Logger? _logger; + final bool enableDds; + final bool enableAuthCodes; + final Logger? logger; final _channels = {}; final _adapters = {}; - DapServer._(this._socket, this._logger) { + DapServer._( + this._socket, { + this.enableDds = true, + this.enableAuthCodes = true, + this.logger, + }) { _socket.listen(_acceptConnection); } @@ -36,25 +43,30 @@ class DapServer { void _acceptConnection(Socket client) { final address = client.remoteAddress; - _logger?.call('Accepted connection from $address'); + logger?.call('Accepted connection from $address'); client.done.then((_) { - _logger?.call('Connection from $address closed'); + logger?.call('Connection from $address closed'); }); - _createAdapter(client.transform(Uint8ListTransformer()), client, _logger); + _createAdapter(client.transform(Uint8ListTransformer()), client); } - void _createAdapter( - Stream> _input, StreamSink> _output, Logger? logger) { + void _createAdapter(Stream> _input, StreamSink> _output) { // TODO(dantup): This is hard-coded to DartCliDebugAdapter but will // ultimately need to support having a factory passed in to support // tests and/or being used in flutter_tools. final channel = ByteStreamServerChannel(_input, _output, logger); - final adapter = DartCliDebugAdapter(channel, logger); + final adapter = DartCliDebugAdapter( + channel, + enableDds: enableDds, + enableAuthCodes: enableAuthCodes, + logger: logger, + ); _channels.add(channel); _adapters.add(adapter); unawaited(channel.closed.then((_) { _channels.remove(channel); _adapters.remove(adapter); + adapter.shutdown(); })); } @@ -62,9 +74,16 @@ class DapServer { static Future create({ String host = 'localhost', int port = 0, + bool enableDdds = true, + bool enableAuthCodes = true, Logger? logger, }) async { final _socket = await ServerSocket.bind(host, port); - return DapServer._(_socket, logger); + return DapServer._( + _socket, + enableDds: enableDdds, + enableAuthCodes: enableAuthCodes, + logger: logger, + ); } } diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart index b71a591b63c..ded46632c9a 100644 --- a/pkg/dds/test/dap/integration/debug_test.dart +++ b/pkg/dds/test/dap/integration/debug_test.dart @@ -2,9 +2,8 @@ // 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 'package:collection/collection.dart'; +import 'package:dds/src/dap/protocol_generated.dart'; import 'package:test/test.dart'; -import 'package:vm_service/vm_service.dart'; import 'test_client.dart'; import 'test_support.dart'; @@ -21,7 +20,7 @@ void main(List args) async { } '''); - var outputEvents = await dap.client.collectOutput( + final outputEvents = await dap.client.collectOutput( launch: () => dap.client.launch( testFile.path, args: ['one', 'two'], @@ -62,7 +61,7 @@ void main(List args) async { expect(response.threads.first.name, equals('main')); }); - test('connects with DDS', () async { + test('runs with DDS', () async { final client = dap.client; final testFile = dap.createTestFile(r''' void main(List args) async { @@ -72,20 +71,60 @@ void main(List args) async { final breakpointLine = lineWith(testFile, '// BREAKPOINT'); await client.hitBreakpoint(testFile, breakpointLine); - final response = await client.custom( - '_getSupportedProtocols', - null, - ); - - // For convenience, use the ProtocolList to deserialise the custom - // response to check if included DDS. - final protocolList = - ProtocolList.parse(response.body as Map?); - final ddsProtocol = protocolList?.protocols - ?.singleWhereOrNull((protocol) => protocol.protocolName == "DDS"); - expect(ddsProtocol, isNot(isNull)); + expect(await client.ddsAvailable, isTrue); }); // These tests can be slow due to starting up the external server process. }, timeout: Timeout.none); + + test('runs with auth codes enabled', () async { + final testFile = dap.createTestFile(r''' +void main(List args) {} + '''); + + final outputEvents = await dap.client.collectOutput(file: testFile); + expect(_hasAuthCode(outputEvents.first), isTrue); + }); }); + + testDap((dap) async { + group('debug mode', () { + test('runs without DDS', () async { + final client = dap.client; + final testFile = dap.createTestFile(r''' +void main(List args) async { + print('Hello!'); // BREAKPOINT +} + '''); + final breakpointLine = lineWith(testFile, '// BREAKPOINT'); + + await client.hitBreakpoint(testFile, breakpointLine); + + expect(await client.ddsAvailable, isFalse); + }); + + test('runs with auth tokens disabled', () async { + final testFile = dap.createTestFile(r''' +void main(List args) {} + '''); + + final outputEvents = await dap.client.collectOutput(file: testFile); + expect(_hasAuthCode(outputEvents.first), isFalse); + }); + // These tests can be slow due to starting up the external server process. + }, timeout: Timeout.none); + }, additionalArgs: ['--no-dds', '--no-auth-codes']); +} + +/// Checks for the presence of an auth token in a VM Service URI in the +/// "Connecting to VM Service" [OutputEvent]. +bool _hasAuthCode(OutputEventBody vmConnection) { + // TODO(dantup): Change this to use the dart.debuggerUris custom event + // if implemented (whch VS Code also needs). + final vmServiceUriPattern = RegExp(r'Connecting to VM Service at ([^\s]+)\s'); + final authCodePattern = RegExp(r'ws://127.0.0.1:\d+/[\w=]{5,15}/ws'); + + final vmServiceUri = + vmServiceUriPattern.firstMatch(vmConnection.output)!.group(1); + + return vmServiceUri != null && authCodePattern.hasMatch(vmServiceUri); } diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart index 391220552bf..0bc4afe964d 100644 --- a/pkg/dds/test/dap/integration/test_client.dart +++ b/pkg/dds/test/dap/integration/test_client.dart @@ -5,12 +5,14 @@ import 'dart:async'; import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:dds/src/dap/adapters/dart.dart'; import 'package:dds/src/dap/logging.dart'; import 'package:dds/src/dap/protocol_generated.dart'; import 'package:dds/src/dap/protocol_stream.dart'; import 'package:dds/src/dap/protocol_stream_transformers.dart'; import 'package:test/test.dart'; +import 'package:vm_service/vm_service.dart' as vm; import 'test_server.dart'; @@ -336,16 +338,20 @@ extension DapTestClientExtension on DapTestClient { /// /// Launch options can be customised by passing a custom [launch] function that /// will be used instead of calling `launch(file.path)`. - Future hitBreakpoint(File file, int line, - {Future Function()? launch}) async { + Future hitBreakpoint( + File file, + int line, { + Future Function()? launch, + }) async { final stop = expectStop('breakpoint', file: file, line: line); await Future.wait([ initialize(), sendRequest( SetBreakpointsArguments( - source: Source(path: file.path), - breakpoints: [SourceBreakpoint(line: line)]), + source: Source(path: file.path), + breakpoints: [SourceBreakpoint(line: line)], + ), ), launch?.call() ?? this.launch(file.path), ], eagerError: true); @@ -353,6 +359,25 @@ extension DapTestClientExtension on DapTestClient { return stop; } + /// Returns whether DDS is available for the VM Service the debug adapter + /// is connected to. + Future get ddsAvailable async { + final response = await custom( + '_getSupportedProtocols', + null, + ); + + // For convenience, use the ProtocolList to deserialise the custom + // response to check if included DDS. + final protocolList = + vm.ProtocolList.parse(response.body as Map?); + + final ddsProtocol = protocolList?.protocols?.singleWhereOrNull( + (protocol) => protocol.protocolName == 'DDS', + ); + return ddsProtocol != null; + } + /// Runs a script and expects to pause at an exception in [file]. Future hitException( File file, [ diff --git a/pkg/dds/test/dap/integration/test_server.dart b/pkg/dds/test/dap/integration/test_server.dart index 1beb3cdf55a..3680256b2ee 100644 --- a/pkg/dds/test/dap/integration/test_server.dart +++ b/pkg/dds/test/dap/integration/test_server.dart @@ -16,10 +16,10 @@ import 'package:pedantic/pedantic.dart'; final _random = Random(); abstract class DapTestServer { + List get errorLogs; String get host; int get port; Future stop(); - List get errorLogs; } /// An instance of a DAP server running in-process (to aid debugging). @@ -69,17 +69,24 @@ class OutOfProcessDapTestServer extends DapTestServer { List get errorLogs => _errors; - OutOfProcessDapTestServer._(this._process, this.host, this.port) { + OutOfProcessDapTestServer._( + this._process, + this.host, + this.port, + Logger? logger, + ) { // The DAP server should generally not write to stdout/stderr (unless -v is // passed), but it may do if it fails to start or crashes. If this happens, - // ensure these are included in the test output. - _process.stdout.transform(utf8.decoder).listen(print); - _process.stderr.transform(utf8.decoder).listen((s) { - _errors.add(s); - throw s; + // and there's no logger, print to stdout. + _process.stdout.transform(utf8.decoder).listen(logger ?? print); + _process.stderr.transform(utf8.decoder).listen((error) { + logger?.call(error); + _errors.add(error); + throw error; }); unawaited(_process.exitCode.then((code) { final message = 'Out-of-process DAP server terminated with code $code'; + logger?.call(message); _errors.add(message); if (!_isShuttingDown && code != 0) { throw message; @@ -94,7 +101,10 @@ class OutOfProcessDapTestServer extends DapTestServer { await _process.exitCode; } - static Future create() async { + static Future create({ + Logger? logger, + List? additionalArgs, + }) async { final ddsEntryScript = await Isolate.resolvePackageUri(Uri.parse('package:dds/dds.dart')); final ddsLibFolder = path.dirname(ddsEntryScript!.toFilePath()); @@ -107,11 +117,14 @@ class OutOfProcessDapTestServer extends DapTestServer { Platform.resolvedExecutable, [ dapServerScript, + 'dap', '--host=$host', '--port=$port', + ...?additionalArgs, + if (logger != null) '--verbose' ], ); - return OutOfProcessDapTestServer._(_process, host, port); + return OutOfProcessDapTestServer._(_process, host, port, logger); } } diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart index 124efc3f2e9..3b1d3ab5f21 100644 --- a/pkg/dds/test/dap/integration/test_support.dart +++ b/pkg/dds/test/dap/integration/test_support.dart @@ -5,12 +5,27 @@ import 'dart:async'; import 'dart:io'; +import 'package:dds/src/dap/logging.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; import 'test_client.dart'; import 'test_server.dart'; +/// A logger to use to log all traffic (both DAP and VM) to stdout. +/// +/// If the enviroment variable is `DAP_TEST_VERBOSE` then `print` will be used, +/// otherwise there will be no verbose logging. +/// +/// DAP_TEST_VERBOSE=true pub run test --chain-stack-traces test/dap/integration +/// +/// +/// When using the out-of-process DAP, this causes `--verbose` to be passed to +/// the server which causes it to write all traffic to `stdout` which is then +/// picked up by [OutOfProcessDapTestServer] and passed to this logger. +final logger = + Platform.environment['DAP_TEST_VERBOSE'] == 'true' ? print : null; + /// Whether to run the DAP server in-process with the tests, or externally in /// another process. /// @@ -33,8 +48,11 @@ int lineWith(File file, String searchText) => /// A helper function to wrap all tests in a library with setup/teardown functions /// to start a shared server for all tests in the library and an individual /// client for each test. -testDap(Future Function(DapTestSession session) tests) { - final session = DapTestSession(); +testDap( + Future Function(DapTestSession session) tests, { + List? additionalArgs, +}) { + final session = DapTestSession(additionalArgs: additionalArgs); setUpAll(session.setUpAll); tearDownAll(session.tearDownAll); @@ -51,6 +69,9 @@ class DapTestSession { late DapTestServer server; late DapTestClient client; final _testFolders = []; + final List? additionalArgs; + + DapTestSession({this.additionalArgs}); /// Creates a file in a temporary folder to be used as an application for testing. /// @@ -68,7 +89,7 @@ class DapTestSession { } Future setUpAll() async { - server = await _startServer(); + server = await _startServer(logger: logger, additionalArgs: additionalArgs); } Future tearDown() => client.stop(); @@ -111,9 +132,15 @@ class DapTestSession { } /// Starts a DAP server that can be shared across tests. - Future _startServer() async { + Future _startServer({ + Logger? logger, + List? additionalArgs, + }) async { return useInProcessDap - ? await InProcessDapTestServer.create() - : await OutOfProcessDapTestServer.create(); + ? await InProcessDapTestServer.create(logger: logger) + : await OutOfProcessDapTestServer.create( + logger: logger, + additionalArgs: additionalArgs, + ); } } diff --git a/pkg/dds/tool/dap/run_server.dart b/pkg/dds/tool/dap/run_server.dart index 69f90f1bbc7..32e08e6391a 100644 --- a/pkg/dds/tool/dap/run_server.dart +++ b/pkg/dds/tool/dap/run_server.dart @@ -2,45 +2,79 @@ // 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 'package:args/args.dart'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; import 'package:dds/src/dap/server.dart'; Future main(List arguments) async { - final args = argParser.parse(arguments); - if (args[argHelp]) { - print(argParser.usage); - return; + // TODO(dantup): "dap_tool" is a placeholder and will likely eventually be a + // "dart" command. + final runner = CommandRunner('dap_tool', 'Dart DAP Tool') + ..addCommand(DapCommand()); + + try { + await runner.run(arguments); + } on UsageException catch (e) { + print(e); + exit(64); } - - final port = int.parse(args[argPort]); - final host = args[argHost]; - - await DapServer.create( - host: host, - port: port, - logger: args[argVerbose] ? print : null, - ); } -const argHelp = 'help'; -const argHost = 'host'; -const argPort = 'port'; -const argVerbose = 'verbose'; -final argParser = ArgParser() - ..addFlag(argHelp, hide: true) - ..addOption( - argHost, - defaultsTo: 'localhost', - help: 'The hostname/IP to bind the server to', - ) - ..addOption( - argPort, - abbr: 'p', - defaultsTo: DapServer.defaultPort.toString(), - help: 'The port to bind the server to', - ) - ..addFlag( - argVerbose, - abbr: 'v', - help: 'Whether to print diagnostic output to stdout', - ); +class DapCommand extends Command { + static const argHost = 'host'; + static const argPort = 'port'; + static const argDds = 'dds'; + static const argAuthCodes = 'auth-codes'; + static const argVerbose = 'verbose'; + + @override + final String description = 'Start a DAP debug server.'; + + @override + final String name = 'dap'; + + DapCommand() { + argParser + ..addOption( + argHost, + defaultsTo: 'localhost', + help: 'The hostname/IP to bind the server to', + ) + ..addOption( + argPort, + abbr: 'p', + defaultsTo: DapServer.defaultPort.toString(), + help: 'The port to bind the server to', + ) + ..addFlag( + argDds, + defaultsTo: true, + help: 'Whether to enable DDS for debug sessions', + ) + ..addFlag( + argAuthCodes, + defaultsTo: true, + help: 'Whether to enable authentication codes for VM Services', + ) + ..addFlag( + argVerbose, + abbr: 'v', + help: 'Whether to print diagnostic output to stdout', + ); + } + + Future run() async { + final args = argResults!; + final port = int.parse(args[argPort]); + final host = args[argHost]; + + await DapServer.create( + host: host, + port: port, + enableDdds: args[argDds], + enableAuthCodes: args[argAuthCodes], + logger: args[argVerbose] ? print : null, + ); + } +}