doctor.dart 31.2 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 8
import 'package:meta/meta.dart';

9
import 'android/android_studio_validator.dart';
10
import 'android/android_workflow.dart';
11
import 'artifacts.dart';
12
import 'base/async_guard.dart';
13
import 'base/context.dart';
14
import 'base/file_system.dart';
15
import 'base/logger.dart';
16
import 'base/process.dart';
17
import 'base/terminal.dart';
18
import 'base/user_messages.dart';
19
import 'base/utils.dart';
20
import 'base/version.dart';
21
import 'cache.dart';
22
import 'device.dart';
23
import 'features.dart';
24
import 'fuchsia/fuchsia_workflow.dart';
25
import 'globals.dart' as globals;
26
import 'intellij/intellij.dart';
27
import 'ios/plist_parser.dart';
28
import 'linux/linux_doctor.dart';
29
import 'linux/linux_workflow.dart';
30
import 'macos/cocoapods_validator.dart';
31
import 'macos/macos_workflow.dart';
32
import 'macos/xcode_validator.dart';
33
import 'proxy_validator.dart';
34
import 'reporting/reporting.dart';
35
import 'tester/flutter_tester.dart';
36
import 'version.dart';
37
import 'vscode/vscode_validator.dart';
38
import 'web/chrome.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 45
abstract class DoctorValidatorsProvider {
  /// The singleton instance, pulled from the [AppContext].
46
  static DoctorValidatorsProvider get instance => context.get<DoctorValidatorsProvider>();
47

48
  static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider();
49 50

  List<DoctorValidator> get validators;
51
  List<Workflow> get workflows;
52 53
}

54
class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
55
  List<DoctorValidator> _validators;
56
  List<Workflow> _workflows;
57

58 59 60 61 62 63 64 65 66 67
  final LinuxWorkflow linuxWorkflow = LinuxWorkflow(
    platform: globals.platform,
    featureFlags: featureFlags,
  );

  final WebWorkflow webWorkflow = WebWorkflow(
    platform: globals.platform,
    featureFlags: featureFlags,
  );

68
  @override
69
  List<DoctorValidator> get validators {
70 71
    if (_validators != null) {
      return _validators;
72
    }
73 74 75 76 77 78 79 80 81

    final List<DoctorValidator> ideValidators = <DoctorValidator>[
      ...AndroidStudioValidator.allValidators,
      ...IntelliJValidator.installedValidators,
      ...VsCodeValidator.installedValidators,
    ];
    _validators = <DoctorValidator>[
      FlutterValidator(),
      if (androidWorkflow.appliesToHostPlatform)
82
        GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]),
83
      if (globals.iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
84
        GroupedValidator(<DoctorValidator>[XcodeValidator(xcode: globals.xcode, userMessages: userMessages), cocoapodsValidator]),
85
      if (webWorkflow.appliesToHostPlatform)
86 87 88 89 90 91 92 93 94
        ChromeValidator(
          chromiumLauncher: ChromiumLauncher(
            browserFinder: findChromeExecutable,
            fileSystem: globals.fs,
            logger: globals.logger,
            operatingSystemUtils: globals.os,
            platform:  globals.platform,
            processManager: globals.processManager,
          ),
95 96
          platform: globals.platform,
        ),
97
      if (linuxWorkflow.appliesToHostPlatform)
98 99
        LinuxDoctorValidator(
          processManager: globals.processManager,
100
          userMessages: userMessages,
101
        ),
102 103 104 105 106 107 108 109 110 111 112
      if (windowsWorkflow.appliesToHostPlatform)
        visualStudioValidator,
      if (ideValidators.isNotEmpty)
        ...ideValidators
      else
        NoIdeValidator(),
      if (ProxyValidator.shouldShow)
        ProxyValidator(),
      if (deviceManager.canListAnything)
        DeviceValidator(),
    ];
113 114
    return _validators;
  }
115 116 117

  @override
  List<Workflow> get workflows {
118 119 120
    if (_workflows == null) {
      _workflows = <Workflow>[];

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

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

129
      if (fuchsiaWorkflow.appliesToHostPlatform) {
130
        _workflows.add(fuchsiaWorkflow);
131
      }
132

133
      if (linuxWorkflow.appliesToHostPlatform) {
134
        _workflows.add(linuxWorkflow);
135
      }
136

137
      if (macOSWorkflow.appliesToHostPlatform) {
138
        _workflows.add(macOSWorkflow);
139
      }
140

141
      if (windowsWorkflow.appliesToHostPlatform) {
142
        _workflows.add(windowsWorkflow);
143
      }
144

145
      if (webWorkflow.appliesToHostPlatform) {
146
        _workflows.add(webWorkflow);
147
      }
148

149
    }
150 151 152
    return _workflows;
  }

153 154 155 156 157 158 159 160 161
}

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

class Doctor {
162 163 164 165 166
  Doctor({
    @required Logger logger,
  }) : _logger = logger;

  final Logger _logger;
167 168 169 170

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

172 173
  /// Return a list of [ValidatorTask] objects and starts validation on all
  /// objects in [validators].
174
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
175
    for (final DoctorValidator validator in validators)
176 177 178 179 180 181 182 183 184 185 186 187 188 189
      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);
          },
        ),
      ),
  ];
190

191
  List<Workflow> get workflows {
192
    return DoctorValidatorsProvider.instance.workflows;
193
  }
194

195
  /// Print a summary of the state of the tooling, as well as how to get more info.
196
  Future<void> summary() async {
197
    _logger.printStatus(await _summaryText());
198
  }
199

200
  Future<String> _summaryText() async {
201
    final StringBuffer buffer = StringBuffer();
202

203 204
    bool missingComponent = false;
    bool sawACrash = false;
205

206
    for (final DoctorValidator validator in validators) {
207
      final StringBuffer lineBuffer = StringBuffer();
208 209 210
      ValidationResult result;
      try {
        result = await asyncGuard<ValidationResult>(() => validator.validate());
211
      } on Exception catch (exception) {
212 213 214 215
        // We're generating a summary, so drop the stack trace.
        result = ValidationResult.crash(exception);
      }
      lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
216
      switch (result.type) {
217 218 219 220
        case ValidationType.crash:
          lineBuffer.write('the doctor check crashed without a result.');
          sawACrash = true;
          break;
221
        case ValidationType.missing:
222
          lineBuffer.write('is not installed.');
223 224
          break;
        case ValidationType.partial:
225
          lineBuffer.write('is partially installed; more components are available.');
226 227
          break;
        case ValidationType.notAvailable:
228
          lineBuffer.write('is not available.');
229 230
          break;
        case ValidationType.installed:
231
          lineBuffer.write('is fully installed.');
232 233
          break;
      }
234

235
      if (result.statusInfo != null) {
236
        lineBuffer.write(' (${result.statusInfo})');
237
      }
238

239 240 241
      buffer.write(wrapText(
        lineBuffer.toString(),
        hangingIndent: result.leadingBox.length + 1,
242 243
        columnWidth: globals.outputPreferences.wrapColumn,
        shouldWrap: globals.outputPreferences.wrapText,
244
      ));
245 246
      buffer.writeln();

247 248 249 250 251 252 253 254
      if (result.type != ValidationType.installed) {
        missingComponent = true;
      }
    }

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

257
    if (missingComponent) {
258
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
259
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
260 261 262 263 264
    }

    return buffer.toString();
  }

265
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
266
    return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
267 268
  }

269
  /// Print information about the state of installed tooling.
270
  Future<bool> diagnose({ bool androidLicenses = false, bool verbose = true, bool showColor = true }) async {
271
    if (androidLicenses) {
272
      return AndroidLicenseValidator.runLicenseManager();
273
    }
274

275
    if (!verbose) {
276
      _logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):');
277
    }
278
    bool doctorResult = true;
279
    int issues = 0;
280

281
    for (final ValidatorTask validatorTask in startValidatorTasks()) {
282
      final DoctorValidator validator = validatorTask.validator;
283
      final Status status = Status.withSpinner(
284
        timeout: timeoutConfiguration.fastOperation,
285
        slowWarningCallback: () => validator.slowWarning,
286
        timeoutConfiguration: timeoutConfiguration,
287
        stopwatch: Stopwatch(),
288
        terminal: globals.terminal,
289
      );
290
      ValidationResult result;
291 292
      try {
        result = await validatorTask.result;
293
        status.stop();
294 295 296
      } on Exception catch (exception, stackTrace) {
        result = ValidationResult.crash(exception, stackTrace);
        status.cancel();
297
      }
298

299
      switch (result.type) {
300 301 302 303
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
          break;
304 305 306 307 308 309 310 311 312 313
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
          break;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
          break;
        case ValidationType.installed:
          break;
314
      }
315

316
      DoctorResultEvent(validator: validator, result: result).send();
317

318
      final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
319
      if (result.statusInfo != null) {
320
        _logger.printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
321 322
            hangingIndent: result.leadingBox.length + 1);
      } else {
323
        _logger.printStatus('$leadingBox ${validator.title}',
324 325
            hangingIndent: result.leadingBox.length + 1);
      }
326

327
      for (final ValidationMessage message in result.messages) {
328 329 330
        if (message.type != ValidationMessageType.information || verbose == true) {
          int hangingIndent = 2;
          int indent = 4;
331
          final String indicator = showColor ? message.coloredIndicator : message.indicator;
332
          for (final String line in '$indicator ${message.message}'.split('\n')) {
333
            _logger.printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
334 335 336
            // Only do hanging indent for the first line.
            hangingIndent = 0;
            indent = 6;
337
          }
338 339
        }
      }
340
      if (verbose) {
341
        _logger.printStatus('');
342
      }
343
    }
344

345
    // Make sure there's always one line before the summary even when not verbose.
346
    if (!verbose) {
347
      _logger.printStatus('');
348
    }
349

350
    if (issues > 0) {
351
      _logger.printStatus('${showColor ? globals.terminal.color('!', TerminalColor.yellow) : '!'}'
352
        ' Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
353
    } else {
354
      _logger.printStatus('${showColor ? globals.terminal.color('•', TerminalColor.green) : '•'}'
355
        ' No issues found!', hangingIndent: 2);
356
    }
357 358

    return doctorResult;
359 360 361 362
  }

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

363
  bool get canLaunchAnything {
364
    if (FlutterTesterDevices.showFlutterTesterDevice) {
365
      return true;
366
    }
367 368
    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
  }
369 370
}

Devon Carew's avatar
Devon Carew committed
371
/// A series of tools and required install steps for a target platform (iOS or Android).
372
abstract class Workflow {
373 374
  const Workflow();

375 376 377 378 379 380 381 382
  /// 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;
383 384 385

  /// Are we functional enough to list emulators?
  bool get canListEmulators;
386 387 388
}

enum ValidationType {
389
  crash,
390 391
  missing,
  partial,
392 393
  notAvailable,
  installed,
394 395
}

396 397 398 399 400 401
enum ValidationMessageType {
  error,
  hint,
  information,
}

402
abstract class DoctorValidator {
403
  const DoctorValidator(this.title);
404

405
  /// This is displayed in the CLI.
406
  final String title;
407

408 409
  String get slowWarning => 'This is taking an unexpectedly long time...';

410
  Future<ValidationResult> validate();
411 412
}

413 414 415 416 417
/// 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 {
418
  GroupedValidator(this.subValidators) : super(subValidators[0].title);
419 420 421

  final List<DoctorValidator> subValidators;

422 423
  List<ValidationResult> _subResults;

424
  /// Sub-validator results.
425
  ///
426
  /// To avoid losing information when results are merged, the sub-results are
427
  /// cached on this field when they are available. The results are in the same
428
  /// order as the sub-validator list.
429 430
  List<ValidationResult> get subResults => _subResults;

431 432 433 434
  @override
  String get slowWarning => _currentSlowWarning;
  String _currentSlowWarning = 'Initializing...';

435
  @override
436
  Future<ValidationResult> validate() async {
437
    final List<ValidatorTask> tasks = <ValidatorTask>[
438
      for (final DoctorValidator validator in subValidators)
439 440 441 442 443
        ValidatorTask(
          validator,
          asyncGuard<ValidationResult>(() => validator.validate()),
        ),
    ];
444 445

    final List<ValidationResult> results = <ValidationResult>[];
446
    for (final ValidatorTask subValidator in tasks) {
447
      _currentSlowWarning = subValidator.validator.slowWarning;
448 449
      try {
        results.add(await subValidator.result);
450
      } on Exception catch (exception, stackTrace) {
451
        results.add(ValidationResult.crash(exception, stackTrace));
452
      }
453
    }
454
    _currentSlowWarning = 'Merging results...';
455 456 457 458 459
    return _mergeValidationResults(results);
  }

  ValidationResult _mergeValidationResults(List<ValidationResult> results) {
    assert(results.isNotEmpty, 'Validation results should not be empty');
460
    _subResults = results;
461 462 463 464
    ValidationType mergedType = results[0].type;
    final List<ValidationMessage> mergedMessages = <ValidationMessage>[];
    String statusInfo;

465
    for (final ValidationResult result in results) {
466 467 468 469 470 471 472
      statusInfo ??= result.statusInfo;
      switch (result.type) {
        case ValidationType.installed:
          if (mergedType == ValidationType.missing) {
            mergedType = ValidationType.partial;
          }
          break;
473
        case ValidationType.notAvailable:
474 475 476
        case ValidationType.partial:
          mergedType = ValidationType.partial;
          break;
477
        case ValidationType.crash:
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
        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);
  }
}
493

494
@immutable
495
class ValidationResult {
496 497
  /// [ValidationResult.type] should only equal [ValidationResult.installed]
  /// if no [messages] are hints or errors.
498
  const ValidationResult(this.type, this.messages, { this.statusInfo });
499

500
  factory ValidationResult.crash(Object error, [StackTrace stackTrace]) {
501
    return ValidationResult(ValidationType.crash, <ValidationMessage>[
502
      const ValidationMessage.error(
503 504 505
          '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.'),
506
      ValidationMessage.error('$error'),
507
      if (stackTrace != null)
508 509
          // Stacktrace is informational. Printed in verbose mode only.
          ValidationMessage('$stackTrace'),
510 511 512
    ], statusInfo: 'the doctor check crashed');
  }

513
  final ValidationType type;
514 515 516
  // A short message about the status.
  final String statusInfo;
  final List<ValidationMessage> messages;
517

518
  String get leadingBox {
519 520
    assert(type != null);
    switch (type) {
521 522
      case ValidationType.crash:
        return '[☠]';
523 524 525 526
      case ValidationType.missing:
        return '[✗]';
      case ValidationType.installed:
        return '[✓]';
527
      case ValidationType.notAvailable:
528 529 530 531
      case ValidationType.partial:
        return '[!]';
    }
    return null;
532
  }
533 534 535 536

  String get coloredLeadingBox {
    assert(type != null);
    switch (type) {
537
      case ValidationType.crash:
538
        return globals.terminal.color(leadingBox, TerminalColor.red);
539
      case ValidationType.missing:
540
        return globals.terminal.color(leadingBox, TerminalColor.red);
541
      case ValidationType.installed:
542
        return globals.terminal.color(leadingBox, TerminalColor.green);
543 544
      case ValidationType.notAvailable:
      case ValidationType.partial:
545
        return globals.terminal.color(leadingBox, TerminalColor.yellow);
546 547 548
    }
    return null;
  }
549 550 551 552 553

  /// The string representation of the type.
  String get typeStr {
    assert(type != null);
    switch (type) {
554 555
      case ValidationType.crash:
        return 'crash';
556 557 558 559 560 561 562 563 564 565 566
      case ValidationType.missing:
        return 'missing';
      case ValidationType.installed:
        return 'installed';
      case ValidationType.notAvailable:
        return 'notAvailable';
      case ValidationType.partial:
        return 'partial';
    }
    return null;
  }
567
}
568

569
@immutable
570
class ValidationMessage {
571 572 573
  const ValidationMessage(this.message) : type = ValidationMessageType.information;
  const ValidationMessage.error(this.message) : type = ValidationMessageType.error;
  const ValidationMessage.hint(this.message) : type = ValidationMessageType.hint;
574

575 576 577
  final ValidationMessageType type;
  bool get isError => type == ValidationMessageType.error;
  bool get isHint => type == ValidationMessageType.hint;
578
  final String message;
579

580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
  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:
595
        return globals.terminal.color(indicator, TerminalColor.red);
596
      case ValidationMessageType.hint:
597
        return globals.terminal.color(indicator, TerminalColor.yellow);
598
      case ValidationMessageType.information:
599
        return globals.terminal.color(indicator, TerminalColor.green);
600 601 602 603
    }
    return null;
  }

604 605
  @override
  String toString() => message;
606 607 608 609 610 611

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
612 613 614
    return other is ValidationMessage
        && other.message == message
        && other.type == type;
615 616 617 618
  }

  @override
  int get hashCode => type.hashCode ^ message.hashCode;
619
}
620

621 622
class FlutterValidator extends DoctorValidator {
  FlutterValidator() : super('Flutter');
623

624
  @override
625
  Future<ValidationResult> validate() async {
626
    final List<ValidationMessage> messages = <ValidationMessage>[];
627
    ValidationType valid = ValidationType.installed;
628 629 630 631
    String versionChannel;
    String frameworkVersion;

    try {
632
      final FlutterVersion version = globals.flutterVersion;
633 634
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
635 636 637 638 639 640 641 642 643
      messages.add(ValidationMessage(userMessages.flutterVersion(
        frameworkVersion,
        Cache.flutterRoot,
      )));
      messages.add(ValidationMessage(userMessages.flutterRevision(
        version.frameworkRevisionShort,
        version.frameworkAge,
        version.frameworkDate,
      )));
644 645
      messages.add(ValidationMessage(userMessages.engineRevision(version.engineRevisionShort)));
      messages.add(ValidationMessage(userMessages.dartRevision(version.dartSdkVersion)));
646 647 648 649 650 651
      if (globals.platform.environment.containsKey('PUB_HOSTED_URL')) {
        messages.add(ValidationMessage(userMessages.pubMirrorURL(globals.platform.environment['PUB_HOSTED_URL'])));
      }
      if (globals.platform.environment.containsKey('FLUTTER_STORAGE_BASE_URL')) {
        messages.add(ValidationMessage(userMessages.flutterMirrorURL(globals.platform.environment['FLUTTER_STORAGE_BASE_URL'])));
      }
652 653 654 655
    } on VersionCheckError catch (e) {
      messages.add(ValidationMessage.error(e.message));
      valid = ValidationType.partial;
    }
656

657
    final String genSnapshotPath =
658
      globals.artifacts.getArtifactPath(Artifact.genSnapshot);
659 660

    // Check that the binaries we downloaded for this platform actually run on it.
661 662
    if (globals.fs.file(genSnapshotPath).existsSync()
        && !_genSnapshotRuns(genSnapshotPath)) {
663
      final StringBuffer buf = StringBuffer();
664
      buf.writeln(userMessages.flutterBinariesDoNotRun);
665
      if (globals.platform.isLinux) {
666
        buf.writeln(userMessages.flutterBinariesLinuxRepairCommands);
667
      }
668
      messages.add(ValidationMessage.error(buf.toString()));
669
      valid = ValidationType.partial;
670
    }
671

672 673 674
    return ValidationResult(
      valid,
      messages,
675
      statusInfo: userMessages.flutterStatusInfo(
676 677 678 679 680
        versionChannel,
        frameworkVersion,
        globals.os.name,
        globals.platform.localeName,
      ),
681
    );
682
  }
683
}
Devon Carew's avatar
Devon Carew committed
684

685
bool _genSnapshotRuns(String genSnapshotPath) {
686
  const int kExpectedExitCode = 255;
687
  try {
688
    return processUtils.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
689
  } on Exception {
690
    return false;
691 692 693
  }
}

694
class NoIdeValidator extends DoctorValidator {
695
  NoIdeValidator() : super('Flutter IDE Support');
696 697 698

  @override
  Future<ValidationResult> validate() async {
699
    return ValidationResult(ValidationType.missing, <ValidationMessage>[
700 701
      ValidationMessage(userMessages.noIdeInstallationInfo),
    ], statusInfo: userMessages.noIdeStatusInfo);
702 703 704
  }
}

705
abstract class IntelliJValidator extends DoctorValidator {
706
  IntelliJValidator(String title, this.installPath) : super(title);
707

708 709
  final String installPath;

710 711
  String get version;
  String get pluginsPath;
712

713
  static final Map<String, String> _idToTitle = <String, String>{
714 715
    'IntelliJIdea': 'IntelliJ IDEA Ultimate Edition',
    'IdeaIC': 'IntelliJ IDEA Community Edition',
716
  };
717

718
  static final Version kMinIdeaVersion = Version(2017, 1, 0);
719

720
  static Iterable<DoctorValidator> get installedValidators {
721
    if (globals.platform.isLinux || globals.platform.isWindows) {
722
      return IntelliJValidatorOnLinuxAndWindows.installed;
723
    }
724
    if (globals.platform.isMacOS) {
725
      return IntelliJValidatorOnMac.installed;
726
    }
727
    return <DoctorValidator>[];
728 729 730 731
  }

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

734
    if (pluginsPath == null) {
735
      messages.add(const ValidationMessage.error('Invalid IntelliJ version number.'));
736 737
    } else {
      messages.add(ValidationMessage(userMessages.intellijLocation(installPath)));
738

739 740 741 742
      final IntelliJPlugins plugins = IntelliJPlugins(pluginsPath);
      plugins.validatePackage(messages, <String>['flutter-intellij', 'flutter-intellij.jar'],
          'Flutter', minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
      plugins.validatePackage(messages, <String>['Dart'], 'Dart');
743

744 745 746
      if (_hasIssues(messages)) {
        messages.add(ValidationMessage(userMessages.intellijPluginInfo));
      }
747

748 749
      _validateIntelliJVersion(messages, kMinIdeaVersion);
    }
750

751
    return ValidationResult(
752 753
      _hasIssues(messages) ? ValidationType.partial : ValidationType.installed,
      messages,
754
      statusInfo: userMessages.intellijStatusInfo(version));
755 756
  }

757 758 759 760 761 762
  bool _hasIssues(List<ValidationMessage> messages) {
    return messages.any((ValidationMessage message) => message.isError);
  }

  void _validateIntelliJVersion(List<ValidationMessage> messages, Version minVersion) {
    // Ignore unknown versions.
763
    if (minVersion == Version.unknown) {
764
      return;
765
    }
766

767
    final Version installedVersion = Version.parse(version);
768
    if (installedVersion == null) {
769
      return;
770
    }
771 772

    if (installedVersion < minVersion) {
773
      messages.add(ValidationMessage.error(userMessages.intellijMinimumVersion(minVersion.toString())));
774 775
    }
  }
776 777
}

778
class IntelliJValidatorOnLinuxAndWindows extends IntelliJValidator {
779
  IntelliJValidatorOnLinuxAndWindows(String title, this.version, String installPath, this.pluginsPath) : super(title, installPath);
780 781

  @override
782
  final String version;
783 784

  @override
785
  final String pluginsPath;
786 787

  static Iterable<DoctorValidator> get installed {
788
    final List<DoctorValidator> validators = <DoctorValidator>[];
789
    if (globals.fsUtils.homeDirPath == null) {
790
      return validators;
791
    }
792 793

    void addValidator(String title, String version, String installPath, String pluginsPath) {
794
      final IntelliJValidatorOnLinuxAndWindows validator =
795
        IntelliJValidatorOnLinuxAndWindows(title, version, installPath, pluginsPath);
796
      for (int index = 0; index < validators.length; ++index) {
797
        final DoctorValidator other = validators[index];
798
        if (other is IntelliJValidatorOnLinuxAndWindows && validator.installPath == other.installPath) {
799
          if (validator.version.compareTo(other.version) > 0) {
800
            validators[index] = validator;
801
          }
802 803 804 805 806 807
          return;
        }
      }
      validators.add(validator);
    }

808 809 810 811 812 813 814 815 816
    final Directory homeDir = globals.fs.directory(globals.fsUtils.homeDirPath);
    for (final Directory dir in homeDir.listSync().whereType<Directory>()) {
      final String name = globals.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 = globals.fs.file(globals.fs.path.join(dir.path, 'system', '.home')).readAsStringSync();
817
          } on Exception {
818
            // ignored
819
          }
820 821 822 823 824 825
          if (installPath != null && globals.fs.isDirectorySync(installPath)) {
            final String pluginsPath = globals.fs.path.join(dir.path, 'config', 'plugins');
            addValidator(title, version, installPath, pluginsPath);
          }
        }
      });
826 827 828 829 830 831
    }
    return validators;
  }
}

class IntelliJValidatorOnMac extends IntelliJValidator {
832
  IntelliJValidatorOnMac(String title, this.id, String installPath) : super(title, installPath);
833 834 835 836

  final String id;

  static final Map<String, String> _dirNameToId = <String, String>{
837 838 839
    'IntelliJ IDEA.app': 'IntelliJIdea',
    'IntelliJ IDEA Ultimate.app': 'IntelliJIdea',
    'IntelliJ IDEA CE.app': 'IdeaIC',
840 841 842
  };

  static Iterable<DoctorValidator> get installed {
843
    final List<DoctorValidator> validators = <DoctorValidator>[];
844 845 846 847
    final List<String> installPaths = <String>[
      '/Applications',
      globals.fs.path.join(globals.fsUtils.homeDirPath, 'Applications'),
    ];
848 849

    void checkForIntelliJ(Directory dir) {
850
      final String name = globals.fs.path.basename(dir.path);
851 852
      _dirNameToId.forEach((String dirName, String id) {
        if (name == dirName) {
853
          final String title = IntelliJValidator._idToTitle[id];
854
          validators.add(IntelliJValidatorOnMac(title, id, dir.path));
855 856 857 858
        }
      });
    }

859
    try {
860
      final Iterable<Directory> installDirs = installPaths
861
              .map<Directory>((String installPath) => globals.fs.directory(installPath))
862 863
              .map<List<FileSystemEntity>>((Directory dir) => dir.existsSync() ? dir.listSync() : <FileSystemEntity>[])
              .expand<FileSystemEntity>((List<FileSystemEntity> mappedDirs) => mappedDirs)
864
              .whereType<Directory>();
865
      for (final Directory dir in installDirs) {
866 867
        checkForIntelliJ(dir);
        if (!dir.path.endsWith('.app')) {
868
          for (final FileSystemEntity subdir in dir.listSync()) {
869 870
            if (subdir is Directory) {
              checkForIntelliJ(subdir);
871
            }
872
          }
873
        }
874
      }
875
    } on FileSystemException catch (e) {
876
      validators.add(ValidatorWithResult(
877
          userMessages.intellijMacUnknownResult,
878
          ValidationResult(ValidationType.missing, <ValidationMessage>[
879
            ValidationMessage.error(e.message),
880 881
          ]),
      ));
882 883 884 885
    }
    return validators;
  }

886 887 888 889 890 891 892
  @visibleForTesting
  String get plistFile {
    _plistFile ??= globals.fs.path.join(installPath, 'Contents', 'Info.plist');
    return _plistFile;
  }
  String _plistFile;

893 894
  @override
  String get version {
895
    _version ??= globals.plistParser.getValueFromFile(
896
        plistFile,
897
        PlistParser.kCFBundleShortVersionStringKey,
898
      ) ?? 'unknown';
899 900 901 902 903 904
    return _version;
  }
  String _version;

  @override
  String get pluginsPath {
905 906 907 908
    if (_pluginsPath != null) {
      return _pluginsPath;
    }

909 910
    final String altLocation = globals.plistParser
      .getValueFromFile(plistFile, 'JetBrainsToolboxApp');
911 912 913 914 915 916

    if (altLocation != null) {
      _pluginsPath = altLocation + '.plugins';
      return _pluginsPath;
    }

917
    final List<String> split = version.split('.');
918 919 920
    if (split.length < 2) {
      return null;
    }
921 922
    final String major = split[0];
    final String minor = split[1];
923 924 925 926

    final String homeDirPath = globals.fsUtils.homeDirPath;
    String pluginsPath = globals.fs.path.join(
      homeDirPath,
927 928
      'Library',
      'Application Support',
929
      'JetBrains',
930
      '$id$major.$minor',
931
      'plugins',
932
    );
933 934 935 936 937 938 939 940 941 942 943
    // Fallback to legacy location from < 2020.
    if (!globals.fs.isDirectorySync(pluginsPath)) {
      pluginsPath = globals.fs.path.join(
        homeDirPath,
        'Library',
        'Application Support',
        '$id$major.$minor',
      );
    }
    _pluginsPath = pluginsPath;

944
    return _pluginsPath;
945
  }
946
  String _pluginsPath;
947 948
}

949
class DeviceValidator extends DoctorValidator {
950
  DeviceValidator() : super('Connected device');
951

952 953 954
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

955 956
  @override
  Future<ValidationResult> validate() async {
957
    final List<Device> devices = await deviceManager.getAllConnectedDevices();
958 959
    List<ValidationMessage> messages;
    if (devices.isEmpty) {
960 961
      final List<String> diagnostics = await deviceManager.getDeviceDiagnostics();
      if (diagnostics.isNotEmpty) {
962
        messages = diagnostics.map<ValidationMessage>((String message) => ValidationMessage(message)).toList();
963
      } else {
964
        messages = <ValidationMessage>[ValidationMessage.hint(userMessages.devicesMissing)];
965
      }
966
    } else {
967
      messages = await Device.descriptions(devices)
968
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
969
    }
970 971

    if (devices.isEmpty) {
972
      return ValidationResult(ValidationType.notAvailable, messages);
973
    } else {
974
      return ValidationResult(ValidationType.installed, messages, statusInfo: userMessages.devicesAvailable(devices.length));
975
    }
976 977
  }
}
978 979 980 981

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

982 983
  final ValidationResult result;

984 985 986
  @override
  Future<ValidationResult> validate() async => result;
}