doctor.dart 28.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6

7
import 'android/android_studio_validator.dart';
8
import 'android/android_workflow.dart';
9
import 'artifacts.dart';
10
import 'base/async_guard.dart';
11
import 'base/common.dart';
12
import 'base/context.dart';
13
import 'base/file_system.dart';
14
import 'base/logger.dart';
15
import 'base/os.dart';
16
import 'base/platform.dart';
17
import 'base/process.dart';
18
import 'base/terminal.dart';
19
import 'base/user_messages.dart';
20
import 'base/utils.dart';
21
import 'base/version.dart';
22
import 'cache.dart';
23
import 'device.dart';
24
import 'fuchsia/fuchsia_workflow.dart';
25
import 'globals.dart';
26
import 'intellij/intellij.dart';
27
import 'ios/ios_workflow.dart';
28
import 'ios/plist_parser.dart';
29
import 'linux/linux_doctor.dart';
30
import 'linux/linux_workflow.dart';
31
import 'macos/cocoapods_validator.dart';
32
import 'macos/macos_workflow.dart';
33
import 'macos/xcode_validator.dart';
34
import 'proxy_validator.dart';
35
import 'reporting/reporting.dart';
36
import 'tester/flutter_tester.dart';
37
import 'version.dart';
38
import 'vscode/vscode_validator.dart';
39 40
import 'web/web_validator.dart';
import 'web/workflow.dart';
41
import 'windows/visual_studio_validator.dart';
42
import 'windows/windows_workflow.dart';
Devon Carew's avatar
Devon Carew committed
43

44
Doctor get doctor => context.get<Doctor>();
45

46 47
abstract class DoctorValidatorsProvider {
  /// The singleton instance, pulled from the [AppContext].
48
  static DoctorValidatorsProvider get instance => context.get<DoctorValidatorsProvider>();
49

50
  static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider();
51 52

  List<DoctorValidator> get validators;
53
  List<Workflow> get workflows;
54 55
}

56
class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
57
  List<DoctorValidator> _validators;
58
  List<Workflow> _workflows;
59

60
  @override
61
  List<DoctorValidator> get validators {
62 63
    if (_validators != null) {
      return _validators;
64
    }
65 66 67 68 69 70 71 72 73 74

    final List<DoctorValidator> ideValidators = <DoctorValidator>[
      ...AndroidStudioValidator.allValidators,
      ...IntelliJValidator.installedValidators,
      ...VsCodeValidator.installedValidators,
    ];

    _validators = <DoctorValidator>[
      FlutterValidator(),
      if (androidWorkflow.appliesToHostPlatform)
75
        GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]),
76
      if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
77
        GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator]),
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
      if (webWorkflow.appliesToHostPlatform)
        const WebValidator(),
      if (linuxWorkflow.appliesToHostPlatform)
        LinuxDoctorValidator(),
      if (windowsWorkflow.appliesToHostPlatform)
        visualStudioValidator,
      if (ideValidators.isNotEmpty)
        ...ideValidators
      else
        NoIdeValidator(),
      if (ProxyValidator.shouldShow)
        ProxyValidator(),
      if (deviceManager.canListAnything)
        DeviceValidator(),
    ];
93 94
    return _validators;
  }
95 96 97

  @override
  List<Workflow> get workflows {
98 99 100
    if (_workflows == null) {
      _workflows = <Workflow>[];

101
      if (iosWorkflow.appliesToHostPlatform) {
102
        _workflows.add(iosWorkflow);
103
      }
104

105
      if (androidWorkflow.appliesToHostPlatform) {
106
        _workflows.add(androidWorkflow);
107
      }
108

109
      if (fuchsiaWorkflow.appliesToHostPlatform) {
110
        _workflows.add(fuchsiaWorkflow);
111
      }
112

113
      if (linuxWorkflow.appliesToHostPlatform) {
114
        _workflows.add(linuxWorkflow);
115
      }
116

117
      if (macOSWorkflow.appliesToHostPlatform) {
118
        _workflows.add(macOSWorkflow);
119
      }
120

121
      if (windowsWorkflow.appliesToHostPlatform) {
122
        _workflows.add(windowsWorkflow);
123
      }
124

125
      if (webWorkflow.appliesToHostPlatform) {
126
        _workflows.add(webWorkflow);
127
      }
128

129
    }
130 131 132
    return _workflows;
  }

133 134 135 136 137 138 139 140 141 142 143 144 145 146
}

class ValidatorTask {
  ValidatorTask(this.validator, this.result);
  final DoctorValidator validator;
  final Future<ValidationResult> result;
}

class Doctor {
  const Doctor();

  List<DoctorValidator> get validators {
    return DoctorValidatorsProvider.instance.validators;
  }
147

148 149
  /// Return a list of [ValidatorTask] objects and starts validation on all
  /// objects in [validators].
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
    for (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>(
          validator.validate,
          onError: (Object exception, StackTrace stackTrace) {
            return ValidationResult.crash(exception, stackTrace);
          },
        ),
      ),
  ];
166

167
  List<Workflow> get workflows {
168
    return DoctorValidatorsProvider.instance.workflows;
169
  }
170

171
  /// Print a summary of the state of the tooling, as well as how to get more info.
172
  Future<void> summary() async {
173
    printStatus(await _summaryText());
174
  }
175

176
  Future<String> _summaryText() async {
177
    final StringBuffer buffer = StringBuffer();
178

179 180
    bool missingComponent = false;
    bool sawACrash = false;
181

182
    for (DoctorValidator validator in validators) {
183
      final StringBuffer lineBuffer = StringBuffer();
184 185 186 187 188 189 190 191
      ValidationResult result;
      try {
        result = await asyncGuard<ValidationResult>(() => validator.validate());
      } catch (exception) {
        // We're generating a summary, so drop the stack trace.
        result = ValidationResult.crash(exception);
      }
      lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
192
      switch (result.type) {
193 194 195 196
        case ValidationType.crash:
          lineBuffer.write('the doctor check crashed without a result.');
          sawACrash = true;
          break;
197
        case ValidationType.missing:
198
          lineBuffer.write('is not installed.');
199 200
          break;
        case ValidationType.partial:
201
          lineBuffer.write('is partially installed; more components are available.');
202 203
          break;
        case ValidationType.notAvailable:
204
          lineBuffer.write('is not available.');
205 206
          break;
        case ValidationType.installed:
207
          lineBuffer.write('is fully installed.');
208 209
          break;
      }
210

211
      if (result.statusInfo != null) {
212
        lineBuffer.write(' (${result.statusInfo})');
213
      }
214

215
      buffer.write(wrapText(lineBuffer.toString(), hangingIndent: result.leadingBox.length + 1));
216 217
      buffer.writeln();

218 219 220 221 222 223 224 225
      if (result.type != ValidationType.installed) {
        missingComponent = true;
      }
    }

    if (sawACrash) {
      buffer.writeln();
      buffer.writeln('Run "flutter doctor" for information about why a doctor check crashed.');
226 227
    }

228
    if (missingComponent) {
229
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
230
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
231 232 233 234 235
    }

    return buffer.toString();
  }

236
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
237
    return Cache.instance.areRemoteArtifactsAvailable(engineVersion: engineRevision);
238 239
  }

240
  /// Print information about the state of installed tooling.
