doctor.dart 23.4 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 'device.dart';
25
import 'doctor_validator.dart';
26
import 'features.dart';
27
import 'fuchsia/fuchsia_workflow.dart';
28
import 'globals.dart' as globals;
29
import 'http_host_validator.dart';
30
import 'intellij/intellij_validator.dart';
31
import 'linux/linux_doctor.dart';
32 33
import 'linux/linux_workflow.dart';
import 'macos/macos_workflow.dart';
34
import 'macos/xcode_validator.dart';
35
import 'proxy_validator.dart';
36
import 'reporting/reporting.dart';
37
import 'tester/flutter_tester.dart';
38
import 'version.dart';
39
import 'vscode/vscode_validator.dart';
40
import 'web/chrome.dart';
41 42
import 'web/web_validator.dart';
import 'web/workflow.dart';
43
import 'windows/visual_studio_validator.dart';
44
import 'windows/windows_workflow.dart';
Devon Carew's avatar
Devon Carew committed
45

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

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

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

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

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

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

70 71 72 73 74
  final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
    platform: globals.platform,
    featureFlags: featureFlags,
  );

75
  @override
76
  List<DoctorValidator> get validators {
77
    if (_validators != null) {
78
      return _validators!;
79
    }
80 81

    final List<DoctorValidator> ideValidators = <DoctorValidator>[
82
      if (androidWorkflow!.appliesToHostPlatform)
83
        ...AndroidStudioValidator.allValidators(globals.config, globals.platform, globals.fs, globals.userMessages),
84 85 86 87 88
      ...IntelliJValidator.installedValidators(
        fileSystem: globals.fs,
        platform: globals.platform,
        userMessages: userMessages,
        plistParser: globals.plistParser,
89
        processManager: globals.processManager,
90
      ),
91
      ...VsCodeValidator.installedValidators(globals.fs, globals.platform, globals.processManager),
92
    ];
93
    final ProxyValidator proxyValidator = ProxyValidator(platform: globals.platform);
94
    _validators = <DoctorValidator>[
95 96 97 98
      FlutterValidator(
        fileSystem: globals.fs,
        platform: globals.platform,
        flutterVersion: () => globals.flutterVersion,
99
        devToolsVersion: () => globals.cache.devToolsVersion,
100 101
        processManager: globals.processManager,
        userMessages: userMessages,
102 103
        artifacts: globals.artifacts!,
        flutterRoot: () => Cache.flutterRoot!,
104 105
        operatingSystemUtils: globals.os,
      ),
106 107 108 109
      if (androidWorkflow!.appliesToHostPlatform)
        GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
      if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
        GroupedValidator(<DoctorValidator>[XcodeValidator(xcode: globals.xcode!, userMessages: userMessages), globals.cocoapodsValidator!]),
110
      if (webWorkflow.appliesToHostPlatform)
111 112 113 114 115 116 117
        ChromeValidator(
          chromiumLauncher: ChromiumLauncher(
            browserFinder: findChromeExecutable,
            fileSystem: globals.fs,
            operatingSystemUtils: globals.os,
            platform:  globals.platform,
            processManager: globals.processManager,
118
            logger: globals.logger,
119
          ),
120 121
          platform: globals.platform,
        ),
122
      if (linuxWorkflow.appliesToHostPlatform)
123 124
        LinuxDoctorValidator(
          processManager: globals.processManager,
125
          userMessages: userMessages,
126
        ),
127 128
      if (windowsWorkflow!.appliesToHostPlatform)
        visualStudioValidator!,
129 130 131 132
      if (ideValidators.isNotEmpty)
        ...ideValidators
      else
        NoIdeValidator(),
133 134
      if (proxyValidator.shouldShow)
        proxyValidator,
135
      if (globals.deviceManager?.canListAnything ?? false)
136 137 138 139
        DeviceValidator(
          deviceManager: globals.deviceManager,
          userMessages: globals.userMessages,
        ),
140 141 142 143 144
      HttpHostValidator(
        platform: globals.platform,
        featureFlags: featureFlags,
        httpClient: globals.httpClientFactory?.call() ?? HttpClient(),
      ),
145
    ];
146
    return _validators!;
147
  }
