| // 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 'package:meta/meta.dart'; |
| |
| import 'base/async_guard.dart'; |
| import 'base/terminal.dart'; |
| import 'globals.dart' as globals; |
| |
| class ValidatorTask { |
| ValidatorTask(this.validator, this.result); |
| final DoctorValidator validator; |
| final Future<ValidationResult> result; |
| } |
| |
| /// A series of tools and required install steps for a target platform (iOS or Android). |
| abstract class Workflow { |
| const Workflow(); |
| |
| /// Whether the workflow applies to this platform (as in, should we ever try and use it). |
| bool get appliesToHostPlatform; |
| |
| /// Are we functional enough to list devices? |
| bool get canListDevices; |
| |
| /// Could this thing launch *something*? It may still have minor issues. |
| bool get canLaunchDevices; |
| |
| /// Are we functional enough to list emulators? |
| bool get canListEmulators; |
| } |
| |
| enum ValidationType { |
| crash, |
| missing, |
| partial, |
| notAvailable, |
| installed, |
| } |
| |
| enum ValidationMessageType { |
| error, |
| hint, |
| information, |
| } |
| |
| abstract class DoctorValidator { |
| const DoctorValidator(this.title); |
| |
| /// This is displayed in the CLI. |
| final String title; |
| |
| String get slowWarning => 'This is taking an unexpectedly long time...'; |
| |
| static const Duration _slowWarningDuration = Duration(seconds: 10); |
| |
| /// Duration before the spinner should display [slowWarning]. |
| Duration get slowWarningDuration => _slowWarningDuration; |
| |
| Future<ValidationResult> validate(); |
| } |
| |
| /// A validator that runs other [DoctorValidator]s and combines their output |
| /// into a single [ValidationResult]. It uses the title of the first validator |
| /// passed to the constructor and reports the statusInfo of the first validator |
| /// that provides one. Other titles and statusInfo strings are discarded. |
| class GroupedValidator extends DoctorValidator { |
| GroupedValidator(this.subValidators) : super(subValidators[0].title); |
| |
| final List<DoctorValidator> subValidators; |
| |
| List<ValidationResult> _subResults = <ValidationResult>[]; |
| |
| /// Sub-validator results. |
| /// |
| /// To avoid losing information when results are merged, the sub-results are |
| /// cached on this field when they are available. The results are in the same |
| /// order as the sub-validator list. |
| List<ValidationResult> get subResults => _subResults; |
| |
| @override |
| String get slowWarning => _currentSlowWarning; |
| String _currentSlowWarning = 'Initializing...'; |
| |
| @override |
| Future<ValidationResult> validate() async { |
| final List<ValidatorTask> tasks = <ValidatorTask>[ |
| for (final DoctorValidator validator in subValidators) |
| ValidatorTask( |
| validator, |
| asyncGuard<ValidationResult>(() => validator.validate()), |
| ), |
| ]; |
| |
| final List<ValidationResult> results = <ValidationResult>[]; |
| for (final ValidatorTask subValidator in tasks) { |
| _currentSlowWarning = subValidator.validator.slowWarning; |
| try { |
| results.add(await subValidator.result); |
| } on Exception catch (exception, stackTrace) { |
| results.add(ValidationResult.crash(exception, stackTrace)); |
| } |
| } |
| _currentSlowWarning = 'Merging results...'; |
| return _mergeValidationResults(results); |
| } |
| |
| ValidationResult _mergeValidationResults(List<ValidationResult> results) { |
| assert(results.isNotEmpty, 'Validation results should not be empty'); |
| _subResults = results; |
| ValidationType mergedType = results[0].type; |
| final List<ValidationMessage> mergedMessages = <ValidationMessage>[]; |
| String? statusInfo; |
| |
| for (final ValidationResult result in results) { |
| statusInfo ??= result.statusInfo; |
| switch (result.type) { |
| case ValidationType.installed: |
| if (mergedType == ValidationType.missing) { |
| mergedType = ValidationType.partial; |
| } |
| break; |
| case ValidationType.notAvailable: |
| case ValidationType.partial: |
| mergedType = ValidationType.partial; |
| break; |
| case ValidationType.crash: |
| case ValidationType.missing: |
| if (mergedType == ValidationType.installed) { |
| mergedType = ValidationType.partial; |
| } |
| break; |
| } |
| mergedMessages.addAll(result.messages); |
| } |
| |
| return ValidationResult(mergedType, mergedMessages, |
| statusInfo: statusInfo); |
| } |
| } |
| |
| @immutable |
| class ValidationResult { |
| /// [ValidationResult.type] should only equal [ValidationResult.installed] |
| /// if no [messages] are hints or errors. |
| const ValidationResult(this.type, this.messages, { this.statusInfo }); |
| |
| factory ValidationResult.crash(Object error, [StackTrace? stackTrace]) { |
| return ValidationResult(ValidationType.crash, <ValidationMessage>[ |
| const ValidationMessage.error( |
| 'Due to an error, the doctor check did not complete. ' |
| 'If the error message below is not helpful, ' |
| 'please let us know about this issue at https://github.com/flutter/flutter/issues.'), |
| ValidationMessage.error('$error'), |
| if (stackTrace != null) |
| // Stacktrace is informational. Printed in verbose mode only. |
| ValidationMessage('$stackTrace'), |
| ], statusInfo: 'the doctor check crashed'); |
| } |
| |
| final ValidationType type; |
| // A short message about the status. |
| final String? statusInfo; |
| final List<ValidationMessage> messages; |
| |
| String get leadingBox { |
| assert(type != null); |
| switch (type) { |
| case ValidationType.crash: |
| return '[☠]'; |
| case ValidationType.missing: |
| return '[✗]'; |
| case ValidationType.installed: |
| return '[✓]'; |
| case ValidationType.notAvailable: |
| case ValidationType.partial: |
| return '[!]'; |
| } |
| } |
| |
| String get coloredLeadingBox { |
| assert(type != null); |
| switch (type) { |
| case ValidationType.crash: |
| return globals.terminal.color(leadingBox, TerminalColor.red); |
| case ValidationType.missing: |
| return globals.terminal.color(leadingBox, TerminalColor.red); |
| case ValidationType.installed: |
| return globals.terminal.color(leadingBox, TerminalColor.green); |
| case ValidationType.notAvailable: |
| case ValidationType.partial: |
| return globals.terminal.color(leadingBox, TerminalColor.yellow); |
| } |
| } |
| |
| /// The string representation of the type. |
| String get typeStr { |
| assert(type != null); |
| switch (type) { |
| case ValidationType.crash: |
| return 'crash'; |
| case ValidationType.missing: |
| return 'missing'; |
| case ValidationType.installed: |
| return 'installed'; |
| case ValidationType.notAvailable: |
| return 'notAvailable'; |
| case ValidationType.partial: |
| return 'partial'; |
| } |
| } |
| } |
| |
| /// A status line for the flutter doctor validation to display. |
| /// |
| /// The [message] is required and represents either an informational statement |
| /// about the particular doctor validation that passed, or more context |
| /// on the cause and/or solution to the validation failure. |
| @immutable |
| class ValidationMessage { |
| /// Create a validation message with information for a passing validator. |
| /// |
| /// By default this is not displayed unless the doctor is run in |
| /// verbose mode. |
| /// |
| /// The [contextUrl] may be supplied to link to external resources. This |
| /// is displayed after the informative message in verbose modes. |
| const ValidationMessage(this.message, { this.contextUrl, String? piiStrippedMessage }) |
| : type = ValidationMessageType.information, piiStrippedMessage = piiStrippedMessage ?? message; |
| |
| /// Create a validation message with information for a failing validator. |
| const ValidationMessage.error(this.message, { String? piiStrippedMessage }) |
| : type = ValidationMessageType.error, |
| piiStrippedMessage = piiStrippedMessage ?? message, |
| contextUrl = null; |
| |
| /// Create a validation message with information for a partially failing |
| /// validator. |
| const ValidationMessage.hint(this.message, { String? piiStrippedMessage }) |
| : type = ValidationMessageType.hint, |
| piiStrippedMessage = piiStrippedMessage ?? message, |
| contextUrl = null; |
| |
| final ValidationMessageType type; |
| final String? contextUrl; |
| final String message; |
| /// Optional message with PII stripped, to show instead of [message]. |
| final String piiStrippedMessage; |
| |
| bool get isError => type == ValidationMessageType.error; |
| |
| bool get isHint => type == ValidationMessageType.hint; |
| |
| bool get isInformation => type == ValidationMessageType.information; |
| |
| String get indicator { |
| switch (type) { |
| case ValidationMessageType.error: |
| return '✗'; |
| case ValidationMessageType.hint: |
| return '!'; |
| case ValidationMessageType.information: |
| return '•'; |
| } |
| } |
| |
| String get coloredIndicator { |
| switch (type) { |
| case ValidationMessageType.error: |
| return globals.terminal.color(indicator, TerminalColor.red); |
| case ValidationMessageType.hint: |
| return globals.terminal.color(indicator, TerminalColor.yellow); |
| case ValidationMessageType.information: |
| return globals.terminal.color(indicator, TerminalColor.green); |
| } |
| } |
| |
| @override |
| String toString() => message; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is ValidationMessage |
| && other.message == message |
| && other.type == type |
| && other.contextUrl == contextUrl; |
| } |
| |
| @override |
| int get hashCode => Object.hash(type, message, contextUrl); |
| } |
| |
| class NoIdeValidator extends DoctorValidator { |
| NoIdeValidator() : super('Flutter IDE Support'); |
| |
| @override |
| Future<ValidationResult> validate() async { |
| return ValidationResult( |
| // Info hint to user they do not have a supported IDE installed |
| ValidationType.notAvailable, |
| globals.userMessages.noIdeInstallationInfo.map((String ideInfo) => ValidationMessage(ideInfo)).toList(), |
| statusInfo: globals.userMessages.noIdeStatusInfo, |
| ); |
| } |
| } |
| |
| class ValidatorWithResult extends DoctorValidator { |
| ValidatorWithResult(super.title, this.result); |
| |
| final ValidationResult result; |
| |
| @override |
| Future<ValidationResult> validate() async => result; |
| } |