blob: 0a5e30e138272b4828a8fd5fb1d6602cea2c7bf8 [file] [log] [blame] [edit]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// 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 'package:dds/devtools_server.dart';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import '../core.dart';
import '../dds_runner.dart';
import '../sdk.dart';
import '../utils.dart';
class DevToolsCommand extends DartdevCommand {
DevToolsCommand({
this.customDevToolsPath,
bool verbose = false,
}) : argParser = DevToolsServer.buildArgParser(
verbose: verbose,
includeHelpOption: false,
usageLineLength: dartdevUsageLineLength,
),
super(
'devtools',
DevToolsServer.commandDescription,
verbose,
);
final String? customDevToolsPath;
@override
final ArgParser argParser;
@override
String get name => 'devtools';
@override
CommandCategory get commandCategory => CommandCategory.tools;
@override
String get description => DevToolsServer.commandDescription;
@override
String get invocation => '${super.invocation} [service protocol uri]';
@override
Future<int> run() async {
final args = argResults!;
final sdkDir = path.dirname(sdk.dart);
final fullSdk = sdkDir.endsWith('bin');
final devToolsBinaries =
fullSdk ? sdk.devToolsBinaries : path.absolute(sdkDir, 'devtools');
final argList = await _performDDSCheck(args);
final server = await DevToolsServer().serveDevToolsWithArgs(
argList,
customDevToolsPath: devToolsBinaries,
);
return server == null ? -1 : 0;
}
/// Attempts to start a DDS instance if there isn't one running for the
/// target application.
///
/// Returns the argument list from [args], which is modified to include the
/// DDS URI instead of the VM service URI if DDS is started by this method
/// or a redirect to DDS would be followed.
Future<List<String>> _performDDSCheck(ArgResults args) async {
String? serviceProtocolUri;
bool positionalServiceUri = false;
if (args.rest.isNotEmpty) {
serviceProtocolUri = args.rest.first;
positionalServiceUri = true;
} else if (args.wasParsed(DevToolsServer.argVmUri)) {
serviceProtocolUri = args.option(DevToolsServer.argVmUri)!;
}
final argList = args.arguments.toList();
// No VM service URI was provided, so the user is going to manually connect
// to their application from DevTools or only use offline tooling.
//
// TODO(bkonyi): we should consider having devtools_server try and spawn
// DDS if users try to connect to an application without a DDS instance.
if (serviceProtocolUri == null) {
return argList;
}
final originalUri = Uri.parse(serviceProtocolUri);
var uri = originalUri;
// The VM service doesn't like it when there's no trailing forward slash at
// the end of the path. Add one if it's missing.
uri = _ensureUriHasTrailingForwardSlash(uri);
// Check to see if the URI is a VM service URI for a VM service instance
// that already has a DDS instance connected.
uri = await checkForRedirectToExistingDDSInstance(uri);
// If this isn't a URI for a VM service with an active DDS instance,
// check to see if this URI points to a running DDS instance. If not, try
// and start DDS.
if (uri == originalUri) {
uri = await maybeStartDDS(
uri: uri,
ddsHost: args.option(DevToolsServer.argDdsHost)!,
ddsPort: args.option(DevToolsServer.argDdsPort)!,
machineMode: args.wasParsed(DevToolsServer.argMachine),
);
}
// If we ended up starting DDS or redirecting to an existing instance,
// update the argument list to include the actual URI for DevTools to
// connect to.
if (uri != originalUri) {
if (positionalServiceUri) {
argList.removeLast();
}
argList.add(uri.toString());
}
return argList;
}
Uri _ensureUriHasTrailingForwardSlash(Uri uri) {
if (uri.pathSegments.isNotEmpty) {
final pathSegments = uri.pathSegments.toList();
if (pathSegments.last.isNotEmpty) {
pathSegments.add('');
}
uri = uri.replace(pathSegments: pathSegments);
}
return uri;
}
@visibleForTesting
static Future<Uri> checkForRedirectToExistingDDSInstance(Uri uri) async {
final request = http.Request('GET', uri)..followRedirects = false;
final response = await request.send();
await response.stream.drain();
// If we're redirected that means that we attempted to speak directly to
// the VM service even though there's an active DDS instance already
// connected to it. In this case, DevTools will fail to connect to the VM
// service directly so we need to modify the target URI in the args list to
// instead point to the DDS instance.
if (response.isRedirect) {
final redirectUri = Uri.parse(response.headers['location']!);
final ddsWsUri = Uri.parse(redirectUri.queryParameters['uri']!);
// Remove '/ws' from the path, add a trailing '/'
var pathSegments = ddsWsUri.pathSegments.toList()
..removeLast()
..add('');
if (pathSegments.length == 1) {
pathSegments.add('');
}
uri = ddsWsUri.replace(
scheme: 'http',
pathSegments: pathSegments,
);
}
return uri;
}
@visibleForTesting
static Future<Uri> maybeStartDDS({
required Uri uri,
required String ddsHost,
required String ddsPort,
bool machineMode = false,
}) async {
final pathSegments = uri.pathSegments.toList();
if (pathSegments.isNotEmpty && pathSegments.last.isEmpty) {
// There's a trailing '/' at the end of the parsed URI, so there's an
// empty string at the end of the path segments list that needs to be
// removed.
pathSegments.removeLast();
}
final authCodesEnabled = pathSegments.isNotEmpty;
final wsUri = uri.replace(
scheme: 'ws',
pathSegments: [
...pathSegments,
'ws',
],
);
final vmService = await vmServiceConnectUri(wsUri.toString());
try {
// If this request throws a RPC error, DDS isn't running and we should
// try and start it.
await vmService.getDartDevelopmentServiceVersion();
} on RPCError {
// If the user wants to start a debugging session we need to do some extra
// work and spawn a Dart Development Service (DDS) instance. DDS is a VM
// service intermediary which implements the VM service protocol and
// provides non-VM specific extensions (e.g., log caching, client
// synchronization).
final debugSession = DDSRunner();
if (await debugSession.start(
vmServiceUri: uri,
ddsHost: ddsHost,
ddsPort: ddsPort,
debugDds: false,
disableServiceAuthCodes: !authCodesEnabled,
// TODO(bkonyi): should we just have DDS serve its own duplicate
// DevTools instance? It shouldn't add much, if any, overhead but will
// allow for developers to access DevTools directly through the VM
// service URI at a later point. This would probably be a fairly niche
// workflow.
enableDevTools: false,
enableServicePortFallback: true,
)) {
uri = debugSession.ddsUri!;
if (!machineMode) {
print(
'Started the Dart Development Service (DDS) at $uri',
);
}
} else if (!machineMode) {
print(
'WARNING: Failed to start the Dart Development Service (DDS). '
'Some development features may be disabled or degraded.',
);
}
} finally {
await vmService.dispose();
}
return uri;
}
}