// 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()!; static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider( platform: globals.platform, featureFlags: featureFlags, ); List get validators; List get workflows; } class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { _DefaultDoctorValidatorsProvider({required this.platform, required this.featureFlags}); List? _validators; List? _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 get validators { if (_validators != null) { return _validators!; } final proxyValidator = ProxyValidator(platform: platform); _validators = [ 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([androidValidator!, androidLicenseValidator!]), if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform) GroupedValidator([ 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 get workflows { return _workflows ??= [ 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 get validators { return DoctorValidatorsProvider._instance.validators; } /// Return a list of [ValidatorTask] objects and starts validation on all /// objects in [validators]. List startValidatorTasks() => [ 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( () { final timeoutCompleter = Completer(); final timer = Timer(doctorDuration, () { timeoutCompleter.completeError( Exception( '${validator.title} exceeded maximum allowed duration of $doctorDuration', ), ); }); final Future validatorFuture = validator.validate(); return Future.any(>[ 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 get workflows { return DoctorValidatorsProvider._instance.workflows; } /// Print a summary of the state of the tooling, as well as how to get more info. Future summary() async { _logger.printStatus(await _summaryText()); } Future _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(() => 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 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 diagnose({ bool androidLicenses = false, bool verbose = true, AndroidLicenseValidator? androidLicenseValidator, bool showPii = true, List? 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 validateImpl() async { final messages = []; 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 = [ ..._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 _validateRequiredBinaries(String flutterRoot) { final ValidationMessage? flutterWarning = _validateSdkBinary('flutter', flutterRoot); final ValidationMessage? dartWarning = _validateSdkBinary('dart', flutterRoot); return [?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([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 validateImpl() async { final List devices = await _deviceManager.refreshAllDevices( timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout, ); var installedMessages = []; if (devices.isNotEmpty) { installedMessages = (await Device.descriptions( devices, )).map((String msg) => ValidationMessage(msg)).toList(); } var diagnosticMessages = []; final List diagnostics = await _deviceManager.getDeviceDiagnostics(); if (diagnostics.isNotEmpty) { diagnosticMessages = diagnostics .map((String message) => ValidationMessage.hint(message)) .toList(); } else if (devices.isEmpty) { diagnosticMessages = [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 text = _runDiagnosis(true); late final Future piiStrippedText = _runDiagnosis(false); // Start the validator tasks only once. late final List _validatorTasks = _doctor.startValidatorTasks(); Future _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'; } } }