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

5 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/os.dart';
19
import 'base/platform.dart';
20
import 'base/terminal.dart';
21
import 'base/user_messages.dart';
22
import 'base/utils.dart';
23
import 'cache.dart';
24
import 'custom_devices/custom_device_workflow.dart';
25
import 'device.dart';
26
import 'doctor_validator.dart';
27
import 'features.dart';
28
import 'fuchsia/fuchsia_workflow.dart';
29
import 'globals.dart' as globals;
30
import 'http_host_validator.dart';
31
import 'intellij/intellij_validator.dart';
32
import 'linux/linux_doctor.dart';
33 34
import 'linux/linux_workflow.dart';
import 'macos/macos_workflow.dart';
35
import 'macos/xcode_validator.dart';
36
import 'proxy_validator.dart';
37
import 'reporting/reporting.dart';
38
import 'tester/flutter_tester.dart';
39
import 'version.dart';
40
import 'vscode/vscode_validator.dart';
41
import 'web/chrome.dart';
42 43
import 'web/web_validator.dart';
import 'web/workflow.dart';
44
import 'windows/visual_studio_validator.dart';
45
import 'windows/windows_version_validator.dart';
46
import 'windows/windows_workflow.dart';
Devon Carew's avatar
Devon Carew committed
47

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  final Logger _logger;
227 228

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

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

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

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

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

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

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

311
      if (result.statusInfo != null) {
312
        lineBuffer.write(' (${result.statusInfo})');
313
      }
314

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

323 324 325 326 327 328 329 330
      if (result.type != ValidationType.installed) {
        missingComponent = true;
      }
    }

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

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

    return buffer.toString();
  }

341
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
342
    return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
343 344
  }

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

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

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

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

390
      switch (result.type) {
391 392 393 394
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
          break;
395 396 397 398 399 400 401 402 403 404
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
          break;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
          break;
        case ValidationType.installed:
          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 == true) {
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

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

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

553 554 555 556 557 558 559 560 561 562 563 564
    ValidationType valid;
    if (messages.every((ValidationMessage message) => message.isInformation)) {
      valid = ValidationType.installed;
    } 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),
      );
    }
565

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

578
  ValidationMessage _getFlutterVersionMessage(String frameworkVersion, String versionChannel) {
579
    String flutterVersionMessage = _userMessages.flutterVersion(frameworkVersion, versionChannel, _flutterRoot());
580 581 582 583 584

    // The tool sets the channel as "unknown", if the current branch is on a
    // "detached HEAD" state or doesn't have an upstream, 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.
585 586 587 588 589 590 591 592
    if (versionChannel != 'unknown' && frameworkVersion != '0.0.0-unknown') {
      return ValidationMessage(flutterVersionMessage);
    }
    if (versionChannel == 'unknown') {
      flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}';
    }
    if (frameworkVersion == '0.0.0-unknown') {
      flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownVersion}';
593
    }
594
    return ValidationMessage.hint(flutterVersionMessage);
595 596 597 598 599 600 601 602 603 604
  }

  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')) {
605
        return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUnknown);
606 607 608 609 610 611 612 613 614 615 616 617
      }
      // 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!));
  }

618 619 620 621 622 623 624
  bool _genSnapshotRuns(String genSnapshotPath) {
    const int kExpectedExitCode = 255;
    try {
      return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
    } on Exception {
      return false;
    }
625 626 627
  }
}

628
class DeviceValidator extends DoctorValidator {
629 630
  // TODO(jmagman): Make required once g3 rolls and is updated.
  DeviceValidator({
631 632 633
    DeviceManager? deviceManager,
    UserMessages? userMessages,
  }) : _deviceManager = deviceManager ?? globals.deviceManager!,
634 635 636 637 638
       _userMessages = userMessages ?? globals.userMessages,
       super('Connected device');

  final DeviceManager _deviceManager;
  final UserMessages _userMessages;
639

640 641 642
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

643 644
  @override
  Future<ValidationResult> validate() async {
645 646 647
    final List<Device> devices = await _deviceManager.getAllConnectedDevices();
    List<ValidationMessage> installedMessages = <ValidationMessage>[];
    if (devices.isNotEmpty) {
648
      installedMessages = (await Device.descriptions(devices))
649
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
650
    }
651

652 653 654 655 656 657 658 659
    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)];
    }

660
    if (devices.isEmpty) {
661 662 663 664 665 666 667 668
      return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
    } else if (diagnostics.isNotEmpty) {
      installedMessages.addAll(diagnosticMessages);
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
669
    } else {
670 671 672 673 674
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
675
    }
676 677
  }
}
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697

/// 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 {
698
      await _doctor.diagnose(startedValidatorTasks: _validatorTasks, showPii: showPii, sendEvent: _sendDoctorEvent);
699 700 701 702 703 704 705 706 707 708
      // 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';
    }
  }
}