241
  Future<bool> diagnose({ bool androidLicenses = false, bool verbose = true, bool showColor = true }) async {
242
    if (androidLicenses) {
243
      return AndroidLicenseValidator.runLicenseManager();
244
    }
245

246 247 248
    if (!verbose) {
      printStatus('Doctor summary (to see all details, run flutter doctor -v):');
    }
249
    bool doctorResult = true;
250
    int issues = 0;
251

252
    for (ValidatorTask validatorTask in startValidatorTasks()) {
253
      final DoctorValidator validator = validatorTask.validator;
254
      final Status status = Status.withSpinner(
255
        timeout: timeoutConfiguration.fastOperation,
256 257
        slowWarningCallback: () => validator.slowWarning,
      );
258
      ValidationResult result;
259 260
      try {
        result = await validatorTask.result;
261
      } catch (exception, stackTrace) {
262
        result = ValidationResult.crash(exception, stackTrace);
263 264
      } finally {
        status.stop();
265
      }
266

267
      switch (result.type) {
268 269 270 271
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
          break;
272 273 274 275 276 277 278 279 280 281
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
          break;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
          break;
        case ValidationType.installed:
          break;
282
      }
283

284
      DoctorResultEvent(validator: validator, result: result).send();
285

286
      final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
287
      if (result.statusInfo != null) {
288
        printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
289 290
            hangingIndent: result.leadingBox.length + 1);
      } else {
291
        printStatus('$leadingBox ${validator.title}',
292 293
            hangingIndent: result.leadingBox.length + 1);
      }
294 295

      for (ValidationMessage message in result.messages) {
296 297 298
        if (message.type != ValidationMessageType.information || verbose == true) {
          int hangingIndent = 2;
          int indent = 4;
299 300
          final String indicator = showColor ? message.coloredIndicator : message.indicator;
          for (String line in '$indicator ${message.message}'.split('\n')) {
301 302 303 304
            printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
            // Only do hanging indent for the first line.
            hangingIndent = 0;
            indent = 6;
305
          }
306 307
        }
      }
308
      if (verbose) {
309
        printStatus('');
310
      }
311
    }
312

313
    // Make sure there's always one line before the summary even when not verbose.
314
    if (!verbose) {
315
      printStatus('');
316
    }
317

318
    if (issues > 0) {
319
      printStatus('${showColor ? terminal.color('!', TerminalColor.yellow) : '!'} Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
320
    } else {
321
      printStatus('${showColor ? terminal.color('•', TerminalColor.green) : '•'} No issues found!', hangingIndent: 2);
322
    }
323 324

    return doctorResult;
325 326 327 328
  }

  bool get canListAnything => workflows.any((Workflow workflow) => workflow.canListDevices);

329
  bool get canLaunchAnything {
330
    if (FlutterTesterDevices.showFlutterTesterDevice) {
331
      return true;
332
    }
333 334
    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
  }
335 336
}

Devon Carew's avatar
Devon Carew committed
337
/// A series of tools and required install steps for a target platform (iOS or Android).
338
abstract class Workflow {
339 340
  const Workflow();

341 342 343 344 345 346 347 348
  /// 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;
349 350 351

  /// Are we functional enough to list emulators?
  bool get canListEmulators;
352 353 354
}

enum ValidationType {
355
  crash,
356 357
  missing,
  partial,
358 359
  notAvailable,
  installed,
360 361
}

362 363 364 365 366 367
enum ValidationMessageType {
  error,
  hint,
  information,
}

368
abstract class DoctorValidator {
369
  const DoctorValidator(this.title);
370

371
  /// This is displayed in the CLI.
372
  final String title;
373

374 375
  String get slowWarning => 'This is taking an unexpectedly long time...';

376
  Future<ValidationResult> validate();
377 378
}

379 380 381 382 383
/// 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 {
384
  GroupedValidator(this.subValidators) : super(subValidators[0].title);
385 386 387

  final List<DoctorValidator> subValidators;

388 389
  List<ValidationResult> _subResults;

390
  /// Sub-validator results.
391
  ///
392
  /// To avoid losing information when results are merged, the sub-results are
393
  /// cached on this field when they are available. The results are in the same
394
  /// order as the sub-validator list.
395 396
  List<ValidationResult> get subResults => _subResults;

397 398 399 400
  @override
  String get slowWarning => _currentSlowWarning;
  String _currentSlowWarning = 'Initializing...';

401
  @override
