doctor.dart 27.3 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
      if (androidWorkflow!.appliesToHostPlatform)
        GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
      if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
141 142 143 144 145 146 147 148
        GroupedValidator(<DoctorValidator>[
          XcodeValidator(
            xcode: globals.xcode!,
            userMessages: userMessages,
            iosSimulatorUtils: globals.iosSimulatorUtils!,
          ),
          globals.cocoapodsValidator!,
        ]),
149
      if (webWorkflow.appliesToHostPlatform)
150 151 152 153 154 155 156
        ChromeValidator(
          chromiumLauncher: ChromiumLauncher(
            browserFinder: findChromeExecutable,
            fileSystem: globals.fs,
            operatingSystemUtils: globals.os,
            platform:  globals.platform,
            processManager: globals.processManager,
157
            logger: globals.logger,
158
          ),
159 160
          platform: globals.platform,
        ),
161
      if (linuxWorkflow.appliesToHostPlatform)
162 163
        LinuxDoctorValidator(
          processManager: globals.processManager,
164
          userMessages: userMessages,
165
        ),
166 167
      if (windowsWorkflow!.appliesToHostPlatform)
        visualStudioValidator!,
168 169 170 171
      if (ideValidators.isNotEmpty)
        ...ideValidators
      else
        NoIdeValidator(),
172 173
      if (proxyValidator.shouldShow)
        proxyValidator,
174
      if (globals.deviceManager?.canListAnything ?? false)
175 176 177 178
        DeviceValidator(
          deviceManager: globals.deviceManager,
          userMessages: globals.userMessages,
        ),
179 180 181 182 183
      HttpHostValidator(
        platform: globals.platform,
        featureFlags: featureFlags,
        httpClient: globals.httpClientFactory?.call() ?? HttpClient(),
      ),
184
    ];
185
    return _validators!;
186
  }
187 188 189

  @override
  List<Workflow> get workflows {
190 191 192
    if (_workflows == null) {
      _workflows = <Workflow>[];

193 194
      if (globals.iosWorkflow!.appliesToHostPlatform) {
        _workflows!.add(globals.iosWorkflow!);
195
      }
196

197
      if (androidWorkflow?.appliesToHostPlatform ?? false) {
198
        _workflows!.add(androidWorkflow!);
199
      }
200

201
      if (fuchsiaWorkflow?.appliesToHostPlatform ?? false) {
202
        _workflows!.add(fuchsiaWorkflow!);
203
      }
204

205
      if (linuxWorkflow.appliesToHostPlatform) {
206
        _workflows!.add(linuxWorkflow);
207
      }
208

209
      if (macOSWorkflow.appliesToHostPlatform) {
210
        _workflows!.add(macOSWorkflow);
211
      }
212

213
      if (windowsWorkflow?.appliesToHostPlatform ?? false) {
214
        _workflows!.add(windowsWorkflow!);
215
      }
216

217
      if (webWorkflow.appliesToHostPlatform) {
218
        _workflows!.add(webWorkflow);
219
      }
220

221 222 223
      if (customDeviceWorkflow.appliesToHostPlatform) {
        _workflows!.add(customDeviceWorkflow);
      }
224
    }
225
    return _workflows!;
226
  }
227 228 229
}

class Doctor {
230
  Doctor({
231
    required Logger logger,
232 233 234
  }) : _logger = logger;

  final Logger _logger;
235 236

  List<DoctorValidator> get validators {
237
    return DoctorValidatorsProvider._instance.validators;
238
  }
239

240 241
  /// Return a list of [ValidatorTask] objects and starts validation on all
  /// objects in [validators].
242
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
243
    for (final DoctorValidator validator in validators)
244 245 246 247 248 249 250
      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>(
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
          () {
            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;
            });
          },
268 269 270 271 272
          onError: (Object exception, StackTrace stackTrace) {
            return ValidationResult.crash(exception, stackTrace);
          },
        ),
      ),
273
    ];
274

275
  List<Workflow> get workflows {
276
    return DoctorValidatorsProvider._instance.workflows;
277
  }
278

279
  /// Print a summary of the state of the tooling, as well as how to get more info.
280
  Future<void> summary() async {
281
    _logger.printStatus(await _summaryText());
282
  }
283

284
  Future<String> _summaryText() async {
285
    final StringBuffer buffer = StringBuffer();
286

287 288
    bool missingComponent = false;
    bool sawACrash = false;
289

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

314
      if (result.statusInfo != null) {
315
        lineBuffer.write(' (${result.statusInfo})');
316
      }
317

318 319 320
      buffer.write(wrapText(
        lineBuffer.toString(),
        hangingIndent: result.leadingBox.length + 1,
321 322
        columnWidth: globals.outputPreferences.wrapColumn,
        shouldWrap: globals.outputPreferences.wrapText,
323
      ));
324 325
      buffer.writeln();

326
      if (result.type != ValidationType.success) {
327 328 329 330 331 332 333
        missingComponent = true;
      }
    }

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

336
    if (missingComponent) {
337
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
338
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
339 340 341 342 343
    }

    return buffer.toString();
  }

344
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
345
    return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
346 347
  }

348 349 350
  /// Maximum allowed duration for an entire validator to take.
  ///
  /// This should only ever be reached if a process is stuck.
351 352 353
  // Reduce this to under 5 minutes to diagnose:
  // https://github.com/flutter/flutter/issues/111686
  static const Duration doctorDuration = Duration(minutes: 4, seconds: 30);
354

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

372
    if (!verbose) {
373
      _logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):');
374
    }
375
    bool doctorResult = true;
376
    int issues = 0;
377

378
    for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) {
379
      final DoctorValidator validator = validatorTask.validator;
380
      final Status status = _logger.startSpinner(
381
        timeout: validator.slowWarningDuration,
382 383
        slowWarningCallback: () => validator.slowWarning,
      );
384
      ValidationResult result;
385 386
      try {
        result = await validatorTask.result;
387
        status.stop();
388 389 390
      } on Exception catch (exception, stackTrace) {
        result = ValidationResult.crash(exception, stackTrace);
        status.cancel();
391
      }
392

393
      switch (result.type) {
394 395 396
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
397 398 399 400 401 402
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
403
        case ValidationType.success:
404
          break;
405
      }
406 407 408
      if (sendEvent) {
        DoctorResultEvent(validator: validator, result: result).send();
      }
409

410
      final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
411
      if (result.statusInfo != null) {
412
        _logger.printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
413 414
            hangingIndent: result.leadingBox.length + 1);
      } else {
415
        _logger.printStatus('$leadingBox ${validator.title}',
416 417
            hangingIndent: result.leadingBox.length + 1);
      }
418

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

440
    // Make sure there's always one line before the summary even when not verbose.
441
    if (!verbose) {
442
      _logger.printStatus('');
443
    }
444

445
    if (issues > 0) {
446
      _logger.printStatus('${showColor ? globals.terminal.color('!', TerminalColor.yellow) : '!'}'
447
        ' Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
448
    } else {
449
      _logger.printStatus('${showColor ? globals.terminal.color('•', TerminalColor.green) : '•'}'
450
        ' No issues found!', hangingIndent: 2);
451
    }
452 453

    return doctorResult;
454 455 456 457
  }

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

458
  bool get canLaunchAnything {
459
    if (FlutterTesterDevices.showFlutterTesterDevice) {
460
      return true;
461
    }
462 463
    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
  }
464 465
}

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

  final Platform _platform;
  final FlutterVersion Function() _flutterVersion;
495
  final String Function() _devToolsVersion;
496 497 498 499 500 501
  final String Function() _flutterRoot;
  final UserMessages _userMessages;
  final FileSystem _fileSystem;
  final Artifacts _artifacts;
  final ProcessManager _processManager;
  final OperatingSystemUtils _operatingSystemUtils;
502

503
  @override
504
  Future<ValidationResult> validate() async {
505
    final List<ValidationMessage> messages = <ValidationMessage>[];
506 507
    String? versionChannel;
    String? frameworkVersion;
508 509

    try {
510
      final FlutterVersion version = _flutterVersion();
511
      final String? gitUrl = _platform.environment['FLUTTER_GIT_URL'];
512 513
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
514

515 516 517 518
      final String flutterRoot = _flutterRoot();
      messages.add(_getFlutterVersionMessage(frameworkVersion, versionChannel, flutterRoot));

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

543
    // Check that the binaries we downloaded for this platform actually run on it.
544 545 546 547 548 549 550 551
    // 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);
552 553 554
      } 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');
555
      }
556
      messages.add(ValidationMessage.error(buffer.toString()));
557
    }
558

559 560
    ValidationType valid;
    if (messages.every((ValidationMessage message) => message.isInformation)) {
561
      valid = ValidationType.success;
562 563 564 565 566 567 568 569 570
    } 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),
      );
    }
571

572 573 574
    return ValidationResult(
      valid,
      messages,
575
      statusInfo: _userMessages.flutterStatusInfo(
576 577
        versionChannel,
        frameworkVersion,
578 579
        _operatingSystemUtils.name,
        _platform.localeName,
580
      ),
581
    );
582
  }
Devon Carew's avatar
Devon Carew committed
583

584 585
  ValidationMessage _getFlutterVersionMessage(String frameworkVersion, String versionChannel, String flutterRoot) {
    String flutterVersionMessage = _userMessages.flutterVersion(frameworkVersion, versionChannel, flutterRoot);
586

587 588 589 590 591
    // 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') {
592 593
      return ValidationMessage(flutterVersionMessage);
    }
594
    if (versionChannel == kUserBranch) {
595 596 597 598
      flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}';
    }
    if (frameworkVersion == '0.0.0-unknown') {
      flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownVersion}';
599
    }
600
    return ValidationMessage.hint(flutterVersionMessage);
601 602
  }

603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
  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();
625
    if (!_filePathContainsDirPath(flutterRoot, resolvedFlutterPath)) {
626 627 628 629 630 631 632 633 634
      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;
  }

635 636 637
  bool _filePathContainsDirPath(String directory, String file) {
    // calling .canonicalize() will normalize for alphabetic case and path
    // separators
638
    return _fileSystem.path.canonicalize(file)
639 640 641
        .startsWith(_fileSystem.path.canonicalize(directory) + _fileSystem.path.separator);
  }

642 643 644 645 646 647 648 649
  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')) {
650
        return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUnknown);
651 652 653 654 655 656 657 658 659 660 661 662
      }
      // 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!));
  }

663 664 665 666 667 668 669
  bool _genSnapshotRuns(String genSnapshotPath) {
    const int kExpectedExitCode = 255;
    try {
      return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
    } on Exception {
      return false;
    }
670 671 672
  }
}

673
class DeviceValidator extends DoctorValidator {
674 675
  // TODO(jmagman): Make required once g3 rolls and is updated.
  DeviceValidator({
676 677 678
    DeviceManager? deviceManager,
    UserMessages? userMessages,
  }) : _deviceManager = deviceManager ?? globals.deviceManager!,
679 680 681 682 683
       _userMessages = userMessages ?? globals.userMessages,
       super('Connected device');

  final DeviceManager _deviceManager;
  final UserMessages _userMessages;
684

685 686 687
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

688 689
  @override
  Future<ValidationResult> validate() async {
690 691 692
    final List<Device> devices = await _deviceManager.refreshAllDevices(
      timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
    );
693 694
    List<ValidationMessage> installedMessages = <ValidationMessage>[];
    if (devices.isNotEmpty) {
695
      installedMessages = (await Device.descriptions(devices))
696
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
697
    }
698

699 700 701 702 703 704 705 706
    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)];
    }

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

/// 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 {
745
      await _doctor.diagnose(startedValidatorTasks: _validatorTasks, showPii: showPii, sendEvent: _sendDoctorEvent);
746 747 748 749 750 751 752 753 754 755
      // 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';
    }
  }
}