blob: 5895deb8f35b74087d2fef14718a66cb148435e9 [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.
// Do not call exit() directly. Use VmInteropHandler.exit() instead.
import 'dart:io' as io hide exit;
import 'dart:isolate';
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:dart_style/src/cli/format_command.dart';
import 'package:dartdev/src/commands/migrate.dart';
import 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart';
import 'package:pub/pub.dart';
import 'package:usage/usage.dart';
import 'src/analytics.dart';
import 'src/commands/analyze.dart';
import 'src/commands/compile.dart';
import 'src/commands/create.dart';
import 'src/commands/fix.dart';
import 'src/commands/language_server.dart';
import 'src/commands/run.dart';
import 'src/commands/test.dart';
import 'src/core.dart';
import 'src/events.dart';
import 'src/experiments.dart';
import 'src/utils.dart';
import 'src/vm_interop_handler.dart';
/// This is typically called from bin/, but given the length of the method and
/// analytics logic, it has been moved here.
Future<void> runDartdev(List<String> args, SendPort port) async {
VmInteropHandler.initialize(port);
if (args.contains('run')) {
// These flags have a format that can't be handled by package:args, so while
// they are valid flags we'll assume the VM has verified them by this point.
args = args
.where(
(element) => !(element.contains('--observe') ||
element.contains('--enable-vm-service') ||
element.contains('--devtools')),
)
.toList();
}
// Finally, call the runner to execute the command; see DartdevRunner.
final runner = DartdevRunner(args);
var exitCode = 1;
try {
exitCode = await runner.run(args);
} on UsageException catch (e) {
// TODO(sigurdm): It is unclear when a UsageException gets to here, and
// when it is in DartdevRunner.runCommand.
io.stderr.writeln('$e');
exitCode = 64;
} finally {
VmInteropHandler.exit(exitCode);
}
}
class DartdevRunner extends CommandRunner<int> {
static const String dartdevDescription =
'A command-line utility for Dart development';
@override
final ArgParser argParser = ArgParser(
usageLineLength: dartdevUsageLineLength,
allowTrailingOptions: false,
);
final bool verbose;
Analytics _analytics;
DartdevRunner(List<String> args)
: verbose = args.contains('-v') || args.contains('--verbose'),
super('dart', '$dartdevDescription.') {
argParser.addFlag('verbose',
abbr: 'v', negatable: false, help: 'Show additional command output.');
argParser.addFlag('version',
negatable: false, help: 'Print the Dart SDK version.');
argParser.addFlag('enable-analytics',
negatable: false, help: 'Enable analytics.');
argParser.addFlag('disable-analytics',
negatable: false, help: 'Disable analytics.');
argParser.addFlag('diagnostics',
negatable: false, help: 'Show tool diagnostic output.', hide: !verbose);
argParser.addFlag(
'analytics',
negatable: true,
help: 'Disable analytics for this `dart *` run',
hide: true,
);
addCommand(AnalyzeCommand(verbose: verbose));
addCommand(CreateCommand(verbose: verbose));
addCommand(CompileCommand(verbose: verbose));
addCommand(FixCommand(verbose: verbose));
addCommand(FormatCommand(verbose: verbose));
addCommand(LanguageServerCommand(verbose: verbose));
addCommand(MigrateCommand(verbose: verbose));
addCommand(pubCommand());
addCommand(RunCommand(verbose: verbose));
addCommand(TestCommand());
}
@visibleForTesting
Analytics get analytics => _analytics;
@override
String get invocation =>
'dart ${verbose ? '[vm-options] ' : ''}<command|dart-file> [arguments]';
@override
String get usageFooter =>
'See https://dart.dev/tools/dart-tool for detailed documentation.';
@override
Future<int> runCommand(ArgResults topLevelResults) async {
final stopwatch = Stopwatch()..start();
// The Analytics instance used to report information back to Google Analytics;
// see lib/src/analytics.dart.
_analytics = createAnalyticsInstance(
topLevelResults.wasParsed('analytics')
? !topLevelResults['analytics']
: false,
);
// If we have not printed the analyticsNoticeOnFirstRunMessage to stdout,
// the user is on a terminal, and the machine is not a bot, then print the
// disclosure and set analytics.disclosureShownOnTerminal to true.
if (analytics is DartdevAnalytics &&
!(analytics as DartdevAnalytics).disclosureShownOnTerminal &&
io.stdout.hasTerminal &&
!isBot()) {
print(analyticsNoticeOnFirstRunMessage);
(analytics as DartdevAnalytics).disclosureShownOnTerminal = true;
}
// When `--disable-analytics` or `--enable-analytics` are called we perform
// the respective intention and print any notices to standard out and exit.
if (topLevelResults['disable-analytics']) {
// This block also potentially catches the case of (disableAnalytics &&
// enableAnalytics), in which we favor the disabling of analytics.
analytics.enabled = false;
// Alert the user that analytics has been disabled.
print(analyticsDisabledNoticeMessage);
return 0;
} else if (topLevelResults['enable-analytics']) {
analytics.enabled = true;
// Alert the user again that data will be collected.
print(analyticsNoticeOnFirstRunMessage);
return 0;
}
if (topLevelResults.command == null &&
topLevelResults.arguments.isNotEmpty) {
final firstArg = topLevelResults.arguments.first;
// If we make it this far, it means the VM couldn't find the file on disk.
if (firstArg.endsWith('.dart')) {
io.stderr.writeln(
"Error when reading '$firstArg': No such file or directory.");
// This is the exit code used by the frontend.
return 254;
}
}
final Ansi ansi = Ansi(Ansi.terminalSupportsAnsi);
log = topLevelResults['diagnostics']
? Logger.verbose(ansi: ansi)
: Logger.standard(ansi: ansi);
var command = topLevelResults.command;
final commandNames = [];
while (command != null) {
commandNames.add(command.name);
if (command.command == null) break;
command = command.command;
}
final path = commandNames.join('/');
// Send the screen view to analytics
unawaited(
analytics.sendScreenView(path),
);
// The exit code for the dartdev process; null indicates that it has not been
// set yet. The value is set in the catch and finally blocks below.
int exitCode;
// Any caught non-UsageExceptions when running the sub command.
Object exception;
StackTrace stackTrace;
try {
exitCode = await super.runCommand(topLevelResults);
if (path != null && analytics.enabled) {
// Send the event to analytics
unawaited(
sendUsageEvent(
analytics,
path,
exitCode: exitCode,
commandFlags:
// This finds the options that where explicitly given to the command
// (and not for an eventual subcommand) without including the actual
// value.
//
// Note that this will also conflate short-options and long-options.
command?.options?.where(command.wasParsed)?.toList(),
specifiedExperiments: topLevelResults.enabledExperiments,
),
);
}
} on UsageException catch (e) {
io.stderr.writeln('$e');
exitCode = 64;
} catch (e, st) {
// Set the exception and stack trace only for non-UsageException cases:
exception = e;
stackTrace = st;
io.stderr.writeln('$e');
io.stderr.writeln('$st');
exitCode = 1;
} finally {
stopwatch.stop();
if (analytics.enabled) {
unawaited(
analytics.sendTiming(
path ?? '',
stopwatch.elapsedMilliseconds,
category: 'commands',
),
);
}
// Set the exitCode, if it wasn't set in the catch block above.
exitCode ??= 0;
// Send analytics before exiting
if (analytics.enabled) {
// And now send the exceptions and events to Google Analytics:
if (exception != null) {
unawaited(
analytics.sendException(
'${exception.runtimeType}\n${sanitizeStacktrace(stackTrace)}',
fatal: true),
);
}
await analytics.waitForLastPing(
timeout: const Duration(milliseconds: 200));
}
// Set the enabled flag in the analytics object to true. Note: this will not
// enable the analytics unless the disclosure was shown (terminal detected),
// and the machine is not detected to be a bot.
if (analytics.firstRun) {
analytics.enabled = true;
}
analytics.close();
}
return exitCode;
}
}