blob: 59250eab2ef214f0fe52801c1293c50ffd6cd596 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. 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 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'android/android_studio_validator.dart';
import 'android/android_workflow.dart';
import 'artifacts.dart';
import 'base/async_guard.dart';
import 'base/context.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/terminal.dart';
import 'base/user_messages.dart';
import 'base/utils.dart';
import 'cache.dart';
import 'custom_devices/custom_device_workflow.dart';
import 'device.dart';
import 'doctor_validator.dart';
import 'features.dart';
import 'fuchsia/fuchsia_workflow.dart';
import 'globals.dart' as globals;
import 'http_host_validator.dart';
import 'intellij/intellij_validator.dart';
import 'linux/linux_doctor.dart';
import 'linux/linux_workflow.dart';
import 'macos/macos_workflow.dart';
import 'macos/xcode_validator.dart';
import 'proxy_validator.dart';
import 'reporting/reporting.dart';
import 'tester/flutter_tester.dart';
import 'version.dart';
import 'vscode/vscode_validator.dart';
import 'web/chrome.dart';
import 'web/web_validator.dart';
import 'web/workflow.dart';
import 'windows/visual_studio_validator.dart';
import 'windows/windows_version_validator.dart';
import 'windows/windows_workflow.dart';
abstract class DoctorValidatorsProvider {
// Allow tests to construct a [_DefaultDoctorValidatorsProvider] with explicit
// [FeatureFlags].
factory DoctorValidatorsProvider.test({
Platform? platform,
required FeatureFlags featureFlags,
}) {
return _DefaultDoctorValidatorsProvider(
featureFlags: featureFlags,
platform: platform ?? FakePlatform(),
);
}
/// The singleton instance, pulled from the [AppContext].
static DoctorValidatorsProvider get _instance => context.get<DoctorValidatorsProvider>()!;
static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider(
platform: globals.platform,
featureFlags: featureFlags,
);
List<DoctorValidator> get validators;
List<Workflow> get workflows;
}
class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
_DefaultDoctorValidatorsProvider({
required this.platform,
required this.featureFlags,
});
List<DoctorValidator>? _validators;
List<Workflow>? _workflows;
final Platform platform;
final FeatureFlags featureFlags;
late final LinuxWorkflow linuxWorkflow = LinuxWorkflow(
platform: platform,
featureFlags: featureFlags,
);
late final WebWorkflow webWorkflow = WebWorkflow(
platform: platform,
featureFlags: featureFlags,
);
late final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
platform: platform,
featureFlags: featureFlags,
);
late final CustomDeviceWorkflow customDeviceWorkflow = CustomDeviceWorkflow(
featureFlags: featureFlags,
);
@override
List<DoctorValidator> get validators {
if (_validators != null) {
return _validators!;
}
final List<DoctorValidator> ideValidators = <DoctorValidator>[
if (androidWorkflow!.appliesToHostPlatform)
...AndroidStudioValidator.allValidators(globals.config, platform, globals.fs, globals.userMessages),
...IntelliJValidator.installedValidators(
fileSystem: globals.fs,
platform: platform,
userMessages: userMessages,
plistParser: globals.plistParser,
processManager: globals.processManager,
),
...VsCodeValidator.installedValidators(globals.fs, platform, globals.processManager),
];
final ProxyValidator proxyValidator = ProxyValidator(platform: platform);
_validators = <DoctorValidator>[
FlutterValidator(
fileSystem: globals.fs,
platform: globals.platform,
flutterVersion: () => globals.flutterVersion,
devToolsVersion: () => globals.cache.devToolsVersion,
processManager: globals.processManager,
userMessages: userMessages,
artifacts: globals.artifacts!,
flutterRoot: () => Cache.flutterRoot!,
operatingSystemUtils: globals.os,
),
if (platform.isWindows)
WindowsVersionValidator(
processManager: globals.processManager,
),
if (androidWorkflow!.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[XcodeValidator(xcode: globals.xcode!, userMessages: userMessages), globals.cocoapodsValidator!]),
if (webWorkflow.appliesToHostPlatform)
ChromeValidator(
chromiumLauncher: ChromiumLauncher(
browserFinder: findChromeExecutable,
fileSystem: globals.fs,
operatingSystemUtils: globals.os,
platform: globals.platform,
processManager: globals.processManager,
logger: globals.logger,
),
platform: globals.platform,
),
if (linuxWorkflow.appliesToHostPlatform)
LinuxDoctorValidator(
processManager: globals.processManager,
userMessages: userMessages,
),
if (windowsWorkflow!.appliesToHostPlatform)
visualStudioValidator!,
if (ideValidators.isNotEmpty)
...ideValidators
else
NoIdeValidator(),
if (proxyValidator.shouldShow)
proxyValidator,
if (globals.deviceManager?.canListAnything ?? false)
DeviceValidator(
deviceManager: globals.deviceManager,
userMessages: globals.userMessages,
),
HttpHostValidator(
platform: globals.platform,
featureFlags: featureFlags,
httpClient: globals.httpClientFactory?.call() ?? HttpClient(),
),
];
return _validators!;
}
@override
List<Workflow> get workflows {
if (_workflows == null) {
_workflows = <Workflow>[];
if (globals.iosWorkflow!.appliesToHostPlatform) {
_workflows!.add(globals.iosWorkflow!);
}
if (androidWorkflow?.appliesToHostPlatform ?? false) {
_workflows!.add(androidWorkflow!);
}
if (fuchsiaWorkflow?.appliesToHostPlatform ?? false) {
_workflows!.add(fuchsiaWorkflow!);
}
if (linuxWorkflow.appliesToHostPlatform) {
_workflows!.add(linuxWorkflow);
}
if (macOSWorkflow.appliesToHostPlatform) {
_workflows!.add(macOSWorkflow);
}
if (windowsWorkflow?.appliesToHostPlatform ?? false) {
_workflows!.add(windowsWorkflow!);
}
if (webWorkflow.appliesToHostPlatform) {
_workflows!.add(webWorkflow);
}
if (customDeviceWorkflow.appliesToHostPlatform) {
_workflows!.add(customDeviceWorkflow);
}
}
return _workflows!;
}
}
class Doctor {
Doctor({
required Logger logger,
}) : _logger = logger;
final Logger _logger;
List<DoctorValidator> get validators {
return DoctorValidatorsProvider._instance.validators;
}
/// Return a list of [ValidatorTask] objects and starts validation on all
/// objects in [validators].
List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
for (final DoctorValidator validator in validators)
ValidatorTask(
validator,
// We use an asyncGuard() here to be absolutely certain that
// DoctorValidators do not result in an uncaught exception. Since the
// Future returned by the asyncGuard() is not awaited, we pass an
// onError callback to it and translate errors into ValidationResults.
asyncGuard<ValidationResult>(
() {
final Completer<ValidationResult> timeoutCompleter = Completer<ValidationResult>();
final Timer timer = Timer(doctorDuration, () {
timeoutCompleter.completeError(
Exception('${validator.title} exceeded maximum allowed duration of $doctorDuration'),
);
});
final Future<ValidationResult> validatorFuture = validator.validate();
return Future.any<ValidationResult>(<Future<ValidationResult>>[
validatorFuture,
// This future can only complete with an error
timeoutCompleter.future,
]).then((ValidationResult result) async {
timer.cancel();
return result;
});
},
onError: (Object exception, StackTrace stackTrace) {
return ValidationResult.crash(exception, stackTrace);
},
),
),
];
List<Workflow> get workflows {
return DoctorValidatorsProvider._instance.workflows;
}
/// Print a summary of the state of the tooling, as well as how to get more info.
Future<void> summary() async {
_logger.printStatus(await _summaryText());
}
Future<String> _summaryText() async {
final StringBuffer buffer = StringBuffer();
bool missingComponent = false;
bool sawACrash = false;
for (final DoctorValidator validator in validators) {
final StringBuffer lineBuffer = StringBuffer();
ValidationResult result;
try {
result = await asyncGuard<ValidationResult>(() => validator.validate());
} on Exception catch (exception) {
// We're generating a summary, so drop the stack trace.
result = ValidationResult.crash(exception);
}
lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
switch (result.type) {
case ValidationType.crash:
lineBuffer.write('the doctor check crashed without a result.');
sawACrash = true;
break;
case ValidationType.missing:
lineBuffer.write('is not installed.');
break;
case ValidationType.partial:
lineBuffer.write('is partially installed; more components are available.');
break;
case ValidationType.notAvailable:
lineBuffer.write('is not available.');
break;
case ValidationType.installed:
lineBuffer.write('is fully installed.');
break;
}
if (result.statusInfo != null) {
lineBuffer.write(' (${result.statusInfo})');
}
buffer.write(wrapText(
lineBuffer.toString(),
hangingIndent: result.leadingBox.length + 1,
columnWidth: globals.outputPreferences.wrapColumn,
shouldWrap: globals.outputPreferences.wrapText,
));
buffer.writeln();
if (result.type != ValidationType.installed) {
missingComponent = true;
}
}
if (sawACrash) {
buffer.writeln();
buffer.writeln('Run "flutter doctor" for information about why a doctor check crashed.');
}
if (missingComponent) {
buffer.writeln();
buffer.writeln('Run "flutter doctor" for information about installing additional components.');
}
return buffer.toString();
}
Future<bool> checkRemoteArtifacts(String engineRevision) async {
return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
}
/// Maximum allowed duration for an entire validator to take.
///
/// This should only ever be reached if a process is stuck.
// Reduce this to under 5 minutes to diagnose:
// https://github.com/flutter/flutter/issues/111686
static const Duration doctorDuration = Duration(minutes: 4, seconds: 30);
/// Print information about the state of installed tooling.
///
/// To exclude personally identifiable information like device names and
/// paths, set [showPii] to false.
Future<bool> diagnose({
bool androidLicenses = false,
bool verbose = true,
AndroidLicenseValidator? androidLicenseValidator,
bool showPii = true,
List<ValidatorTask>? startedValidatorTasks,
bool sendEvent = true,
}) async {
final bool showColor = globals.terminal.supportsColor;
if (androidLicenses && androidLicenseValidator != null) {
return androidLicenseValidator.runLicenseManager();
}
if (!verbose) {
_logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):');
}
bool doctorResult = true;
int issues = 0;
for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator;
final Status status = _logger.startSpinner(
timeout: validator.slowWarningDuration,
slowWarningCallback: () => validator.slowWarning,
);
ValidationResult result;
try {
result = await validatorTask.result;
status.stop();
} on Exception catch (exception, stackTrace) {
result = ValidationResult.crash(exception, stackTrace);
status.cancel();
}
switch (result.type) {
case ValidationType.crash:
doctorResult = false;
issues += 1;
break;
case ValidationType.missing:
doctorResult = false;
issues += 1;
break;
case ValidationType.partial:
case ValidationType.notAvailable:
issues += 1;
break;
case ValidationType.installed:
break;
}
if (sendEvent) {
DoctorResultEvent(validator: validator, result: result).send();
}
final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
if (result.statusInfo != null) {
_logger.printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
hangingIndent: result.leadingBox.length + 1);
} else {
_logger.printStatus('$leadingBox ${validator.title}',
hangingIndent: result.leadingBox.length + 1);
}
for (final ValidationMessage message in result.messages) {
if (!message.isInformation || verbose == true) {
int hangingIndent = 2;
int indent = 4;
final String indicator = showColor ? message.coloredIndicator : message.indicator;
for (final String line in '$indicator ${showPii ? message.message : message.piiStrippedMessage}'.split('\n')) {
_logger.printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
// Only do hanging indent for the first line.
hangingIndent = 0;
indent = 6;
}
if (message.contextUrl != null) {
_logger.printStatus('🔨 ${message.contextUrl}', hangingIndent: hangingIndent, indent: indent, emphasis: true);
}
}
}
if (verbose) {
_logger.printStatus('');
}
}
// Make sure there's always one line before the summary even when not verbose.
if (!verbose) {
_logger.printStatus('');
}
if (issues > 0) {
_logger.printStatus('${showColor ? globals.terminal.color('!', TerminalColor.yellow) : '!'}'
' Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
} else {
_logger.printStatus('${showColor ? globals.terminal.color('', TerminalColor.green) : ''}'
' No issues found!', hangingIndent: 2);
}
return doctorResult;
}
bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);
bool get canLaunchAnything {
if (FlutterTesterDevices.showFlutterTesterDevice) {
return true;
}
return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
}
}
/// A validator that checks the version of Flutter, as well as some auxiliary information
/// such as the pub or Flutter cache overrides.
///
/// This is primarily useful for diagnosing issues on Github bug reports by displaying
/// specific commit information.
class FlutterValidator extends DoctorValidator {
FlutterValidator({
required Platform platform,
required FlutterVersion Function() flutterVersion,
required String Function() devToolsVersion,
required UserMessages userMessages,
required FileSystem fileSystem,
required Artifacts artifacts,
required ProcessManager processManager,
required String Function() flutterRoot,
required OperatingSystemUtils operatingSystemUtils,
}) : _flutterVersion = flutterVersion,
_devToolsVersion = devToolsVersion,
_platform = platform,
_userMessages = userMessages,
_fileSystem = fileSystem,
_artifacts = artifacts,
_processManager = processManager,
_flutterRoot = flutterRoot,
_operatingSystemUtils = operatingSystemUtils,
super('Flutter');
final Platform _platform;
final FlutterVersion Function() _flutterVersion;
final String Function() _devToolsVersion;
final String Function() _flutterRoot;
final UserMessages _userMessages;
final FileSystem _fileSystem;
final Artifacts _artifacts;
final ProcessManager _processManager;
final OperatingSystemUtils _operatingSystemUtils;
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
String? versionChannel;
String? frameworkVersion;
try {
final FlutterVersion version = _flutterVersion();
final String? gitUrl = _platform.environment['FLUTTER_GIT_URL'];
versionChannel = version.channel;
frameworkVersion = version.frameworkVersion;
final String flutterRoot = _flutterRoot();
messages.add(_getFlutterVersionMessage(frameworkVersion, versionChannel, flutterRoot));
_validateRequiredBinaries(flutterRoot).forEach(messages.add);
messages.add(_getFlutterUpstreamMessage(version));
if (gitUrl != null) {
messages.add(ValidationMessage(_userMessages.flutterGitUrl(gitUrl)));
}
messages.add(ValidationMessage(_userMessages.flutterRevision(
version.frameworkRevisionShort,
version.frameworkAge,
version.frameworkCommitDate,
)));
messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort)));
messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion)));
messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
final String? pubUrl = _platform.environment['PUB_HOSTED_URL'];
if (pubUrl != null) {
messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
}
final String? storageBaseUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (storageBaseUrl != null) {
messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
}
} on VersionCheckError catch (e) {
messages.add(ValidationMessage.error(e.message));
}
// Check that the binaries we downloaded for this platform actually run on it.
// If the binaries are not downloaded (because android is not enabled), then do
// not run this check.
final String genSnapshotPath = _artifacts.getArtifactPath(Artifact.genSnapshot);
if (_fileSystem.file(genSnapshotPath).existsSync() && !_genSnapshotRuns(genSnapshotPath)) {
final StringBuffer buffer = StringBuffer();
buffer.writeln(_userMessages.flutterBinariesDoNotRun);
if (_platform.isLinux) {
buffer.writeln(_userMessages.flutterBinariesLinuxRepairCommands);
}
messages.add(ValidationMessage.error(buffer.toString()));
}
ValidationType valid;
if (messages.every((ValidationMessage message) => message.isInformation)) {
valid = ValidationType.installed;
} else {
// The issues for this validator stem from broken git configuration of the local install;
// in that case, make it clear that it is fine to continue, but freshness check/upgrades
// won't be supported.
valid = ValidationType.partial;
messages.add(
ValidationMessage(_userMessages.flutterValidatorErrorIntentional),
);
}
return ValidationResult(
valid,
messages,
statusInfo: _userMessages.flutterStatusInfo(
versionChannel,
frameworkVersion,
_operatingSystemUtils.name,
_platform.localeName,
),
);
}
ValidationMessage _getFlutterVersionMessage(String frameworkVersion, String versionChannel, String flutterRoot) {
String flutterVersionMessage = _userMessages.flutterVersion(frameworkVersion, versionChannel, flutterRoot);
// The tool sets the channel as "unknown", if the current branch is on a
// "detached HEAD" state or doesn't have an upstream, and sets the
// frameworkVersion as "0.0.0-unknown" if "git describe" on HEAD doesn't
// produce an expected format to be parsed for the frameworkVersion.
if (versionChannel != 'unknown' && frameworkVersion != '0.0.0-unknown') {
return ValidationMessage(flutterVersionMessage);
}
if (versionChannel == 'unknown') {
flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}';
}
if (frameworkVersion == '0.0.0-unknown') {
flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownVersion}';
}
return ValidationMessage.hint(flutterVersionMessage);
}
List<ValidationMessage> _validateRequiredBinaries(String flutterRoot) {
final ValidationMessage? flutterWarning = _validateSdkBinary('flutter', flutterRoot);
final ValidationMessage? dartWarning = _validateSdkBinary('dart', flutterRoot);
return <ValidationMessage>[
if (flutterWarning != null) flutterWarning,
if (dartWarning != null) dartWarning,
];
}
/// Return a warning if the provided [binary] on the user's path does not
/// resolve within the Flutter SDK.
ValidationMessage? _validateSdkBinary(String binary, String flutterRoot) {
final String flutterBinDir = _fileSystem.path.join(flutterRoot, 'bin');
final File? flutterBin = _operatingSystemUtils.which(binary);
if (flutterBin == null) {
return ValidationMessage.hint(
'The $binary binary is not on your path. Consider adding '
'$flutterBinDir to your path.',
);
}
final String resolvedFlutterPath = flutterBin.resolveSymbolicLinksSync();
if (!resolvedFlutterPath.contains(flutterRoot)) {
final String hint = 'Warning: `$binary` on your path resolves to '
'$resolvedFlutterPath, which is not inside your current Flutter '
'SDK checkout at $flutterRoot. Consider adding $flutterBinDir to '
'the front of your path.';
return ValidationMessage.hint(hint);
}
return null;
}
ValidationMessage _getFlutterUpstreamMessage(FlutterVersion version) {
final String? repositoryUrl = version.repositoryUrl;
final VersionCheckError? upstreamValidationError = VersionUpstreamValidator(version: version, platform: _platform).run();
// VersionUpstreamValidator can produce an error if repositoryUrl is null
if (upstreamValidationError != null) {
final String errorMessage = upstreamValidationError.message;
if (errorMessage.contains('could not determine the remote upstream which is being tracked')) {
return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUnknown);
}
// At this point, repositoryUrl must not be null
if (errorMessage.contains('Flutter SDK is tracking a non-standard remote')) {
return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUrlNonStandard(repositoryUrl!));
}
if (errorMessage.contains('Either remove "FLUTTER_GIT_URL" from the environment or set it to')){
return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUrlEnvMismatch(repositoryUrl!));
}
}
return ValidationMessage(_userMessages.flutterUpstreamRepositoryUrl(repositoryUrl!));
}
bool _genSnapshotRuns(String genSnapshotPath) {
const int kExpectedExitCode = 255;
try {
return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
} on Exception {
return false;
}
}
}
class DeviceValidator extends DoctorValidator {
// TODO(jmagman): Make required once g3 rolls and is updated.
DeviceValidator({
DeviceManager? deviceManager,
UserMessages? userMessages,
}) : _deviceManager = deviceManager ?? globals.deviceManager!,
_userMessages = userMessages ?? globals.userMessages,
super('Connected device');
final DeviceManager _deviceManager;
final UserMessages _userMessages;
@override
String get slowWarning => 'Scanning for devices is taking a long time...';
@override
Future<ValidationResult> validate() async {
final List<Device> devices = await _deviceManager.getAllConnectedDevices();
List<ValidationMessage> installedMessages = <ValidationMessage>[];
if (devices.isNotEmpty) {
installedMessages = (await Device.descriptions(devices))
.map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
}
List<ValidationMessage> diagnosticMessages = <ValidationMessage>[];
final List<String> diagnostics = await _deviceManager.getDeviceDiagnostics();
if (diagnostics.isNotEmpty) {
diagnosticMessages = diagnostics.map<ValidationMessage>((String message) => ValidationMessage.hint(message)).toList();
} else if (devices.isEmpty) {
diagnosticMessages = <ValidationMessage>[ValidationMessage.hint(_userMessages.devicesMissing)];
}
if (devices.isEmpty) {
return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
} else if (diagnostics.isNotEmpty) {
installedMessages.addAll(diagnosticMessages);
return ValidationResult(
ValidationType.installed,
installedMessages,
statusInfo: _userMessages.devicesAvailable(devices.length)
);
} else {
return ValidationResult(
ValidationType.installed,
installedMessages,
statusInfo: _userMessages.devicesAvailable(devices.length)
);
}
}
}
/// Wrapper for doctor to run multiple times with PII and without, running the validators only once.
class DoctorText {
DoctorText(
BufferLogger logger, {
@visibleForTesting Doctor? doctor,
}) : _doctor = doctor ?? Doctor(logger: logger), _logger = logger;
final BufferLogger _logger;
final Doctor _doctor;
bool _sendDoctorEvent = true;
late final Future<String> text = _runDiagnosis(true);
late final Future<String> piiStrippedText = _runDiagnosis(false);
// Start the validator tasks only once.
late final List<ValidatorTask> _validatorTasks = _doctor.startValidatorTasks();
Future<String> _runDiagnosis(bool showPii) async {
try {
await _doctor.diagnose(startedValidatorTasks: _validatorTasks, showPii: showPii, sendEvent: _sendDoctorEvent);
// Do not send the doctor event a second time.
_sendDoctorEvent = false;
final String text = _logger.statusText;
_logger.clear();
return text;
} on Exception catch (error, trace) {
return 'encountered exception: $error\n\n${trace.toString().trim()}\n';
}
}
}