Skip to content

Commit 0797b56

Browse files
authored
Add initial DDS support (#1092)
Add initial DDS support See #1091
1 parent c0f7373 commit 0797b56

File tree

4 files changed

+116
-4
lines changed

4 files changed

+116
-4
lines changed

dwds/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
- Add support for the Dart Development Service (DDS). Introduces 'single
4+
client mode', which prevents additional direct connections to DWDS when
5+
DDS is connected.
6+
17
## 6.0.0
28

39
- Depend on the latest `package:devtools` and `package:devtools_server`.

dwds/lib/src/dwds_vm_client.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class DwdsVmClient {
2121
final StreamController<Map<String, Object>> _requestController;
2222
final StreamController<Map<String, Object>> _responseController;
2323

24+
static const int kFeatureDisabled = 100;
25+
static const String kFeatureDisabledMessage = 'Feature is disabled.';
26+
2427
/// Null until [close] is called.
2528
///
2629
/// All subsequent calls to [close] will return this future.
@@ -100,6 +103,28 @@ class DwdsVmClient {
100103
});
101104
await client.registerService('ext.dwds.screenshot', 'DWDS');
102105

106+
client.registerServiceCallback('_yieldControlToDDS', (request) async {
107+
final ddsUri = request['uri'] as String;
108+
if (ddsUri == null) {
109+
return RPCError(
110+
request['method'] as String,
111+
RPCError.kInvalidParams,
112+
"'Missing parameter: 'uri'",
113+
).toMap();
114+
}
115+
return DebugService.yieldControlToDDS(ddsUri)
116+
? {'result': Success().toJson()}
117+
: {
118+
'error': {
119+
'code': kFeatureDisabled,
120+
'message': kFeatureDisabledMessage,
121+
'details':
122+
'Existing VM service clients prevent DDS from taking control.',
123+
},
124+
};
125+
});
126+
await client.registerService('_yieldControlToDDS', 'DWDS');
127+
103128
return DwdsVmClient(client, requestController, responseController);
104129
}
105130
}

dwds/lib/src/services/debug_service.dart

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import '../utilities/shared.dart';
2727
import 'chrome_proxy_service.dart';
2828
import 'expression_compiler.dart';
2929

30+
bool _acceptNewConnections = true;
31+
int _clientsConnected = 0;
32+
3033
void Function(WebSocketChannel, String) _createNewConnectionHandler(
3134
ChromeProxyService chromeProxyService,
3235
ServiceExtensionRegistry serviceExtensionRegistry, {
@@ -51,9 +54,18 @@ void Function(WebSocketChannel, String) _createNewConnectionHandler(
5154
if (onRequest != null) onRequest(request);
5255
return request;
5356
});
54-
57+
++_clientsConnected;
5558
VmServerConnection(inputStream, responseController.sink,
56-
serviceExtensionRegistry, chromeProxyService);
59+
serviceExtensionRegistry, chromeProxyService)
60+
.done
61+
.whenComplete(() async {
62+
--_clientsConnected;
63+
if (!_acceptNewConnections && _clientsConnected == 0) {
64+
// DDS has disconnected so we can allow for clients to connect directly
65+
// to DWDS.
66+
_acceptNewConnections = true;
67+
}
68+
});
5769
};
5870
}
5971

@@ -80,16 +92,27 @@ Future<void> _handleSseConnections(
8092
if (onRequest != null) onRequest(request);
8193
return request;
8294
});
95+
++_clientsConnected;
8396
var vmServerConnection = VmServerConnection(inputStream,
8497
responseController.sink, serviceExtensionRegistry, chromeProxyService);
85-
unawaited(vmServerConnection.done.whenComplete(sub.cancel));
98+
unawaited(vmServerConnection.done.whenComplete(() {
99+
--_clientsConnected;
100+
if (!_acceptNewConnections && _clientsConnected == 0) {
101+
// DDS has disconnected so we can allow for clients to connect directly
102+
// to DWDS.
103+
_acceptNewConnections = true;
104+
}
105+
return sub.cancel();
106+
}));
86107
}
87108
}
88109

89110
/// A Dart Web Debug Service.
90111
///
91112
/// Creates a [ChromeProxyService] from an existing Chrome instance.
92113
class DebugService {
114+
static String _ddsUri;
115+
93116
final VmServiceInterface chromeProxyService;
94117
final String hostname;
95118
final ServiceExtensionRegistry serviceExtensionRegistry;
@@ -124,6 +147,15 @@ class DebugService {
124147
: Uri(scheme: 'ws', host: hostname, port: port, path: '$_authToken')
125148
.toString();
126149

150+
static bool yieldControlToDDS(String uri) {
151+
if (_clientsConnected > 1) {
152+
return false;
153+
}
154+
_ddsUri = uri;
155+
_acceptNewConnections = false;
156+
return true;
157+
}
158+
127159
static Future<DebugService> start(
128160
String hostname,
129161
RemoteDebugger remoteDebugger,
@@ -163,6 +195,13 @@ class DebugService {
163195
chromeProxyService, serviceExtensionRegistry,
164196
onRequest: onRequest, onResponse: onResponse));
165197
handler = (shelf.Request request) {
198+
if (!_acceptNewConnections) {
199+
return shelf.Response.forbidden(
200+
'Cannot connect directly to the VM service as a Dart Development '
201+
'Service (DDS) instance has taken control and can be found at '
202+
'$_ddsUri.',
203+
);
204+
}
166205
if (request.url.pathSegments.first != authToken) {
167206
return shelf.Response.forbidden('Incorrect auth token');
168207
}

dwds/test/debug_service_test.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
@TestOn('vm')
6+
import 'dart:async';
7+
import 'dart:convert';
68
import 'dart:io';
79

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

2931
test('Accepts connections with the auth token', () async {
30-
expect(WebSocket.connect('${context.debugConnection.uri}/ws'), completes);
32+
expect(
33+
WebSocket.connect('${context.debugConnection.uri}/ws')
34+
.then((ws) => ws.close()),
35+
completes);
36+
});
37+
38+
test('Refuses additional connections when in single client mode', () async {
39+
final ddsWs = await WebSocket.connect(
40+
'${context.debugConnection.uri}/ws',
41+
);
42+
final completer = Completer<void>();
43+
ddsWs.listen((event) {
44+
final response = json.decode(event as String);
45+
expect(response['id'], '0');
46+
expect(response.containsKey('result'), isTrue);
47+
final result = response['result'] as Map<String, dynamic>;
48+
expect(result['type'], 'Success');
49+
completer.complete();
50+
});
51+
52+
const yieldControlToDDS = <String, dynamic>{
53+
'jsonrpc': '2.0',
54+
'id': '0',
55+
'method': '_yieldControlToDDS',
56+
'params': {
57+
'uri': 'http://localhost:123',
58+
},
59+
};
60+
ddsWs.add(json.encode(yieldControlToDDS));
61+
await completer.future;
62+
63+
// While DDS is connected, expect additional connections to fail.
64+
await expectLater(WebSocket.connect('${context.debugConnection.uri}/ws'),
65+
throwsA(isA<WebSocketException>()));
66+
67+
// However, once DDS is disconnected, additional clients can connect again.
68+
await ddsWs.close();
69+
expect(
70+
WebSocket.connect('${context.debugConnection.uri}/ws')
71+
.then((ws) => ws.close()),
72+
completes);
3173
});
3274
}

0 commit comments

Comments
 (0)