| // 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' | |
| r"If that doesn't fix the issue, try deleting the 'bin/cache/flutter.version.json' file in your Flutter SDK directory and then reinstall Flutter by following instructions at https://flutter.dev/setup."; | |
| String get flutterUnknownVersion => | |
| 'Cannot resolve current version, possibly due to local changes.\n' | |
| r"If that doesn't fix the issue, try deleting the 'bin/cache/flutter.version.json' file in your Flutter SDK directory and then 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'; | |
| } | |
| } | |
| } |