mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 00:39:49 +00:00
[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:
parent
155ac440f7
commit
06fc5c522e
|
@ -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.
|
||||
|
|
|
@ -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)}'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue