doctor.dart 18.9 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
import 'package:process/process.dart';
6

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

41 42
abstract class DoctorValidatorsProvider {
  /// The singleton instance, pulled from the [AppContext].
43
  static DoctorValidatorsProvider get _instance => context.get<DoctorValidatorsProvider>()!;
44

45
  static final DoctorValidatorsProvider defaultInstance = _DefaultDoctorValidatorsProvider();
46 47

  List<DoctorValidator> get validators;
48
  List<Workflow> get workflows;
49 50
}

51
class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
52 53
  List<DoctorValidator>? _validators;
  List<Workflow>? _workflows;
54

55 56 57 58 59 60 61 62 63 64
  final LinuxWorkflow linuxWorkflow = LinuxWorkflow(
    platform: globals.platform,
    featureFlags: featureFlags,
  );

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

65 66 67 68 69
  final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
    platform: globals.platform,
    featureFlags: featureFlags,
  );

70
  @override
71
  List<DoctorValidator> get validators {
72
    if (_validators != null) {
73
      return _validators!;
74
    }
75 76

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

  @override
  List<Workflow> get workflows {
141 142 143
    if (_workflows == null) {
      _workflows = <Workflow>[];

144 145
      if (globals.iosWorkflow!.appliesToHostPlatform) {
        _workflows!.add(globals.iosWorkflow!);
146
      }
147

148 149
      if (androidWorkflow?.appliesToHostPlatform == true) {
        _workflows!.add(androidWorkflow!);
150
      }
151

152 153
      if (fuchsiaWorkflow?.appliesToHostPlatform == true) {
        _workflows!.add(fuchsiaWorkflow!);
154
      }
155

156
      if (linuxWorkflow.appliesToHostPlatform) {
157
        _workflows!.add(linuxWorkflow);
158
      }
159

160
      if (macOSWorkflow.appliesToHostPlatform) {
161
        _workflows!.add(macOSWorkflow);
162
      }
163

164 165
      if (windowsWorkflow?.appliesToHostPlatform == true) {
        _workflows!.add(windowsWorkflow!);
166
      }
167

168
      if (webWorkflow.appliesToHostPlatform) {
169
        _workflows!.add(webWorkflow);
170
      }
171

172
    }
173
    return _workflows!;
174
  }
175 176 177
}

class Doctor {
178
  Doctor({
179
    required Logger logger,
180 181 182
  }) : _logger = logger;

  final Logger _logger;
183 184

  List<DoctorValidator> get validators {
185
    return DoctorValidatorsProvider._instance.validators;
186
  }
187

188 189
  /// Return a list of [ValidatorTask] objects and starts validation on all
  /// objects in [validators].
190
  List<ValidatorTask> startValidatorTasks() => <ValidatorTask>[
191
    for (final DoctorValidator validator in validators)
192 193 194 195 196 197 198 199 200 201 202 203 204 205
      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>(
          validator.validate,
          onError: (Object exception, StackTrace stackTrace) {
            return ValidationResult.crash(exception, stackTrace);
          },
        ),
      ),
  ];
206

207
  List<Workflow> get workflows {
208
    return DoctorValidatorsProvider._instance.workflows;
209
  }
210

211
  /// Print a summary of the state of the tooling, as well as how to get more info.
212
  Future<void> summary() async {
213
    _logger.printStatus(await _summaryText());
214
  }
215

216
  Future<String> _summaryText() async {
217
    final StringBuffer buffer = StringBuffer();
218

219 220
    bool missingComponent = false;
    bool sawACrash = false;
221

222
    for (final DoctorValidator validator in validators) {
223
      final StringBuffer lineBuffer = StringBuffer();
224 225 226
      ValidationResult result;
      try {
        result = await asyncGuard<ValidationResult>(() => validator.validate());
227
      } on Exception catch (exception) {
228 229 230 231
        // We're generating a summary, so drop the stack trace.
        result = ValidationResult.crash(exception);
      }
      lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
232
      switch (result.type) {
233 234 235 236
        case ValidationType.crash:
          lineBuffer.write('the doctor check crashed without a result.');
          sawACrash = true;
          break;
237
        case ValidationType.missing:
238
          lineBuffer.write('is not installed.');
239 240
          break;
        case ValidationType.partial:
241
          lineBuffer.write('is partially installed; more components are available.');
242 243
          break;
        case ValidationType.notAvailable:
244
          lineBuffer.write('is not available.');
245 246
          break;
        case ValidationType.installed:
247
          lineBuffer.write('is fully installed.');
248 249
          break;
      }
250

251
      if (result.statusInfo != null) {
252
        lineBuffer.write(' (${result.statusInfo})');
253
      }
254

255 256 257
      buffer.write(wrapText(
        lineBuffer.toString(),
        hangingIndent: result.leadingBox.length + 1,
258 259
        columnWidth: globals.outputPreferences.wrapColumn,
        shouldWrap: globals.outputPreferences.wrapText,
260
      ));
261 262
      buffer.writeln();

263 264 265 266 267 268 269 270
      if (result.type != ValidationType.installed) {
        missingComponent = true;
      }
    }

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

273
    if (missingComponent) {
274
      buffer.writeln();
Devon Carew's avatar
Devon Carew committed
275
      buffer.writeln('Run "flutter doctor" for information about installing additional components.');
276 277 278 279 280
    }

    return buffer.toString();
  }

281
  Future<bool> checkRemoteArtifacts(String engineRevision) async {
282
    return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision);
283 284
  }

285
  /// Print information about the state of installed tooling.
286 287 288 289
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
290
    AndroidLicenseValidator? androidLicenseValidator,
291 292 293
  }) async {
    if (androidLicenses && androidLicenseValidator != null) {
      return androidLicenseValidator.runLicenseManager();
294
    }
295

296
    if (!verbose) {
297
      _logger.printStatus('Doctor summary (to see all details, run flutter doctor -v):');
298
    }
299
    bool doctorResult = true;
300
    int issues = 0;
301

302
    for (final ValidatorTask validatorTask in startValidatorTasks()) {
303
      final DoctorValidator validator = validatorTask.validator;
304
      final Status status = _logger.startSpinner();
305
      ValidationResult result;
306 307
      try {
        result = await validatorTask.result;
308
        status.stop();
309 310 311
      } on Exception catch (exception, stackTrace) {
        result = ValidationResult.crash(exception, stackTrace);
        status.cancel();
312
      }
313

314
      switch (result.type) {
315 316 317 318
        case ValidationType.crash:
          doctorResult = false;
          issues += 1;
          break;
319 320 321 322 323 324 325 326 327 328
        case ValidationType.missing:
          doctorResult = false;
          issues += 1;
          break;
        case ValidationType.partial:
        case ValidationType.notAvailable:
          issues += 1;
          break;
        case ValidationType.installed:
          break;
329
      }
330

331
      DoctorResultEvent(validator: validator, result: result).send();
332

333
      final String leadingBox = showColor ? result.coloredLeadingBox : result.leadingBox;
334
      if (result.statusInfo != null) {
335
        _logger.printStatus('$leadingBox ${validator.title} (${result.statusInfo})',
336 337
            hangingIndent: result.leadingBox.length + 1);
      } else {
338
        _logger.printStatus('$leadingBox ${validator.title}',
339 340
            hangingIndent: result.leadingBox.length + 1);
      }
341

342
      for (final ValidationMessage message in result.messages) {
343 344 345
        if (message.type != ValidationMessageType.information || verbose == true) {
          int hangingIndent = 2;
          int indent = 4;
346
          final String indicator = showColor ? message.coloredIndicator : message.indicator;
347
          for (final String line in '$indicator ${message.message}'.split('\n')) {
348
            _logger.printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
349 350 351
            // Only do hanging indent for the first line.
            hangingIndent = 0;
            indent = 6;
352
          }
353 354 355
          if (message.contextUrl != null) {
            _logger.printStatus('🔨 ${message.contextUrl}', hangingIndent: hangingIndent, indent: indent, emphasis: true);
          }
356 357
        }
      }
358
      if (verbose) {
359
        _logger.printStatus('');
360
      }
361
    }
362

363
    // Make sure there's always one line before the summary even when not verbose.
364
    if (!verbose) {
365
      _logger.printStatus('');
366
    }
367

368
    if (issues > 0) {
369
      _logger.printStatus('${showColor ? globals.terminal.color('!', TerminalColor.yellow) : '!'}'
370
        ' Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
371
    } else {
372
      _logger.printStatus('${showColor ? globals.terminal.color('•', TerminalColor.green) : '•'}'
373
        ' No issues found!', hangingIndent: 2);
374
    }
375 376

    return doctorResult;
377 378 379 380
  }

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

381
  bool get canLaunchAnything {
382
    if (FlutterTesterDevices.showFlutterTesterDevice) {
383
      return true;
384
    }
385 386
    return workflows.any((Workflow workflow) => workflow.canLaunchDevices);
  }
387 388
}

389
/// A validator that checks the version of Flutter, as well as some auxiliary information
390 391 392 393
/// such as the pub or Flutter cache overrides.
///
/// This is primarily useful for diagnosing issues on Github bug reports by displaying
/// specific commit information.
394
class FlutterValidator extends DoctorValidator {
395
  FlutterValidator({
396 397
    required Platform platform,
    required FlutterVersion Function() flutterVersion,
398
    required String Function() devToolsVersion,
399 400 401 402 403 404
    required UserMessages userMessages,
    required FileSystem fileSystem,
    required Artifacts artifacts,
    required ProcessManager processManager,
    required String Function() flutterRoot,
    required OperatingSystemUtils operatingSystemUtils,
405
  }) : _flutterVersion = flutterVersion,
406
       _devToolsVersion = devToolsVersion,
407 408 409 410 411 412 413 414 415 416 417
       _platform = platform,
       _userMessages = userMessages,
       _fileSystem = fileSystem,
       _artifacts = artifacts,
       _processManager = processManager,
       _flutterRoot = flutterRoot,
       _operatingSystemUtils = operatingSystemUtils,
       super('Flutter');

  final Platform _platform;
  final FlutterVersion Function() _flutterVersion;
418
  final String Function() _devToolsVersion;
419 420 421 422 423 424
  final String Function() _flutterRoot;
  final UserMessages _userMessages;
  final FileSystem _fileSystem;
  final Artifacts _artifacts;
  final ProcessManager _processManager;
  final OperatingSystemUtils _operatingSystemUtils;
425

426
  @override
427
  Future<ValidationResult> validate() async {
428
    final List<ValidationMessage> messages = <ValidationMessage>[];
429
    ValidationType valid = ValidationType.installed;
430 431
    String? versionChannel;
    String? frameworkVersion;
432 433

    try {
434
      final FlutterVersion version = _flutterVersion();
435 436
      versionChannel = version.channel;
      frameworkVersion = version.frameworkVersion;
437
      messages.add(ValidationMessage(_userMessages.flutterVersion(
438
        frameworkVersion,
439
        _flutterRoot(),
440
      )));
441
      messages.add(ValidationMessage(_userMessages.flutterUpstreamRepositoryUrl(version.repositoryUrl ?? 'unknown')));
442 443 444
      final String? gitUrl = _platform.environment['FLUTTER_GIT_URL'];
      if (gitUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterGitUrl(gitUrl)));
445
      }
446
      messages.add(ValidationMessage(_userMessages.flutterRevision(
447 448
        version.frameworkRevisionShort,
        version.frameworkAge,
449
        version.frameworkCommitDate,
450
      )));
451 452
      messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort)));
      messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion)));
453
      messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
454 455 456
      final String? pubUrl = _platform.environment['PUB_HOSTED_URL'];
      if (pubUrl != null) {
        messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
457
      }
458 459 460
      final String? storageBaseUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
      if (storageBaseUrl != null) {
        messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
461
      }
462 463 464 465
    } on VersionCheckError catch (e) {
      messages.add(ValidationMessage.error(e.message));
      valid = ValidationType.partial;
    }
466

467
    // Check that the binaries we downloaded for this platform actually run on it.
468 469 470 471 472 473 474 475
    // 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);
476
      }
477
      messages.add(ValidationMessage.error(buffer.toString()));
478
      valid = ValidationType.partial;
479
    }
480

481 482 483
    return ValidationResult(
      valid,
      messages,
484
      statusInfo: _userMessages.flutterStatusInfo(
485 486
        versionChannel,
        frameworkVersion,
487 488
        _operatingSystemUtils.name,
        _platform.localeName,
489
      ),
490
    );
491
  }
Devon Carew's avatar
Devon Carew committed
492

493 494 495 496 497 498 499
  bool _genSnapshotRuns(String genSnapshotPath) {
    const int kExpectedExitCode = 255;
    try {
      return _processManager.runSync(<String>[genSnapshotPath]).exitCode == kExpectedExitCode;
    } on Exception {
      return false;
    }
500 501 502
  }
}

503
class DeviceValidator extends DoctorValidator {
504 505
  // TODO(jmagman): Make required once g3 rolls and is updated.
  DeviceValidator({
506 507 508
    DeviceManager? deviceManager,
    UserMessages? userMessages,
  }) : _deviceManager = deviceManager ?? globals.deviceManager!,
509 510 511 512 513
       _userMessages = userMessages ?? globals.userMessages,
       super('Connected device');

  final DeviceManager _deviceManager;
  final UserMessages _userMessages;
514

515 516 517
  @override
  String get slowWarning => 'Scanning for devices is taking a long time...';

518 519
  @override
  Future<ValidationResult> validate() async {
520 521 522 523
    final List<Device> devices = await _deviceManager.getAllConnectedDevices();
    List<ValidationMessage> installedMessages = <ValidationMessage>[];
    if (devices.isNotEmpty) {
      installedMessages = await Device.descriptions(devices)
524
          .map<ValidationMessage>((String msg) => ValidationMessage(msg)).toList();
525
    }
526

527 528 529 530 531 532 533 534
    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)];
    }

535
    if (devices.isEmpty) {
536 537 538 539 540 541 542 543
      return ValidationResult(ValidationType.notAvailable, diagnosticMessages);
    } else if (diagnostics.isNotEmpty) {
      installedMessages.addAll(diagnosticMessages);
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
544
    } else {
545 546 547 548 549
      return ValidationResult(
        ValidationType.installed,
        installedMessages,
        statusInfo: _userMessages.devicesAvailable(devices.length)
      );
550
    }
551 552
  }
}