402
  Future<ValidationResult> validate() async {
403 404 405 406 407 408 409
    final List<ValidatorTask> tasks = <ValidatorTask>[
      for (DoctorValidator validator in subValidators)
        ValidatorTask(
          validator,
          asyncGuard<ValidationResult>(() => validator.validate()),
        ),
    ];
410 411 412

    final List<ValidationResult> results = <ValidationResult>[];
    for (ValidatorTask subValidator in tasks) {
413
      _currentSlowWarning = subValidator.validator.slowWarning;
414 415 416
      try {
        results.add(await subValidator.result);
      } catch (exception, stackTrace) {
417
        results.add(ValidationResult.crash(exception, stackTrace));
418
      }
419
    }
420
    _currentSlowWarning = 'Merging results...';
421 422 423 424 425
    return _mergeValidationResults(results);
  }

  ValidationResult _mergeValidationResults(List<ValidationResult> results) {
    assert(results.isNotEmpty, 'Validation results should not be empty');
426
    _subResults = results;
427 428 429 430 431 432 433 434 435 436 437 438
    ValidationType mergedType = results[0].type;
    final List<ValidationMessage> mergedMessages = <ValidationMessage>[];
    String statusInfo;

    for (ValidationResult result in results) {
      statusInfo ??= result.statusInfo;
      switch (result.type) {
        case ValidationType.installed:
          if (mergedType == ValidationType.missing) {
            mergedType = ValidationType.partial;
          }
          break;
439
        case ValidationType.notAvailable:
440 441 442
        case ValidationType.partial:
          mergedType = ValidationType.partial;
          break;
443
        case ValidationType.crash:
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
        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: statusInfo);
  }
}
459

460
class ValidationResult {
461 462
  /// [ValidationResult.type] should only equal [ValidationResult.installed]
  /// if no [messages] are hints or errors.
463
  ValidationResult(this.type, this.messages, { this.statusInfo });
464

465
  factory ValidationResult.crash(Object error, [StackTrace stackTrace]) {
466 467 468 469 470
    return ValidationResult(ValidationType.crash, <ValidationMessage>[
      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.'),
471
      ValidationMessage.error('$error'),
472
      if (stackTrace != null)
473 474
          // Stacktrace is informational. Printed in verbose mode only.
          ValidationMessage('$stackTrace'),
475 476 477
    ], statusInfo: 'the doctor check crashed');
  }

478
  final ValidationType type;
479 480 481
  // A short message about the status.
  final String statusInfo;
  final List<ValidationMessage> messages;
482

483
  String get leadingBox {
484 485
    assert(type != null);
    switch (type) {
486 487
      case ValidationType.crash:
        return '[☠]';
488 489 490 491
      case ValidationType.missing:
        return '[✗]';
      case ValidationType.installed:
        return '[✓]';
492
      case ValidationType.notAvailable:
493 494 495 496
      case ValidationType.partial:
        return '[!]';
    }
    return null;
497
  }
498 499 500 501

  String get coloredLeadingBox {
    assert(type != null);
    switch (type) {
502 503
      case ValidationType.crash:
        return terminal.color(leadingBox, TerminalColor.red);
504 505 506 507 508 509
      case ValidationType.missing:
        return terminal.color(leadingBox, TerminalColor.red);
      case ValidationType.installed:
        return terminal.color(leadingBox, TerminalColor.green);
      case ValidationType.notAvailable:
      case ValidationType.partial:
510
        return terminal.color(leadingBox, TerminalColor.yellow);
511 512 513
    }
    return null;
  }
514 515 516 517 518

  /// The string representation of the type.
  String get typeStr {
    assert(type != null);
    switch (type) {
519 520
      case ValidationType.crash:
        return 'crash';
521 522 523 524 525 526 527 528 529 530 531
      case ValidationType.missing:
        return 'missing';
      case ValidationType.installed:
        return 'installed';
      case ValidationType.notAvailable:
        return 'notAvailable';
      case ValidationType.partial:
        return 'partial';
    }
    return null;
  }
532
}
533

534
class ValidationMessage {
535 536 537
  ValidationMessage(this.message) : type = ValidationMessageType.information;
  ValidationMessage.error(this.message) : type = ValidationMessageType.error;
  ValidationMessage.hint(this.message) : type = ValidationMessageType.hint;
538

539 540 541
  final ValidationMessageType type;
  bool get isError => type == ValidationMessageType.error;
  bool get isHint => type == ValidationMessageType.hint;
542
  final String message;
543

544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
  String get indicator {
    switch (type) {
      case ValidationMessageType.error:
        return '✗';
      case ValidationMessageType.hint:
        return '!';
      case ValidationMessageType.information:
        return '•';
    }
    return null;
  }

