| // 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; |
| } |
| } |