blob: 6ba76e09fa8a3b041e8443a2a4da271be2d809b8 [file] [log] [blame]
// Copyright (c) 2020, 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 'package:args/args.dart';
import 'package:dds/dds.dart';
import 'package:path/path.dart';
import '../core.dart';
import '../experiments.dart';
import '../sdk.dart';
import '../utils.dart';
class RunCommand extends DartdevCommand<int> {
@override
final ArgParser argParser = ArgParser.allowAnything();
@override
final bool verbose;
RunCommand({this.verbose = false}) : super('run', '''
Run a Dart file.''');
@override
String get invocation => '${super.invocation} <dart file | package target>';
@override
void printUsage() {
// Override [printUsage] for invocations of 'dart help run' which won't
// execute [run] below. Without this, the 'dart help run' reports the
// command pub with no commands or flags.
final command = sdk.dart;
final args = [
'--disable-dart-dev',
'--help',
if (verbose) '--verbose',
];
log.trace('$command ${args.first}');
// Call 'dart --help'
// Process.runSync(..) is used since [printUsage] is not an async method,
// and we want to guarantee that the result (the help text for the console)
// is printed before command exits.
final result = Process.runSync(command, args);
if (result.stderr.isNotEmpty) {
stderr.write(result.stderr);
}
if (result.stdout.isNotEmpty) {
stdout.write(result.stdout);
}
}
@override
FutureOr<int> run() async {
// The command line arguments after 'run'
var args = argResults.arguments.toList();
var argsContainFileOrHelp = false;
for (var arg in args) {
// The arg.contains('.') matches a file name pattern, i.e. some 'foo.dart'
if (arg.contains('.') ||
arg == '--help' ||
arg == '-h' ||
arg == 'help') {
argsContainFileOrHelp = true;
break;
}
}
final cwd = Directory.current;
if (!argsContainFileOrHelp && cwd.existsSync()) {
var foundImplicitFileToRun = false;
var cwdName = cwd.name;
for (var entity in cwd.listSync(followLinks: false)) {
if (entity is Directory && entity.name == 'bin') {
var filesInBin =
entity.listSync(followLinks: false).whereType<File>();
// Search for a dart file in bin/ with the pattern foo/bin/foo.dart
for (var fileInBin in filesInBin) {
if (fileInBin.isDartFile && fileInBin.name == '$cwdName.dart') {
args.add('bin/${fileInBin.name}');
foundImplicitFileToRun = true;
break;
}
}
// break here, no actions taken on any entities that are not bin/
break;
}
}
if (!foundImplicitFileToRun) {
log.stderr(
'Could not find the implicit file to run: '
'bin$separator$cwdName.dart.',
);
}
}
// Pass any --enable-experiment options along.
if (args.isNotEmpty && wereExperimentsSpecified) {
List<String> experimentIds = specifiedExperiments;
args = [
'--$experimentFlagName=${experimentIds.join(',')}',
...args,
];
}
// 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).
if (args.any((element) =>
element.startsWith('--observe') ||
element.startsWith('--enable-vm-service'))) {
return await _DebuggingSession(this, args).start();
} else {
// Starting in ProcessStartMode.inheritStdio mode means the child process
// can detect support for ansi chars.
final process = await Process.start(
sdk.dart, ['--disable-dart-dev', ...args],
mode: ProcessStartMode.inheritStdio);
return process.exitCode;
}
}
}
class _DebuggingSession {
_DebuggingSession(this._runCommand, List<String> args)
: _args = args.toList() {
// Process flags that are meant to configure the VM service HTTP server or
// dump VM service connection information to a file. Since the VM service
// clients won't actually be connecting directly to the service, we'll make
// DDS appear as if it is the actual VM service.
for (final arg in _args) {
final isObserve = arg.startsWith('--observe');
if (isObserve || arg.startsWith('--enable-vm-service')) {
if (isObserve) {
_observe = true;
}
if (arg.contains('=') || arg.contains(':')) {
// These flags can be provided by the embedder so we need to check for
// both `=` and `:` separators.
final observatoryBindInfo =
(arg.contains('=') ? arg.split('=') : arg.split(':'))[1]
.split('/');
_port = int.tryParse(observatoryBindInfo.first) ?? 0;
if (observatoryBindInfo.length > 1) {
try {
_bindAddress = Uri.http(observatoryBindInfo[1], '');
} on FormatException {
// TODO(bkonyi): log invalid parse? The VM service just ignores
// bad input flags.
// Ignore.
}
}
}
} else if (arg.startsWith('--write-service-info=')) {
try {
final split = arg.split('=');
if (split[1].isNotEmpty) {
_serviceInfoUri = Uri.parse(split[1]);
} else {
_runCommand.usageException(
'Invalid URI argument to --write-service-info: "${split[1]}"');
}
} on FormatException {
// TODO(bkonyi): log invalid parse? The VM service just ignores bad
// input flags.
// Ignore.
}
} else if (arg == '--disable-service-auth-codes') {
_disableServiceAuthCodes = true;
}
}
// Strip --observe and --write-service-info from the arguments as we'll be
// providing our own.
_args.removeWhere(
(arg) =>
arg.startsWith('--observe') ||
arg.startsWith('--enable-vm-service') ||
arg.startsWith('--write-service-info'),
);
}
FutureOr<int> start() async {
// Output the service information for the target process to a temporary
// file so we can avoid scraping stderr for the service URI.
final serviceInfoDir =
await Directory.systemTemp.createTemp('dart_service');
final serviceInfoUri = serviceInfoDir.uri.resolve('service_info.json');
final serviceInfoFile = await File.fromUri(serviceInfoUri).create();
// Start using ProcessStartMode.normal and forward stdio manually as we
// need to filter the true VM service URI and replace it with the DDS URI.
_process = await Process.start(
sdk.dart,
[
'--disable-dart-dev',
// We don't care which port the VM service binds to.
_observe ? '--observe=0' : '--enable-vm-service=0',
'--write-service-info=$serviceInfoUri',
..._args,
],
);
_forwardAndFilterStdio(_process);
// Start DDS once the VM service has finished starting up.
await Future.any([
_waitForRemoteServiceUri(serviceInfoFile).then(_startDDS),
_process.exitCode,
]);
return _process.exitCode.then((exitCode) async {
// Shutdown DDS if it was started and wait for the process' stdio streams
// to close so we don't truncate program output.
await Future.wait([
if (_dds != null) _dds.shutdown(),
_stderrDone,
_stdoutDone,
]);
return exitCode;
});
}
Future<Uri> _waitForRemoteServiceUri(File serviceInfoFile) async {
// Wait for VM service to write its connection info to disk.
while (await serviceInfoFile.length() <= 5) {
await Future.delayed(const Duration(milliseconds: 50));
}
final serviceInfoStr = await serviceInfoFile.readAsString();
return Uri.parse(jsonDecode(serviceInfoStr)['uri']);
}
Future<void> _startDDS(Uri remoteVmServiceUri) async {
_dds = await DartDevelopmentService.startDartDevelopmentService(
remoteVmServiceUri,
serviceUri: _bindAddress.replace(port: _port),
enableAuthCodes: !_disableServiceAuthCodes,
);
if (_serviceInfoUri != null) {
// Output the service connection information.
await File.fromUri(_serviceInfoUri).writeAsString(
json.encode({
'uri': _dds.uri.toString(),
}),
);
}
_ddsCompleter.complete();
}
void _forwardAndFilterStdio(Process process) {
// Since VM service clients cannot connect to the real VM service once DDS
// has started, replace all instances of the real VM service's URI with the
// DDS URI. Clients should only know that they are connected to DDS if they
// explicitly request that information via the protocol.
String filterObservatoryUri(String msg) {
if (_dds == null) {
return msg;
}
if (msg.contains('Observatory listening on') ||
msg.contains('Connect to Observatory at')) {
// Search for the VM service URI in the message and replace it.
msg = msg.replaceFirst(
RegExp(r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.'
r'[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)'),
_dds.uri.toString(),
);
}
return msg;
}
// Wait for DDS to start before handling any stdio events from the target
// to ensure we don't let any unfiltered messages slip through.
// TODO(bkonyi): consider filtering on bytes rather than decoding the UTF8.
_stderrDone = process.stderr
.transform(const Utf8Decoder(allowMalformed: true))
.listen((event) async {
await _waitForDDS();
stderr.write(filterObservatoryUri(event));
}).asFuture();
_stdoutDone = process.stdout
.transform(const Utf8Decoder(allowMalformed: true))
.listen((event) async {
await _waitForDDS();
stdout.write(filterObservatoryUri(event));
}).asFuture();
stdin.listen(
(event) async {
await _waitForDDS();
process.stdin.add(event);
},
);
}
Future<void> _waitForDDS() async {
if (!_ddsCompleter.isCompleted) {
// No need to wait for DDS if the process has already exited.
await Future.any([
_ddsCompleter.future,
_process.exitCode,
]);
}
}
Uri _bindAddress = Uri.http('127.0.0.1', '');
bool _disableServiceAuthCodes = false;
DartDevelopmentService _dds;
bool _observe = false;
int _port = 8181;
Process _process;
Uri _serviceInfoUri;
Future _stderrDone;
Future _stdoutDone;
final List<String> _args;
final Completer<void> _ddsCompleter = Completer();
final RunCommand _runCommand;
}