// Copyright 2016 The Chromium 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 'android/android_studio_validator.dart'; import 'android/android_workflow.dart'; import 'artifacts.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/os.dart'; import 'base/platform.dart'; import 'base/process_manager.dart'; import 'base/version.dart'; import 'cache.dart'; import 'device.dart'; import 'globals.dart'; import 'intellij/intellij.dart'; import 'ios/ios_workflow.dart'; import 'ios/plist_utils.dart'; import 'tester/flutter_tester.dart'; import 'version.dart'; import 'vscode/vscode_validator.dart'; Doctor get doctor => context[Doctor]; abstract class DoctorValidatorsProvider { /// The singleton instance, pulled from the [AppContext]. static DoctorValidatorsProvider get instance => context[DoctorValidatorsProvider]; static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider(); List get validators; List get workflows; } class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { List _validators; List _workflows; @override List get validators { if (_validators == null) { _validators = []; _validators.add(_FlutterValidator()); if (androidWorkflow.appliesToHostPlatform) _validators.add(androidValidator); if (iosWorkflow.appliesToHostPlatform) _validators.add(iosValidator); final List ideValidators = []; ideValidators.addAll(AndroidStudioValidator.allValidators); ideValidators.addAll(IntelliJValidator.installedValidators); ideValidators.addAll(VsCodeValidator.installedValidators); if (ideValidators.isNotEmpty) _validators.addAll(ideValidators); else _validators.add(NoIdeValidator()); if (deviceManager.canListAnything) _validators.add(DeviceValidator()); } return _validators; } @override List get workflows { if (_workflows == null) { _workflows = []; if (iosWorkflow.appliesToHostPlatform) _workflows.add(iosWorkflow); if (androidWorkflow.appliesToHostPlatform) _workflows.add(androidWorkflow); } return _workflows; } } class ValidatorTask { ValidatorTask(this.validator, this.result); final DoctorValidator validator; final Future result; } class Doctor { const Doctor(); List get validators { return DoctorValidatorsProvider.instance.validators; } /// Return a list of [ValidatorTask] objects and starts validation on all /// objects in [validators]. List startValidatorTasks() { final List tasks = []; for (DoctorValidator validator in validators) { tasks.add(ValidatorTask(validator, validator.validate())); } return tasks; } 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 { printStatus(await summaryText); } Future get summaryText async { final StringBuffer buffer = StringBuffer(); bool allGood = true; final Set finishedGroups = Set(); for (DoctorValidator validator in validators) { final ValidatorCategory currentCategory = validator.category; ValidationResult result; if (currentCategory.isGrouped) { if (finishedGroups.contains(currentCategory)) { // We already handled this category via a previous validator. continue; } // Skip ahead and get results for the other validators in this category. final List results = []; for (DoctorValidator subValidator in validators.where( (DoctorValidator v) => v.category == currentCategory)) { results.add(await subValidator.validate()); } result = _mergeValidationResults(results); finishedGroups.add(currentCategory); } else { result = await validator.validate(); } buffer.write('${result.leadingBox} ${validator.title} is '); if (result.type == ValidationType.missing) buffer.write('not installed.'); else if (result.type == ValidationType.partial) buffer.write('partially installed; more components are available.'); else buffer.write('fully installed.'); if (result.statusInfo != null) buffer.write(' (${result.statusInfo})'); buffer.writeln(); if (result.type != ValidationType.installed) allGood = false; } if (!allGood) { buffer.writeln(); buffer.writeln('Run "flutter doctor" for information about installing additional components.'); } return buffer.toString(); } /// Print information about the state of installed tooling. Future diagnose({ bool androidLicenses = false, bool verbose = true }) async { if (androidLicenses) return AndroidValidator.runLicenseManager(); if (!verbose) { printStatus('Doctor summary (to see all details, run flutter doctor -v):'); } bool doctorResult = true; int issues = 0; final List taskList = startValidatorTasks(); final Set finishedGroups = Set(); for (ValidatorTask validatorTask in taskList) { final DoctorValidator validator = validatorTask.validator; final ValidatorCategory currentCategory = validator.category; final Status status = Status.withSpinner(); ValidationResult result; if (currentCategory.isGrouped) { if (finishedGroups.contains(currentCategory)) { continue; } final List results = []; for (ValidatorTask subValidator in taskList.where( (ValidatorTask t) => t.validator.category == currentCategory)) { try { results.add(await subValidator.result); } catch (exception) { status.cancel(); rethrow; } } result = _mergeValidationResults(results); finishedGroups.add(currentCategory); } else { try { result = await validatorTask.result; } catch (exception) { status.cancel(); rethrow; } } status.stop(); if (result.type == ValidationType.missing) { doctorResult = false; } if (result.type != ValidationType.installed) { issues += 1; } if (result.statusInfo != null) printStatus('${result.leadingBox} ${validator.title} (${result.statusInfo})'); else printStatus('${result.leadingBox} ${validator.title}'); for (ValidationMessage message in result.messages) { if (message.isError || message.isHint || verbose == true) { final String text = message.message.replaceAll('\n', '\n '); if (message.isError) { printStatus(' ✗ $text', emphasis: true); } else if (message.isHint) { printStatus(' ! $text'); } else { printStatus(' • $text'); } } } if (verbose) printStatus(''); } // Make sure there's always one line before the summary even when not verbose. if (!verbose) printStatus(''); if (issues > 0) { printStatus('! Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.'); } else { printStatus('• No issues found!'); } return doctorResult; } ValidationResult _mergeValidationResults(List results) { ValidationType mergedType = results[0].type; final List mergedMessages = []; for (ValidationResult result in results) { switch (result.type) { case ValidationType.installed: if (mergedType == ValidationType.missing) { mergedType = ValidationType.partial; } break; case ValidationType.partial: mergedType = ValidationType.partial; break; case ValidationType.missing: if (mergedType == ValidationType.installed) { mergedType = ValidationType.partial; } break; default: throw 'Unrecognized validation type: ' + result.type.toString(); } mergedMessages.addAll(result.messages); } return ValidationResult(mergedType, mergedMessages, statusInfo: results[0].statusInfo); } 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 series of tools and required install steps for a target platform (iOS or Android). abstract class 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 { missing, partial, installed } /// Validator output is grouped by category. class ValidatorCategory { final String name; // Whether we should bundle results for validators sharing this cateogry, // or let each stand alone. final bool isGrouped; const ValidatorCategory(this.name, this.isGrouped); static const ValidatorCategory androidToolchain = ValidatorCategory('androidToolchain', true); static const ValidatorCategory androidStudio = ValidatorCategory('androidStudio', false); static const ValidatorCategory ios = ValidatorCategory('ios', true); static const ValidatorCategory flutter = ValidatorCategory('flutter', false); static const ValidatorCategory ide = ValidatorCategory('ide', false); static const ValidatorCategory device = ValidatorCategory('device', false); static const ValidatorCategory other = ValidatorCategory('other', false); } abstract class DoctorValidator { const DoctorValidator(this.title, [this.category = ValidatorCategory.other]); final String title; final ValidatorCategory category; Future validate(); } class ValidationResult { /// [ValidationResult.type] should only equal [ValidationResult.installed] /// if no [messages] are hints or errors. ValidationResult(this.type, this.messages, { this.statusInfo }); final ValidationType type; // A short message about the status. final String statusInfo; final List messages; String get leadingBox { assert(type != null); switch (type) { case ValidationType.missing: return '[✗]'; case ValidationType.installed: return '[✓]'; case ValidationType.partial: return '[!]'; } return null; } } class ValidationMessage { ValidationMessage(this.message) : isError = false, isHint = false; ValidationMessage.error(this.message) : isError = true, isHint = false; ValidationMessage.hint(this.message) : isError = false, isHint = true; final bool isError; final bool isHint; final String message; @override String toString() => message; } class _FlutterValidator extends DoctorValidator { _FlutterValidator() : super('Flutter', ValidatorCategory.flutter); @override Future validate() async { final List messages = []; ValidationType valid = ValidationType.installed; final FlutterVersion version = FlutterVersion.instance; messages.add(ValidationMessage('Flutter version ${version.frameworkVersion} at ${Cache.flutterRoot}')); messages.add(ValidationMessage( 'Framework revision ${version.frameworkRevisionShort} ' '(${version.frameworkAge}), ${version.frameworkDate}' )); messages.add(ValidationMessage('Engine revision ${version.engineRevisionShort}')); messages.add(ValidationMessage('Dart version ${version.dartSdkVersion}')); final String genSnapshotPath = artifacts.getArtifactPath(Artifact.genSnapshot); // Check that the binaries we downloaded for this platform actually run on it. if (!_genSnapshotRuns(genSnapshotPath)) { final StringBuffer buf = StringBuffer(); buf.writeln('Downloaded executables cannot execute on host.'); buf.writeln('See https://github.com/flutter/flutter/issues/6207 for more information'); if (platform.isLinux) { buf.writeln('On Debian/Ubuntu/Mint: sudo apt-get install lib32stdc++6'); buf.writeln('On Fedora: dnf install libstdc++.i686'); buf.writeln('On Arch: pacman -S lib32-libstdc++5'); } messages.add(ValidationMessage.error(buf.toString())); valid = ValidationType.partial; } return ValidationResult(valid, messages, statusInfo: 'Channel ${version.channel}, v${version.frameworkVersion}, on ${os.name}, locale ${platform.localeName}' ); } } bool _genSnapshotRuns(String genSnapshotPath) { const int kExpectedExitCode = 255; try { return processManager.runSync([genSnapshotPath]).exitCode == kExpectedExitCode; } catch (error) { return false; } } class NoIdeValidator extends DoctorValidator { NoIdeValidator() : super('Flutter IDE Support',ValidatorCategory.ide); @override Future validate() async { return ValidationResult(ValidationType.missing, [ ValidationMessage('IntelliJ - https://www.jetbrains.com/idea/'), ], statusInfo: 'No supported IDEs installed'); } } abstract class IntelliJValidator extends DoctorValidator { final String installPath; IntelliJValidator(String title, this.installPath) : super(title, ValidatorCategory.ide); String get version; String get pluginsPath; static final Map _idToTitle = { 'IntelliJIdea' : 'IntelliJ IDEA Ultimate Edition', 'IdeaIC' : 'IntelliJ IDEA Community Edition', }; static final Version kMinIdeaVersion = Version(2017, 1, 0); static Iterable get installedValidators { if (platform.isLinux || platform.isWindows) return IntelliJValidatorOnLinuxAndWindows.installed; if (platform.isMacOS) return IntelliJValidatorOnMac.installed; return []; } @override Future validate() async { final List messages = []; messages.add(ValidationMessage('IntelliJ at $installPath')); final IntelliJPlugins plugins = IntelliJPlugins(pluginsPath); plugins.validatePackage(messages, ['flutter-intellij', 'flutter-intellij.jar'], 'Flutter', minVersion: IntelliJPlugins.kMinFlutterPluginVersion); plugins.validatePackage(messages, ['Dart'], 'Dart'); if (_hasIssues(messages)) { messages.add(ValidationMessage( 'For information about installing plugins, see\n' 'https://flutter.io/intellij-setup/#installing-the-plugins' )); } _validateIntelliJVersion(messages, kMinIdeaVersion); return ValidationResult( _hasIssues(messages) ? ValidationType.partial : ValidationType.installed, messages, statusInfo: 'version $version' ); } bool _hasIssues(List messages) { return messages.any((ValidationMessage message) => message.isError); } void _validateIntelliJVersion(List messages, Version minVersion) { // Ignore unknown versions. if (minVersion == Version.unknown) return; final Version installedVersion = Version.parse(version); if (installedVersion == null) return; if (installedVersion < minVersion) { messages.add(ValidationMessage.error( 'This install is older than the minimum recommended version of $minVersion.' )); } } } class IntelliJValidatorOnLinuxAndWindows extends IntelliJValidator { IntelliJValidatorOnLinuxAndWindows(String title, this.version, String installPath, this.pluginsPath) : super(title, installPath); @override final String version; @override final String pluginsPath; static Iterable get installed { final List validators = []; if (homeDirPath == null) return validators; void addValidator(String title, String version, String installPath, String pluginsPath) { final IntelliJValidatorOnLinuxAndWindows validator = IntelliJValidatorOnLinuxAndWindows(title, version, installPath, pluginsPath); for (int index = 0; index < validators.length; ++index) { final DoctorValidator other = validators[index]; if (other is IntelliJValidatorOnLinuxAndWindows && validator.installPath == other.installPath) { if (validator.version.compareTo(other.version) > 0) validators[index] = validator; return; } } validators.add(validator); } for (FileSystemEntity dir in fs.directory(homeDirPath).listSync()) { if (dir is Directory) { final String name = fs.path.basename(dir.path); IntelliJValidator._idToTitle.forEach((String id, String title) { if (name.startsWith('.$id')) { final String version = name.substring(id.length + 1); String installPath; try { installPath = fs.file(fs.path.join(dir.path, 'system', '.home')).readAsStringSync(); } catch (e) { // ignored } if (installPath != null && fs.isDirectorySync(installPath)) { final String pluginsPath = fs.path.join(dir.path, 'config', 'plugins'); addValidator(title, version, installPath, pluginsPath); } } }); } } return validators; } } class IntelliJValidatorOnMac extends IntelliJValidator { IntelliJValidatorOnMac(String title, this.id, String installPath) : super(title, installPath); final String id; static final Map _dirNameToId = { 'IntelliJ IDEA.app' : 'IntelliJIdea', 'IntelliJ IDEA Ultimate.app' : 'IntelliJIdea', 'IntelliJ IDEA CE.app' : 'IdeaIC', }; static Iterable get installed { final List validators = []; final List installPaths = ['/Applications', fs.path.join(homeDirPath, 'Applications')]; void checkForIntelliJ(Directory dir) { final String name = fs.path.basename(dir.path); _dirNameToId.forEach((String dirName, String id) { if (name == dirName) { final String title = IntelliJValidator._idToTitle[id]; validators.add(IntelliJValidatorOnMac(title, id, dir.path)); } }); } try { final Iterable installDirs = installPaths .map((String installPath) => fs.directory(installPath)) .map((Directory dir) => dir.existsSync() ? dir.listSync() : []) .expand((List mappedDirs) => mappedDirs) .whereType(); for (Directory dir in installDirs) { checkForIntelliJ(dir); if (!dir.path.endsWith('.app')) { for (FileSystemEntity subdir in dir.listSync()) { if (subdir is Directory) { checkForIntelliJ(subdir); } } } } } on FileSystemException catch (e) { validators.add(ValidatorWithResult( 'Cannot determine if IntelliJ is installed', ValidationResult(ValidationType.missing, [ ValidationMessage.error(e.message), ]), )); } return validators; } @override String get version { if (_version == null) { final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist'); _version = iosWorkflow.getPlistValueFromFile( plistFile, kCFBundleShortVersionStringKey, ) ?? 'unknown'; } return _version; } String _version; @override String get pluginsPath { final List split = version.split('.'); final String major = split[0]; final String minor = split[1]; return fs.path.join(homeDirPath, 'Library', 'Application Support', '$id$major.$minor'); } } class DeviceValidator extends DoctorValidator { DeviceValidator() : super('Connected devices', ValidatorCategory.device); @override Future validate() async { final List devices = await deviceManager.getAllConnectedDevices().toList(); List messages; if (devices.isEmpty) { final List diagnostics = await deviceManager.getDeviceDiagnostics(); if (diagnostics.isNotEmpty) { messages = diagnostics.map((String message) => ValidationMessage(message)).toList(); } else { messages = [ValidationMessage.hint('No devices available')]; } } else { messages = await Device.descriptions(devices) .map((String msg) => ValidationMessage(msg)).toList(); } if (devices.isEmpty) { return ValidationResult(ValidationType.partial, messages); } else { return ValidationResult(ValidationType.installed, messages, statusInfo: '${devices.length} available'); } } } class ValidatorWithResult extends DoctorValidator { final ValidationResult result; ValidatorWithResult(String title, this.result) : super(title); @override Future validate() async => result; }