[ 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>
This commit is contained in:
Ben Konyi
2024-06-13 20:27:22 +00:00
committed by Commit Queue
parent 02faed0a9a
commit 9304826679
5 changed files with 154 additions and 439 deletions
+5 -141
View File
@@ -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);
}
-7
View File
@@ -16,7 +16,6 @@ abstract class DartDevelopmentServiceOptions {
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 @@ abstract class DartDevelopmentServiceOptions {
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);
-240
View File
@@ -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);
}
+149
View File
@@ -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(),
}));
}
-51
View File
@@ -6,7 +6,6 @@ import 'dart:async';
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 @@ void main() {
},
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,
);
}