  String get coloredIndicator {
    switch (type) {
      case ValidationMessageType.error:
        return terminal.color(indicator, TerminalColor.red);
      case ValidationMessageType.hint:
        return terminal.color(indicator, TerminalColor.yellow);
      case ValidationMessageType.information:
        return terminal.color(indicator, TerminalColor.green);
    }
    return null;
  }

568 569
  @override
  String toString() => message;
570 571 572 573 574 575

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
576 577 578
    return other is ValidationMessage
        && other.message == message
        && other.type == type;
579 580 581 582
  }

  @override
  int get hashCode => type.hashCode ^ message.hashCode;
583
}
584

585 586
class FlutterValidator extends DoctorValidator {
  FlutterValidator() : super('Flutter');
587

588
  @override
589
  Future<ValidationResult> validate() async {
590
    final List<ValidationMessage> messages = <ValidationMessage>[];
591
    ValidationType valid = ValidationType.installed;
592 593 594 595 596 597 598
    String versionChannel;
    String frameworkVersion;

    try {
      final FlutterVersion version = FlutterVersion.instance;
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
599 600 601 602 603 604 605 606 607
      messages.add(ValidationMessage(userMessages.flutterVersion(
        frameworkVersion,
        Cache.flutterRoot,
      )));
      messages.add(ValidationMessage(userMessages.flutterRevision(
        version.frameworkRevisionShort,
        version.frameworkAge,
        version.frameworkDate,
      )));
608 609 610 611 612 613
      messages.add(ValidationMessage(userMessages.engineRevision(version.engineRevisionShort)));
      messages.add(ValidationMessage(userMessages.dartRevision(version.dartSdkVersion)));
    } on VersionCheckError catch (e) {
      messages.add(ValidationMessage.error(e.message));
      valid = ValidationType.partial;
    }
614

615 616 617 618 619
    final String genSnapshotPath =
      artifacts.getArtifactPath(Artifact.genSnapshot);

    // Check that the binaries we downloaded for this platform actually run on it.
    if (!_genSnapshotRuns(genSnapshotPath)) {
620
      final StringBuffer buf = StringBuffer();
621
      buf.writeln(userMessages.flutterBinariesDoNotRun);
622
      if (platform.isLinux) {
623
        buf.writeln(userMessages.flutterBinariesLinuxRepairCommands);
624
      }
625
      messages.add(ValidationMessage.error(buf.toString()));
626
      valid = ValidationType.partial;
627
    }
628

629
    return ValidationResult(valid, messages,
630
      statusInfo: userMessages.flutterStatusInfo(versionChannel, frameworkVersion, os.name, platform.localeName),
631
    );
632
  }
633
}
Devon Carew's avatar
Devon Carew committed
634

635
bool _genSnapshotRuns(String genSnapshotPath) {
636
  const int kExpectedExitCode = 255;
637
  try {
638
    return processUtils.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
639 640
  } catch (error) {
    return false;
641 642 643
  }
}

644
class NoIdeValidator extends DoctorValidator {
645
  NoIdeValidator() : super('Flutter IDE Support');
646 647 648

  @override
  Future<ValidationResult> validate() async {
649
    return ValidationResult(ValidationType.missing, <ValidationMessage>[
650 651
      ValidationMessage(userMessages.noIdeInstallationInfo),
    ], statusInfo: userMessages.noIdeStatusInfo);
652 653 654
  }
}

655
abstract class IntelliJValidator extends DoctorValidator {
656
  IntelliJValidator(String title, this.installPath) : super(title);
657

658 659
  final String installPath;

660 661
  String get version;
  String get pluginsPath;
662

663
  static final Map<String, String> _idToTitle = <String, String>{
664 665
    'IntelliJIdea': 'IntelliJ IDEA Ultimate Edition',
    'IdeaIC': 'IntelliJ IDEA Community Edition',
666
  };
667

668
  static final Version kMinIdeaVersion = Version(2017, 1, 0);
669

670
  static Iterable<DoctorValidator> get installedValidators {
671
    if (platform.isLinux || platform.isWindows) {
672
      return IntelliJValidatorOnLinuxAndWindows.installed;
673 674
    }
    if (platform.isMacOS) {
675
      return IntelliJValidatorOnMac.installed;
676
    }
677
    return <DoctorValidator>[];
678 679 680 681
  }