148 149 150

  @override
  List<Workflow> get workflows {
151 152 153
    if (_workflows == null) {
      _workflows = <Workflow>[];

154 155
      if (globals.iosWorkflow!.appliesToHostPlatform) {
        _workflows!.add(globals.iosWorkflow!);
156
      }
157

158
      if (androidWorkflow?.appliesToHostPlatform ?? false) {
159
        _workflows!.add(androidWorkflow!);
160
      }
161

162
      if (fuchsiaWorkflow?.appliesToHostPlatform ?? false) {
163
        _workflows!.add(fuchsiaWorkflow!);
164
      }
165

166
      if (linuxWorkflow.appliesToHostPlatform) {
167
        _workflows!.add(linuxWorkflow);
168
      }
169

170
      if (macOSWorkflow.appliesToHostPlatform) {
171
        _workflows!.add(macOSWorkflow);
172
      }
173

174
      if (windowsWorkflow?.appliesToHostPlatform ?? false) {
175
        _workflows!.add(windowsWorkflow!);
176
      }
177

178
      if (webWorkflow.appliesToHostPlatform) {
179
        _workflows!.add(webWorkflow);
180
      }
181

182
    }
183
    return _workflows!;
184
  }
185 186 187
}

class Doctor {
188
  Doctor({
189
    required Logger logger,
190 191 192
  }) : _logger = logger;

  final Logger _logger;
193 194

  List<DoctorValidator> get validators {
195
    return DoctorValidatorsProvider._instance.validators;
196
  }
197

198 199
  /// Return a list of [ValidatorTask] objects and starts validation on all
  /// objects in [validators].
200
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
201
    for (final DoctorValidator validator in validators)
202 203 204 205 206 207 208
      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>(
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
          () {
            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;
            });
          },
226 227 228 229 230
          onError: (Object exception, StackTrace stackTrace) {
            return ValidationResult.crash(exception, stackTrace);
          },
        ),
      ),
231
    ];
232

233
  List<Workflow> get workflows {
234
    return DoctorValidatorsProvider._instance.workflows;
235
  }
236

237
  /// Print a summary of the state of the tooling, as well as how to get more info.
238
  Future<void> summary() async {
239
    _logger.printStatus(await _summaryText());
240
  }
241

242
  Future<String> _summaryText() async {
243
    final StringBuffer buffer = StringBuffer();
244

245 246
    bool missingComponent = false;
    bool sawACrash = false;
247

248
    for (final DoctorValidator validator in validators) {
249
      final StringBuffer lineBuffer = StringBuffer();
250 251 252
      ValidationResult result;
      try {
        result = await asyncGuard<ValidationResult>(() => validator.validate());
253
      } on Exception catch (exception) {
254 255 256 257
        // We're generating a summary, so drop the stack trace.
        result = ValidationResult.crash(exception);
      }
      lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
258
      switch (result.type) {
259 260 261 262
        case ValidationType.crash:
          lineBuffer.write('the doctor check crashed without a result.');
          sawACrash = true;
          break;
263
        case ValidationType.missing:
264
          lineBuffer.write('is not installed.');
265 266
          break;
        case ValidationType.partial:
267
          lineBuffer.write('is partially installed; more components are available.');
268 269
          break;
        case ValidationType.notAvailable:
270
          lineBuffer.write('is not available.');
271 272
          break;
        case ValidationType.installed:
273
          lineBuffer.write('is fully installed.');
274 275
          break;
      }
276

277
      if (result.statusInfo != null) {
278
        lineBuffer.write(' (${result.statusInfo})');
279
      }
280

281 282 283
      buffer.write(wrapText(
        lineBuffer.toString(),
        hangingIndent: result.leadingBox.length + 1,
284 285
        columnWidth: globals.outputPreferences.wrapColumn,
        shouldWrap: globals.outputPreferences.wrapText,
286
      ));
287 288
      buffer.writeln();

289 290 291 292 293 294 295 296
      if (result.type != ValidationType.installed) {
        missingComponent = true;
      }
    }

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

299
    if (missingComponent) {
300
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
301
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
302 303 304 305 306
    }

    return buffer.toString();
  }

307
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
308
    return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
309 310
  }

311 312 313 314 315
  /// Maximum allowed duration for an entire validator to take.
  ///
  /// This should only ever be reached if a process is stuck.
  static const Duration doctorDuration = Duration(minutes: 10);

316
  /// Print information about the state of installed tooling.
317 318 319
  ///
  /// To exclude personally identifiable information like device names and
  /// paths, set [showPii] to false.
320 321 322
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
323
    AndroidLicenseValidator? androidLicenseValidator,
324 325 326
    bool showPii = true,
    List<ValidatorTask>? startedValidatorTasks,
    bool sendEvent = true,
