doctor.dart 21.6 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 323
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
324
    AndroidLicenseValidator? androidLicenseValidator,
325 326 327
    bool showPii = true,
    List<ValidatorTask>? startedValidatorTasks,
    bool sendEvent = true,
328 329 330
  }) async {
    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 385 386
        if (message.type != ValidationMessageType.information || verbose == true) {
          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
    ValidationType valid = ValidationType.installed;
471 472
    String? versionChannel;
    String? frameworkVersion;
473 474

    try {
475
      final FlutterVersion version = _flutterVersion();
476 477
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
478
      messages.add(ValidationMessage(_userMessages.flutterVersion(
479
        frameworkVersion,
480
        _flutterRoot(),
481
      )));
482
      messages.add(ValidationMessage(_userMessages.flutterUpstreamRepositoryUrl(version.repositoryUrl ?? 'unknown')));
483 484 485
      final String? gitUrl = _platform.environment['FLUTTER_GIT_URL'];
      if (gitUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterGitUrl(gitUrl)));
486
      }
487
      messages.add(ValidationMessage(_userMessages.flutterRevision(
488 489
        version.frameworkRevisionShort,
        version.frameworkAge,
490
        version.frameworkCommitDate,
491
      )));
492 493
      messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort)));
      messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion)));
494
      messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
495 496 497
      final String? pubUrl = _platform.environment['PUB_HOSTED_URL'];
      if (pubUrl != null) {
        messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
498
      }
499 500 501
      final String? storageBaseUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
      if (storageBaseUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
502
      }
503 504 505 506
    } on VersionCheckError catch (e) {
      messages.add(ValidationMessage.error(e.message));
      valid = ValidationType.partial;
    }
507

508
    // Check that the binaries we downloaded for this platform actually run on it.
509 510 511 512 513 514 515 516
    // 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);
517
      }
518
      messages.add(ValidationMessage.error(buffer.toString()));
519
      valid = ValidationType.partial;
520
    }
521

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

534 535 536 537 538 539 540
  bool _genSnapshotRuns(String genSnapshotPath) {
    const int kExpectedExitCode = 255;
    try {
      return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
    } on Exception {
      return false;
    }
541 542 543
  }
}

544
class DeviceValidator extends DoctorValidator {
545 546
  // TODO(jmagman): Make required once g3 rolls and is updated.
  DeviceValidator({
547 548 549
    DeviceManager? deviceManager,
    UserMessages? userMessages,
  }) : _deviceManager = deviceManager ?? globals.deviceManager!,
550 551 552 553 554
       _userMessages = userMessages ?? globals.userMessages,
       super('Connected device');

  final DeviceManager _deviceManager;
  final UserMessages _userMessages;
555

556 557 558
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

559 560
  @override
  Future<ValidationResult> validate() async {
561 562 563
    final List<Device> devices = await _deviceManager.getAllConnectedDevices();
    List<ValidationMessage> installedMessages = <ValidationMessage>[];
    if (devices.isNotEmpty) {
564
      installedMessages = (await Device.descriptions(devices))
565
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
566
    }
567

568 569 570 571 572 573 574 575
    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)];
    }

576
    if (devices.isEmpty) {
577 578 579 580 581 582 583 584
      return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
    } else if (diagnostics.isNotEmpty) {
      installedMessages.addAll(diagnosticMessages);
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
585
    } else {
586 587 588 589 590
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
591
    }
592 593
  }
}
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624

/// 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 {
      await _doctor.diagnose(showColor: false, startedValidatorTasks: _validatorTasks, showPii: showPii, sendEvent: _sendDoctorEvent);
      // 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';
    }
  }
}