blob: 4b6be36871443e8729f1f18309bd5bb1332e279a [file] [log] [blame]
// Copyright (c) 2023, 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 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:args/args.dart';
import 'package:shelf/shelf.dart';
import 'package:sse/server/sse_handler.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'src/constants.dart';
import 'src/dtd_client.dart';
import 'src/dtd_client_manager.dart';
import 'src/dtd_stream_manager.dart';
import 'src/service/file_system_service.dart';
import 'src/service/unified_analytics_service.dart';
/// Contains all the flags and options used by the DTD argument parser.
enum DartToolingDaemonOptions {
// Used when executing a training run while generating an AppJIT snapshot as
// part of an SDK build.
train.flag(negatable: false, hide: true),
machine.flag(
negatable: false,
help: 'Sets output format to JSON for consumption in tools.',
),
port.option(
defaultsTo: '0',
help: 'Sets the port to bind DTD to (0 for automatic port).',
),
unrestricted.flag(
negatable: false,
help: 'Disables restrictions on services registered by DTD.',
),
fakeAnalytics.flag(
negatable: false,
help: 'Uses fake analytics instances for the UnifiedAnalytics service.',
hide: true,
);
const DartToolingDaemonOptions.flag({
this.negatable = true,
this.hide = false,
this.help,
}) : _kind = _DartToolingDaemonOptionKind.flag,
defaultsTo = null;
const DartToolingDaemonOptions.option({
this.defaultsTo,
this.help,
}) : _kind = _DartToolingDaemonOptionKind.option,
negatable = false,
hide = false;
final _DartToolingDaemonOptionKind _kind;
final String? defaultsTo;
final bool negatable;
final bool hide;
final String? help;
/// Returns an argument parser that can be used to configure the daemon.
static ArgParser createArgParser({
int? usageLineLength,
}) {
final argParser = ArgParser(usageLineLength: usageLineLength);
for (final entry in DartToolingDaemonOptions.values) {
switch (entry._kind) {
case _DartToolingDaemonOptionKind.flag:
argParser.addFlag(
entry.name,
negatable: entry.negatable,
hide: entry.hide,
help: entry.help,
);
case _DartToolingDaemonOptionKind.option:
argParser.addOption(
entry.name,
hide: entry.hide,
help: entry.help,
defaultsTo: entry.defaultsTo,
);
}
}
return argParser;
}
}
/// The kind of command line argument.
enum _DartToolingDaemonOptionKind {
flag,
option,
}
/// TODO(https://github.com/dart-lang/sdk/issues/54429): Add shutdown behavior.
/// A service that facilitates communication between dart tools.
class DartToolingDaemon {
DartToolingDaemon._({
required this.secret,
required bool unrestrictedMode,
bool ipv6 = false,
bool shouldLogRequests = false,
bool useFakeAnalytics = false,
}) : _ipv6 = ipv6,
_shouldLogRequests = shouldLogRequests {
streamManager = DTDStreamManager(this);
clientManager = DTDClientManager();
fileSystemService = FileSystemService(
secret: secret,
unrestrictedMode: unrestrictedMode,
);
unifiedAnalyticsService = UnifiedAnalyticsService(fake: useFakeAnalytics);
}
static const _kSseHandlerPath = '\$debugHandler';
/// Manages the streams for the current [DartToolingDaemon] service.
late final DTDStreamManager streamManager;
/// Manages the connected clients of the current [DartToolingDaemon] service.
late final DTDClientManager clientManager;
final bool _ipv6;
late HttpServer _server;
final bool _shouldLogRequests;
late final FileSystemService fileSystemService;
/// Provides interaction with package:unified_analytics for DTD clients.
late final UnifiedAnalyticsService unifiedAnalyticsService;
final String secret;
/// Any requests to DTD must have this token as the first element of the
/// uri path.
///
/// This provides an obfuscation step to prevent bad actors from stumbling
/// onto the dtd address.
final String _uriToken = _generateSecret();
/// The uri of the current [DartToolingDaemon] service.
Uri? get uri => _uri;
Uri? _uri;
Future<void> _startService({required int port}) async {
final host =
(_ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4)
.host;
// Start the DTD server. Run in an error Zone to ensure that asynchronous
// exceptions encountered during request handling are handled, as exceptions
// thrown during request handling shouldn't take down the entire service.
late String errorMessage;
final tmpServer = await runZonedGuarded(
() async {
Future<HttpServer?> startServer() async {
try {
return await io.serve(_handlers(), host, port);
} on SocketException catch (e) {
errorMessage = e.message;
if (e.osError != null) {
errorMessage += ' (${e.osError!.message})';
}
errorMessage += ': ${e.address?.host}:${e.port}';
return null;
}
}
return await startServer();
},
(error, stack) {
if (_shouldLogRequests) {
print('Asynchronous error: $error\n$stack');
}
},
);
if (tmpServer == null) {
throw DartToolingDaemonException.connectionIssue(errorMessage);
}
_server = tmpServer;
_uri = Uri(
scheme: 'ws',
host: host,
port: _server.port,
path: '/$_uriToken',
);
}
/// Starts a [DartToolingDaemon] service.
///
/// Set [ipv6] to true to have the service use ipv6 instead of ipv4.
///
/// Set [shouldLogRequests] to true to enable logging.
///
/// When [sendPort] is non-null, information about the DTD connection will be
/// sent over [port] instead of being printed to stdout.
static Future<DartToolingDaemon?> startService(
List<String> args, {
bool ipv6 = false,
bool shouldLogRequests = false,
SendPort? sendPort,
}) async {
final argParser = DartToolingDaemonOptions.createArgParser();
final parsedArgs = argParser.parse(args);
if (parsedArgs.wasParsed(DartToolingDaemonOptions.train.name)) {
return null;
}
final machineMode = parsedArgs[DartToolingDaemonOptions.machine.name];
final unrestrictedMode =
parsedArgs[DartToolingDaemonOptions.unrestricted.name];
final useFakeAnalytics =
parsedArgs[DartToolingDaemonOptions.fakeAnalytics.name];
final port =
int.tryParse(parsedArgs[DartToolingDaemonOptions.port.name]) ?? 0;
final secret = _generateSecret();
final dtd = DartToolingDaemon._(
secret: secret,
unrestrictedMode: unrestrictedMode,
ipv6: ipv6,
shouldLogRequests: shouldLogRequests,
useFakeAnalytics: useFakeAnalytics,
);
await dtd._startService(port: port);
if (machineMode) {
final encoded = jsonEncode({
'tooling_daemon_details': {
'uri': dtd.uri.toString(),
...(!unrestrictedMode ? {'trusted_client_secret': secret} : {}),
},
});
if (sendPort == null) {
print(encoded);
} else {
sendPort.send(encoded);
}
} else {
print(
'The Dart Tooling Daemon is listening on '
'${dtd.uri.toString()}',
);
if (!unrestrictedMode) {
print('Trusted Client Secret: $secret');
}
}
return dtd;
}
// Attempt to upgrade HTTP requests to a websocket before processing them as
// standard HTTP requests. The websocket handler will fail quickly if the
// request doesn't appear to be a websocket upgrade request.
Handler _handlers() {
return Pipeline().addMiddleware(_uriTokenHandler).addHandler(
Cascade().add(_webSocketHandler()).add(_sseHandler()).handler,
);
}
Handler _uriTokenHandler(Handler innerHandler) => (Request request) {
final forbidden =
Response.forbidden('missing or invalid authentication code');
final pathSegments = request.url.pathSegments;
if (pathSegments.isEmpty) {
return forbidden;
}
final token = pathSegments[0];
if (token != _uriToken) {
return forbidden;
}
return innerHandler(request);
};
Handler _webSocketHandler() => webSocketHandler((WebSocketChannel ws) {
final client = DTDClient.fromWebSocket(
this,
ws,
);
_registerInternalServiceMethods(client);
clientManager.addClient(client);
});
Handler _sseHandler() {
final handler = SseHandler(
Uri.parse('/$_kSseHandlerPath'),
keepAlive: sseKeepAlive,
);
handler.connections.rest.listen((sseConnection) {
final client = DTDClient.fromSSEConnection(
this,
sseConnection,
);
_registerInternalServiceMethods(client);
clientManager.addClient(client);
});
return handler.handler;
}
void _registerInternalServiceMethods(DTDClient client) {
fileSystemService.register(client);
unifiedAnalyticsService.register(client);
}
static String _generateSecret() {
String upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
String lower = 'abcdefghijklmnopqrstuvwxyz';
String numbers = '1234567890';
int secretLength = 16;
String seed = upper + lower + numbers;
String password = '';
List<String> list = seed.split('').toList();
Random rand = Random();
for (int i = 0; i < secretLength; i++) {
int index = rand.nextInt(list.length);
password += list[index];
}
return password;
}
Future<void> close() async {
await clientManager.shutdown();
await _server.close(force: true);
}
}
// TODO(danchevalier): clean up these exceptions so they are more relevant to
// DTD. Also add docs to the factories that remain.
class DartToolingDaemonException implements Exception {
// TODO(danchevalier): add a relevant dart doc here
static const int existingDtdInstanceError = 1;
/// Set when the connection to the remote VM service terminates unexpectedly
/// during Dart Development Service startup.
static const int failedToStartError = 2;
/// Set when a connection error has occurred after startup.
static const int connectionError = 3;
factory DartToolingDaemonException.existingDtdInstance(
String message, {
Uri? dtdUri,
}) {
return ExistingDTDImplException._(message, dtdUri: dtdUri);
}
factory DartToolingDaemonException.failedToStart() {
return DartToolingDaemonException._(
failedToStartError,
'Failed to start Dart Development Service',
);
}
factory DartToolingDaemonException.connectionIssue(String message) {
return DartToolingDaemonException._(connectionError, message);
}
DartToolingDaemonException._(this.errorCode, this.message);
@override
String toString() => 'DartDevelopmentServiceException: $message';
final int errorCode;
final String message;
}
class ExistingDTDImplException extends DartToolingDaemonException {
ExistingDTDImplException._(
String message, {
this.dtdUri,
}) : super._(
DartToolingDaemonException.existingDtdInstanceError,
message,
);
/// The URI of the existing DTD instance, if available.
///
/// This URL is the base HTTP URI such as `http://127.0.0.1:1234/AbcDefg=/`,
/// not the WebSocket URI (which can be obtained by mapping the scheme to
/// `ws` (or `wss`) and appending `ws` to the path segments).
final Uri? dtdUri;
}