  @override
  Future<ValidationResult> validate() async {
682
    final List<ValidationMessage> messages = <ValidationMessage>[];
683

684
    messages.add(ValidationMessage(userMessages.intellijLocation(installPath)));
685

686
    final IntelliJPlugins plugins = IntelliJPlugins(pluginsPath);
687 688 689
    plugins.validatePackage(messages, <String>['flutter-intellij', 'flutter-intellij.jar'],
        'Flutter', minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
    plugins.validatePackage(messages, <String>['Dart'], 'Dart');
690

691
    if (_hasIssues(messages)) {
692
      messages.add(ValidationMessage(userMessages.intellijPluginInfo));
693 694
    }

695
    _validateIntelliJVersion(messages, kMinIdeaVersion);
696

697
    return ValidationResult(
698 699
      _hasIssues(messages) ? ValidationType.partial : ValidationType.installed,
      messages,
700
      statusInfo: userMessages.intellijStatusInfo(version));
701 702
  }

703 704 705 706 707 708
  bool _hasIssues(List<ValidationMessage> messages) {
    return messages.any((ValidationMessage message) => message.isError);
  }

  void _validateIntelliJVersion(List<ValidationMessage> messages, Version minVersion) {
    // Ignore unknown versions.
709
    if (minVersion == Version.unknown) {
710
      return;
711
    }
712

713
    final Version installedVersion = Version.parse(version);
714
    if (installedVersion == null) {
715
      return;
716
    }
717 718

    if (installedVersion < minVersion) {
719
      messages.add(ValidationMessage.error(userMessages.intellijMinimumVersion(minVersion.toString())));
720 721
    }
  }
722 723
}

724
class IntelliJValidatorOnLinuxAndWindows extends IntelliJValidator {
725
  IntelliJValidatorOnLinuxAndWindows(String title, this.version, String installPath, this.pluginsPath) : super(title, installPath);
726 727

  @override
728
  final String version;
729 730

  @override
731
  final String pluginsPath;
732 733

  static Iterable<DoctorValidator> get installed {
734
    final List<DoctorValidator> validators = <DoctorValidator>[];
735
    if (homeDirPath == null) {
736
      return validators;
737
    }
738 739

    void addValidator(String title, String version, String installPath, String pluginsPath) {
740
      final IntelliJValidatorOnLinuxAndWindows validator =
741
        IntelliJValidatorOnLinuxAndWindows(title, version, installPath, pluginsPath);
742
      for (int index = 0; index < validators.length; ++index) {
743
        final DoctorValidator other = validators[index];
744
        if (other is IntelliJValidatorOnLinuxAndWindows && validator.installPath == other.installPath) {
745
          if (validator.version.compareTo(other.version) > 0) {
746
            validators[index] = validator;
747
          }
748 749 750 751 752 753
          return;
        }
      }
      validators.add(validator);
    }

754
    for (FileSystemEntity dir in fs.directory(homeDirPath).listSync()) {
755
      if (dir is Directory) {
756
        final String name = fs.path.basename(dir.path);
757 758
        IntelliJValidator._idToTitle.forEach((String id, String title) {
          if (name.startsWith('.$id')) {
759
            final String version = name.substring(id.length + 1);
760 761
            String installPath;
            try {
762
              installPath = fs.file(fs.path.join(dir.path, 'system', '.home')).readAsStringSync();
763 764 765
            } catch (e) {
              // ignored
            }
766
            if (installPath != null && fs.isDirectorySync(installPath)) {
767
              final String pluginsPath = fs.path.join(dir.path, 'config', 'plugins');
768
              addValidator(title, version, installPath, pluginsPath);
769 770 771 772 773 774 775 776 777 778
            }
          }
        });
      }
    }
    return validators;
  }
}

class IntelliJValidatorOnMac extends IntelliJValidator {
779
  IntelliJValidatorOnMac(String title, this.id, String installPath) : super(title, installPath);
780 781 782 783

