[dds] Spawn DDS in-process with DAP

+ Switch to package:args CommandRunner
+ Add switches to enable/disable DDS/auth tokens
+ Improve verbose logging for debugging tests
+ Fix a race condition in initial unpausing of Isolates

Change-Id: I7f7ee0ef798e198ee07c1c663bce3edb0b5c7fc9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/204143
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
Danny Tuppeny 2021-06-24 18:07:49 +00:00 committed by commit-bot@chromium.org
parent 155ac440f7
commit 06fc5c522e
10 changed files with 353 additions and 105 deletions

View file

@ -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<T extends DartLaunchRequestArguments>
/// 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<T extends DartLaunchRequestArguments>
/// The caller should handle any other normalisation (such as adding /ws to
/// the end if required).
Future<void> 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<T extends DartLaunchRequestArguments>
void Function() sendResponse,
) async {
await disconnectImpl();
await shutdown();
sendResponse();
}
@ -597,6 +630,18 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
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<void> 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<T extends DartLaunchRequestArguments>
void Function() sendResponse,
) async {
await terminateImpl();
await shutdown();
sendResponse();
}
@ -838,6 +884,25 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
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<T extends DartLaunchRequestArguments>
);
}
void _handleDebugEvent(vm.Event event) {
_isolateManager.handleEvent(event);
Future<void> _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<void> _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.

View file

@ -41,8 +41,17 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
@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<void> debuggerConnected(vm.VM vmInfo) async {
if (!isAttach) {
@ -97,16 +106,14 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
);
}
// 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 = <String>[
'--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)}'

View file

@ -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<TArg> = Future<void> Function(
abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
int _sequence = 1;
final ByteStreamServerChannel _channel;
final Logger? logger;
BaseDebugAdapter(this._channel, this.logger) {
BaseDebugAdapter(this._channel) {
_channel.listen(_handleIncomingMessage);
}

View file

@ -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;

View file

@ -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 = <ByteStreamServerChannel>{};
final _adapters = <DartDebugAdapter>{};
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<List<int>> _input, StreamSink<List<int>> _output, Logger? logger) {
void _createAdapter(Stream<List<int>> _input, StreamSink<List<int>> _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<DapServer> 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,
);
}
}

View file

@ -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<String> 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<String> 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<String> args) async {
@ -72,20 +71,60 @@ void main(List<String> 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<String, Object?>?);
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<String> 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<String> 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<String> 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);
}

View file

@ -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<StoppedEventBody> hitBreakpoint(File file, int line,
{Future<Response> Function()? launch}) async {
Future<StoppedEventBody> hitBreakpoint(
File file,
int line, {
Future<Response> 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<bool> 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<String, Object?>?);
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<StoppedEventBody> hitException(
File file, [

View file

@ -16,10 +16,10 @@ import 'package:pedantic/pedantic.dart';
final _random = Random();
abstract class DapTestServer {
List<String> get errorLogs;
String get host;
int get port;
Future<void> stop();
List<String> get errorLogs;
}
/// An instance of a DAP server running in-process (to aid debugging).
@ -69,17 +69,24 @@ class OutOfProcessDapTestServer extends DapTestServer {
List<String> 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<OutOfProcessDapTestServer> create() async {
static Future<OutOfProcessDapTestServer> create({
Logger? logger,
List<String>? 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);
}
}

View file

@ -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<void> Function(DapTestSession session) tests) {
final session = DapTestSession();
testDap(
Future<void> Function(DapTestSession session) tests, {
List<String>? 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 = <Directory>[];
final List<String>? 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<void> setUpAll() async {
server = await _startServer();
server = await _startServer(logger: logger, additionalArgs: additionalArgs);
}
Future<void> tearDown() => client.stop();
@ -111,9 +132,15 @@ class DapTestSession {
}
/// Starts a DAP server that can be shared across tests.
Future<DapTestServer> _startServer() async {
Future<DapTestServer> _startServer({
Logger? logger,
List<String>? additionalArgs,
}) async {
return useInProcessDap
? await InProcessDapTestServer.create()
: await OutOfProcessDapTestServer.create();
? await InProcessDapTestServer.create(logger: logger)
: await OutOfProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,
);
}
}

View file

@ -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<void> main(List<String> 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<void> 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,
);
}
}