Skip to content

Commit 8db4393

Browse files
bkonyiCommit Queue
authored and
Commit Queue
committed
[ CLI ] Start DDS instance if VM service URI passed to dart devtools doesn't have a DDS connection
This is can happen if a Dart or Flutter application is started in a non-standard way (e.g., not through the `dart` or `flutter` CLI tooling). Developers using custom embedders need to use `dart devtools` to start a DevTools instance, so it's a good opportunity to check for a DDS instance and launch DDS for the target application if one isn't found. Change-Id: I817ca0951c7839e8e9a0d1c78756caa45381cea1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/346820 Reviewed-by: Kenzie Davisson <[email protected]> Commit-Queue: Ben Konyi <[email protected]>
1 parent a995f79 commit 8db4393

File tree

7 files changed

+535
-131
lines changed

7 files changed

+535
-131
lines changed

pkg/dartdev/lib/src/commands/devtools.dart

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44

55
import 'package:args/args.dart';
66
import 'package:dds/devtools_server.dart';
7+
import 'package:dds_service_extensions/dds_service_extensions.dart';
8+
import 'package:http/http.dart' as http;
9+
import 'package:meta/meta.dart';
710
import 'package:path/path.dart' as path;
11+
import 'package:vm_service/vm_service.dart';
12+
import 'package:vm_service/vm_service_io.dart';
813

914
import '../core.dart';
15+
import '../dds_runner.dart';
1016
import '../sdk.dart';
1117
import '../utils.dart';
1218