  final String id;

  static final Map<String, String> _dirNameToId = <String, String>{
784 785 786
    'IntelliJ IDEA.app': 'IntelliJIdea',
    'IntelliJ IDEA Ultimate.app': 'IntelliJIdea',
    'IntelliJ IDEA CE.app': 'IdeaIC',
787 788 789
  };

  static Iterable<DoctorValidator> get installed {
790 791
    final List<DoctorValidator> validators = <DoctorValidator>[];
    final List<String> installPaths = <String>['/Applications', fs.path.join(homeDirPath, 'Applications')];
792 793

    void checkForIntelliJ(Directory dir) {
794
      final String name = fs.path.basename(dir.path);
795 796
      _dirNameToId.forEach((String dirName, String id) {
        if (name == dirName) {
797
          final String title = IntelliJValidator._idToTitle[id];
798
          validators.add(IntelliJValidatorOnMac(title, id, dir.path));
799 800 801 802
        }
      });
    }

803
    try {
804
      final Iterable<Directory> installDirs = installPaths
805 806 807
              .map<Directory>((String installPath) => fs.directory(installPath))
              .map<List<FileSystemEntity>>((Directory dir) => dir.existsSync() ? dir.listSync() : <FileSystemEntity>[])
              .expand<FileSystemEntity>((List<FileSystemEntity> mappedDirs) => mappedDirs)
808 809 810 811 812 813 814
              .whereType<Directory>();
      for (Directory dir in installDirs) {
        checkForIntelliJ(dir);
        if (!dir.path.endsWith('.app')) {
          for (FileSystemEntity subdir in dir.listSync()) {
            if (subdir is Directory) {
              checkForIntelliJ(subdir);
815
            }
816
          }
817
        }
818
      }
819
    } on FileSystemException catch (e) {
820
      validators.add(ValidatorWithResult(
821
          userMessages.intellijMacUnknownResult,
822
          ValidationResult(ValidationType.missing, <ValidationMessage>[
823
            ValidationMessage.error(e.message),
824 825
          ]),
      ));
826 827 828 829 830 831 832
    }
    return validators;
  }

  @override
  String get version {
    if (_version == null) {
833
      final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist');
834
      _version = PlistParser.instance.getValueFromFile(
835
        plistFile,
836
        PlistParser.kCFBundleShortVersionStringKey,
837
      ) ?? 'unknown';
838 839 840 841 842 843 844
    }
    return _version;
  }
  String _version;

  @override
  String get pluginsPath {
845 846 847
    final List<String> split = version.split('.');
    final String major = split[0];
    final String minor = split[1];
848
    return fs.path.join(homeDirPath, 'Library', 'Application Support', '$id$major.$minor');
849 850 851
  }
}

852
class DeviceValidator extends DoctorValidator {
853
  DeviceValidator() : super('Connected device');
854

855 856 857
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

858 859
  @override
  Future<ValidationResult> validate() async {
860
    final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
861 862
    List<ValidationMessage> messages;
    if (devices.isEmpty) {
863 864
      final List<String> diagnostics = await deviceManager.getDeviceDiagnostics();
      if (diagnostics.isNotEmpty) {
865
        messages = diagnostics.map<ValidationMessage>((String message) => ValidationMessage(message)).toList();
866
      } else {
867
        messages = <ValidationMessage>[ValidationMessage.hint(userMessages.devicesMissing)];
868
      }
869
    } else {
870
      messages = await Device.descriptions(devices)
871
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
872
    }
873 874

    if (devices.isEmpty) {
875
      return ValidationResult(ValidationType.notAvailable, messages);
876
    } else {
877
      return ValidationResult(ValidationType.installed, messages, statusInfo: userMessages.devicesAvailable(devices.length));
878
    }
879 880
  }
}
881 882 883 884

class ValidatorWithResult extends DoctorValidator {
  ValidatorWithResult(String title, this.result) : super(title);

885 886
  final ValidationResult result;

887 888 889
  @override
  Future<ValidationResult> validate() async => result;
}