327
  }) async {
328
    final bool showColor = globals.terminal.supportsColor;
329 330
    if (androidLicenses && androidLicenseValidator != null) {
      return androidLicenseValidator.runLicenseManager();
331
    }
332

333
    if (!verbose) {
334
      _logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):');
335
    }
336
    bool doctorResult = true;
337
    int issues = 0;
338

339
    for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) {
340
      final DoctorValidator validator = validatorTask.validator;
341
      final Status status = _logger.startSpinner(
342
        timeout: validator.slowWarningDuration,
343 344
        slowWarningCallback: () => validator.slowWarning,
      );
345
      ValidationResult result;
346 347
      try {
        result = await validatorTask.result;
348
        status.stop();
349 350 351
      } on Exception catch (exception, stackTrace) {
        result = ValidationResult.crash(exception, stackTrace);
        status.cancel();
352
      }
353

354
      switch (result.type) {
355 356 357 358
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
          break;
359 360 361 362 363 364 365 366 367 368
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
          break;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
          break;
        case ValidationType.installed:
          break;
369
      }
370 371 372
      if (sendEvent) {
        DoctorResultEvent(validator: validator, result: result).send();
      }
373

374
      final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
375
      if (result.statusInfo != null) {
376
        _logger.printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
377 378
            hangingIndent: result.leadingBox.length + 1);
      } else {
379
        _logger.printStatus('$leadingBox ${validator.title}',
380 381
            hangingIndent: result.leadingBox.length + 1);
      }
382

383
      for (final ValidationMessage message in result.messages) {
384
        if (!message.isInformation || verbose == true) {
385 386
          int hangingIndent = 2;
          int indent = 4;
387
          final String indicator = showColor ? message.coloredIndicator : message.indicator;
388
          for (final String line in '$indicator ${showPii ? message.message : message.piiStrippedMessage}'.split('\n')) {
389
            _logger.printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
390 391 392
            // Only do hanging indent for the first line.
            hangingIndent = 0;
            indent = 6;
393
          }
394 395 396
          if (message.contextUrl != null) {
            _logger.printStatus('🔨 ${message.contextUrl}', hangingIndent: hangingIndent, indent: indent, emphasis: true);
          }
397 398
        }
      }
399
      if (verbose) {
400
        _logger.printStatus('');
401
      }
402
    }
403

404
    // Make sure there's always one line before the summary even when not verbose.
405
    if (!verbose) {
406
      _logger.printStatus('');
407
    }
408

409
    if (issues > 0) {
410
      _logger.printStatus('${showColor ? globals.terminal.color('!', TerminalColor.yellow) : '!'}'
411
        ' Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
412
    } else {
413
      _logger.printStatus('${showColor ? globals.terminal.color('•', TerminalColor.green) : '•'}'
414
        ' No issues found!', hangingIndent: 2);
415
    }
416 417

    return doctorResult;
418 419 420 421
  }

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

422
  bool get canLaunchAnything {
423
    if (FlutterTesterDevices.showFlutterTesterDevice) {
424
      return true;
425
    }
426 427
    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
  }
428 429
}

430
/// A validator that checks the version of Flutter, as well as some auxiliary information
431 432 433 434
/// such as the pub or Flutter cache overrides.
///
/// This is primarily useful for diagnosing issues on Github bug reports by displaying
/// specific commit information.
435
class FlutterValidator extends DoctorValidator {
436
  FlutterValidator({
437 438
    required Platform platform,
    required FlutterVersion Function() flutterVersion,
439
    required String Function() devToolsVersion,
440 441 442 443 444 445
    required UserMessages userMessages,
    required FileSystem fileSystem,
    required Artifacts artifacts,
    required ProcessManager processManager,
    required String Function() flutterRoot,
    required OperatingSystemUtils operatingSystemUtils,
446
  }) : _flutterVersion = flutterVersion,
447
       _devToolsVersion = devToolsVersion,
448 449 450 451 452 453 454 455 456 457 458
       _platform = platform,
       _userMessages = userMessages,
       _fileSystem = fileSystem,
       _artifacts = artifacts,
       _processManager = processManager,
       _flutterRoot = flutterRoot,
       _operatingSystemUtils = operatingSystemUtils,
       super('Flutter');

