Skip to content

Add initial DDS support #1092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dwds/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

- Add support for the Dart Development Service (DDS). Introduces 'single
client mode', which prevents additional direct connections to DWDS when
DDS is connected.

## 6.0.0

- Depend on the latest `package:devtools` and `package:devtools_server`.
Expand Down
25 changes: 25 additions & 0 deletions dwds/lib/src/dwds_vm_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class DwdsVmClient {
final StreamController<Map<String, Object>> _requestController;
final StreamController<Map<String, Object>> _responseController;

static const int kFeatureDisabled = 100;
static const String kFeatureDisabledMessage = 'Feature is disabled.';

/// Null until [close] is called.
///
/// All subsequent calls to [close] will return this future.
Expand Down Expand Up @@ -100,6 +103,28 @@ class DwdsVmClient {
});
await client.registerService('ext.dwds.screenshot', 'DWDS');

client.registerServiceCallback('_yieldControlToDDS', (request) async {
final ddsUri = request['uri'] as String;
if (ddsUri == null) {
return RPCError(
request['method'] as String,
RPCError.kInvalidParams,
"'Missing parameter: 'uri'",
).toMap();
}
return DebugService.yieldControlToDDS(ddsUri)
? {'result': Success().toJson()}
: {
'error': {
'code': kFeatureDisabled,
'message': kFeatureDisabledMessage,
'details':
'Existing VM service clients prevent DDS from taking control.',
},
};
});
await client.registerService('_yieldControlToDDS', 'DWDS');

return DwdsVmClient(client, requestController, responseController);
}
}
Expand Down
45 changes: 42 additions & 3 deletions dwds/lib/src/services/debug_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import '../utilities/shared.dart';
import 'chrome_proxy_service.dart';
import 'expression_compiler.dart';

bool _acceptNewConnections = true;
int _clientsConnected = 0;

void Function(WebSocketChannel, String) _createNewConnectionHandler(
ChromeProxyService chromeProxyService,
ServiceExtensionRegistry serviceExtensionRegistry, {
Expand All @@ -51,9 +54,18 @@ void Function(WebSocketChannel, String) _createNewConnectionHandler(
if (onRequest != null) onRequest(request);
return request;
});

++_clientsConnected;
VmServerConnection(inputStream, responseController.sink,
serviceExtensionRegistry, chromeProxyService);
serviceExtensionRegistry, chromeProxyService)
.done
.whenComplete(() async {
--_clientsConnected;
if (!_acceptNewConnections && _clientsConnected == 0) {
// DDS has disconnected so we can allow for clients to connect directly
// to DWDS.
_acceptNewConnections = true;
}
});
};
}

Expand All @@ -80,16 +92,27 @@ Future<void> _handleSseConnections(
if (onRequest != null) onRequest(request);
return request;
});
++_clientsConnected;
var vmServerConnection = VmServerConnection(inputStream,
responseController.sink, serviceExtensionRegistry, chromeProxyService);
unawaited(vmServerConnection.done.whenComplete(sub.cancel));
unawaited(vmServerConnection.done.whenComplete(() {
--_clientsConnected;
if (!_acceptNewConnections && _clientsConnected == 0) {
// DDS has disconnected so we can allow for clients to connect directly
// to DWDS.
_acceptNewConnections = true;
}
return sub.cancel();
}));
}
}

/// A Dart Web Debug Service.
///
/// Creates a [ChromeProxyService] from an existing Chrome instance.
class DebugService {
static String _ddsUri;

final VmServiceInterface chromeProxyService;
final String hostname;
final ServiceExtensionRegistry serviceExtensionRegistry;
Expand Down Expand Up @@ -124,6 +147,15 @@ class DebugService {
: Uri(scheme: 'ws', host: hostname, port: port, path: '$_authToken')
.toString();

static bool yieldControlToDDS(String uri) {
if (_clientsConnected > 1) {
return false;
}
_ddsUri = uri;
_acceptNewConnections = false;
return true;
}

static Future<DebugService> start(
String hostname,
RemoteDebugger remoteDebugger,
Expand Down Expand Up @@ -163,6 +195,13 @@ class DebugService {
chromeProxyService, serviceExtensionRegistry,
onRequest: onRequest, onResponse: onResponse));
handler = (shelf.Request request) {
if (!_acceptNewConnections) {
return shelf.Response.forbidden(
'Cannot connect directly to the VM service as a Dart Development '
'Service (DDS) instance has taken control and can be found at '
'$_ddsUri.',
);
}
if (request.url.pathSegments.first != authToken) {
return shelf.Response.forbidden('Incorrect auth token');
}
Expand Down
44 changes: 43 additions & 1 deletion dwds/test/debug_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.

@TestOn('vm')
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:test/test.dart';
Expand All @@ -27,6 +29,46 @@ void main() {
});

test('Accepts connections with the auth token', () async {
expect(WebSocket.connect('${context.debugConnection.uri}/ws'), completes);
expect(
WebSocket.connect('${context.debugConnection.uri}/ws')
.then((ws) => ws.close()),
completes);
});

test('Refuses additional connections when in single client mode', () async {
final ddsWs = await WebSocket.connect(
'${context.debugConnection.uri}/ws',
);
final completer = Completer<void>();
ddsWs.listen((event) {
final response = json.decode(event as String);
expect(response['id'], '0');
expect(response.containsKey('result'), isTrue);
final result = response['result'] as Map<String, dynamic>;
expect(result['type'], 'Success');
completer.complete();
});

const yieldControlToDDS = <String, dynamic>{
'jsonrpc': '2.0',
'id': '0',
'method': '_yieldControlToDDS',
'params': {
'uri': 'http://localhost:123',
},
};
ddsWs.add(json.encode(yieldControlToDDS));
await completer.future;

// While DDS is connected, expect additional connections to fail.
await expectLater(WebSocket.connect('${context.debugConnection.uri}/ws'),
throwsA(isA<WebSocketException>()));

// However, once DDS is disconnected, additional clients can connect again.
await ddsWs.close();
expect(
WebSocket.connect('${context.debugConnection.uri}/ws')
.then((ws) => ws.close()),
completes);
});
}