doctor.dart 27.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 6
import 'dart:async';

7
import 'package:meta/meta.dart';
8
import 'package:process/process.dart';
9

10
import 'android/android_studio_validator.dart';
11
import 'android/android_workflow.dart';
12
import 'artifacts.dart';
13
import 'base/async_guard.dart';
14
import 'base/context.dart';
15
import 'base/file_system.dart';
16
import 'base/io.dart';
17
import 'base/logger.dart';
18
import 'base/net.dart';
19
import 'base/os.dart';
20
import 'base/platform.dart';
21
import 'base/terminal.dart';
22
import 'base/user_messages.dart';
23
import 'base/utils.dart';
24
import 'cache.dart';
25
import 'custom_devices/custom_device_workflow.dart';
26
import 'device.dart';
27
import 'doctor_validator.dart';
28
import 'features.dart';
29
import 'fuchsia/fuchsia_workflow.dart';
30
import 'globals.dart' as globals;
31
import 'http_host_validator.dart';
32
import 'intellij/intellij_validator.dart';
33
import 'linux/linux_doctor.dart';
34 35
import 'linux/linux_workflow.dart';
import 'macos/macos_workflow.dart';
36
import 'macos/xcode_validator.dart';
37
import 'proxy_validator.dart';
38
import 'reporting/reporting.dart';
39
import 'tester/flutter_tester.dart';
40
import 'version.dart';
41
import 'vscode/vscode_validator.dart';
42
import 'web/chrome.dart';
43 44
import 'web/web_validator.dart';
import 'web/workflow.dart';
45
import 'windows/visual_studio_validator.dart';
46
import 'windows/windows_version_validator.dart';
47
import 'windows/windows_workflow.dart';
Devon Carew's avatar
Devon Carew committed
48

49
abstract class DoctorValidatorsProvider {
50 51 52 53 54 55 56 57 58 59 60
  // Allow tests to construct a [_DefaultDoctorValidatorsProvider] with explicit
  // [FeatureFlags].
  factory DoctorValidatorsProvider.test({
    Platform? platform,
    required FeatureFlags featureFlags,
  }) {
    return _DefaultDoctorValidatorsProvider(
      featureFlags: featureFlags,
      platform: platform ?? FakePlatform(),
    );
  }
61
  /// The singleton instance, pulled from the [AppContext].
62
  static DoctorValidatorsProvider get _instance => context.get<DoctorValidatorsProvider>()!;
63

64 65 66 67
  static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider(
    platform: globals.platform,
    featureFlags: featureFlags,
  );
68 69

  List<DoctorValidator> get validators;
70
  List<Workflow> get workflows;
71 72
}

73
class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
74 75 76 77 78
  _DefaultDoctorValidatorsProvider({
    required this.platform,
    required this.featureFlags,
  });

79 80
  List<DoctorValidator>? _validators;
  List<Workflow>? _workflows;
81 82
  final Platform platform;
  final FeatureFlags featureFlags;
83

84 85
  late final LinuxWorkflow linuxWorkflow = LinuxWorkflow(
    platform: platform,
86 87 88
    featureFlags: featureFlags,
  );

89 90
  late final WebWorkflow webWorkflow = WebWorkflow(
    platform: platform,
91 92 93
    featureFlags: featureFlags,
  );

94 95
  late final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
    platform: platform,
96 97 98
    featureFlags: featureFlags,
  );

99 100 101 102
  late final CustomDeviceWorkflow customDeviceWorkflow = CustomDeviceWorkflow(
    featureFlags: featureFlags,
  );

103
  @override
104
  List<DoctorValidator> get validators {
105
    if (_validators != null) {
106
      return _validators!;
107
    }
108 109

    final List<DoctorValidator> ideValidators = <DoctorValidator>[
110
      if (androidWorkflow!.appliesToHostPlatform)
111
        ...AndroidStudioValidator.allValidators(globals.config, platform, globals.fs, globals.userMessages),
112 113
      ...IntelliJValidator.installedValidators(
        fileSystem: globals.fs,
114
        platform: platform,
115 116
        userMessages: userMessages,
        plistParser: globals.plistParser,
117
        processManager: globals.processManager,
118
      ),
119
      ...VsCodeValidator.installedValidators(globals.fs, platform, globals.processManager),
120
    ];
121
    final ProxyValidator proxyValidator = ProxyValidator(platform: platform);
122
    _validators = <DoctorValidator>[
123 124 125
      FlutterValidator(
        fileSystem: globals.fs,
        platform: globals.platform,
126
        flutterVersion: () => globals.flutterVersion.fetchTagsAndGetVersion(clock: globals.systemClock),
127
        devToolsVersion: () => globals.cache.devToolsVersion,
128 129
        processManager: globals.processManager,
        userMessages: userMessages,
130 131
        artifacts: globals.artifacts!,
        flutterRoot: () => Cache.flutterRoot!,
132 133
        operatingSystemUtils: globals.os,
      ),
134 135
      if (platform.isWindows)
        WindowsVersionValidator(
136
          operatingSystemUtils: globals.os,
137
        ),
138 139 140 141
      if (androidWorkflow!.appliesToHostPlatform)
        GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
      if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
        GroupedValidator(<DoctorValidator>[XcodeValidator(xcode: globals.xcode!, userMessages: userMessages), globals.cocoapodsValidator!]),
142
      if (webWorkflow.appliesToHostPlatform)
143 144 145 146 147 148 149
        ChromeValidator(
          chromiumLauncher: ChromiumLauncher(
            browserFinder: findChromeExecutable,
            fileSystem: globals.fs,
            operatingSystemUtils: globals.os,
            platform:  globals.platform,
            processManager: globals.processManager,
150
            logger: globals.logger,
151
          ),
152 153
          platform: globals.platform,
        ),
154
      if (linuxWorkflow.appliesToHostPlatform)
155 156
        LinuxDoctorValidator(
          processManager: globals.processManager,
157
          userMessages: userMessages,
158
        ),
159 160
      if (windowsWorkflow!.appliesToHostPlatform)
        visualStudioValidator!,
161 162 163 164
      if (ideValidators.isNotEmpty)
        ...ideValidators
      else
        NoIdeValidator(),
165 166
      if (proxyValidator.shouldShow)
        proxyValidator,
167
      if (globals.deviceManager?.canListAnything ?? false)
168 169 170 171
        DeviceValidator(
          deviceManager: globals.deviceManager,
          userMessages: globals.userMessages,
        ),
172 173 174 175 176
      HttpHostValidator(
        platform: globals.platform,
        featureFlags: featureFlags,
        httpClient: globals.httpClientFactory?.call() ?? HttpClient(),
      ),
177
    ];
178
    return _validators!;
179
  }
180 181 182

  @override
  List<Workflow> get workflows {
183 184 185
    if (_workflows == null) {
      _workflows = <Workflow>[];

186 187
      if (globals.iosWorkflow!.appliesToHostPlatform) {
        _workflows!.add(globals.iosWorkflow!);
188
      }
189

190
      if (androidWorkflow?.appliesToHostPlatform ?? false) {
191
        _workflows!.add(androidWorkflow!);
192
      }
193

194
      if (fuchsiaWorkflow?.appliesToHostPlatform ?? false) {
195
        _workflows!.add(fuchsiaWorkflow!);
196
      }
197

198
      if (linuxWorkflow.appliesToHostPlatform) {
199
        _workflows!.add(linuxWorkflow);
200
      }
201

202
      if (macOSWorkflow.appliesToHostPlatform) {
203
        _workflows!.add(macOSWorkflow);
204
      }
205

206
      if (windowsWorkflow?.appliesToHostPlatform ?? false) {
207
        _workflows!.add(windowsWorkflow!);
208
      }
209

210
      if (webWorkflow.appliesToHostPlatform) {
211
        _workflows!.add(webWorkflow);
212
      }
213

214 215 216
      if (customDeviceWorkflow.appliesToHostPlatform) {
        _workflows!.add(customDeviceWorkflow);
      }
217
    }
218
    return _workflows!;
219
  }