@@ -48,10 +54,183 @@ class DevToolsCommand extends DartdevCommand {
4854
final devToolsBinaries =
4955
fullSdk ? sdk.devToolsBinaries : path.absolute(sdkDir, 'devtools');
5056

57+
final argList = await _performDDSCheck(args);
5158
final server = await DevToolsServer().serveDevToolsWithArgs(
52-
args.arguments,
59+
argList,
5360
customDevToolsPath: devToolsBinaries,
5461
);
5562
return server == null ? -1 : 0;
5663
}
64+
65+
/// Attempts to start a DDS instance if there isn't one running for the
66+
/// target application.
67+
///
68+
/// Returns the argument list from [args], which is modified to include the
69+
/// DDS URI instead of the VM service URI if DDS is started by this method
70+
/// or a redirect to DDS would be followed.
71+
Future<List<String>> _performDDSCheck(ArgResults args) async {
72+
String? serviceProtocolUri;
73+
bool positionalServiceUri = false;
74+
if (args.rest.isNotEmpty) {
75+
serviceProtocolUri = args.rest.first;
76+
positionalServiceUri = true;
77+
} else if (args.wasParsed(DevToolsServer.argVmUri)) {
78+
serviceProtocolUri = args[DevToolsServer.argVmUri];
79+
}
80+
81+
final argList = args.arguments.toList();
82+
83+
// No VM service URI was provided, so the user is going to manually connect
84+
// to their application from DevTools or only use offline tooling.
85+
//
86+
// TODO(bkonyi): we should consider having devtools_server try and spawn
87+
// DDS if users try to connect to an application without a DDS instance.
88+
if (serviceProtocolUri == null) {
89+
return argList;
90+
}
91+
92+
final originalUri = Uri.parse(serviceProtocolUri);
93+
var uri = originalUri;
94+
// The VM service doesn't like it when there's no trailing forward slash at
95+
// the end of the path. Add one if it's missing.
96+
uri = _ensureUriHasTrailingForwardSlash(uri);
97+
98+
// Check to see if the URI is a VM service URI for a VM service instance
99+
// that already has a DDS instance connected.
100+
uri = await checkForRedirectToExistingDDSInstance(uri);
101+
102+
// If this isn't a URI for a VM service with an active DDS instance,
103+
// check to see if this URI points to a running DDS instance. If not, try
104+
// and start DDS.
105+
if (uri == originalUri) {
106+
uri = await maybeStartDDS(
107+
uri: uri,
108+
ddsHost: args[DevToolsServer.argDdsHost],
109+
ddsPort: args[DevToolsServer.argDdsPort],
110+
machineMode: args.wasParsed(DevToolsServer.argMachine),
111+
);
112+
}
113+
114+
// If we ended up starting DDS or redirecting to an existing instance,
115+
// update the argument list to include the actual URI for DevTools to
116+
// connect to.
117+
if (uri != originalUri) {
118+
if (positionalServiceUri) {
119+
argList.removeLast();
120+
}
121+
argList.add(uri.toString());
122+
}
123+
124+
return argList;
125+
}
126+
127+
Uri _ensureUriHasTrailingForwardSlash(Uri uri) {
128+
if (uri.pathSegments.isNotEmpty) {
129+
final pathSegments = uri.pathSegments.toList();
130+
if (pathSegments.last.isNotEmpty) {
131+
pathSegments.add('');
132+
}
133+
uri = uri.replace(pathSegments: pathSegments);
134+
}
135+
return uri;
136+
}
137+
138+
@visibleForTesting
139+
static Future<Uri> checkForRedirectToExistingDDSInstance(Uri uri) async {
140+
final request = http.Request('GET', uri)..followRedirects = false;
141+
final response = await request.send();
142+
await response.stream.drain();
143+
144+
// If we're redirected that means that we attempted to speak directly to
145+
// the VM service even though there's an active DDS instance already
146+
// connected to it. In this case, DevTools will fail to connect to the VM
147+
// service directly so we need to modify the target URI in the args list to
148+
// instead point to the DDS instance.
149+
if (response.isRedirect) {
150+
final redirectUri = Uri.parse(response.headers['location']!);
151+
final ddsWsUri = Uri.parse(redirectUri.queryParameters['uri']!);
152+
153+
// Remove '/ws' from the path, add a trailing '/'
154+
var pathSegments = ddsWsUri.pathSegments.toList()
155+
..removeLast()
156+
..add('');
157+
if (pathSegments.length == 1) {
158+
pathSegments.add('');
159+
}
160+
uri = ddsWsUri.replace(
161+
scheme: 'http',
162+
pathSegments: pathSegments,
163+
);
164+
}
165+
return uri;
166+
}
167+
168+
@visibleForTesting
169+
static Future<Uri> maybeStartDDS({
170+
required Uri uri,
171+
required String ddsHost,
172+
required String ddsPort,
173+
bool machineMode = false,
174+
}) async {
175+
final pathSegments = uri.pathSegments.toList();
176+
if (pathSegments.isNotEmpty && pathSegments.last.isEmpty) {
177+
// There's a trailing '/' at the end of the parsed URI, so there's an
178+
// empty string at the end of the path segments list that needs to be
179+
// removed.
180+
pathSegments.removeLast();
181+
}
182+
183+
final authCodesEnabled = pathSegments.isNotEmpty;
184+
final wsUri = uri.replace(
185+
scheme: 'ws',
186+
pathSegments: [
187+
...pathSegments,
188+
'ws',
189+
],
190+
);
191+
192+
final vmService = await vmServiceConnectUri(wsUri.toString());
193+
194+
try {
195+
// If this request throws a RPC error, DDS isn't running and we should
196+
// try and start it.
197+
await vmService.getDartDevelopmentServiceVersion();
198+
} on RPCError {
199+
// If the user wants to start a debugging session we need to do some extra
200+
// work and spawn a Dart Development Service (DDS) instance. DDS is a VM
201+
// service intermediary which implements the VM service protocol and
202+
// provides non-VM specific extensions (e.g., log caching, client
203+
// synchronization).
204+
final debugSession = DDSRunner();
205+
if (await debugSession.start(
206+
vmServiceUri: uri,
207+
ddsHost: ddsHost,
208+
ddsPort: ddsPort,
209+
debugDds: false,
210+
disableServiceAuthCodes: !authCodesEnabled,
211+
// TODO(bkonyi): should we just have DDS serve its own duplicate
212+
// DevTools instance? It shouldn't add much, if any, overhead but will
213+
// allow for developers to access DevTools directly through the VM
214+
// service URI at a later point. This would probably be a fairly niche
215+
// workflow.
216+
enableDevTools: false,
217+
enableServicePortFallback: true,
218+
)) {
219+
uri = debugSession.ddsUri!;
220+
if (!machineMode) {
221+
print(
222+
'Started the Dart Development Service (DDS) at $uri',
223+
);
224+
}
225+
} else if (!machineMode) {
226+
print(
227+
'WARNING: Failed to start the Dart Development Service (DDS). '
228+
'Some development features may be disabled or degraded.',
229+
);
230+
}
231+
} finally {
232+
await vmService.dispose();
233+
}
234+
return uri;
235+
}
57236
}

pkg/dartdev/lib/src/commands/run.dart

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

55
import 'dart:async';
6-
import 'dart:convert';
7-
import 'dart:developer';
86
import 'dart:io';
97

108
import 'package:args/args.dart';
@@ -14,12 +12,12 @@ import 'package:path/path.dart';
1412
import 'package:pub/pub.dart';
1513

