blob: 6aaf78fab764aaae5923ec270fab8d8993d4ddef [file]
// 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:async';
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:meta/meta.dart';
import 'package:pub/pub.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'src/commands/analyze.dart';
import 'src/commands/build.dart';
import 'src/commands/compilation_server.dart';
import 'src/commands/compile.dart';
import 'src/commands/create.dart';
import 'src/commands/dart_mcp_server.dart';
import 'src/commands/debug_adapter.dart';
import 'src/commands/development_service.dart';
import 'src/commands/devtools.dart';
import 'src/commands/doc.dart';
import 'src/commands/fix.dart';
import 'src/commands/info.dart';
import 'src/commands/install.dart';
import 'src/commands/installed.dart';
import 'src/commands/language_server.dart';
import 'src/commands/migrate.dart';
import 'src/commands/run.dart';
import 'src/commands/test.dart';
import 'src/commands/tooling_daemon.dart';
import 'src/commands/uninstall.dart';
import 'src/core.dart';
import 'src/experiments.dart';
import 'src/progress.dart';
import 'src/sdk.dart';
import 'src/unified_analytics.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 {
int? exitCode = 1;
try {
VmInteropHandler.initialize(port);
// Set the DART_ROOT environment variable to the SDK path.
await VmInteropHandler.setEnvironmentVariable('DART_ROOT', sdk.sdkPath);
// Call the runner to execute the command; see DartdevRunner.
final runner = DartdevRunner(args, vmArgs: io.Platform.executableArguments);
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.
log.stderr('$e');
exitCode = DartdevRunner.usageExitCode;
} catch (e, st) {
// Unexpected error encountered.
log.stderr('An unexpected error was encountered by the Dart CLI.');
log.stderr(
'Please file an issue at '
'https://github.com/dart-lang/sdk/issues/new with the following '
'details:\n',
);
log.stderr("Invocation: 'dart ${args.join(' ')}'");
log.stderr("Exception: '$e'");
log.stderr('Stack Trace:');
log.stderr(st.toString());
exitCode = 255;
} finally {
VmInteropHandler.exit(exitCode);
}
}
const _commandsWithPubspecTelemetry = {
'analyze',
'build',
'compile',
'create',
'doc',
'fix',
'pub',
'run',
'test',
};
class DartdevRunner extends CommandRunner<int> {
static const String dartdevDescription =
'A command-line utility for Dart development';
@override
final ArgParser argParser;
final bool verbose;
final List<String> vmEnabledExperiments;
Analytics? _unifiedAnalytics;
final bool _isAnalyticsTest;
/// Overrides `io.stdout.hasTerminal`, for testing.
final bool? _stdoutHasTerminal;
/// Overrides `io.Platform.environment` when reading the inherited
/// `DASH__SUPPRESS_ANALYTICS` and `DASH__TOOL` values, when detecting
/// whether this is a bot, and when detecting the AI agent executing the
/// tool, for testing.
final Map<String, String>? _environment;
DartdevRunner(
List<String> args, {
Analytics? analyticsOverride,
this._isAnalyticsTest = false,
List<String> vmArgs = const [],
@visibleForTesting this._stdoutHasTerminal,
@visibleForTesting this._environment,
}) : verbose = args.contains('-v') || args.contains('--verbose'),
argParser = globalDartdevOptionsParser(
verbose: args.contains('-v') || args.contains('--verbose'),
),
vmEnabledExperiments = parseVmEnabledExperiments(vmArgs),
_unifiedAnalytics = analyticsOverride,
super('dart', '$dartdevDescription.') {
// The list of commands should be kept in sync with
// `DartDevIsolate::ShouldParseCommand` in `runtime/bin/dartdev_isolate.cc`.
addCommand(AnalyzeCommand(verbose: verbose));
addCommand(CompilationServerCommand(verbose: verbose));
final nativeAssetsExperimentEnabled = nativeAssetsEnabled(
vmEnabledExperiments,
);
final dataAssetsExperimentEnabled = dataAssetsEnabled(vmEnabledExperiments);
if (nativeAssetsExperimentEnabled) {
final recordUseExperimentEnabled = recordUseEnabled(vmEnabledExperiments);
addCommand(
BuildCommand(
verbose: verbose,
recordUseEnabled: recordUseExperimentEnabled,
dataAssetsExperimentEnabled: dataAssetsExperimentEnabled,
),
);
}
addCommand(
CompileCommand(
verbose: verbose,
nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
),
);
addCommand(CreateCommand(verbose: verbose));
addCommand(DebugAdapterCommand(verbose: verbose));
addCommand(DevelopmentServiceCommand(verbose: verbose));
addCommand(DevToolsCommand(verbose: verbose));
addCommand(DocCommand(verbose: verbose));
addCommand(FixCommand(verbose: verbose));
addCommand(
FormatCommand(
verbose: verbose,
category: CommandCategory.sourceCode.name,
),
);
addCommand(InfoCommand(verbose: verbose));
addCommand(LanguageServerCommand(verbose: verbose));
addCommand(DartMCPServerCommand(verbose: verbose));
addCommand(MigrateCommand(verbose: verbose));
addCommand(
pubCommand(
isVerbose: () => verbose,
category: CommandCategory.project.name,
progressGracePeriod: progressGracePeriod,
),
);
addCommand(
RunCommand(
verbose: verbose,
nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
dataAssetsExperimentEnabled: dataAssetsExperimentEnabled,
vmArgs: vmArgs,
),
);
addCommand(
TestCommand(
nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
dataAssetsExperimentEnabled: dataAssetsExperimentEnabled,
),
);
addCommand(ToolingDaemonCommand(verbose: verbose));
if (nativeAssetsExperimentEnabled) {
addCommand(InstallCommand(verbose: verbose));
addCommand(InstalledCommand(verbose: verbose));
addCommand(UninstallCommand(verbose: verbose));
}
}
@visibleForTesting
Analytics get unifiedAnalytics => _unifiedAnalytics!;
@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.';
/// The exit code returned for command-line usage errors (`EX_USAGE`).
static const int usageExitCode = 64;
@override
Future<int> runCommand(ArgResults topLevelResults) async {
final stopwatch = Stopwatch()..start();
final environment = _environment ?? io.Platform.environment;
// The name of the AI coding agent executing the tool, if any.
final agentName = AiAgent.detectAgentName(environment);
// We don't want to run analytics when we're running in a CI environment
// unless we're explicitly testing analytics for dartdev, or the tool is
// being run by an AI agent.
final implicitlySuppressAnalytics =
isBot(environment: environment) &&
!_isAnalyticsTest &&
agentName == null;
final envSuppressAnalytics =
environment[DashEnvVar.suppressAnalytics.name] == 'true';
final suppressAnalytics =
!topLevelResults.flag('analytics') ||
topLevelResults.flag('suppress-analytics') ||
implicitlySuppressAnalytics ||
envSuppressAnalytics;
if (topLevelResults.wasParsed('analytics')) {
log.stderr(
'`--[no-]analytics` is deprecated. Use `--suppress-analytics` '
'to disable analytics for one run instead.',
);
}
final enableAnalytics = topLevelResults.flag('enable-analytics');
final disableAnalytics = topLevelResults.flag('disable-analytics');
if (!implicitlySuppressAnalytics &&
!envSuppressAnalytics &&
suppressAnalytics &&
(enableAnalytics || disableAnalytics)) {
// This isn't an error if we're implicitly disabling analytics because
// we're running in a CI environment.
log.stderr(
'`--suppress-analytics` cannot be used with either'
' `--enable-analytics` or `--disable-analytics`.',
);
return 254;
}
// The Analytics instance used to report information back to Google
// Analytics; see lib/src/unified_analytics.dart.
//
// This must be created before analytics environment variables are
// propagated to sub-tools, since the propagated value depends on whether
// the consent message is shown on this run.
_unifiedAnalytics ??= createUnifiedAnalytics(
disableAnalytics: suppressAnalytics,
agent: agentName,
);
// If we have not printed the analytics notification to stdout, the user is
// on a terminal, and analytics aren't suppressed because we're on a bot,
// then the disclosure is printed below.
//
// NOTE: `shouldShowMessage` must be read here, before
// `clientShowedMessage()` is called below, since that call clears it.
final showConsentMessage = shouldPrintConsentMessage(
consentPending: unifiedAnalytics.shouldShowMessage,
hasTerminal: _stdoutHasTerminal ?? io.stdout.hasTerminal,
botSuppressed: implicitlySuppressAnalytics,
);
// Propagate analytics environment variables to sub-tools.
//
// On the run where the consent message is shown, sub-tools are suppressed
// too; see `shouldSuppressSubtools`.
// Since VmInteropHandler.setEnvironmentVariable is non-overwriting by design
// in C++, we unset the variable first to ensure the explicitly resolved
// value takes precedence.
await VmInteropHandler.setEnvironmentVariable(
DashEnvVar.suppressAnalytics.name,
null,
);
await VmInteropHandler.setEnvironmentVariable(
DashEnvVar.suppressAnalytics.name,
shouldSuppressSubtools(
suppressAnalytics: suppressAnalytics,
showsConsentMessage: showConsentMessage,
).toString(),
);
final envTool = environment[DashEnvVar.tool.name];
if (envTool == null) {
await VmInteropHandler.setEnvironmentVariable(
DashEnvVar.tool.name,
DashTool.dartTool.label,
);
}
if (showConsentMessage) {
log.stdout(unifiedAnalytics.getConsentMessage);
unifiedAnalytics.clientShowedMessage();
}
// When `--disable-analytics` or `--enable-analytics` are called we perform
// the respective intention and print any notices to standard out and exit.
if (disableAnalytics) {
// Disable sending data via the unified analytics package.
await unifiedAnalytics.setTelemetry(false);
try {
await unifiedAnalytics.close().timeout(
const Duration(milliseconds: 250),
);
} on TimeoutException catch (_) {}
// Alert the user that analytics has been disabled.
log.stdout(analyticsDisabledNoticeMessage);
return 0;
} else if (enableAnalytics) {
// Enable sending data via the unified analytics package.
await unifiedAnalytics.setTelemetry(true);
try {
await unifiedAnalytics.close().timeout(
const Duration(milliseconds: 250),
);
} on TimeoutException catch (_) {}
// Alert the user again that data will be collected.
if (!showConsentMessage) {
log.stdout(unifiedAnalytics.getConsentMessage);
}
return 0;
}
return await withDartdevLogger(
() async {
late final List<String> experimentErrors = validateExperiments(
vmEnabledExperiments,
);
if (experimentErrors.isNotEmpty) {
experimentErrors.forEach(log.stderr);
return 254;
}
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')) {
log.stderr(
"Error when reading '$firstArg': No such file or directory.",
);
// This is the exit code used by the frontend.
return 254;
}
}
var command = topLevelResults.command;
final commandNames = [];
while (command != null) {
commandNames.add(command.name);
if (command.command == null) break;
command = command.command;
}
// 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.
try {
exitCode = await super.runCommand(topLevelResults);
if (unifiedAnalytics.telemetryEnabled) {
// Send the event to analytics
final path = commandNames.join('/');
final experiments = topLevelResults.enabledExperiments
..sort((a, b) => a.compareTo(b));
final rootCommand = commandNames.firstOrNull;
final shouldCollectPubspec = _commandsWithPubspecTelemetry.contains(
rootCommand,
);
final pubspecTelemetry = shouldCollectPubspec
? collectPubspecTelemetry()
: null;
unifiedAnalytics.send(
Event.dartCliCommandExecuted(
name: path,
enabledExperiments: experiments.join(','),
pubspecHasFlutterSdk: pubspecTelemetry?.hasFlutterSdk,
pubspecDependencies: pubspecTelemetry?.publicDependencies,
pubspecEnvironmentSdk: pubspecTelemetry?.environmentSdk,
),
);
}
} on UsageException catch (e) {
log.stderr('$e');
exitCode = usageExitCode;
} catch (e, st) {
// Set the exception and stack trace only for non-UsageException cases:
log.stderr('$e');
log.stderr('$st');
exitCode = 1;
} finally {
stopwatch.stop();
// Set the exitCode, if it wasn't set in the catch block above.
exitCode ??= 0;
try {
await unifiedAnalytics.close().timeout(
const Duration(milliseconds: 250),
);
} on TimeoutException catch (_) {}
}
return exitCode;
},
delegate: topLevelResults.flag('diagnostics')
? Logger.verbose(ansi: ansi)
: null,
);
}
}