blob: 5b1dd7b11f850452ff7c11c53500c2cb0a283a70 [file]
// 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 'package:unified_analytics/unified_analytics.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/net.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/terminal.dart';
import 'base/time.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 'globals.dart' as globals;
import 'http_host_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 'tester/flutter_tester.dart';
import 'version.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(platform: platform, featureFlags: featureFlags);
late final webWorkflow = WebWorkflow(platform: platform, featureFlags: featureFlags);
late final macOSWorkflow = MacOSWorkflow(platform: platform, featureFlags: featureFlags);
late final customDeviceWorkflow = CustomDeviceWorkflow(featureFlags: featureFlags);
@override
List<DoctorValidator> get validators {
if (_validators != null) {
return _validators!;
}
final proxyValidator = ProxyValidator(platform: platform);
_validators = <DoctorValidator>[
FlutterValidator(
fileSystem: globals.fs,
platform: globals.platform,
flutterVersion: () =>
globals.flutterVersion.fetchTagsAndGetVersion(clock: globals.systemClock),
devToolsVersion: () => globals.cache.devToolsVersion,
processManager: globals.processManager,
artifacts: globals.artifacts!,
flutterRoot: () => Cache.flutterRoot!,
operatingSystemUtils: globals.os,
featureFlags: featureFlags,
),
if (platform.isWindows)
WindowsVersionValidator(
operatingSystemUtils: globals.os,
processLister: ProcessLister(globals.processManager),
versionExtractor: WindowsVersionExtractor(
processManager: globals.processManager,
logger: globals.logger,
),
),
if (androidWorkflow!.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[
XcodeValidator(
xcode: globals.xcode!,
userMessages: globals.userMessages,
iosSimulatorUtils: globals.iosSimulatorUtils!,
),
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: globals.userMessages,
),
if (windowsWorkflow!.appliesToHostPlatform) visualStudioValidator!,
if (proxyValidator.shouldShow) proxyValidator,
if (globals.deviceManager?.canListAnything ?? false)
DeviceValidator(deviceManager: globals.deviceManager),
HttpHostValidator(
platform: globals.platform,
featureFlags: featureFlags,
httpClient: globals.httpClientFactory?.call() ?? HttpClient(),
),
];
return _validators!;
}
@override
List<Workflow> get workflows {
return _workflows ??= <Workflow>[
if (globals.iosWorkflow!.appliesToHostPlatform) globals.iosWorkflow!,
if (androidWorkflow?.appliesToHostPlatform ?? false) androidWorkflow!,
if (linuxWorkflow.appliesToHostPlatform) linuxWorkflow,
if (macOSWorkflow.appliesToHostPlatform) macOSWorkflow,
if (windowsWorkflow?.appliesToHostPlatform ?? false) windowsWorkflow!,
if (webWorkflow.appliesToHostPlatform) webWorkflow,
if (customDeviceWorkflow.appliesToHostPlatform) customDeviceWorkflow,
];
}
}
class Doctor {
Doctor({required Logger logger, required SystemClock clock, Analytics? analytics})
: _logger = logger,
_clock = clock,
_analytics = analytics ?? globals.analytics;
final Logger _logger;
final SystemClock _clock;
final Analytics _analytics;
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 timeoutCompleter = Completer<ValidationResult>();
final 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 buffer = StringBuffer();
var missingComponent = false;
var sawACrash = false;
for (final DoctorValidator validator in validators) {
final lineBuffer = StringBuffer();
ValidationResult result;
try {
result = await asyncGuard<ValidationResult>(() => validator.validateImpl());
} 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;
case ValidationType.missing:
lineBuffer.write('is not installed.');
case ValidationType.partial:
lineBuffer.write('is partially installed; more components are available.');
case ValidationType.notAvailable:
lineBuffer.write('is not available.');
case ValidationType.success:
lineBuffer.write('is fully installed.');
}
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.success) {
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 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):');
}
var doctorResult = true;
var issues = 0;
// This timestamp will be used on the backend of GA4 to group each of the events that
// were sent for each doctor validator and its result
final int analyticsTimestamp = _clock.now().millisecondsSinceEpoch;
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;
case ValidationType.missing:
doctorResult = false;
issues += 1;
case ValidationType.partial:
case ValidationType.notAvailable:
issues += 1;
case ValidationType.success:
break;
}
if (sendEvent) {
if (validator is GroupedValidator) {
for (var i = 0; i < validator.subValidators.length; i++) {
final DoctorValidator subValidator = validator.subValidators[i];
// Ensure that all of the subvalidators in the group have
// a corresponding subresult in case a validator crashed
final ValidationResult subResult;
try {
subResult = validator.subResults[i];
} on RangeError {
continue;
}
_analytics.send(
Event.doctorValidatorResult(
validatorName: subValidator.title,
result: subResult.typeStr,
statusInfo: subResult.statusInfo,
partOfGroupedValidator: true,
doctorInvocationId: analyticsTimestamp,
),
);
}
} else {
_analytics.send(
Event.doctorValidatorResult(
validatorName: validator.title,
result: result.typeStr,
statusInfo: result.statusInfo,
partOfGroupedValidator: false,
doctorInvocationId: analyticsTimestamp,
),
);
}
}
final String executionDuration = () {
final Duration? executionTime = result.executionTime;
if (!verbose || executionTime == null) {
return '';
}
final String formatted = executionTime.inSeconds < 2
? getElapsedAsMilliseconds(executionTime)
: getElapsedAsSeconds(executionTime);
return ' [$formatted]';
}();
final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
if (result.statusInfo != null) {
_logger.printStatus(
'$leadingBox ${validator.title} (${result.statusInfo})$executionDuration',
hangingIndent: result.leadingBox.length + 1,
);
} else {
_logger.printStatus(
'$leadingBox ${validator.title}$executionDuration',
hangingIndent: result.leadingBox.length + 1,
);
}
for (final ValidationMessage message in result.messages) {
if (!message.isInformation || verbose) {
var hangingIndent = 2;
var 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 FileSystem fileSystem,
required Artifacts artifacts,
required ProcessManager processManager,
required String Function() flutterRoot,
required OperatingSystemUtils operatingSystemUtils,
required FeatureFlags featureFlags,
}) : _flutterVersion = flutterVersion,
_devToolsVersion = devToolsVersion,
_platform = platform,
_fileSystem = fileSystem,
_artifacts = artifacts,
_processManager = processManager,
_flutterRoot = flutterRoot,
_operatingSystemUtils = operatingSystemUtils,
_featureFlags = featureFlags,
super('Flutter');
final Platform _platform;
final FlutterVersion Function() _flutterVersion;
final String Function() _devToolsVersion;
final String Function() _flutterRoot;
final FileSystem _fileSystem;
final Artifacts _artifacts;
final ProcessManager _processManager;
final OperatingSystemUtils _operatingSystemUtils;
final FeatureFlags _featureFlags;
@override
Future<ValidationResult> validateImpl() async {
final 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(flutterGitUrl(gitUrl)));
}
messages.add(
ValidationMessage(
flutterRevision(
version.frameworkRevisionShort,
version.frameworkAge,
version.frameworkCommitDate,
),
),
);
messages.add(ValidationMessage(engineRevision(version.engineRevisionShort)));
messages.add(ValidationMessage(dartRevision(version.dartSdkVersion)));
messages.add(ValidationMessage(devToolsVersion(_devToolsVersion())));
final String? pubUrl = _platform.environment[kPubDevOverride];
if (pubUrl != null) {
messages.add(ValidationMessage(pubMirrorURL(pubUrl)));
}
final String? storageBaseUrl = _platform.environment[kFlutterStorageBaseUrl];
if (storageBaseUrl != null) {
messages.add(ValidationMessage(flutterMirrorURL(storageBaseUrl)));
}
// Add feature flags that are either enabled, or disabled compared to the default setting.
final featureFlags = <String>[
..._featureFlags.allConfigurableFeatures
.where((Feature f) {
final bool enabled = _featureFlags.isEnabled(f);
final bool defaults = f.getSettingForChannel(version.channel).enabledByDefault;
return enabled || enabled != defaults;
})
.map((Feature f) {
// SAFETY: allConfigurableFeatures only returns features with a non-null configSetting
String setting = f.configSetting!;
if (!_featureFlags.isEnabled(f)) {
setting = 'no-$setting';
}
return setting;
}),
];
messages.add(ValidationMessage('Feature flags: ${featureFlags.join(', ')}'));
} 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 buffer = StringBuffer();
buffer.writeln(flutterBinariesDoNotRun);
if (_platform.isLinux) {
buffer.writeln(flutterBinariesLinuxRepairCommands);
} else if (_platform.isMacOS &&
_operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm64) {
buffer.writeln(
'Flutter requires the Rosetta translation environment on ARM Macs. Try running:',
);
buffer.writeln(' sudo softwareupdate --install-rosetta --agree-to-license');
}
messages.add(ValidationMessage.error(buffer.toString()));
}
ValidationType valid;
if (messages.every((ValidationMessage message) => message.isInformation)) {
valid = ValidationType.success;
} 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(flutterValidatorErrorIntentional));
}
return ValidationResult(
valid,
messages,
statusInfo: flutterStatusInfo(
versionChannel,
frameworkVersion,
_operatingSystemUtils.name,
_platform.localeName,
),
);
}
String flutterStatusInfo(String? channel, String? version, String os, String locale) =>
'Channel ${channel ?? 'unknown'}, ${version ?? 'unknown version'}, on $os, locale $locale';
String flutterVersion(String version, String channel, String flutterRoot) =>
'Flutter version $version on channel $channel at $flutterRoot';
String get flutterUnknownChannel =>
'Currently on an unknown channel. Run `flutter channel` to switch to an official channel.\n'
"If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/setup.";
String get flutterUnknownVersion =>
'Cannot resolve current version, possibly due to local changes.\n'
'Reinstall Flutter by following instructions at https://flutter.dev/setup.';
String flutterRevision(String revision, String age, String date) =>
'Framework revision $revision ($age), $date';
String flutterUpstreamRepositoryUrl(String url) => 'Upstream repository $url';
String get flutterUpstreamRepositoryUnknown =>
'Unknown upstream repository.\n'
'Reinstall Flutter by following instructions at https://flutter.dev/setup.';
String flutterUpstreamRepositoryUrlEnvMismatch(String url) =>
'Upstream repository $url is not the same as FLUTTER_GIT_URL';
String flutterUpstreamRepositoryUrlNonStandard(String url) =>
'Upstream repository $url is not a standard remote.\n'
'Set environment variable "FLUTTER_GIT_URL" to $url to dismiss this error.';
String flutterGitUrl(String url) => 'FLUTTER_GIT_URL = $url';
String engineRevision(String revision) => 'Engine revision $revision';
String dartRevision(String revision) => 'Dart version $revision';
String devToolsVersion(String version) => 'DevTools version $version';
String pubMirrorURL(String url) => 'Pub download mirror $url';
String flutterMirrorURL(String url) => 'Flutter download mirror $url';
String get flutterBinariesDoNotRun =>
'Downloaded executables cannot execute on host.\n'
'See https://github.com/flutter/flutter/issues/6207 for more information.';
String get flutterBinariesLinuxRepairCommands =>
'On Debian/Ubuntu/Mint: sudo apt-get install lib32stdc++6\n'
'On Fedora: dnf install libstdc++.i686\n'
'On Arch: pacman -S lib32-gcc-libs';
String get flutterValidatorErrorIntentional =>
'If those were intentional, you can disregard the above warnings; however it is '
'recommended to use "git" directly to perform update checks and upgrades.';
ValidationMessage _getFlutterVersionMessage(
String frameworkVersion,
String versionChannel,
String flutterRoot,
) {
String flutterVersionMessage = flutterVersion(frameworkVersion, versionChannel, flutterRoot);
// The tool sets the channel as kUserBranch, if the current branch is on a
// "detached HEAD" state, doesn't have an upstream, or is on a user branch,
// 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 != kUserBranch && frameworkVersion != '0.0.0-unknown') {
return ValidationMessage(flutterVersionMessage);
}
if (versionChannel == kUserBranch) {
flutterVersionMessage = '$flutterVersionMessage\n$flutterUnknownChannel';
}
if (frameworkVersion == '0.0.0-unknown') {
flutterVersionMessage = '$flutterVersionMessage\n$flutterUnknownVersion';
}
return ValidationMessage.hint(flutterVersionMessage);
}
List<ValidationMessage> _validateRequiredBinaries(String flutterRoot) {
final ValidationMessage? flutterWarning = _validateSdkBinary('flutter', flutterRoot);
final ValidationMessage? dartWarning = _validateSdkBinary('dart', flutterRoot);
return <ValidationMessage>[?flutterWarning, ?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 (!_filePathContainsDirPath(flutterRoot, resolvedFlutterPath)) {
final 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;
}
bool _filePathContainsDirPath(String directory, String file) {
// calling .canonicalize() will normalize for alphabetic case and path
// separators
return _fileSystem.path
.canonicalize(file)
.startsWith(_fileSystem.path.canonicalize(directory) + _fileSystem.path.separator);
}
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(flutterUpstreamRepositoryUnknown);
}
// At this point, repositoryUrl must not be null
if (errorMessage.contains('Flutter SDK is tracking a non-standard remote')) {
return ValidationMessage.hint(flutterUpstreamRepositoryUrlNonStandard(repositoryUrl!));
}
if (errorMessage.contains(
'Either remove "FLUTTER_GIT_URL" from the environment or set it to',
)) {
return ValidationMessage.hint(flutterUpstreamRepositoryUrlEnvMismatch(repositoryUrl!));
}
}
return ValidationMessage(flutterUpstreamRepositoryUrl(repositoryUrl!));
}
bool _genSnapshotRuns(String genSnapshotPath) {
const 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})
: _deviceManager = deviceManager ?? globals.deviceManager!,
super('Connected device');
final DeviceManager _deviceManager;
@override
String get slowWarning => 'Scanning for devices is taking a long time...';
String get devicesMissing => 'No devices available';
String devicesAvailable(int devices) => '$devices available';
@override
Future<ValidationResult> validateImpl() async {
final List<Device> devices = await _deviceManager.refreshAllDevices(
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
);
var installedMessages = <ValidationMessage>[];
if (devices.isNotEmpty) {
installedMessages = (await Device.descriptions(
devices,
)).map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
}
var 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(devicesMissing)];
}
if (devices.isEmpty) {
return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
} else if (diagnostics.isNotEmpty) {
installedMessages.addAll(diagnosticMessages);
return ValidationResult(
ValidationType.success,
installedMessages,
statusInfo: devicesAvailable(devices.length),
);
} else {
return ValidationResult(
ValidationType.success,
installedMessages,
statusInfo: devicesAvailable(devices.length),
);
}
}
}
/// Wrapper for doctor to run multiple times with PII and without, running the validators only once.
class DoctorText {
DoctorText(BufferLogger logger, {SystemClock? clock, @visibleForTesting Doctor? doctor})
: _doctor = doctor ?? Doctor(logger: logger, clock: clock ?? globals.systemClock),
_logger = logger;
final BufferLogger _logger;
final Doctor _doctor;
var _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,
verbose: _logger.isVerbose,
);
// 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';
}
}
}