• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'android/android_studio_validator.dart';
8import 'android/android_workflow.dart';
9import 'artifacts.dart';
10import 'base/common.dart';
11import 'base/context.dart';
12import 'base/file_system.dart';
13import 'base/logger.dart';
14import 'base/os.dart';
15import 'base/platform.dart';
16import 'base/process_manager.dart';
17import 'base/terminal.dart';
18import 'base/user_messages.dart';
19import 'base/utils.dart';
20import 'base/version.dart';
21import 'cache.dart';
22import 'device.dart';
23import 'fuchsia/fuchsia_workflow.dart';
24import 'globals.dart';
25import 'intellij/intellij.dart';
26import 'ios/ios_workflow.dart';
27import 'ios/plist_parser.dart';
28import 'linux/linux_doctor.dart';
29import 'linux/linux_workflow.dart';
30import 'macos/cocoapods_validator.dart';
31import 'macos/macos_workflow.dart';
32import 'macos/xcode_validator.dart';
33import 'proxy_validator.dart';
34import 'reporting/reporting.dart';
35import 'tester/flutter_tester.dart';
36import 'version.dart';
37import 'vscode/vscode_validator.dart';
38import 'web/web_validator.dart';
39import 'web/workflow.dart';
40import 'windows/visual_studio_validator.dart';
41import 'windows/windows_workflow.dart';
42
43Doctor get doctor => context.get<Doctor>();
44
45abstract class DoctorValidatorsProvider {
46  /// The singleton instance, pulled from the [AppContext].
47  static DoctorValidatorsProvider get instance => context.get<DoctorValidatorsProvider>();
48
49  static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider();
50
51  List<DoctorValidator> get validators;
52  List<Workflow> get workflows;
53}
54
55class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
56  List<DoctorValidator> _validators;
57  List<Workflow> _workflows;
58
59  @override
60  List<DoctorValidator> get validators {
61    if (_validators == null) {
62      final List<DoctorValidator> ideValidators = <DoctorValidator>[
63        ...AndroidStudioValidator.allValidators,
64        ...IntelliJValidator.installedValidators,
65        ...VsCodeValidator.installedValidators,
66      ];
67
68      _validators = <DoctorValidator>[
69        FlutterValidator(),
70        if (androidWorkflow.appliesToHostPlatform)
71          GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]),
72        if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
73          GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator]),
74        if (webWorkflow.appliesToHostPlatform)
75          const WebValidator(),
76        if (linuxWorkflow.appliesToHostPlatform)
77          LinuxDoctorValidator(),
78        if (windowsWorkflow.appliesToHostPlatform)
79          visualStudioValidator,
80        if (ideValidators.isNotEmpty)
81          ...ideValidators
82        else
83          NoIdeValidator(),
84        if (ProxyValidator.shouldShow)
85          ProxyValidator(),
86        if (deviceManager.canListAnything)
87          DeviceValidator(),
88      ];
89    }
90    return _validators;
91  }
92
93  @override
94  List<Workflow> get workflows {
95    if (_workflows == null) {
96      _workflows = <Workflow>[];
97
98      if (iosWorkflow.appliesToHostPlatform)
99        _workflows.add(iosWorkflow);
100
101      if (androidWorkflow.appliesToHostPlatform)
102        _workflows.add(androidWorkflow);
103
104      if (fuchsiaWorkflow.appliesToHostPlatform)
105        _workflows.add(fuchsiaWorkflow);
106
107      if (linuxWorkflow.appliesToHostPlatform)
108        _workflows.add(linuxWorkflow);
109
110      if (macOSWorkflow.appliesToHostPlatform)
111        _workflows.add(macOSWorkflow);
112
113      if (windowsWorkflow.appliesToHostPlatform)
114        _workflows.add(windowsWorkflow);
115
116    }
117    return _workflows;
118  }
119
120}
121
122class ValidatorTask {
123  ValidatorTask(this.validator, this.result);
124  final DoctorValidator validator;
125  final Future<ValidationResult> result;
126}
127
128class Doctor {
129  const Doctor();
130
131  List<DoctorValidator> get validators {
132    return DoctorValidatorsProvider.instance.validators;
133  }
134
135  /// Return a list of [ValidatorTask] objects and starts validation on all
136  /// objects in [validators].
137  List<ValidatorTask> startValidatorTasks() {
138    final List<ValidatorTask> tasks = <ValidatorTask>[];
139    for (DoctorValidator validator in validators) {
140      tasks.add(ValidatorTask(validator, validator.validate()));
141    }
142    return tasks;
143  }
144
145  List<Workflow> get workflows {
146    return DoctorValidatorsProvider.instance.workflows;
147  }
148
149  /// Print a summary of the state of the tooling, as well as how to get more info.
150  Future<void> summary() async {
151    printStatus(await summaryText);
152  }
153
154  Future<String> get summaryText async {
155    final StringBuffer buffer = StringBuffer();
156
157    bool allGood = true;
158
159    for (DoctorValidator validator in validators) {
160      final StringBuffer lineBuffer = StringBuffer();
161      final ValidationResult result = await validator.validate();
162      lineBuffer.write('${result.coloredLeadingBox} ${validator.title} is ');
163      switch (result.type) {
164        case ValidationType.missing:
165          lineBuffer.write('not installed.');
166          break;
167        case ValidationType.partial:
168          lineBuffer.write('partially installed; more components are available.');
169          break;
170        case ValidationType.notAvailable:
171          lineBuffer.write('not available.');
172          break;
173        case ValidationType.installed:
174          lineBuffer.write('fully installed.');
175          break;
176      }
177
178      if (result.statusInfo != null)
179        lineBuffer.write(' (${result.statusInfo})');
180
181      buffer.write(wrapText(lineBuffer.toString(), hangingIndent: result.leadingBox.length + 1));
182      buffer.writeln();
183
184      if (result.type != ValidationType.installed)
185        allGood = false;
186    }
187
188    if (!allGood) {
189      buffer.writeln();
190      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
191    }
192
193    return buffer.toString();
194  }
195
196  Future<bool> checkRemoteArtifacts(String engineRevision) async {
197    return Cache.instance.areRemoteArtifactsAvailable(engineVersion: engineRevision);
198  }
199
200  /// Print information about the state of installed tooling.
201  Future<bool> diagnose({ bool androidLicenses = false, bool verbose = true }) async {
202    if (androidLicenses)
203      return AndroidLicenseValidator.runLicenseManager();
204
205    if (!verbose) {
206      printStatus('Doctor summary (to see all details, run flutter doctor -v):');
207    }
208    bool doctorResult = true;
209    int issues = 0;
210
211    for (ValidatorTask validatorTask in startValidatorTasks()) {
212      final DoctorValidator validator = validatorTask.validator;
213      final Status status = Status.withSpinner(
214        timeout: timeoutConfiguration.fastOperation,
215        slowWarningCallback: () => validator.slowWarning,
216      );
217      ValidationResult result;
218      try {
219        result = await validatorTask.result;
220      } catch (exception) {
221        status.cancel();
222        rethrow;
223      }
224      status.stop();
225
226      switch (result.type) {
227        case ValidationType.missing:
228          doctorResult = false;
229          issues += 1;
230          break;
231        case ValidationType.partial:
232        case ValidationType.notAvailable:
233          issues += 1;
234          break;
235        case ValidationType.installed:
236          break;
237      }
238
239      DoctorResultEvent(validator: validator, result: result).send();
240
241      if (result.statusInfo != null) {
242        printStatus('${result.coloredLeadingBox} ${validator.title} (${result.statusInfo})',
243            hangingIndent: result.leadingBox.length + 1);
244      } else {
245        printStatus('${result.coloredLeadingBox} ${validator.title}',
246            hangingIndent: result.leadingBox.length + 1);
247      }
248
249      for (ValidationMessage message in result.messages) {
250        if (message.type != ValidationMessageType.information || verbose == true) {
251          int hangingIndent = 2;
252          int indent = 4;
253          for (String line in '${message.coloredIndicator} ${message.message}'.split('\n')) {
254            printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
255            // Only do hanging indent for the first line.
256            hangingIndent = 0;
257            indent = 6;
258          }
259        }
260      }
261      if (verbose)
262        printStatus('');
263    }
264
265    // Make sure there's always one line before the summary even when not verbose.
266    if (!verbose)
267      printStatus('');
268
269    if (issues > 0) {
270      printStatus('${terminal.color('!', TerminalColor.yellow)} Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
271    } else {
272      printStatus('${terminal.color('•', TerminalColor.green)} No issues found!', hangingIndent: 2);
273    }
274
275    return doctorResult;
276  }
277
278  bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);
279
280  bool get canLaunchAnything {
281    if (FlutterTesterDevices.showFlutterTesterDevice)
282      return true;
283    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
284  }
285}
286
287/// A series of tools and required install steps for a target platform (iOS or Android).
288abstract class Workflow {
289  const Workflow();
290
291  /// Whether the workflow applies to this platform (as in, should we ever try and use it).
292  bool get appliesToHostPlatform;
293
294  /// Are we functional enough to list devices?
295  bool get canListDevices;
296
297  /// Could this thing launch *something*? It may still have minor issues.
298  bool get canLaunchDevices;
299
300  /// Are we functional enough to list emulators?
301  bool get canListEmulators;
302}
303
304enum ValidationType {
305  missing,
306  partial,
307  notAvailable,
308  installed,
309}
310
311enum ValidationMessageType {
312  error,
313  hint,
314  information,
315}
316
317abstract class DoctorValidator {
318  const DoctorValidator(this.title);
319
320  /// This is displayed in the CLI.
321  final String title;
322
323  String get slowWarning => 'This is taking an unexpectedly long time...';
324
325  Future<ValidationResult> validate();
326}
327
328/// A validator that runs other [DoctorValidator]s and combines their output
329/// into a single [ValidationResult]. It uses the title of the first validator
330/// passed to the constructor and reports the statusInfo of the first validator
331/// that provides one. Other titles and statusInfo strings are discarded.
332class GroupedValidator extends DoctorValidator {
333  GroupedValidator(this.subValidators) : super(subValidators[0].title);
334
335  final List<DoctorValidator> subValidators;
336
337  List<ValidationResult> _subResults;
338
339  /// Subvalidator results.
340  ///
341  /// To avoid losing information when results are merged, the subresults are
342  /// cached on this field when they are available. The results are in the same
343  /// order as the subvalidator list.
344  List<ValidationResult> get subResults => _subResults;
345
346  @override
347  String get slowWarning => _currentSlowWarning;
348  String _currentSlowWarning = 'Initializing...';
349
350  @override
351  Future<ValidationResult> validate() async {
352    final List<ValidatorTask> tasks = <ValidatorTask>[];
353    for (DoctorValidator validator in subValidators) {
354      tasks.add(ValidatorTask(validator, validator.validate()));
355    }
356
357    final List<ValidationResult> results = <ValidationResult>[];
358    for (ValidatorTask subValidator in tasks) {
359      _currentSlowWarning = subValidator.validator.slowWarning;
360      results.add(await subValidator.result);
361    }
362    _currentSlowWarning = 'Merging results...';
363    return _mergeValidationResults(results);
364  }
365
366  ValidationResult _mergeValidationResults(List<ValidationResult> results) {
367    assert(results.isNotEmpty, 'Validation results should not be empty');
368    _subResults = results;
369    ValidationType mergedType = results[0].type;
370    final List<ValidationMessage> mergedMessages = <ValidationMessage>[];
371    String statusInfo;
372
373    for (ValidationResult result in results) {
374      statusInfo ??= result.statusInfo;
375      switch (result.type) {
376        case ValidationType.installed:
377          if (mergedType == ValidationType.missing) {
378            mergedType = ValidationType.partial;
379          }
380          break;
381        case ValidationType.notAvailable:
382        case ValidationType.partial:
383          mergedType = ValidationType.partial;
384          break;
385        case ValidationType.missing:
386          if (mergedType == ValidationType.installed) {
387            mergedType = ValidationType.partial;
388          }
389          break;
390        default:
391          throw 'Unrecognized validation type: ' + result.type.toString();
392      }
393      mergedMessages.addAll(result.messages);
394    }
395
396    return ValidationResult(mergedType, mergedMessages,
397        statusInfo: statusInfo);
398  }
399}
400
401class ValidationResult {
402  /// [ValidationResult.type] should only equal [ValidationResult.installed]
403  /// if no [messages] are hints or errors.
404  ValidationResult(this.type, this.messages, { this.statusInfo });
405
406  final ValidationType type;
407  // A short message about the status.
408  final String statusInfo;
409  final List<ValidationMessage> messages;
410
411  String get leadingBox {
412    assert(type != null);
413    switch (type) {
414      case ValidationType.missing:
415        return '[✗]';
416      case ValidationType.installed:
417        return '[✓]';
418      case ValidationType.notAvailable:
419      case ValidationType.partial:
420        return '[!]';
421    }
422    return null;
423  }
424
425  String get coloredLeadingBox {
426    assert(type != null);
427    switch (type) {
428      case ValidationType.missing:
429        return terminal.color(leadingBox, TerminalColor.red);
430      case ValidationType.installed:
431        return terminal.color(leadingBox, TerminalColor.green);
432      case ValidationType.notAvailable:
433      case ValidationType.partial:
434       return terminal.color(leadingBox, TerminalColor.yellow);
435    }
436    return null;
437  }
438
439  /// The string representation of the type.
440  String get typeStr {
441    assert(type != null);
442    switch (type) {
443      case ValidationType.missing:
444        return 'missing';
445      case ValidationType.installed:
446        return 'installed';
447      case ValidationType.notAvailable:
448        return 'notAvailable';
449      case ValidationType.partial:
450        return 'partial';
451    }
452    return null;
453  }
454}
455
456class ValidationMessage {
457  ValidationMessage(this.message) : type = ValidationMessageType.information;
458  ValidationMessage.error(this.message) : type = ValidationMessageType.error;
459  ValidationMessage.hint(this.message) : type = ValidationMessageType.hint;
460
461  final ValidationMessageType type;
462  bool get isError => type == ValidationMessageType.error;
463  bool get isHint => type == ValidationMessageType.hint;
464  final String message;
465
466  String get indicator {
467    switch (type) {
468      case ValidationMessageType.error:
469        return '✗';
470      case ValidationMessageType.hint:
471        return '!';
472      case ValidationMessageType.information:
473        return '•';
474    }
475    return null;
476  }
477
478  String get coloredIndicator {
479    switch (type) {
480      case ValidationMessageType.error:
481        return terminal.color(indicator, TerminalColor.red);
482      case ValidationMessageType.hint:
483        return terminal.color(indicator, TerminalColor.yellow);
484      case ValidationMessageType.information:
485        return terminal.color(indicator, TerminalColor.green);
486    }
487    return null;
488  }
489
490  @override
491  String toString() => message;
492
493  @override
494  bool operator ==(Object other) {
495    if (other.runtimeType != runtimeType) {
496      return false;
497    }
498    final ValidationMessage typedOther = other;
499    return typedOther.message == message
500      && typedOther.type == type;
501  }
502
503  @override
504  int get hashCode => type.hashCode ^ message.hashCode;
505}
506
507class FlutterValidator extends DoctorValidator {
508  FlutterValidator() : super('Flutter');
509
510  @override
511  Future<ValidationResult> validate() async {
512    final List<ValidationMessage> messages = <ValidationMessage>[];
513    ValidationType valid = ValidationType.installed;
514
515    final FlutterVersion version = FlutterVersion.instance;
516
517    messages.add(ValidationMessage(userMessages.flutterVersion(version.frameworkVersion, Cache.flutterRoot)));
518    messages.add(ValidationMessage(userMessages.flutterRevision(version.frameworkRevisionShort, version.frameworkAge, version.frameworkDate)));
519    messages.add(ValidationMessage(userMessages.engineRevision(version.engineRevisionShort)));
520    messages.add(ValidationMessage(userMessages.dartRevision(version.dartSdkVersion)));
521    final String genSnapshotPath =
522      artifacts.getArtifactPath(Artifact.genSnapshot);
523
524    // Check that the binaries we downloaded for this platform actually run on it.
525    if (!_genSnapshotRuns(genSnapshotPath)) {
526      final StringBuffer buf = StringBuffer();
527      buf.writeln(userMessages.flutterBinariesDoNotRun);
528      if (platform.isLinux) {
529        buf.writeln(userMessages.flutterBinariesLinuxRepairCommands);
530      }
531      messages.add(ValidationMessage.error(buf.toString()));
532      valid = ValidationType.partial;
533    }
534
535    return ValidationResult(valid, messages,
536      statusInfo: userMessages.flutterStatusInfo(version.channel, version.frameworkVersion, os.name, platform.localeName),
537    );
538  }
539}
540
541bool _genSnapshotRuns(String genSnapshotPath) {
542  const int kExpectedExitCode = 255;
543  try {
544    return processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
545  } catch (error) {
546    return false;
547  }
548}
549
550class NoIdeValidator extends DoctorValidator {
551  NoIdeValidator() : super('Flutter IDE Support');
552
553  @override
554  Future<ValidationResult> validate() async {
555    return ValidationResult(ValidationType.missing, <ValidationMessage>[
556      ValidationMessage(userMessages.noIdeInstallationInfo),
557    ], statusInfo: userMessages.noIdeStatusInfo);
558  }
559}
560
561abstract class IntelliJValidator extends DoctorValidator {
562  IntelliJValidator(String title, this.installPath) : super(title);
563
564  final String installPath;
565
566  String get version;
567  String get pluginsPath;
568
569  static final Map<String, String> _idToTitle = <String, String>{
570    'IntelliJIdea': 'IntelliJ IDEA Ultimate Edition',
571    'IdeaIC': 'IntelliJ IDEA Community Edition',
572  };
573
574  static final Version kMinIdeaVersion = Version(2017, 1, 0);
575
576  static Iterable<DoctorValidator> get installedValidators {
577    if (platform.isLinux || platform.isWindows)
578      return IntelliJValidatorOnLinuxAndWindows.installed;
579    if (platform.isMacOS)
580      return IntelliJValidatorOnMac.installed;
581    return <DoctorValidator>[];
582  }
583
584  @override
585  Future<ValidationResult> validate() async {
586    final List<ValidationMessage> messages = <ValidationMessage>[];
587
588    messages.add(ValidationMessage(userMessages.intellijLocation(installPath)));
589
590    final IntelliJPlugins plugins = IntelliJPlugins(pluginsPath);
591    plugins.validatePackage(messages, <String>['flutter-intellij', 'flutter-intellij.jar'],
592        'Flutter', minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
593    plugins.validatePackage(messages, <String>['Dart'], 'Dart');
594
595    if (_hasIssues(messages)) {
596      messages.add(ValidationMessage(userMessages.intellijPluginInfo));
597    }
598
599    _validateIntelliJVersion(messages, kMinIdeaVersion);
600
601    return ValidationResult(
602      _hasIssues(messages) ? ValidationType.partial : ValidationType.installed,
603      messages,
604      statusInfo: userMessages.intellijStatusInfo(version));
605  }
606
607  bool _hasIssues(List<ValidationMessage> messages) {
608    return messages.any((ValidationMessage message) => message.isError);
609  }
610
611  void _validateIntelliJVersion(List<ValidationMessage> messages, Version minVersion) {
612    // Ignore unknown versions.
613    if (minVersion == Version.unknown)
614      return;
615
616    final Version installedVersion = Version.parse(version);
617    if (installedVersion == null)
618      return;
619
620    if (installedVersion < minVersion) {
621      messages.add(ValidationMessage.error(userMessages.intellijMinimumVersion(minVersion.toString())));
622    }
623  }
624}
625
626class IntelliJValidatorOnLinuxAndWindows extends IntelliJValidator {
627  IntelliJValidatorOnLinuxAndWindows(String title, this.version, String installPath, this.pluginsPath) : super(title, installPath);
628
629  @override
630  final String version;
631
632  @override
633  final String pluginsPath;
634
635  static Iterable<DoctorValidator> get installed {
636    final List<DoctorValidator> validators = <DoctorValidator>[];
637    if (homeDirPath == null)
638      return validators;
639
640    void addValidator(String title, String version, String installPath, String pluginsPath) {
641      final IntelliJValidatorOnLinuxAndWindows validator =
642        IntelliJValidatorOnLinuxAndWindows(title, version, installPath, pluginsPath);
643      for (int index = 0; index < validators.length; ++index) {
644        final DoctorValidator other = validators[index];
645        if (other is IntelliJValidatorOnLinuxAndWindows && validator.installPath == other.installPath) {
646          if (validator.version.compareTo(other.version) > 0)
647            validators[index] = validator;
648          return;
649        }
650      }
651      validators.add(validator);
652    }
653
654    for (FileSystemEntity dir in fs.directory(homeDirPath).listSync()) {
655      if (dir is Directory) {
656        final String name = fs.path.basename(dir.path);
657        IntelliJValidator._idToTitle.forEach((String id, String title) {
658          if (name.startsWith('.$id')) {
659            final String version = name.substring(id.length + 1);
660            String installPath;
661            try {
662              installPath = fs.file(fs.path.join(dir.path, 'system', '.home')).readAsStringSync();
663            } catch (e) {
664              // ignored
665            }
666            if (installPath != null && fs.isDirectorySync(installPath)) {
667              final String pluginsPath = fs.path.join(dir.path, 'config', 'plugins');
668              addValidator(title, version, installPath, pluginsPath);
669            }
670          }
671        });
672      }
673    }
674    return validators;
675  }
676}
677
678class IntelliJValidatorOnMac extends IntelliJValidator {
679  IntelliJValidatorOnMac(String title, this.id, String installPath) : super(title, installPath);
680
681  final String id;
682
683  static final Map<String, String> _dirNameToId = <String, String>{
684    'IntelliJ IDEA.app': 'IntelliJIdea',
685    'IntelliJ IDEA Ultimate.app': 'IntelliJIdea',
686    'IntelliJ IDEA CE.app': 'IdeaIC',
687  };
688
689  static Iterable<DoctorValidator> get installed {
690    final List<DoctorValidator> validators = <DoctorValidator>[];
691    final List<String> installPaths = <String>['/Applications', fs.path.join(homeDirPath, 'Applications')];
692
693    void checkForIntelliJ(Directory dir) {
694      final String name = fs.path.basename(dir.path);
695      _dirNameToId.forEach((String dirName, String id) {
696        if (name == dirName) {
697          final String title = IntelliJValidator._idToTitle[id];
698          validators.add(IntelliJValidatorOnMac(title, id, dir.path));
699        }
700      });
701    }
702
703    try {
704      final Iterable<Directory> installDirs = installPaths
705              .map<Directory>((String installPath) => fs.directory(installPath))
706              .map<List<FileSystemEntity>>((Directory dir) => dir.existsSync() ? dir.listSync() : <FileSystemEntity>[])
707              .expand<FileSystemEntity>((List<FileSystemEntity> mappedDirs) => mappedDirs)
708              .whereType<Directory>();
709      for (Directory dir in installDirs) {
710        checkForIntelliJ(dir);
711        if (!dir.path.endsWith('.app')) {
712          for (FileSystemEntity subdir in dir.listSync()) {
713            if (subdir is Directory) {
714              checkForIntelliJ(subdir);
715            }
716          }
717        }
718      }
719    } on FileSystemException catch (e) {
720      validators.add(ValidatorWithResult(
721          userMessages.intellijMacUnknownResult,
722          ValidationResult(ValidationType.missing, <ValidationMessage>[
723              ValidationMessage.error(e.message),
724          ]),
725      ));
726    }
727    return validators;
728  }
729
730  @override
731  String get version {
732    if (_version == null) {
733      final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist');
734      _version = PlistParser.instance.getValueFromFile(
735        plistFile,
736        PlistParser.kCFBundleShortVersionStringKey,
737      ) ?? 'unknown';
738    }
739    return _version;
740  }
741  String _version;
742
743  @override
744  String get pluginsPath {
745    final List<String> split = version.split('.');
746    final String major = split[0];
747    final String minor = split[1];
748    return fs.path.join(homeDirPath, 'Library', 'Application Support', '$id$major.$minor');
749  }
750}
751
752class DeviceValidator extends DoctorValidator {
753  DeviceValidator() : super('Connected device');
754
755  @override
756  String get slowWarning => 'Scanning for devices is taking a long time...';
757
758  @override
759  Future<ValidationResult> validate() async {
760    final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
761    List<ValidationMessage> messages;
762    if (devices.isEmpty) {
763      final List<String> diagnostics = await deviceManager.getDeviceDiagnostics();
764      if (diagnostics.isNotEmpty) {
765        messages = diagnostics.map<ValidationMessage>((String message) => ValidationMessage(message)).toList();
766      } else {
767        messages = <ValidationMessage>[ValidationMessage.hint(userMessages.devicesMissing)];
768      }
769    } else {
770      messages = await Device.descriptions(devices)
771          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
772    }
773
774    if (devices.isEmpty) {
775      return ValidationResult(ValidationType.notAvailable, messages);
776    } else {
777      return ValidationResult(ValidationType.installed, messages, statusInfo: userMessages.devicesAvailable(devices.length));
778    }
779  }
780}
781
782class ValidatorWithResult extends DoctorValidator {
783  ValidatorWithResult(String title, this.result) : super(title);
784
785  final ValidationResult result;
786
787  @override
788  Future<ValidationResult> validate() async => result;
789}
790