  final Platform _platform;
  final FlutterVersion Function() _flutterVersion;
459
  final String Function() _devToolsVersion;
460 461 462 463 464 465
  final String Function() _flutterRoot;
  final UserMessages _userMessages;
  final FileSystem _fileSystem;
  final Artifacts _artifacts;
  final ProcessManager _processManager;
  final OperatingSystemUtils _operatingSystemUtils;
466

467
  @override
468
  Future<ValidationResult> validate() async {
469
    final List<ValidationMessage> messages = <ValidationMessage>[];
470 471
    String? versionChannel;
    String? frameworkVersion;
472 473

    try {
474
      final FlutterVersion version = _flutterVersion();
475
      final String? gitUrl = _platform.environment['FLUTTER_GIT_URL'];
476 477
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
478 479 480

      messages.add(_getFlutterVersionMessage(frameworkVersion, versionChannel));
      messages.add(_getFlutterUpstreamMessage(version));
481 482
      if (gitUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterGitUrl(gitUrl)));
483
      }
484
      messages.add(ValidationMessage(_userMessages.flutterRevision(
485 486
        version.frameworkRevisionShort,
        version.frameworkAge,
487
        version.frameworkCommitDate,
488
      )));
489 490
      messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort)));
      messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion)));
491
      messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
492 493 494
      final String? pubUrl = _platform.environment['PUB_HOSTED_URL'];
      if (pubUrl != null) {
        messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
495
      }
496 497 498
      final String? storageBaseUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
      if (storageBaseUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
499
      }
500 501 502
    } on VersionCheckError catch (e) {
      messages.add(ValidationMessage.error(e.message));
    }
503

504
    // Check that the binaries we downloaded for this platform actually run on it.
505 506 507 508 509 510 511 512
    // 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);
513
      }
514
      messages.add(ValidationMessage.error(buffer.toString()));
515
    }
516

517 518 519 520
    final ValidationType valid = messages.every((ValidationMessage message) => message.isInformation)
      ? ValidationType.installed
      : ValidationType.partial;

521 522 523
    return ValidationResult(
      valid,
      messages,
524
      statusInfo: _userMessages.flutterStatusInfo(
525 526
        versionChannel,
        frameworkVersion,
527 528
        _operatingSystemUtils.name,
        _platform.localeName,
529
      ),
530
    );
531
  }
Devon Carew's avatar
Devon Carew committed
532

533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
  ValidationMessage _getFlutterVersionMessage(String frameworkVersion, String versionChannel) {
    final String flutterVersionMessage = _userMessages.flutterVersion(frameworkVersion, versionChannel, _flutterRoot());

    // 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.
    if (versionChannel == 'unknown' || frameworkVersion == '0.0.0-unknown') {
      return ValidationMessage.hint(flutterVersionMessage);
    }
    return ValidationMessage(flutterVersionMessage);
  }

  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')) {
        return ValidationMessage.hint(_userMessages.flutterUpstreamRepositoryUrl('unknown'));
      }
      // 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!));
  }

567 568 569 570 571 572 573
  bool _genSnapshotRuns(String genSnapshotPath) {
    const int kExpectedExitCode = 255;
    try {
      return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
    } on Exception {
      return false;
    }
574 575 576
  }
}

577
class DeviceValidator extends DoctorValidator {
578 579
  // TODO(jmagman): Make required once g3 rolls and is updated.
  DeviceValidator({
580 581 582
    DeviceManager? deviceManager,
    UserMessages? userMessages,
  }) : _deviceManager = deviceManager ?? globals.deviceManager!,
583 584 585 586 587
       _userMessages = userMessages ?? globals.userMessages,
       super('Connected device');

  final DeviceManager _deviceManager;
  final UserMessages _userMessages;
588

589 590 591
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

592 593
  @override
  Future<ValidationResult> validate() async {
594 595 596
    final List<Device> devices = await _deviceManager.getAllConnectedDevices();
    List<ValidationMessage> installedMessages = <ValidationMessage>[];
    if (devices.isNotEmpty) {
597
      installedMessages = (await Device.descriptions(devices))
598
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
599
    }
600

601 602 603 604 605 606 607 608
    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)];
    }

609
    if (devices.isEmpty) {
610 611 612 613 614 615 616 617
      return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
    } else if (diagnostics.isNotEmpty) {
      installedMessages.addAll(diagnosticMessages);
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
618
    } else {
619 620 621 622 623
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
624
    }
625 626
  }
}
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646

/// 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 {
647
      await _doctor.diagnose(startedValidatorTasks: _validatorTasks, showPii: showPii, sendEvent: _sendDoctorEvent);
648 649 650 651 652 653 654 655 656 657
      // 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';
    }
  }
}