[ DDS ] Refactor DDS entrypoint to allow for overriding behavior in google3
This moves the CLI logic into lib/src/dds_cli_entrypoint.dart so it can
be invoked by a custom google3 entrypoint (bin/dds.dart can't be
imported using a package: URI).
Since google3 will be able to perform some custom configuration of DDS,
google3 related logic (e.g., `BazelUriConverter`) is also removed. This
is technically a breaking change, but should be safe as this
functionality isn't currently being used.
Change-Id: I54d8a9927ff2df70e013ca5c8bc1d510b0b95f02
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/371520
Reviewed-by: Derek Xu <derekx@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
Auto-Submit: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/bin/dds.dart b/pkg/dds/bin/dds.dart
index 2741855..9a90fc3 100644
--- a/pkg/dds/bin/dds.dart
+++ b/pkg/dds/bin/dds.dart
@@ -2,147 +2,11 @@
// 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 'dart:convert';
-import 'dart:io';
-
-import 'package:dds/dds.dart';
-import 'package:dds/src/arg_parser.dart';
-import 'package:dds/src/bazel_uri_converter.dart';
-
-import 'package:path/path.dart' as path;
-
-Uri _getDevToolsAssetPath() {
- final dartDir = File(Platform.resolvedExecutable).parent.path;
- final fullSdk = dartDir.endsWith('bin');
- return Uri.file(
- fullSdk
- ? path.absolute(
- dartDir,
- 'resources',
- 'devtools',
- )
- : path.absolute(
- dartDir,
- 'devtools',
- ),
- );
-}
+import 'package:dds/src/dds_cli_entrypoint.dart';
Future<void> main(List<String> args) async {
- final argParser = DartDevelopmentServiceOptions.createArgParser(
- includeHelp: true,
- );
- final argResults = argParser.parse(args);
- if (args.isEmpty || argResults.wasParsed('help')) {
- print('''
-Starts a Dart Development Service (DDS) instance.
-
-Usage:
-${argParser.usage}
- ''');
- return;
- }
-
- // This URI is provided by the VM service directly so don't bother doing a
- // lookup.
- final remoteVmServiceUri = Uri.parse(
- argResults[DartDevelopmentServiceOptions.vmServiceUriOption],
- );
-
- // Resolve the address which is potentially provided by the user.
- late InternetAddress address;
- final bindAddress =
- argResults[DartDevelopmentServiceOptions.bindAddressOption];
- try {
- final addresses = await InternetAddress.lookup(bindAddress);
- // Prefer IPv4 addresses.
- for (int i = 0; i < addresses.length; i++) {
- address = addresses[i];
- if (address.type == InternetAddressType.IPv4) break;
- }
- } on SocketException catch (e, st) {
- writeErrorResponse('Invalid bind address: $bindAddress', st);
- return;
- }
-
- final portString = argResults[DartDevelopmentServiceOptions.bindPortOption];
- int port;
- try {
- port = int.parse(portString);
- } on FormatException catch (e, st) {
- writeErrorResponse('Invalid port: $portString', st);
- return;
- }
- final serviceUri = Uri(
- scheme: 'http',
- host: address.address,
- port: port,
- );
- final disableServiceAuthCodes =
- argResults[DartDevelopmentServiceOptions.disableServiceAuthCodesFlag];
-
- final serveDevTools =
- argResults[DartDevelopmentServiceOptions.serveDevToolsFlag];
- final devToolsServerAddressStr =
- argResults[DartDevelopmentServiceOptions.devToolsServerAddressOption];
- Uri? devToolsBuildDirectory;
- final devToolsServerAddress = devToolsServerAddressStr == null
- ? null
- : Uri.parse(devToolsServerAddressStr);
- if (serveDevTools) {
- devToolsBuildDirectory = _getDevToolsAssetPath();
- }
- final enableServicePortFallback =
- argResults[DartDevelopmentServiceOptions.enableServicePortFallbackFlag];
- final cachedUserTags =
- argResults[DartDevelopmentServiceOptions.cachedUserTagsOption];
- final google3WorkspaceRoot =
- argResults[DartDevelopmentServiceOptions.google3WorkspaceRootOption];
-
- try {
- final dds = await DartDevelopmentService.startDartDevelopmentService(
- remoteVmServiceUri,
- serviceUri: serviceUri,
- enableAuthCodes: !disableServiceAuthCodes,
- ipv6: address.type == InternetAddressType.IPv6,
- devToolsConfiguration: serveDevTools && devToolsBuildDirectory != null
- ? DevToolsConfiguration(
- enable: serveDevTools,
- customBuildDirectoryPath: devToolsBuildDirectory,
- devToolsServerAddress: devToolsServerAddress,
- )
- : null,
- enableServicePortFallback: enableServicePortFallback,
- cachedUserTags: cachedUserTags,
- uriConverter: google3WorkspaceRoot != null
- ? BazelUriConverter(google3WorkspaceRoot).uriToPath
- : null,
- );
- final dtdInfo = dds.hostedDartToolingDaemon;
- stderr.write(json.encode({
- 'state': 'started',
- 'ddsUri': dds.uri.toString(),
- if (dds.devToolsUri != null) 'devToolsUri': dds.devToolsUri.toString(),
- if (dtdInfo != null)
- 'dtd': {
- 'uri': dtdInfo.uri,
- },
- }));
- stderr.close();
- } catch (e, st) {
- writeErrorResponse(e, st);
- } finally {
- // Always close stderr to notify tooling that DDS has finished writing
- // launch details.
- await stderr.close();
- }
-}
-
-void writeErrorResponse(Object e, StackTrace st) {
- stderr.write(json.encode({
- 'state': 'error',
- 'error': '$e',
- 'stacktrace': '$st',
- if (e is DartDevelopmentServiceException) 'ddsExceptionDetails': e.toJson(),
- }));
+ // This level of indirection is only here so DDS can be configured for
+ // google3 specific functionality as it's not possible to import files under
+ // a package's bin directory to wrap the entrypoint.
+ await runDartDevelopmentServiceFromCLI(args);
}
diff --git a/pkg/dds/lib/src/arg_parser.dart b/pkg/dds/lib/src/arg_parser.dart
index fd605b0..c609edc 100644
--- a/pkg/dds/lib/src/arg_parser.dart
+++ b/pkg/dds/lib/src/arg_parser.dart
@@ -16,7 +16,6 @@
static const enableServicePortFallbackFlag = 'enable-service-port-fallback';
static const cachedUserTagsOption = 'cached-user-tags';
static const devToolsServerAddressOption = 'devtools-server-address';
- static const google3WorkspaceRootOption = 'google3-workspace-root';
static ArgParser createArgParser({
int? usageLineLength,
@@ -78,12 +77,6 @@
help: 'A set of UserTag names used to determine which CPU samples are '
'cached by DDS.',
defaultsTo: <String>[],
- )
- ..addOption(
- google3WorkspaceRootOption,
- help: 'Sets the Google3 workspace root used for google3:// URI '
- 'resolution.',
- hide: !verbose,
);
if (includeHelp) {
argParser.addFlag('help', negatable: false);
diff --git a/pkg/dds/lib/src/bazel_uri_converter.dart b/pkg/dds/lib/src/bazel_uri_converter.dart
deleted file mode 100644
index 6710c34..0000000
--- a/pkg/dds/lib/src/bazel_uri_converter.dart
+++ /dev/null
@@ -1,240 +0,0 @@
-// Copyright (c) 2024, 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.
-
-// TODO(bkonyi): consider moving to lib/ once this package is no longer shipped
-// via pub.
-
-import 'dart:async';
-import 'dart:collection';
-import 'dart:io' as io;
-
-import 'package:path/path.dart';
-
-/// A URI converter able to handle google3 URIs.
-class BazelUriConverter {
- final _absoluteUriToFileCache = HashMap<Uri, String>();
- final Context _context = context;
- final _binPaths = <String>[];
- final String _root;
- String? _genfiles;
- final _bazelCandidateFiles = StreamController<BazelSearchInfo>.broadcast();
-
- BazelUriConverter(String originalPath) : _root = originalPath {
- _ensureAbsoluteAndNormalized(originalPath);
- // Note: The analyzer code this code is based on checked multiple things
- // while trying to find a google3 workspace - the presence of a blaze-out
- // directory, a READONLY file or a WORKSPACE file. If the blaze-out
- // structure changes, then potentially check for one of the others.
- final blazeOutDir =
- io.Directory(normalize(join(originalPath, 'blaze-out')));
- if (blazeOutDir.existsSync()) {
- _binPaths.addAll(_findBinFolderPaths(blazeOutDir));
- _binPaths.add(normalize(join(originalPath, 'blaze-bin')));
- _genfiles = normalize(join(originalPath, 'blaze-genfiles'));
- }
- }
-
- String? uriToPath(String uriStr) {
- final uri = Uri.parse(uriStr);
- final cached = _absoluteUriToFileCache[uri];
- if (cached != null) {
- return cached;
- }
-
- if (uri.isScheme('file')) {
- final path = fileUriToNormalizedPath(_context, uri);
- final pathRelativeToRoot = _relativeToRoot(path);
- if (pathRelativeToRoot == null) return null;
- final fullFilePath = _context.join(_root, pathRelativeToRoot);
- final file = _findFile(fullFilePath);
- if (file != null) {
- _absoluteUriToFileCache[uri] = file.path;
- return file.path;
- }
- }
- // If the URI passed has a google3 scheme, this means we don't need to
- // convert from a package URI and we only need to prepend the root path
- // (i.e. the path that contains the user's workspace name).
- if (uri.isScheme('google3')) {
- // Remove the first character since uri.path starts with '/', though we
- // know this is not an absolute path.
- return _context.join(_root, uri.path.substring(1));
- }
- if (!uri.isScheme('package')) {
- // TODO(b/261234406): Handle `dart:` URIs.
- if (uri.isScheme('dart')) {
- // This doesn't return the actual location of the Dart SDK, which is
- // more complicated. The purpose of returning something here is to
- // avoid having the external version of URI converter resolving a path
- // that is incorrect in a way that confuse VS Code's stack trace.
- return _context.join(_root, uri.path);
- }
- return null;
- }
- final uriPath = Uri.decodeComponent(uri.path);
- final slash = uriPath.indexOf('/');
-
- // If the path either starts with a slash or has no slash, it is invalid.
- if (slash < 1) {
- return null;
- }
-
- if (uriPath.contains('//') || uriPath.contains('..')) {
- return null;
- }
-
- final packageName = uriPath.substring(0, slash);
-
- final fileUriPart = uriPath.substring(slash + 1);
- if (fileUriPart.isEmpty) {
- return null;
- }
-
- final filePath = fileUriPart.replaceAll('/', _context.separator);
-
- if (!packageName.contains('.')) {
- final fullFilePath = _context.join(
- _root, 'third_party', 'dart', packageName, 'lib', filePath);
- final file = _findFile(fullFilePath);
- if (file != null) {
- _absoluteUriToFileCache[uri] = file.path;
- return file.path;
- }
- } else {
- final packagePath = packageName.replaceAll('.', _context.separator);
- final fullFilePath = _context.join(_root, packagePath, 'lib', filePath);
- final file = _findFile(fullFilePath);
- if (file != null) {
- _absoluteUriToFileCache[uri] = file.path;
- return file.path;
- }
- }
- return null;
- }
-
- String fileUriToNormalizedPath(Context context, Uri fileUri) {
- assert(fileUri.isScheme('file'));
- var contextPath = context.fromUri(fileUri);
- contextPath = context.normalize(contextPath);
- return contextPath;
- }
-
- /// The file system abstraction supports only absolute and normalized paths.
- /// This method is used to validate any input paths to prevent errors later.
- void _ensureAbsoluteAndNormalized(String path) {
- assert(() {
- if (!_context.isAbsolute(path)) {
- throw ArgumentError('Path must be absolute : $path');
- }
- if (_context.normalize(path) != path) {
- throw ArgumentError('Path must be normalized : $path');
- }
- return true;
- }());
- }
-
- List<String> _findBinFolderPaths(io.Directory blazeOutDir) {
- final binPaths = <String>[];
- for (final child
- in blazeOutDir.listSync(recursive: false).whereType<io.Directory>()) {
- // Children are folders denoting architectures and build flags, like
- // 'k8-opt', 'k8-fastbuild', perhaps 'host'.
-
- final possibleBin = io.Directory(normalize(join(child.path, 'bin')));
- if (possibleBin.existsSync()) {
- binPaths.add(possibleBin.path);
- }
- }
- return binPaths;
- }
-
- String? _relativeToRoot(String p) {
- // genfiles
- if (_genfiles != null && _context.isWithin(_genfiles!, p)) {
- return context.relative(p, from: _genfiles!);
- }
- // bin
- for (final bin in _binPaths) {
- if (context.isWithin(bin, p)) {
- return context.relative(p, from: bin);
- }
- }
-
- // We are no longer checking for READONLY? Or should I add back?
- // READONLY
- // final readonly = this.readonly;
- // if (readonly != null) {
- // if (context.isWithin(readonly, p)) {
- // return context.relative(p, from: readonly);
- // }
- // }
- // Not generated
- if (context.isWithin(_root, p)) {
- return context.relative(p, from: _root);
- }
- // Failed reverse lookup
- return null;
- }
-
- /// Return the file with the given [absolutePath], looking first into
- /// directories for generated files: `bazel-bin` and `bazel-genfiles`, and
- /// then into the workspace originalPath. The file in the workspace originalPath is returned
- /// even if it does not exist. Return `null` if the given [absolutePath] is
- /// not in the workspace [originalPath].
- io.File? _findFile(String absolutePath) {
- try {
- final relative = _context.relative(absolutePath, from: _root);
- if (relative == '.') {
- return null;
- }
- // First check genfiles and bin directories. Note that we always use the
- // symlinks and not the [binPaths] or [genfiles] to make sure we use the
- // files corresponding to the most recent build configuration and get
- // consistent view of all the generated files.
- final generatedCandidates = ['blaze-genfiles', 'blaze-bin']
- .map((prefix) => context.join(_root, context.join(prefix, relative)));
- for (final path in generatedCandidates) {
- _ensureAbsoluteAndNormalized(path);
- final file = io.File(path);
- if (file.existsSync()) {
- _bazelCandidateFiles
- .add(BazelSearchInfo(relative, generatedCandidates.toList()));
- return file;
- }
- }
- // Writable
- _ensureAbsoluteAndNormalized(absolutePath);
- final writableFile = io.File(absolutePath);
- if (writableFile.existsSync()) {
- return writableFile;
- }
-
- // If we couldn't find the file, assume that it has not yet been
- // generated, so send an event with all the paths that we tried.
- _bazelCandidateFiles
- .add(BazelSearchInfo(relative, generatedCandidates.toList()));
- // Not generated, return the default one.
- return writableFile;
- } catch (_) {
- return null;
- }
- }
-}
-
-/// Notification that we issue when searching for generated files in a Bazel
-/// workspace.
-///
-/// This allows clients to watch for changes to the generated files.
-class BazelSearchInfo {
- /// Candidate paths that we searched.
- final List<String> candidatePaths;
-
- /// Absolute path that we tried searching for.
- ///
- /// This is not necessarily the path of the actual file that will be used. See
- /// `BazelWorkspace.findFile` for details.
- final String requestedPath;
-
- BazelSearchInfo(this.requestedPath, this.candidatePaths);
-}
diff --git a/pkg/dds/lib/src/dds_cli_entrypoint.dart b/pkg/dds/lib/src/dds_cli_entrypoint.dart
new file mode 100644
index 0000000..1680cba
--- /dev/null
+++ b/pkg/dds/lib/src/dds_cli_entrypoint.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2024, 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.
+
+// TODO(bkonyi): move this file to lib/dds_cli_entrypoint.dart once package:dds
+// is no longer shipped via pub.
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import '../dds.dart';
+import 'arg_parser.dart';
+
+Uri _getDevToolsAssetPath() {
+ final dartDir = File(Platform.resolvedExecutable).parent.path;
+ final fullSdk = dartDir.endsWith('bin');
+ return Uri.file(
+ fullSdk
+ ? path.absolute(
+ dartDir,
+ 'resources',
+ 'devtools',
+ )
+ : path.absolute(
+ dartDir,
+ 'devtools',
+ ),
+ );
+}
+
+// TODO(bkonyi): allow for injection of custom DevTools handlers in google3.
+Future<void> runDartDevelopmentServiceFromCLI(
+ List<String> args, {
+ String? Function(String)? uriConverter,
+}) async {
+ final argParser = DartDevelopmentServiceOptions.createArgParser(
+ includeHelp: true,
+ );
+ final argResults = argParser.parse(args);
+ if (args.isEmpty || argResults.wasParsed('help')) {
+ print('''
+Starts a Dart Development Service (DDS) instance.
+
+Usage:
+${argParser.usage}
+ ''');
+ return;
+ }
+
+ // This URI is provided by the VM service directly so don't bother doing a
+ // lookup.
+ final remoteVmServiceUri = Uri.parse(
+ argResults[DartDevelopmentServiceOptions.vmServiceUriOption],
+ );
+
+ // Resolve the address which is potentially provided by the user.
+ late InternetAddress address;
+ final bindAddress =
+ argResults[DartDevelopmentServiceOptions.bindAddressOption];
+ try {
+ final addresses = await InternetAddress.lookup(bindAddress);
+ // Prefer IPv4 addresses.
+ for (int i = 0; i < addresses.length; i++) {
+ address = addresses[i];
+ if (address.type == InternetAddressType.IPv4) break;
+ }
+ } on SocketException catch (e, st) {
+ writeErrorResponse('Invalid bind address: $bindAddress', st);
+ return;
+ }
+
+ final portString = argResults[DartDevelopmentServiceOptions.bindPortOption];
+ int port;
+ try {
+ port = int.parse(portString);
+ } on FormatException catch (e, st) {
+ writeErrorResponse('Invalid port: $portString', st);
+ return;
+ }
+ final serviceUri = Uri(
+ scheme: 'http',
+ host: address.address,
+ port: port,
+ );
+ final disableServiceAuthCodes =
+ argResults[DartDevelopmentServiceOptions.disableServiceAuthCodesFlag];
+
+ final serveDevTools =
+ argResults[DartDevelopmentServiceOptions.serveDevToolsFlag];
+ final devToolsServerAddressStr =
+ argResults[DartDevelopmentServiceOptions.devToolsServerAddressOption];
+ Uri? devToolsBuildDirectory;
+ final devToolsServerAddress = devToolsServerAddressStr == null
+ ? null
+ : Uri.parse(devToolsServerAddressStr);
+ if (serveDevTools) {
+ devToolsBuildDirectory = _getDevToolsAssetPath();
+ }
+ final enableServicePortFallback =
+ argResults[DartDevelopmentServiceOptions.enableServicePortFallbackFlag];
+ final cachedUserTags =
+ argResults[DartDevelopmentServiceOptions.cachedUserTagsOption];
+
+ try {
+ final dds = await DartDevelopmentService.startDartDevelopmentService(
+ remoteVmServiceUri,
+ serviceUri: serviceUri,
+ enableAuthCodes: !disableServiceAuthCodes,
+ ipv6: address.type == InternetAddressType.IPv6,
+ devToolsConfiguration: serveDevTools && devToolsBuildDirectory != null
+ ? DevToolsConfiguration(
+ enable: serveDevTools,
+ customBuildDirectoryPath: devToolsBuildDirectory,
+ devToolsServerAddress: devToolsServerAddress,
+ )
+ : null,
+ enableServicePortFallback: enableServicePortFallback,
+ cachedUserTags: cachedUserTags,
+ uriConverter: uriConverter,
+ );
+ final dtdInfo = dds.hostedDartToolingDaemon;
+ stderr.write(json.encode({
+ 'state': 'started',
+ 'ddsUri': dds.uri.toString(),
+ if (dds.devToolsUri != null) 'devToolsUri': dds.devToolsUri.toString(),
+ if (dtdInfo != null)
+ 'dtd': {
+ 'uri': dtdInfo.uri,
+ },
+ }));
+ stderr.close();
+ } catch (e, st) {
+ writeErrorResponse(e, st);
+ } finally {
+ // Always close stderr to notify tooling that DDS has finished writing
+ // launch details.
+ await stderr.close();
+ }
+}
+
+void writeErrorResponse(Object e, StackTrace st) {
+ stderr.write(json.encode({
+ 'state': 'error',
+ 'error': '$e',
+ 'stacktrace': '$st',
+ if (e is DartDevelopmentServiceException) 'ddsExceptionDetails': e.toJson(),
+ }));
+}
diff --git a/pkg/dds/test/uri_converter_test.dart b/pkg/dds/test/uri_converter_test.dart
index 656982f..588655c 100644
--- a/pkg/dds/test/uri_converter_test.dart
+++ b/pkg/dds/test/uri_converter_test.dart
@@ -6,7 +6,6 @@
import 'dart:io';
import 'package:dds/dds.dart';
-import 'package:dds/src/bazel_uri_converter.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
@@ -77,54 +76,4 @@
},
timeout: Timeout.none,
);
-
- test(
- 'DDS can handle basic google3:// paths',
- () async {
- Uri serviceUri = remoteVmServiceUri;
- dds = await DartDevelopmentService.startDartDevelopmentService(
- remoteVmServiceUri,
- uriConverter: BazelUriConverter('/workspace/root').uriToPath,
- );
- serviceUri = dds!.wsUri!;
- expect(dds!.isRunning, true);
- final service = await vmServiceConnectUri(serviceUri.toString());
-
- IsolateRef isolate;
- while (true) {
- final vm = await service.getVM();
- if (vm.isolates!.isNotEmpty) {
- isolate = vm.isolates!.first;
- try {
- isolate = await service.getIsolate(isolate.id!);
- if ((isolate as Isolate).runnable!) {
- break;
- }
- } on SentinelException {
- // ignore
- }
- }
- await Future.delayed(const Duration(seconds: 1));
- }
- expect(isolate, isNotNull);
-
- final unresolvedUris = <String>[
- // TODO(b/261234406): Handle `dart:` URIs.
- 'dart:io', // dart:io -> file:///workspace/root/io
- 'google3://workspace_name/my/script.dart', // google3://workspace_name/my/script.dart -> file:///workspace/root/my/script.dart
- 'package:foo/foo.dart', // package:foo/foo.dart -> file:///workspace/root/third_party/dart/foo/lib/foo.dart
- ];
- final result = await service.lookupResolvedPackageUris(
- isolate.id!,
- unresolvedUris,
- local: true,
- );
-
- expect(result.uris?[0], 'file:///workspace/root/io');
- expect(result.uris?[1], 'file:///workspace/root/my/script.dart');
- expect(result.uris?[2],
- 'file:///workspace/root/third_party/dart/foo/lib/foo.dart');
- },
- timeout: Timeout.none,
- );
}