1614
import '../core.dart';
15+
import '../dds_runner.dart';
1716
import '../experiments.dart';
1817
import '../generate_kernel.dart';
1918
import '../native_assets.dart';
2019
import '../resident_frontend_constants.dart';
2120
import '../resident_frontend_utils.dart';
22-
import '../sdk.dart';
2321
import '../utils.dart';
2422
import '../vm_interop_handler.dart';
2523
import 'compilation_server.dart';
@@ -306,16 +304,16 @@ class RunCommand extends DartdevCommand {
306304
// service intermediary which implements the VM service protocol and
307305
// provides non-VM specific extensions (e.g., log caching, client
308306
// synchronization).
309-
_DebuggingSession debugSession;
307+
DDSRunner debugSession;
310308
if (launchDds) {
311-
debugSession = _DebuggingSession();
312-
if (!await debugSession.start(
313-
ddsHost,
314-
ddsPort,
315-
disableServiceAuthCodes,
316-
launchDevTools,
317-
debugDds,
318-
enableServicePortFallback,
309+
debugSession = DDSRunner();
310+
if (!await debugSession.startForCurrentProcess(
311+
ddsHost: ddsHost,
312+
ddsPort: ddsPort,
313+
disableServiceAuthCodes: disableServiceAuthCodes,
314+
enableDevTools: launchDevTools,
315+
debugDds: debugDds,
316+
enableServicePortFallback: enableServicePortFallback,
319317
)) {
320318
return errorExitCode;
321319
}
@@ -402,110 +400,6 @@ class RunCommand extends DartdevCommand {
402400
}
403401
}
404402

405-
class _DebuggingSession {
406-
Future<bool> start(
407-
String host,
408-
String port,
409-
bool disableServiceAuthCodes,
410-
bool enableDevTools,
411-
bool debugDds,
412-
bool enableServicePortFallback,
413-
) async {
414-
final sdkDir = dirname(sdk.dart);
415-
final fullSdk = sdkDir.endsWith('bin');
416-
final devToolsBinaries =
417-
fullSdk ? sdk.devToolsBinaries : absolute(sdkDir, 'devtools');
418-
String snapshotName = fullSdk
419-
? sdk.ddsAotSnapshot
420-
: absolute(sdkDir, 'dds_aot.dart.snapshot');
421-
String execName = sdk.dartAotRuntime;
422-
// Check to see if the AOT snapshot and dartaotruntime are available.
423-
// If not, fall back to running from the AppJIT snapshot.
424-
//
425-
// This can happen if:
426-
// - The SDK is built for IA32 which doesn't support AOT compilation
427-
// - We only have artifacts available from the 'runtime' build
428-
// configuration, which the VM SDK build bots frequently run from
429-
if (!Sdk.checkArtifactExists(snapshotName, logError: false) ||
430-
!Sdk.checkArtifactExists(sdk.dartAotRuntime, logError: false)) {
431-
snapshotName =
432-
fullSdk ? sdk.ddsSnapshot : absolute(sdkDir, 'dds.dart.snapshot');
433-
if (!Sdk.checkArtifactExists(snapshotName)) {
434-
return false;
435-
}
436-
execName = sdk.dart;
437-
}
438-
ServiceProtocolInfo serviceInfo = await Service.getInfo();
439-
// Wait for VM service to publish its connection info.
440-
while (serviceInfo.serverUri == null) {
441-
await Future.delayed(Duration(milliseconds: 10));
442-
serviceInfo = await Service.getInfo();
443-
}
444-
final process = await Process.start(
445-
execName,
446-
[
447-
if (debugDds) '--enable-vm-service=0',
448-
snapshotName,
449-
serviceInfo.serverUri.toString(),
450-
host,
451-
port,
452-
disableServiceAuthCodes.toString(),
453-
enableDevTools.toString(),
454-
devToolsBinaries,
455-
debugDds.toString(),
456-
enableServicePortFallback.toString(),
457-
],
458-
mode: ProcessStartMode.detachedWithStdio,
459-
);
460-
final completer = Completer<void>();
461-
const devToolsMessagePrefix =
462-
'The Dart DevTools debugger and profiler is available at:';
463-
if (debugDds) {
464-
late StreamSubscription stdoutSub;
465-
stdoutSub = process.stdout.transform(utf8.decoder).listen((event) {
466-
if (event.startsWith(devToolsMessagePrefix)) {
467-
final ddsDebuggingUri = event.split(' ').last;
468-
print(
469-
'A DevTools debugger for DDS is available at: $ddsDebuggingUri',
470-
);
471-
stdoutSub.cancel();
472-
}
473-
});
474-
}
475-
late StreamSubscription stderrSub;
476-
stderrSub = process.stderr.transform(utf8.decoder).listen((event) {
477-
final result = json.decode(event) as Map<String, dynamic>;
478-
final state = result['state'];
479-
if (state == 'started') {
480-
if (result.containsKey('devToolsUri')) {
481-
final devToolsUri = result['devToolsUri'];
482-
print('$devToolsMessagePrefix $devToolsUri');
483-
}
484-
stderrSub.cancel();
485-
completer.complete();
486-
} else {
487-
stderrSub.cancel();
488-
final error = result['error'] ?? event;
489-
final stacktrace = result['stacktrace'] ?? '';
490-
String message = 'Could not start the VM service: ';
491-
if (error.contains('Failed to create server socket')) {
492-
message += '$host:$port is already in use.\n';
493-
} else {
494-
message += '$error\n$stacktrace\n';
495-
}
496-
completer.completeError(message);
497-
}
498-
});
499-
try {
500-
await completer.future;
501-
return true;
502-
} catch (e) {
503-
stderr.write(e);
504-
return false;
505-
}
506-
}
507-
}
508-
509403
/// Keep in sync with [getExecutableForCommand].
510404
///
511405
/// Returns `null` if root package should be used.

0 commit comments

Comments
 (0)