diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 72fbd73de8a..98f40560c49 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:meta/meta.dart'; import 'package:process/process.dart'; @@ -204,13 +206,29 @@ class Doctor { // Future returned by the asyncGuard() is not awaited, we pass an // onError callback to it and translate errors into ValidationResults. asyncGuard( - validator.validate, + () { + final Completer timeoutCompleter = Completer(); + final Timer timer = Timer(doctorDuration, () { + timeoutCompleter.completeError( + Exception('${validator.title} exceeded maximum allowed duration of $doctorDuration'), + ); + }); + final Future validatorFuture = validator.validate(); + return Future.any(>[ + validatorFuture, + // This future can only complete with an error + timeoutCompleter.future, + ]).then((ValidationResult result) async { + timer.cancel(); + return result; + }); + }, onError: (Object exception, StackTrace stackTrace) { return ValidationResult.crash(exception, stackTrace); }, ), ), - ]; + ]; List get workflows { return DoctorValidatorsProvider._instance.workflows; @@ -290,6 +308,11 @@ class Doctor { return globals.cache.areRemoteArtifactsAvailable(engineVersion: engineRevision); } + /// 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); + /// Print information about the state of installed tooling. /// /// To exclude personally identifiable information like device names and @@ -316,7 +339,7 @@ class Doctor { for (final ValidatorTask validatorTask in startedValidatorTasks ?? startValidatorTasks()) { final DoctorValidator validator = validatorTask.validator; final Status status = _logger.startSpinner( - timeout: const Duration(seconds: 2), + timeout: validator.slowWarningDuration, slowWarningCallback: () => validator.slowWarning, ); ValidationResult result; diff --git a/packages/flutter_tools/lib/src/doctor_validator.dart b/packages/flutter_tools/lib/src/doctor_validator.dart index 6530b5fd379..8dbefb8c461 100644 --- a/packages/flutter_tools/lib/src/doctor_validator.dart +++ b/packages/flutter_tools/lib/src/doctor_validator.dart @@ -53,6 +53,11 @@ abstract class DoctorValidator { String get slowWarning => 'This is taking an unexpectedly long time...'; + static const Duration _slowWarningDuration = Duration(seconds: 10); + + /// Duration before the spinner should display [slowWarning]. + Duration get slowWarningDuration => _slowWarningDuration; + Future validate(); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart index 65cc24b391c..68d0361ada4 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart @@ -359,6 +359,16 @@ void main() { expect(logger.statusText, contains('#0 CrashingValidator.validate')); }); + testUsingContext('validate tool exit when exceeding timeout', () async { + FakeAsync().run((FakeAsync time) { + final Doctor doctor = FakeAsyncStuckDoctor(logger); + doctor.diagnose(verbose: false); + time.elapse(Doctor.doctorDuration + const Duration(seconds: 1)); + time.flushMicrotasks(); + }); + + expect(logger.statusText, contains('Stuck validator that never completes exceeded maximum allowed duration of ')); + }); testUsingContext('validate non-verbose output format for run with an async crash', () async { final Completer completer = Completer(); @@ -816,6 +826,18 @@ class NotAvailableValidator extends DoctorValidator { } } +class StuckValidator extends DoctorValidator { + StuckValidator() : super('Stuck validator that never completes'); + + @override + Future validate() { + final Completer completer = Completer(); + + // This future will never complete + return completer.future; + } +} + class PartialValidatorWithErrors extends DoctorValidator { PartialValidatorWithErrors() : super('Partial Validator with Errors'); @@ -966,6 +988,25 @@ class FakeCrashingDoctor extends Doctor { } } +/// A doctor with a validator that will never finish. +class FakeAsyncStuckDoctor extends Doctor { + FakeAsyncStuckDoctor(Logger logger) : super(logger: logger); + + List _validators; + @override + List get validators { + if (_validators == null) { + _validators = []; + _validators.add(PassingValidator('Passing Validator')); + _validators.add(PassingValidator('Another Passing Validator')); + _validators.add(StuckValidator()); + _validators.add(PassingValidator('Validators are fun')); + _validators.add(PassingValidator('Four score and seven validators ago')); + } + return _validators; + } +} + /// A doctor with a validator that throws an exception. class FakeAsyncCrashingDoctor extends Doctor { FakeAsyncCrashingDoctor(this._time, Logger logger) : super(logger: logger);