220 221 222
}

class Doctor {
223
  Doctor({
224
    required Logger logger,
225 226 227
  }) : _logger = logger;

  final Logger _logger;
228 229

  List<DoctorValidator> get validators {
230
    return DoctorValidatorsProvider._instance.validators;
231
  }
232

233 234
  /// Return a list of [ValidatorTask] objects and starts validation on all
  /// objects in [validators].
235
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
236
    for (final DoctorValidator validator in validators)
237 238 239 240 241 242 243
      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>(
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
          () {
            final Completer<ValidationResult> timeoutCompleter = Completer<ValidationResult>();
            final Timer timer = Timer(doctorDuration, () {
              timeoutCompleter.completeError(
                Exception('${validator.title} exceeded maximum allowed duration of $doctorDuration'),
              );
            });
            final Future<ValidationResult> validatorFuture = validator.validate();
            return Future.any<ValidationResult>(<Future<ValidationResult>>[
              validatorFuture,
              // This future can only complete with an error
              timeoutCompleter.future,
            ]).then((ValidationResult result) async {
              timer.cancel();
              return result;
            });
          },
261 262 263 264 265
          onError: (Object exception, StackTrace stackTrace) {
            return ValidationResult.crash(exception, stackTrace);
          },
        ),
      ),
266
    ];
267

268
  List<Workflow> get workflows {
269
    return DoctorValidatorsProvider._instance.workflows;
270
  }
271

272
  /// Print a summary of the state of the tooling, as well as how to get more info.
273
  Future<void> summary() async {
274
    _logger.printStatus(await _summaryText());
275
  }
276

277
  Future<String> _summaryText() async {
278
    final StringBuffer buffer = StringBuffer();
279

280 281
    bool missingComponent = false;
    bool sawACrash = false;
282

283
    for (final DoctorValidator validator in validators) {
284
      final StringBuffer lineBuffer = StringBuffer();
285 286 287
      ValidationResult result;
      try {
        result = await asyncGuard<ValidationResult>(() => validator.validate());
288
      } on Exception catch (exception) {
289 290 291 292
        // We're generating a summary, so drop the stack trace.
        result = ValidationResult.crash(exception);
      }
      lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
293
      switch (result.type) {
294 295 296
        case ValidationType.crash:
          lineBuffer.write('the doctor check crashed without a result.');
          sawACrash = true;
297
        case ValidationType.missing:
298
          lineBuffer.write('is not installed.');
299
        case ValidationType.partial:
300
          lineBuffer.write('is partially installed; more components are available.');
301
        case ValidationType.notAvailable:
302
          lineBuffer.write('is not available.');
303
        case ValidationType.success:
304
          lineBuffer.write('is fully installed.');
305
      }
306

307
      if (result.statusInfo != null) {
308
        lineBuffer.write(' (${result.statusInfo})');
309
      }
310

311 312 313
      buffer.write(wrapText(
        lineBuffer.toString(),
        hangingIndent: result.leadingBox.length + 1,
314 315
        columnWidth: globals.outputPreferences.wrapColumn,
        shouldWrap: globals.outputPreferences.wrapText,
316
      ));
317 318
      buffer.writeln();

319
      if (result.type != ValidationType.success) {
320 321 322 323 324 325 326
        missingComponent = true;
      }
    }

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

329
    if (missingComponent) {
330
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
331
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
332 333 334 335 336
    }

    return buffer.toString();
  }

337
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
338
    return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
339 340
  }

341 342 343
  /// Maximum allowed duration for an entire validator to take.
  ///
  /// This should only ever be reached if a process is stuck.
344 345 346
  // Reduce this to under 5 minutes to diagnose:
  // https://github.com/flutter/flutter/issues/111686
  static const Duration doctorDuration = Duration(minutes: 4, seconds: 30);
347

348
  /// Print information about the state of installed tooling.
349 350 351
  ///
  /// To exclude personally identifiable information like device names and
  /// paths, set [showPii] to false.
352 353 354
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
355
    AndroidLicenseValidator? androidLicenseValidator,
356 357 358
    bool showPii = true,
    List<ValidatorTask>? startedValidatorTasks,
    bool sendEvent = true,
359
  }) async {
360
    final bool showColor = globals.terminal.supportsColor;
361 362
    if (androidLicenses && androidLicenseValidator != null) {
      return androidLicenseValidator.runLicenseManager();
363
    }
364

365
    if (!verbose) {
366
      _logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):');
367
    }
368
    bool doctorResult = true;
369
    int issues = 0;
370

371
    for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) {
372
      final DoctorValidator validator = validatorTask.validator;
373
      final Status status = _logger.startSpinner(
374
        timeout: validator.slowWarningDuration,
375 376
        slowWarningCallback: () => validator.slowWarning,
      );
377
      ValidationResult result;
378 379
      try {
        result = await validatorTask.result;
380
        status.stop();
381 382 383
      } on Exception catch (exception, stackTrace) {
        result = ValidationResult.crash(exception, stackTrace);
        status.cancel();
384
      }
385

386
      switch (result.type) {
387 388 389
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
390 391 392 393 394 395
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
396
        case ValidationType.success:
397
          break;
398
      }
399 400 401
      if (sendEvent) {
        DoctorResultEvent(validator: validator, result: result).send();
      }
402

403
      final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
404
      if (result.statusInfo != null) {
405
        _logger.printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
406 407
            hangingIndent: result.leadingBox.length + 1);
      } else {
408
        _logger.printStatus('$leadingBox ${validator.title}',
409 410
            hangingIndent: result.leadingBox.length + 1);
      }
411

412
      for (final ValidationMessage message in result.messages) {
413
        if (!message.isInformation || verbose) {
414 415
          int hangingIndent = 2;
          int indent = 4;
416
          final String indicator = showColor ? message.coloredIndicator : message.indicator;
417
          for (final String line in '$indicator ${showPii ? message.message : message.piiStrippedMessage}'.split('\n')) {
418
            _logger.printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
419 420 421
            // Only do hanging indent for the first line.
            hangingIndent = 0;
            indent = 6;
422
          }
423 424 425
          if (message.contextUrl != null) {
            _logger.printStatus('🔨 ${message.contextUrl}', hangingIndent: hangingIndent, indent: indent, emphasis: true);
          }
426 427
        }
      }
428
      if (verbose) {
429
        _logger.printStatus('');
430
      }
431
    }
432

433
    // Make sure there's always one line before the summary even when not verbose.
434
    if (!verbose) {
435
      _logger.printStatus('');
436
    }
437

438
    if (issues > 0) {
439
      _logger.printStatus('${showColor ? globals.terminal.color('!', TerminalColor.yellow) : '!'}'
440
        ' Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
441
    } else {
442
      _logger.printStatus('${showColor ? globals.terminal.color('•', TerminalColor.green) : '•'}'
443
        ' No issues found!', hangingIndent: 2);
444
    }
445 446

    return doctorResult;
447 448 449 450
  }

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

451
  bool get canLaunchAnything {
452
    if (FlutterTesterDevices.showFlutterTesterDevice) {
453
      return true;
454
    }
455 456
    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
  }
457 458
}

459
/// A validator that checks the version of Flutter, as well as some auxiliary information
460 461 462 463
/// such as the pub or Flutter cache overrides.
///
/// This is primarily useful for diagnosing issues on Github bug reports by displaying
/// specific commit information.
464
class FlutterValidator extends DoctorValidator {
465
  FlutterValidator({
466 467
    required Platform platform,
    required FlutterVersion Function() flutterVersion,
468
    required String Function() devToolsVersion,
469 470 471 472 473 474
    required UserMessages userMessages,
    required FileSystem fileSystem,
    required Artifacts artifacts,
    required ProcessManager processManager,
    required String Function() flutterRoot,
    required OperatingSystemUtils operatingSystemUtils,
475
  }) : _flutterVersion = flutterVersion,
476
       _devToolsVersion = devToolsVersion,
477 478 479 480 481 482 483 484 485 486 487
       _platform = platform,
       _userMessages = userMessages,
       _fileSystem = fileSystem,
       _artifacts = artifacts,
       _processManager = processManager,
       _flutterRoot = flutterRoot,
       _operatingSystemUtils = operatingSystemUtils,
       super('Flutter');

  final Platform _platform;
  final FlutterVersion Function() _flutterVersion;
488
  final String Function() _devToolsVersion;
489 490 491 492 493 494
  final String Function() _flutterRoot;
  final UserMessages _userMessages;
  final FileSystem _fileSystem;
  final Artifacts _artifacts;
  final ProcessManager _processManager;
  final OperatingSystemUtils _operatingSystemUtils;
495

496
  @override
497
  Future<ValidationResult> validate() async {
498
    final List<ValidationMessage> messages = <ValidationMessage>[];
499 500
    String? versionChannel;
    String? frameworkVersion;
501 502

    try {
503
      final FlutterVersion version = _flutterVersion();
504
      final String? gitUrl = _platform.environment['FLUTTER_GIT_URL'];
505 506
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
507

508 509 510 511
      final String flutterRoot = _flutterRoot();
      messages.add(_getFlutterVersionMessage(frameworkVersion, versionChannel, flutterRoot));

      _validateRequiredBinaries(flutterRoot).forEach(messages.add);
512
      messages.add(_getFlutterUpstreamMessage(version));
513 514
      if (gitUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterGitUrl(gitUrl)));
515
      }
516
      messages.add(ValidationMessage(_userMessages.flutterRevision(
517 518
        version.frameworkRevisionShort,
        version.frameworkAge,
519
        version.frameworkCommitDate,
520
      )));
521 522
      messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort)));
      messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion)));
523
      messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
524
      final String? pubUrl = _platform.environment[kPubDevOverride];
525 526
      if (pubUrl != null) {
        messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
527
      }
528
      final String? storageBaseUrl = _platform.environment[kFlutterStorageBaseUrl];
529 530
      if (storageBaseUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
531
      }
532 533 534
    } on VersionCheckError catch (e) {
      messages.add(ValidationMessage.error(e.message));
    }
535

536
    // Check that the binaries we downloaded for this platform actually run on it.
537 538 539 540 541 542 543 544
    // If the binaries are not downloaded (because android is not enabled), then do
    // not run this check.
    final String genSnapshotPath = _artifacts.getArtifactPath(Artifact.genSnapshot);
    if (_fileSystem.file(genSnapshotPath).existsSync() && !_genSnapshotRuns(genSnapshotPath)) {
      final StringBuffer buffer = StringBuffer();
      buffer.writeln(_userMessages.flutterBinariesDoNotRun);
      if (_platform.isLinux) {
        buffer.writeln(_userMessages.flutterBinariesLinuxRepairCommands);
545 546 547
      } else if (_platform.isMacOS && _operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm64) {
        buffer.writeln('Flutter requires the Rosetta translation environment on ARM Macs. Try running:');
        buffer.writeln('  sudo softwareupdate --install-rosetta --agree-to-license');
548
      }
549
      messages.add(ValidationMessage.error(buffer.toString()));
550
    }
551

552 553
    ValidationType valid;
    if (messages.every((ValidationMessage message) => message.isInformation)) {
554
      valid = ValidationType.success;
555 556 557 558 559 560 561 562 563
    } else {
      // The issues for this validator stem from broken git configuration of the local install;
      // in that case, make it clear that it is fine to continue, but freshness check/upgrades
      // won't be supported.
      valid = ValidationType.partial;
      messages.add(
        ValidationMessage(_userMessages.flutterValidatorErrorIntentional),
      );
    }
564

565 566 567
    return ValidationResult(
      valid,
      messages,
568
      statusInfo: _userMessages.flutterStatusInfo(
569 570
        versionChannel,
        frameworkVersion,
571 572
        _operatingSystemUtils.name,
        _platform.localeName,
573
      ),
574
    );
575
  }
Devon Carew's avatar
Devon Carew committed
576

577 578
  ValidationMessage _getFlutterVersionMessage(String frameworkVersion, String versionChannel, String flutterRoot) {
    String flutterVersionMessage = _userMessages.flutterVersion(frameworkVersion, versionChannel, flutterRoot);
579

580 581 582 583 584
    // The tool sets the channel as kUserBranch, if the current branch is on a
    // "detached HEAD" state, doesn't have an upstream, or is on a user branch,
    // and sets the frameworkVersion as "0.0.0-unknown" if "git describe" on
    // HEAD doesn't produce an expected format to be parsed for the frameworkVersion.
    if (versionChannel != kUserBranch && frameworkVersion != '0.0.0-unknown') {
585 586
      return ValidationMessage(flutterVersionMessage);
    }
587
    if (versionChannel == kUserBranch) {
588 589 590 591
      flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}';
    }
    if (frameworkVersion == '0.0.0-unknown') {
      flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownVersion}';
592
    }
593
    return ValidationMessage.hint(flutterVersionMessage);
594 595
  }

596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
  List<ValidationMessage> _validateRequiredBinaries(String flutterRoot) {
    final ValidationMessage? flutterWarning = _validateSdkBinary('flutter', flutterRoot);
    final ValidationMessage? dartWarning = _validateSdkBinary('dart', flutterRoot);
    return <ValidationMessage>[
      if (flutterWarning != null) flutterWarning,
      if (dartWarning != null) dartWarning,
    ];
  }

  /// Return a warning if the provided [binary] on the user's path does not
  /// resolve within the Flutter SDK.
  ValidationMessage? _validateSdkBinary(String binary, String flutterRoot) {
    final String flutterBinDir = _fileSystem.path.join(flutterRoot, 'bin');

    final File? flutterBin = _operatingSystemUtils.which(binary);
    if (flutterBin == null) {
      return ValidationMessage.hint(
        'The $binary binary is not on your path. Consider adding '
        '$flutterBinDir to your path.',
      );
    }
    final String resolvedFlutterPath = flutterBin.resolveSymbolicLinksSync();
618
    if (!_filePathContainsDirPath(flutterRoot, resolvedFlutterPath)) {
619 620 621 622 623 624 625 626 627
      final String hint = 'Warning: `$binary` on your path resolves to '
          '$resolvedFlutterPath, which is not inside your current Flutter '
          'SDK checkout at $flutterRoot. Consider adding $flutterBinDir to '
          'the front of your path.';
      return ValidationMessage.hint(hint);
    }
    return null;
  }

628 629 630
  bool _filePathContainsDirPath(String directory, String file) {
    // calling .canonicalize() will normalize for alphabetic case and path
    // separators
631
    return _fileSystem.path.canonicalize(file)
632 633 634
        .startsWith(_fileSystem.path.canonicalize(directory) + _fileSystem.path.separator);
  }

635 636 637 638 639 640 641 642
  ValidationMessage _getFlutterUpstreamMessage(FlutterVersion version) {
    final String? repositoryUrl = version.repositoryUrl;
    final VersionCheckError? upstreamValidationError = VersionUpstreamValidator(version: version, platform: _platform).run();

    // VersionUpstreamValidator can produce an error if repositoryUrl is null
    if (upstreamValidationError != null) {
      final String errorMessage = upstreamValidationError.message;
      if (errorMessage.contains('could not determine the remote upstream which is being tracked')) {
643
        return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUnknown);
644 645 646 647 648 649 650 651 652 653 654 655
      }
      // At this point, repositoryUrl must not be null
      if (errorMessage.contains('Flutter SDK is tracking a non-standard remote')) {
        return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUrlNonStandard(repositoryUrl!));
      }
      if (errorMessage.contains('Either remove "FLUTTER_GIT_URL" from the environment or set it to')){
        return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUrlEnvMismatch(repositoryUrl!));
      }
    }
    return ValidationMessage(_userMessages.flutterUpstreamRepositoryUrl(repositoryUrl!));
  }

656 657 658 659 660 661 662
  bool _genSnapshotRuns(String genSnapshotPath) {
    const int kExpectedExitCode = 255;
    try {
      return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
    } on Exception {
      return false;
    }
663 664 665
  }
}

666
class DeviceValidator extends DoctorValidator {
667 668
  // TODO(jmagman): Make required once g3 rolls and is updated.
  DeviceValidator({
669 670 671
    DeviceManager? deviceManager,
    UserMessages? userMessages,
  }) : _deviceManager = deviceManager ?? globals.deviceManager!,
672 673 674 675 676
       _userMessages = userMessages ?? globals.userMessages,
       super('Connected device');

  final DeviceManager _deviceManager;
  final UserMessages _userMessages;
677

678 679 680
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

681 682
  @override
  Future<ValidationResult> validate() async {
683 684 685
    final List<Device> devices = await _deviceManager.refreshAllDevices(
      timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
    );
686 687
    List<ValidationMessage> installedMessages = <ValidationMessage>[];
    if (devices.isNotEmpty) {
688
      installedMessages = (await Device.descriptions(devices))
689
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
690
    }
691

692 693 694 695 696 697 698 699
    List<ValidationMessage> diagnosticMessages = <ValidationMessage>[];
    final List<String> diagnostics = await _deviceManager.getDeviceDiagnostics();
    if (diagnostics.isNotEmpty) {
      diagnosticMessages = diagnostics.map<ValidationMessage>((String message) => ValidationMessage.hint(message)).toList();
    } else if (devices.isEmpty) {
      diagnosticMessages = <ValidationMessage>[ValidationMessage.hint(_userMessages.devicesMissing)];
    }

700
    if (devices.isEmpty) {
701 702 703 704
      return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
    } else if (diagnostics.isNotEmpty) {
      installedMessages.addAll(diagnosticMessages);
      return ValidationResult(
705
        ValidationType.success,
706 707 708
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
709
    } else {
710
      return ValidationResult(
711
        ValidationType.success,
712 713 714
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
715
    }
716 717
  }
}
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737

/// Wrapper for doctor to run multiple times with PII and without, running the validators only once.
class DoctorText {
  DoctorText(
    BufferLogger logger, {
    @visibleForTesting Doctor? doctor,
  }) : _doctor = doctor ?? Doctor(logger: logger), _logger = logger;

  final BufferLogger _logger;
  final Doctor _doctor;
  bool _sendDoctorEvent = true;

  late final Future<String> text = _runDiagnosis(true);
  late final Future<String> piiStrippedText = _runDiagnosis(false);

  // Start the validator tasks only once.
  late final List<ValidatorTask> _validatorTasks = _doctor.startValidatorTasks();

  Future<String> _runDiagnosis(bool showPii) async {
    try {
738
      await _doctor.diagnose(startedValidatorTasks: _validatorTasks, showPii: showPii, sendEvent: _sendDoctorEvent);
739 740 741 742 743 744 745 746 747 748
      // Do not send the doctor event a second time.
      _sendDoctorEvent = false;
      final String text = _logger.statusText;
      _logger.clear();
      return text;
    } on Exception catch (error, trace) {
      return 'encountered exception: $error\n\n${trace.toString().trim()}\n';
    }
  }
}