Unverified Commit 3ceef86b authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tool] Handle crashes from doctor validators (#38920)

parent 6e34e805
...@@ -7,6 +7,7 @@ import 'dart:async'; ...@@ -7,6 +7,7 @@ import 'dart:async';
import 'android/android_studio_validator.dart'; import 'android/android_studio_validator.dart';
import 'android/android_workflow.dart'; import 'android/android_workflow.dart';
import 'artifacts.dart'; import 'artifacts.dart';
import 'base/async_guard.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/context.dart'; import 'base/context.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
...@@ -19,6 +20,7 @@ import 'base/user_messages.dart'; ...@@ -19,6 +20,7 @@ import 'base/user_messages.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'base/version.dart'; import 'base/version.dart';
import 'cache.dart'; import 'cache.dart';
import 'commands/doctor.dart';
import 'device.dart'; import 'device.dart';
import 'fuchsia/fuchsia_workflow.dart'; import 'fuchsia/fuchsia_workflow.dart';
import 'globals.dart'; import 'globals.dart';
...@@ -32,6 +34,7 @@ import 'macos/macos_workflow.dart'; ...@@ -32,6 +34,7 @@ import 'macos/macos_workflow.dart';
import 'macos/xcode_validator.dart'; import 'macos/xcode_validator.dart';
import 'proxy_validator.dart'; import 'proxy_validator.dart';
import 'reporting/reporting.dart'; import 'reporting/reporting.dart';
import 'runner/flutter_command.dart';
import 'tester/flutter_tester.dart'; import 'tester/flutter_tester.dart';
import 'version.dart'; import 'version.dart';
import 'vscode/vscode_validator.dart'; import 'vscode/vscode_validator.dart';
...@@ -58,19 +61,29 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { ...@@ -58,19 +61,29 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
@override @override
List<DoctorValidator> get validators { List<DoctorValidator> get validators {
if (_validators == null) { if (_validators != null) {
return _validators;
}
final List<DoctorValidator> ideValidators = <DoctorValidator>[ final List<DoctorValidator> ideValidators = <DoctorValidator>[
...AndroidStudioValidator.allValidators, ...AndroidStudioValidator.allValidators,
...IntelliJValidator.installedValidators, ...IntelliJValidator.installedValidators,
...VsCodeValidator.installedValidators, ...VsCodeValidator.installedValidators,
]; ];
bool verbose = false;
if (FlutterCommand.current is DoctorCommand) {
verbose = (FlutterCommand.current as DoctorCommand).verbose;
}
_validators = <DoctorValidator>[ _validators = <DoctorValidator>[
FlutterValidator(), FlutterValidator(),
if (androidWorkflow.appliesToHostPlatform) if (androidWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]), GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator],
verbose: verbose),
if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform) if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator]), GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator],
verbose: verbose),
if (webWorkflow.appliesToHostPlatform) if (webWorkflow.appliesToHostPlatform)
const WebValidator(), const WebValidator(),
if (linuxWorkflow.appliesToHostPlatform) if (linuxWorkflow.appliesToHostPlatform)
...@@ -86,7 +99,6 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { ...@@ -86,7 +99,6 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
if (deviceManager.canListAnything) if (deviceManager.canListAnything)
DeviceValidator(), DeviceValidator(),
]; ];
}
return _validators; return _validators;
} }
...@@ -137,7 +149,9 @@ class Doctor { ...@@ -137,7 +149,9 @@ class Doctor {
List<ValidatorTask> startValidatorTasks() { List<ValidatorTask> startValidatorTasks() {
final List<ValidatorTask> tasks = <ValidatorTask>[]; final List<ValidatorTask> tasks = <ValidatorTask>[];
for (DoctorValidator validator in validators) { for (DoctorValidator validator in validators) {
tasks.add(ValidatorTask(validator, validator.validate())); final Future<ValidationResult> result =
asyncGuard<ValidationResult>(() => validator.validate());
tasks.add(ValidatorTask(validator, result));
} }
return tasks; return tasks;
} }
...@@ -148,44 +162,62 @@ class Doctor { ...@@ -148,44 +162,62 @@ class Doctor {
/// Print a summary of the state of the tooling, as well as how to get more info. /// Print a summary of the state of the tooling, as well as how to get more info.
Future<void> summary() async { Future<void> summary() async {
printStatus(await summaryText); printStatus(await _summaryText());
} }
Future<String> get summaryText async { Future<String> _summaryText() async {
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
bool allGood = true; bool missingComponent = false;
bool sawACrash = false;
for (DoctorValidator validator in validators) { for (DoctorValidator validator in validators) {
final StringBuffer lineBuffer = StringBuffer(); final StringBuffer lineBuffer = StringBuffer();
final ValidationResult result = await validator.validate(); ValidationResult result;
lineBuffer.write('${result.coloredLeadingBox} ${validator.title} is '); try {
result = await asyncGuard<ValidationResult>(() => validator.validate());
} catch (exception) {
// We're generating a summary, so drop the stack trace.
result = ValidationResult.crash(exception);
}
lineBuffer.write('${result.coloredLeadingBox} ${validator.title}: ');
switch (result.type) { switch (result.type) {
case ValidationType.crash:
lineBuffer.write('the doctor check crashed without a result.');
sawACrash = true;
break;
case ValidationType.missing: case ValidationType.missing:
lineBuffer.write('not installed.'); lineBuffer.write('is not installed.');
break; break;
case ValidationType.partial: case ValidationType.partial:
lineBuffer.write('partially installed; more components are available.'); lineBuffer.write('is partially installed; more components are available.');
break; break;
case ValidationType.notAvailable: case ValidationType.notAvailable:
lineBuffer.write('not available.'); lineBuffer.write('is not available.');
break; break;
case ValidationType.installed: case ValidationType.installed:
lineBuffer.write('fully installed.'); lineBuffer.write('is fully installed.');
break; break;
} }
if (result.statusInfo != null) if (result.statusInfo != null) {
lineBuffer.write(' (${result.statusInfo})'); lineBuffer.write(' (${result.statusInfo})');
}
buffer.write(wrapText(lineBuffer.toString(), hangingIndent: result.leadingBox.length + 1)); buffer.write(wrapText(lineBuffer.toString(), hangingIndent: result.leadingBox.length + 1));
buffer.writeln(); buffer.writeln();
if (result.type != ValidationType.installed) if (result.type != ValidationType.installed) {
allGood = false; missingComponent = true;
}
} }
if (!allGood) { if (sawACrash) {
buffer.writeln();
buffer.writeln('Run "flutter doctor" for information about why a doctor check crashed.');
}
if (missingComponent) {
buffer.writeln(); buffer.writeln();
buffer.writeln('Run "flutter doctor" for information about installing additional components.'); buffer.writeln('Run "flutter doctor" for information about installing additional components.');
} }
...@@ -217,13 +249,19 @@ class Doctor { ...@@ -217,13 +249,19 @@ class Doctor {
ValidationResult result; ValidationResult result;
try { try {
result = await validatorTask.result; result = await validatorTask.result;
} catch (exception) { } catch (exception, stackTrace) {
status.cancel(); // Only include the stacktrace in verbose mode.
rethrow; result = ValidationResult.crash(
} exception, stackTrace: verbose ? stackTrace : null);
} finally {
status.stop(); status.stop();
}
switch (result.type) { switch (result.type) {
case ValidationType.crash:
doctorResult = false;
issues += 1;
break;
case ValidationType.missing: case ValidationType.missing:
doctorResult = false; doctorResult = false;
issues += 1; issues += 1;
...@@ -258,13 +296,15 @@ class Doctor { ...@@ -258,13 +296,15 @@ class Doctor {
} }
} }
} }
if (verbose) if (verbose) {
printStatus(''); printStatus('');
} }
}
// Make sure there's always one line before the summary even when not verbose. // Make sure there's always one line before the summary even when not verbose.
if (!verbose) if (!verbose) {
printStatus(''); printStatus('');
}
if (issues > 0) { if (issues > 0) {
printStatus('${terminal.color('!', TerminalColor.yellow)} Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2); printStatus('${terminal.color('!', TerminalColor.yellow)} Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
...@@ -302,6 +342,7 @@ abstract class Workflow { ...@@ -302,6 +342,7 @@ abstract class Workflow {
} }
enum ValidationType { enum ValidationType {
crash,
missing, missing,
partial, partial,
notAvailable, notAvailable,
...@@ -330,9 +371,12 @@ abstract class DoctorValidator { ...@@ -330,9 +371,12 @@ abstract class DoctorValidator {
/// passed to the constructor and reports the statusInfo of the first validator /// passed to the constructor and reports the statusInfo of the first validator
/// that provides one. Other titles and statusInfo strings are discarded. /// that provides one. Other titles and statusInfo strings are discarded.
class GroupedValidator extends DoctorValidator { class GroupedValidator extends DoctorValidator {
GroupedValidator(this.subValidators) : super(subValidators[0].title); GroupedValidator(this.subValidators, {
this.verbose = false,
}) : super(subValidators[0].title);
final List<DoctorValidator> subValidators; final List<DoctorValidator> subValidators;
final bool verbose;
List<ValidationResult> _subResults; List<ValidationResult> _subResults;
...@@ -351,13 +395,20 @@ class GroupedValidator extends DoctorValidator { ...@@ -351,13 +395,20 @@ class GroupedValidator extends DoctorValidator {
Future<ValidationResult> validate() async { Future<ValidationResult> validate() async {
final List<ValidatorTask> tasks = <ValidatorTask>[]; final List<ValidatorTask> tasks = <ValidatorTask>[];
for (DoctorValidator validator in subValidators) { for (DoctorValidator validator in subValidators) {
tasks.add(ValidatorTask(validator, validator.validate())); final Future<ValidationResult> result =
asyncGuard<ValidationResult>(() => validator.validate());
tasks.add(ValidatorTask(validator, result));
} }
final List<ValidationResult> results = <ValidationResult>[]; final List<ValidationResult> results = <ValidationResult>[];
for (ValidatorTask subValidator in tasks) { for (ValidatorTask subValidator in tasks) {
_currentSlowWarning = subValidator.validator.slowWarning; _currentSlowWarning = subValidator.validator.slowWarning;
try {
results.add(await subValidator.result); results.add(await subValidator.result);
} catch (exception, stackTrace) {
results.add(ValidationResult.crash(
exception, stackTrace: verbose ? stackTrace : null));
}
} }
_currentSlowWarning = 'Merging results...'; _currentSlowWarning = 'Merging results...';
return _mergeValidationResults(results); return _mergeValidationResults(results);
...@@ -382,6 +433,7 @@ class GroupedValidator extends DoctorValidator { ...@@ -382,6 +433,7 @@ class GroupedValidator extends DoctorValidator {
case ValidationType.partial: case ValidationType.partial:
mergedType = ValidationType.partial; mergedType = ValidationType.partial;
break; break;
case ValidationType.crash:
case ValidationType.missing: case ValidationType.missing:
if (mergedType == ValidationType.installed) { if (mergedType == ValidationType.installed) {
mergedType = ValidationType.partial; mergedType = ValidationType.partial;
...@@ -403,6 +455,19 @@ class ValidationResult { ...@@ -403,6 +455,19 @@ class ValidationResult {
/// if no [messages] are hints or errors. /// if no [messages] are hints or errors.
ValidationResult(this.type, this.messages, { this.statusInfo }); ValidationResult(this.type, this.messages, { this.statusInfo });
factory ValidationResult.crash(Object error, { StackTrace stackTrace }) {
return ValidationResult(ValidationType.crash, <ValidationMessage>[
ValidationMessage.error(
'Due to an error, the doctor check did not complete. '
'If the error message below is not helpful, '
'please let us know about this issue at https://github.com/flutter/flutter/issues.'),
if (stackTrace == null)
ValidationMessage.error('$error'),
if (stackTrace != null)
ValidationMessage.error('$error:\n$stackTrace'),
], statusInfo: 'the doctor check crashed');
}
final ValidationType type; final ValidationType type;
// A short message about the status. // A short message about the status.
final String statusInfo; final String statusInfo;
...@@ -411,6 +476,8 @@ class ValidationResult { ...@@ -411,6 +476,8 @@ class ValidationResult {
String get leadingBox { String get leadingBox {
assert(type != null); assert(type != null);
switch (type) { switch (type) {
case ValidationType.crash:
return '[☠]';
case ValidationType.missing: case ValidationType.missing:
return '[✗]'; return '[✗]';
case ValidationType.installed: case ValidationType.installed:
...@@ -425,6 +492,8 @@ class ValidationResult { ...@@ -425,6 +492,8 @@ class ValidationResult {
String get coloredLeadingBox { String get coloredLeadingBox {
assert(type != null); assert(type != null);
switch (type) { switch (type) {
case ValidationType.crash:
return terminal.color(leadingBox, TerminalColor.red);
case ValidationType.missing: case ValidationType.missing:
return terminal.color(leadingBox, TerminalColor.red); return terminal.color(leadingBox, TerminalColor.red);
case ValidationType.installed: case ValidationType.installed:
...@@ -440,6 +509,8 @@ class ValidationResult { ...@@ -440,6 +509,8 @@ class ValidationResult {
String get typeStr { String get typeStr {
assert(type != null); assert(type != null);
switch (type) { switch (type) {
case ValidationType.crash:
return 'crash';
case ValidationType.missing: case ValidationType.missing:
return 'missing'; return 'missing';
case ValidationType.installed: case ValidationType.installed:
......
...@@ -308,6 +308,23 @@ void main() { ...@@ -308,6 +308,23 @@ void main() {
)); ));
}, overrides: noColorTerminalOverride); }, overrides: noColorTerminalOverride);
testUsingContext('validate non-verbose output format for run with crash', () async {
expect(await FakeCrashingDoctor().diagnose(verbose: false), isFalse);
expect(testLogger.statusText, equals(
'Doctor summary (to see all details, run flutter doctor -v):\n'
'[✓] Passing Validator (with statusInfo)\n'
'[✓] Another Passing Validator (with statusInfo)\n'
'[☠] Crashing validator (the doctor check crashed)\n'
' ✗ Due to an error, the doctor check did not complete. If the error message below is not helpful, '
'please let us know about this issue at https://github.com/flutter/flutter/issues.\n'
' ✗ fatal error\n'
'[✓] Validators are fun (with statusInfo)\n'
'[✓] Four score and seven validators ago (with statusInfo)\n'
'\n'
'! Doctor found issues in 1 category.\n'
));
}, overrides: noColorTerminalOverride);
testUsingContext('validate non-verbose output format when only one category fails', () async { testUsingContext('validate non-verbose output format when only one category fails', () async {
expect(await FakeSinglePassingDoctor().diagnose(verbose: false), isTrue); expect(await FakeSinglePassingDoctor().diagnose(verbose: false), isTrue);
expect(testLogger.statusText, equals( expect(testLogger.statusText, equals(
...@@ -647,6 +664,15 @@ class PartialValidatorWithHintsOnly extends DoctorValidator { ...@@ -647,6 +664,15 @@ class PartialValidatorWithHintsOnly extends DoctorValidator {
} }
} }
class CrashingValidator extends DoctorValidator {
CrashingValidator() : super('Crashing validator');
@override
Future<ValidationResult> validate() async {
throw 'fatal error';
}
}
/// A doctor that fails with a missing [ValidationResult]. /// A doctor that fails with a missing [ValidationResult].
class FakeDoctor extends Doctor { class FakeDoctor extends Doctor {
List<DoctorValidator> _validators; List<DoctorValidator> _validators;
...@@ -711,6 +737,23 @@ class FakeQuietDoctor extends Doctor { ...@@ -711,6 +737,23 @@ class FakeQuietDoctor extends Doctor {
} }
} }
/// A doctor with a validator that throws an exception.
class FakeCrashingDoctor extends Doctor {
List<DoctorValidator> _validators;
@override
List<DoctorValidator> get validators {
if (_validators == null) {
_validators = <DoctorValidator>[];
_validators.add(PassingValidator('Passing Validator'));
_validators.add(PassingValidator('Another Passing Validator'));
_validators.add(CrashingValidator());
_validators.add(PassingValidator('Validators are fun'));
_validators.add(PassingValidator('Four score and seven validators ago'));
}
return _validators;
}
}
/// A DoctorValidatorsProvider that overrides the default validators without /// A DoctorValidatorsProvider that overrides the default validators without
/// overriding the doctor. /// overriding the doctor.
class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider { class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment