Unverified Commit d29668dd authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Improve network resources doctor check (#120417)

...and various other minor cleanup:

* Moved "FLUTTER_STORAGE_BASE_URL" into a constant throughout the code. There are other strings that we should do that to but this one was relevant to the code I was changing.

* Fixed the logger's handling of slow warnings. Previously it deleted too much text. Fixed the test for that to actually verify it entirely, too.

* Made the logger delete the slow warning when it's finished.

* Fixed 'Please choose one (To quit, press "q/Q")' message to be the cleaner 'Please choose one (or "q" to quit)'.

* Added a debug toString to ValidationResult for debugging purposes (not used).

* In http_host_validator:

  - Shortened constant names to be clearer (e.g. kPubDevHttpHost -> kPubDev).
  - Added GitHub as a tested host since when you run `flutter` we hit that immediately.
  - Renamed the check "Network resources".
  - Updated the `slowWarning` of the check to say which hosts are pending.
  - Removed all timeout logic. Timeouts violate our style guide.
  - Removed `int.parse(... ?? '10')`; passing a constant to `int.parse` is inefficient.
  - Replaced the `_HostValidationResult` class with `String?` for simplicity.
  - Improved the error messages to be more detailed.
  - Removed all checks that dependened on the stringification of exceptions. That's very brittle.
  - Added a warning specifically for HandshakeException that talks about the implications (MITM attacks).
  - Replaced exception-message-parsing logic with just calling `Uri.tryParse` and validating the result.
  - Replaced a lot of list-filtering logic with just a single for loop to check the results.
  - Replaced code that added a constant to a known-empty list with just returning a constant list.
  - Revamped the logic for deciding which hosts to check to just use a single chain of if/else blocks instead of getters, lists literals with `if` expressions, `??`, functions, etc spread over multiple places in the code.
parent 9d94a51b
...@@ -16,6 +16,7 @@ import '../base/deferred_component.dart'; ...@@ -16,6 +16,7 @@ import '../base/deferred_component.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/net.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/terminal.dart'; import '../base/terminal.dart';
...@@ -696,7 +697,7 @@ void printHowToConsumeAar({ ...@@ -696,7 +697,7 @@ void printHowToConsumeAar({
1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')} 1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')}
2. Ensure you have the repositories configured, otherwise add them: 2. Ensure you have the repositories configured, otherwise add them:
String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com" String storageUrl = System.env.$kFlutterStorageBaseUrl ?: "https://storage.googleapis.com"
repositories { repositories {
maven { maven {
url '${repoDirectory.path}' url '${repoDirectory.path}'
......
...@@ -1360,7 +1360,9 @@ class AnonymousSpinnerStatus extends Status { ...@@ -1360,7 +1360,9 @@ class AnonymousSpinnerStatus extends Status {
if (seemsSlow) { if (seemsSlow) {
if (!timedOut) { if (!timedOut) {
timedOut = true; timedOut = true;
_clear(_currentLineLength); if (_currentLineLength > _lastAnimationFrameLength) {
_clear(_currentLineLength - _lastAnimationFrameLength);
}
} }
if (_slowWarning == '' && slowWarningCallback != null) { if (_slowWarning == '' && slowWarningCallback != null) {
_slowWarning = slowWarningCallback!(); _slowWarning = slowWarningCallback!();
...@@ -1398,7 +1400,8 @@ class AnonymousSpinnerStatus extends Status { ...@@ -1398,7 +1400,8 @@ class AnonymousSpinnerStatus extends Status {
assert(timer!.isActive); assert(timer!.isActive);
timer?.cancel(); timer?.cancel();
timer = null; timer = null;
_clear(_lastAnimationFrameLength); _clear(_lastAnimationFrameLength + _slowWarning.length);
_slowWarning = '';
_lastAnimationFrameLength = 0; _lastAnimationFrameLength = 0;
super.finish(); super.finish();
} }
......
...@@ -14,6 +14,7 @@ import 'logger.dart'; ...@@ -14,6 +14,7 @@ import 'logger.dart';
import 'platform.dart'; import 'platform.dart';
const int kNetworkProblemExitCode = 50; const int kNetworkProblemExitCode = 50;
const String kFlutterStorageBaseUrl = 'FLUTTER_STORAGE_BASE_URL';
typedef HttpClientFactory = HttpClient Function(); typedef HttpClientFactory = HttpClient Function();
...@@ -106,15 +107,16 @@ class Net { ...@@ -106,15 +107,16 @@ class Net {
} }
response = await request.close(); response = await request.close();
} on ArgumentError catch (error) { } on ArgumentError catch (error) {
final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL']; final String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl];
if (overrideUrl != null && url.toString().contains(overrideUrl)) { if (overrideUrl != null && url.toString().contains(overrideUrl)) {
_logger.printError(error.toString()); _logger.printError(error.toString());
throwToolExit( throwToolExit(
'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be ' 'The value of $kFlutterStorageBaseUrl ($overrideUrl) could not be '
'parsed as a valid url. Please see https://flutter.dev/community/china ' 'parsed as a valid url. Please see https://flutter.dev/community/china '
'for an example of how to use it.\n' 'for an example of how to use it.\n'
'Full URL: $url', 'Full URL: $url',
exitCode: kNetworkProblemExitCode,); exitCode: kNetworkProblemExitCode,
);
} }
_logger.printError(error.toString()); _logger.printError(error.toString());
rethrow; rethrow;
......
...@@ -274,7 +274,7 @@ class UserMessages { ...@@ -274,7 +274,7 @@ class UserMessages {
'Found $count devices with name or id matching $deviceId:'; 'Found $count devices with name or id matching $deviceId:';
String get flutterMultipleDevicesFound => 'Multiple devices found:'; String get flutterMultipleDevicesFound => 'Multiple devices found:';
String flutterChooseDevice(int option, String name, String deviceId) => '[$option]: $name ($deviceId)'; String flutterChooseDevice(int option, String name, String deviceId) => '[$option]: $name ($deviceId)';
String get flutterChooseOne => 'Please choose one (To quit, press "q/Q")'; String get flutterChooseOne => 'Please choose one (or "q" to quit)';
String get flutterSpecifyDeviceWithAllOption => String get flutterSpecifyDeviceWithAllOption =>
'More than one device connected; please specify a device with ' 'More than one device connected; please specify a device with '
"the '-d <deviceId>' flag, or use '-d all' to act on all devices."; "the '-d <deviceId>' flag, or use '-d all' to act on all devices.";
......
...@@ -107,8 +107,8 @@ class DevelopmentArtifact { ...@@ -107,8 +107,8 @@ class DevelopmentArtifact {
/// ///
/// To enable Flutter users in these environments, the Flutter tool supports /// To enable Flutter users in these environments, the Flutter tool supports
/// custom artifact mirrors that the administrators of such environments may /// custom artifact mirrors that the administrators of such environments may
/// provide. To use an artifact mirror, the user defines the /// provide. To use an artifact mirror, the user defines the [kFlutterStorageBaseUrl]
/// `FLUTTER_STORAGE_BASE_URL` environment variable that points to the mirror. /// (`FLUTTER_STORAGE_BASE_URL`) environment variable that points to the mirror.
/// Flutter tool reads this variable and uses it instead of the default URLs. /// Flutter tool reads this variable and uses it instead of the default URLs.
/// ///
/// For more details on specific URLs used to download artifacts, see /// For more details on specific URLs used to download artifacts, see
...@@ -450,15 +450,15 @@ class Cache { ...@@ -450,15 +450,15 @@ class Cache {
/// during the installation of the Flutter SDK. /// during the installation of the Flutter SDK.
/// ///
/// By default the base URL is https://storage.googleapis.com. However, if /// By default the base URL is https://storage.googleapis.com. However, if
/// `FLUTTER_STORAGE_BASE_URL` environment variable is provided, the /// `FLUTTER_STORAGE_BASE_URL` environment variable ([kFlutterStorageBaseUrl])
/// environment variable value is returned instead. /// is provided, the environment variable value is returned instead.
/// ///
/// See also: /// See also:
/// ///
/// * [cipdBaseUrl], which determines how CIPD artifacts are fetched. /// * [cipdBaseUrl], which determines how CIPD artifacts are fetched.
/// * [Cache] class-level dartdocs that explain how artifact mirrors work. /// * [Cache] class-level dartdocs that explain how artifact mirrors work.
String get storageBaseUrl { String get storageBaseUrl {
final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL']; final String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl];
if (overrideUrl == null) { if (overrideUrl == null) {
return 'https://storage.googleapis.com'; return 'https://storage.googleapis.com';
} }
...@@ -466,7 +466,7 @@ class Cache { ...@@ -466,7 +466,7 @@ class Cache {
try { try {
Uri.parse(overrideUrl); Uri.parse(overrideUrl);
} on FormatException catch (err) { } on FormatException catch (err) {
throwToolExit('"FLUTTER_STORAGE_BASE_URL" contains an invalid URI:\n$err'); throwToolExit('"$kFlutterStorageBaseUrl" contains an invalid URL:\n$err');
} }
_maybeWarnAboutStorageOverride(overrideUrl); _maybeWarnAboutStorageOverride(overrideUrl);
return overrideUrl; return overrideUrl;
...@@ -479,8 +479,8 @@ class Cache { ...@@ -479,8 +479,8 @@ class Cache {
/// from [storageBaseUrl]. /// from [storageBaseUrl].
/// ///
/// By default the base URL is https://chrome-infra-packages.appspot.com/dl. /// By default the base URL is https://chrome-infra-packages.appspot.com/dl.
/// However, if `FLUTTER_STORAGE_BASE_URL` environment variable is provided, /// However, if `FLUTTER_STORAGE_BASE_URL` environment variable is provided
/// then the following value is used: /// ([kFlutterStorageBaseUrl]), then the following value is used:
/// ///
/// FLUTTER_STORAGE_BASE_URL/flutter_infra_release/cipd /// FLUTTER_STORAGE_BASE_URL/flutter_infra_release/cipd
/// ///
...@@ -492,7 +492,7 @@ class Cache { ...@@ -492,7 +492,7 @@ class Cache {
/// which contains information about CIPD. /// which contains information about CIPD.
/// * [Cache] class-level dartdocs that explain how artifact mirrors work. /// * [Cache] class-level dartdocs that explain how artifact mirrors work.
String get cipdBaseUrl { String get cipdBaseUrl {
final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL']; final String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl];
if (overrideUrl == null) { if (overrideUrl == null) {
return 'https://chrome-infra-packages.appspot.com/dl'; return 'https://chrome-infra-packages.appspot.com/dl';
} }
...@@ -501,7 +501,7 @@ class Cache { ...@@ -501,7 +501,7 @@ class Cache {
try { try {
original = Uri.parse(overrideUrl); original = Uri.parse(overrideUrl);
} on FormatException catch (err) { } on FormatException catch (err) {
throwToolExit('"FLUTTER_STORAGE_BASE_URL" contains an invalid URI:\n$err'); throwToolExit('"$kFlutterStorageBaseUrl" contains an invalid URL:\n$err');
} }
final String cipdOverride = original.replace( final String cipdOverride = original.replace(
...@@ -1065,11 +1065,11 @@ class ArtifactUpdater { ...@@ -1065,11 +1065,11 @@ class ArtifactUpdater {
} }
continue; continue;
} on ArgumentError catch (error) { } on ArgumentError catch (error) {
final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL']; final String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl];
if (overrideUrl != null && url.toString().contains(overrideUrl)) { if (overrideUrl != null && url.toString().contains(overrideUrl)) {
_logger.printError(error.toString()); _logger.printError(error.toString());
throwToolExit( throwToolExit(
'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be ' 'The value of $kFlutterStorageBaseUrl ($overrideUrl) could not be '
'parsed as a valid url. Please see https://flutter.dev/community/china ' 'parsed as a valid url. Please see https://flutter.dev/community/china '
'for an example of how to use it.\n' 'for an example of how to use it.\n'
'Full URL: $url', 'Full URL: $url',
......
...@@ -136,7 +136,7 @@ class UpdatePackagesCommand extends FlutterCommand { ...@@ -136,7 +136,7 @@ class UpdatePackagesCommand extends FlutterCommand {
); );
Future<void> _downloadCoverageData() async { Future<void> _downloadCoverageData() async {
final String urlBase = globals.platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com'; final String urlBase = globals.platform.environment[kFlutterStorageBaseUrl] ?? 'https://storage.googleapis.com';
final Uri coverageUri = Uri.parse('$urlBase/flutter_infra_release/flutter/coverage/lcov.info'); final Uri coverageUri = Uri.parse('$urlBase/flutter_infra_release/flutter/coverage/lcov.info');
final List<int>? data = await _net.fetchUrl( final List<int>? data = await _net.fetchUrl(
coverageUri, coverageUri,
......
...@@ -15,6 +15,7 @@ import 'base/context.dart'; ...@@ -15,6 +15,7 @@ import 'base/context.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/io.dart'; import 'base/io.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'base/net.dart';
import 'base/os.dart'; import 'base/os.dart';
import 'base/platform.dart'; import 'base/platform.dart';
import 'base/terminal.dart'; import 'base/terminal.dart';
...@@ -528,11 +529,11 @@ class FlutterValidator extends DoctorValidator { ...@@ -528,11 +529,11 @@ class FlutterValidator extends DoctorValidator {
messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort))); messages.add(ValidationMessage(_userMessages.engineRevision(version.engineRevisionShort)));
messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion))); messages.add(ValidationMessage(_userMessages.dartRevision(version.dartSdkVersion)));
messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion()))); messages.add(ValidationMessage(_userMessages.devToolsVersion(_devToolsVersion())));
final String? pubUrl = _platform.environment['PUB_HOSTED_URL']; final String? pubUrl = _platform.environment[kPubDevOverride];
if (pubUrl != null) { if (pubUrl != null) {
messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl))); messages.add(ValidationMessage(_userMessages.pubMirrorURL(pubUrl)));
} }
final String? storageBaseUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL']; final String? storageBaseUrl = _platform.environment[kFlutterStorageBaseUrl];
if (storageBaseUrl != null) { if (storageBaseUrl != null) {
messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl))); messages.add(ValidationMessage(_userMessages.flutterMirrorURL(storageBaseUrl)));
} }
......
...@@ -207,6 +207,11 @@ class ValidationResult { ...@@ -207,6 +207,11 @@ class ValidationResult {
return 'partial'; return 'partial';
} }
} }
@override
String toString() {
return '$runtimeType($type, $messages, $statusInfo)';
}
} }
/// A status line for the flutter doctor validation to display. /// A status line for the flutter doctor validation to display.
......
...@@ -5,32 +5,20 @@ ...@@ -5,32 +5,20 @@
import 'dart:async'; import 'dart:async';
import 'base/io.dart'; import 'base/io.dart';
import 'base/net.dart';
import 'base/platform.dart'; import 'base/platform.dart';
import 'doctor_validator.dart'; import 'doctor_validator.dart';
import 'features.dart'; import 'features.dart';
// Overridable environment variables
const String kEnvPubHostedUrl = 'PUB_HOSTED_URL';
const String kEnvCloudUrl = 'FLUTTER_STORAGE_BASE_URL';
const String kDoctorHostTimeout = 'FLUTTER_DOCTOR_HOST_TIMEOUT';
/// Common Flutter HTTP hosts. /// Common Flutter HTTP hosts.
const String kPubDevHttpHost = 'https://pub.dev/'; const String kCloudHost = 'https://storage.googleapis.com/';
const String kgCloudHttpHost = 'https://cloud.google.com/'; const String kCocoaPods = 'https://cocoapods.org/';
const String kGitHub = 'https://github.com/';
/// MacOS specific required HTTP hosts. const String kMaven = 'https://maven.google.com/';
const List<String> macOSRequiredHttpHosts = <String>[ const String kPubDev = 'https://pub.dev/';
'https://cocoapods.org/',
];
/// Android specific required HTTP hosts. // Overridable environment variables.
List<String> androidRequiredHttpHosts(Platform platform) { const String kPubDevOverride = 'PUB_HOSTED_URL'; // https://dart.dev/tools/pub/environment-variables
return <String>[
// If kEnvCloudUrl is set, it will be used as the maven host
if (!platform.environment.containsKey(kEnvCloudUrl))
'https://maven.google.com/',
];
}
// Validator that checks all provided hosts are reachable and responsive // Validator that checks all provided hosts are reachable and responsive
class HttpHostValidator extends DoctorValidator { class HttpHostValidator extends DoctorValidator {
...@@ -41,103 +29,121 @@ class HttpHostValidator extends DoctorValidator { ...@@ -41,103 +29,121 @@ class HttpHostValidator extends DoctorValidator {
}) : _platform = platform, }) : _platform = platform,
_featureFlags = featureFlags, _featureFlags = featureFlags,
_httpClient = httpClient, _httpClient = httpClient,
super('HTTP Host Availability'); super('Network resources');
final Platform _platform; final Platform _platform;
final FeatureFlags _featureFlags; final FeatureFlags _featureFlags;
final HttpClient _httpClient; final HttpClient _httpClient;
@override final Set<Uri> _activeHosts = <Uri>{};
String get slowWarning => 'HTTP Host availability check is taking a long time...';
List<String> get _requiredHosts => <String>[ @override
if (_featureFlags.isMacOSEnabled) ...macOSRequiredHttpHosts, String get slowWarning {
if (_featureFlags.isAndroidEnabled) ...androidRequiredHttpHosts(_platform), if (_activeHosts.isEmpty) {
_platform.environment[kEnvPubHostedUrl] ?? kPubDevHttpHost, return 'Network resources check is taking a long time...';
_platform.environment[kEnvCloudUrl] ?? kgCloudHttpHost, }
]; return 'Attempting to reach ${_activeHosts.map((Uri url) => url.host).join(", ")}...';
}
/// Make a head request to the HTTP host for checking availability /// Make a head request to the HTTP host for checking availability
Future<_HostValidationResult> _checkHostAvailability(String host) async { Future<String?> _checkHostAvailability(Uri host) async {
late final int timeout;
try { try {
timeout = int.parse(_platform.environment[kDoctorHostTimeout] ?? '10'); assert(!_activeHosts.contains(host));
final HttpClientRequest req = await _httpClient.headUrl(Uri.parse(host)); _activeHosts.add(host);
await req.close().timeout(Duration(seconds: timeout)); final HttpClientRequest req = await _httpClient.headUrl(host);
// HTTP host is available if no exception happened await req.close();
return _HostValidationResult.success(host); // HTTP host is available if no exception happened.
} on TimeoutException { return null;
return _HostValidationResult.fail(host, 'Failed to connect to host in $timeout second${timeout == 1 ? '': 's'}'); } on SocketException catch (error) {
} on SocketException catch (e) { return 'A network error occurred while checking "$host": ${error.message}';
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}'); } on HttpException catch (error) {
} on HttpException catch (e) { return 'An HTTP error occurred while checking "$host": ${error.message}';
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}'); } on HandshakeException catch (error) {
} on HandshakeException catch (e) { return 'A crytographic error occurred while checking "$host": ${error.message}\n'
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}'); 'You may be experiencing a man-in-the-middle attack, your network may be '
} on OSError catch (e) { 'compromised, or you may have malware installed on your computer.';
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}'); } on OSError catch (error) {
} on FormatException catch (e) { return 'An error occurred while checking "$host": ${error.message}';
if (e.message.contains('Invalid radix-10 number')) { } finally {
return _HostValidationResult.fail(host, 'The value of $kDoctorHostTimeout(${_platform.environment[kDoctorHostTimeout]}) is not a valid duration in seconds'); _activeHosts.remove(host);
} else if (e.message.contains('Invalid empty scheme')){
// Check if the invalid host is kEnvPubHostedUrl, else it must be kEnvCloudUrl
final String? pubHostedUrl = _platform.environment[kEnvPubHostedUrl];
if (pubHostedUrl != null && host == pubHostedUrl) {
return _HostValidationResult.fail(host, 'The value of $kEnvPubHostedUrl(${_platform.environment[kEnvPubHostedUrl]}) could not be parsed as a valid url');
}
return _HostValidationResult.fail(host, 'The value of $kEnvCloudUrl(${_platform.environment[kEnvCloudUrl]}) could not be parsed as a valid url');
}
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: ${e.message}');
} on ArgumentError catch (e) {
final String exceptionMessage = e.message.toString();
if (exceptionMessage.contains('No host specified')) {
// Check if the invalid host is kEnvPubHostedUrl, else it must be kEnvCloudUrl
final String? pubHostedUrl = _platform.environment[kEnvPubHostedUrl];
if (pubHostedUrl != null && host == pubHostedUrl) {
return _HostValidationResult.fail(host, 'The value of $kEnvPubHostedUrl(${_platform.environment[kEnvPubHostedUrl]}) is not a valid host');
}
return _HostValidationResult.fail(host, 'The value of $kEnvCloudUrl(${_platform.environment[kEnvCloudUrl]}) is not a valid host');
}
return _HostValidationResult.fail(host, 'An error occurred while checking the HTTP host: $exceptionMessage');
} }
} }
static Uri? _parseUrl(String value) {
final Uri? url = Uri.tryParse(value);
if (url == null || !url.hasScheme || !url.hasAuthority || (!url.hasEmptyPath && !url.hasAbsolutePath) || url.hasFragment) {
return null;
}
return url;
}
@override @override
Future<ValidationResult> validate() async { Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[]; final List<String?> availabilityResults = <String?>[];
final Iterable<Future<_HostValidationResult>> availabilityResultFutures = _requiredHosts.map(_checkHostAvailability);
final List<_HostValidationResult> availabilityResults = await Future.wait(availabilityResultFutures); final List<Uri> requiredHosts = <Uri>[];
if (_platform.environment.containsKey(kPubDevOverride)) {
if (availabilityResults.every((_HostValidationResult result) => result.available)) { final Uri? url = _parseUrl(_platform.environment[kPubDevOverride]!);
return ValidationResult( if (url == null) {
ValidationType.success, availabilityResults.add(
messages..add(const ValidationMessage('All required HTTP hosts are available')), 'Environment variable $kPubDevOverride does not specify a valid URL: "${_platform.environment[kPubDevOverride]}"\n'
'Please see https://flutter.dev/community/china for an example of how to use it.'
); );
} else {
requiredHosts.add(url);
}
} else {
requiredHosts.add(Uri.parse(kPubDev));
} }
if (_platform.environment.containsKey(kFlutterStorageBaseUrl)) {
final Uri? url = _parseUrl(_platform.environment[kFlutterStorageBaseUrl]!);
if (url == null) {
availabilityResults.add(
'Environment variable $kFlutterStorageBaseUrl does not specify a valid URL: "${_platform.environment[kFlutterStorageBaseUrl]}"\n'
'Please see https://flutter.dev/community/china for an example of how to use it.'
);
} else {
requiredHosts.add(url);
}
} else {
requiredHosts.add(Uri.parse(kCloudHost));
if (_featureFlags.isAndroidEnabled) {
// if kFlutterStorageBaseUrl is set it is used instead of Maven
requiredHosts.add(Uri.parse(kMaven));
}
}
if (_featureFlags.isMacOSEnabled) {
requiredHosts.add(Uri.parse(kCocoaPods));
}
requiredHosts.add(Uri.parse(kGitHub));
availabilityResults.removeWhere((_HostValidationResult result) => result.available); // Check all the hosts simultaneously.
availabilityResults.addAll(await Future.wait<String?>(requiredHosts.map(_checkHostAvailability)));
for (final _HostValidationResult result in availabilityResults) { int failures = 0;
messages.add(ValidationMessage.error('HTTP host "${result.host}" is not reachable. Reason: ${result.failResultInfo}')); int successes = 0;
final List<ValidationMessage> messages = <ValidationMessage>[];
for (final String? message in availabilityResults) {
if (message == null) {
successes += 1;
} else {
failures += 1;
messages.add(ValidationMessage.error(message));
}
} }
if (failures == 0) {
assert(successes > 0);
assert(messages.isEmpty);
return const ValidationResult(
ValidationType.success,
<ValidationMessage>[ValidationMessage('All expected network resources are available.')],
);
}
assert(messages.isNotEmpty);
return ValidationResult( return ValidationResult(
availabilityResults.length == _requiredHosts.length successes == 0 ? ValidationType.notAvailable : ValidationType.partial,
? ValidationType.notAvailable
: ValidationType.partial,
messages, messages,
); );
} }
} }
class _HostValidationResult {
_HostValidationResult.success(this.host)
: failResultInfo = '',
available = true;
_HostValidationResult.fail(this.host, this.failResultInfo) : available = false;
final String failResultInfo;
final String host;
final bool available;
}
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
...@@ -19,7 +18,6 @@ const String kTestEnvGCloudHost = 'https://storage.flutter-io.cn'; ...@@ -19,7 +18,6 @@ const String kTestEnvGCloudHost = 'https://storage.flutter-io.cn';
const Map<String, String> kTestEnvironment = <String, String>{ const Map<String, String> kTestEnvironment = <String, String>{
'PUB_HOSTED_URL': kTestEnvPubHost, 'PUB_HOSTED_URL': kTestEnvPubHost,
'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost, 'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost,
'FLUTTER_DOCTOR_HOST_TIMEOUT': '1',
}; };
void main() { void main() {
...@@ -31,7 +29,7 @@ void main() { ...@@ -31,7 +29,7 @@ void main() {
final FakeHttpClient mockClient = FakeHttpClient.any(); final FakeHttpClient mockClient = FakeHttpClient.any();
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(operatingSystem: os), platform: FakePlatform(operatingSystem: os),
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
...@@ -48,16 +46,17 @@ void main() { ...@@ -48,16 +46,17 @@ void main() {
testWithoutContext('all http hosts are not available', () async { testWithoutContext('all http hosts are not available', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os); final Platform platform = FakePlatform(operatingSystem: os);
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: platform, platform: platform,
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(androidRequiredHttpHosts(platform)[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kCocoaPods), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kMaven), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kPubDev), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
]), ]),
); );
...@@ -71,16 +70,17 @@ void main() { ...@@ -71,16 +70,17 @@ void main() {
testWithoutContext('one http host is not available', () async { testWithoutContext('one http host is not available', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os); final Platform platform = FakePlatform(operatingSystem: os);
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: platform, platform: platform,
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(androidRequiredHttpHosts(platform)[0]), method: HttpMethod.head), FakeRequest(Uri.parse(kCocoaPods), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head), FakeRequest(Uri.parse(kMaven), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDev), method: HttpMethod.head),
]), ]),
); );
...@@ -98,7 +98,7 @@ void main() { ...@@ -98,7 +98,7 @@ void main() {
final FakeHttpClient mockClient = FakeHttpClient.any(); final FakeHttpClient mockClient = FakeHttpClient.any();
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(operatingSystem: os, environment: kTestEnvironment), platform: FakePlatform(operatingSystem: os, environment: kTestEnvironment),
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
...@@ -115,15 +115,16 @@ void main() { ...@@ -115,15 +115,16 @@ void main() {
testWithoutContext('all http hosts are not available', () async { testWithoutContext('all http hosts are not available', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment); final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment);
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: platform, platform: platform,
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kCocoaPods), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
]), ]),
); );
...@@ -137,15 +138,16 @@ void main() { ...@@ -137,15 +138,16 @@ void main() {
testWithoutContext('one http host is not available', () async { testWithoutContext('one http host is not available', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment); final Platform platform = FakePlatform(operatingSystem: os, environment: kTestEnvironment);
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: platform, platform: platform,
featureFlags: TestFeatureFlags(), featureFlags: TestFeatureFlags(),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kCocoaPods), method: HttpMethod.head),
FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)), FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head), FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
]), ]),
); );
...@@ -157,39 +159,12 @@ void main() { ...@@ -157,39 +159,12 @@ void main() {
} }
}); });
testWithoutContext('does not throw on invalid user-defined timeout', () async {
final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(
environment: <String,String> {
'PUB_HOSTED_URL': kTestEnvPubHost,
'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost,
'FLUTTER_DOCTOR_HOST_TIMEOUT' : 'deadbeef',
},
),
featureFlags: TestFeatureFlags(isAndroidEnabled: false),
httpClient: FakeHttpClient.any(),
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
expect(result.type, equals(ValidationType.notAvailable));
expect(
result.messages,
contains(const ValidationMessage.error(
'HTTP host "$kTestEnvPubHost" is not reachable. '
'Reason: The value of FLUTTER_DOCTOR_HOST_TIMEOUT(deadbeef) is not a valid duration in seconds',
)),
);
});
testWithoutContext('does not throw on unparseable user-defined host uri', () async { testWithoutContext('does not throw on unparseable user-defined host uri', () async {
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform( platform: FakePlatform(
environment: <String,String> { environment: <String,String> {
'PUB_HOSTED_URL': '::Not A Uri::', 'PUB_HOSTED_URL': '::Not A Uri::',
'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost, 'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost,
'FLUTTER_DOCTOR_HOST_TIMEOUT' : '1',
}, },
), ),
featureFlags: TestFeatureFlags(isAndroidEnabled: false), featureFlags: TestFeatureFlags(isAndroidEnabled: false),
...@@ -203,8 +178,8 @@ void main() { ...@@ -203,8 +178,8 @@ void main() {
expect( expect(
result.messages, result.messages,
contains(const ValidationMessage.error( contains(const ValidationMessage.error(
'HTTP host "::Not A Uri::" is not reachable. ' 'Environment variable PUB_HOSTED_URL does not specify a valid URL: "::Not A Uri::"\n'
'Reason: The value of PUB_HOSTED_URL(::Not A Uri::) could not be parsed as a valid url', 'Please see https://flutter.dev/community/china for an example of how to use it.',
)), )),
); );
}); });
...@@ -215,7 +190,6 @@ void main() { ...@@ -215,7 +190,6 @@ void main() {
environment: <String,String> { environment: <String,String> {
'PUB_HOSTED_URL': kTestEnvPubHost, 'PUB_HOSTED_URL': kTestEnvPubHost,
'FLUTTER_STORAGE_BASE_URL': '', 'FLUTTER_STORAGE_BASE_URL': '',
'FLUTTER_DOCTOR_HOST_TIMEOUT' : '1',
}, },
), ),
featureFlags: TestFeatureFlags(isAndroidEnabled: false), featureFlags: TestFeatureFlags(isAndroidEnabled: false),
...@@ -229,8 +203,8 @@ void main() { ...@@ -229,8 +203,8 @@ void main() {
expect( expect(
result.messages, result.messages,
contains(const ValidationMessage.error( contains(const ValidationMessage.error(
'HTTP host "" is not reachable. ' 'Environment variable FLUTTER_STORAGE_BASE_URL does not specify a valid URL: ""\n'
'Reason: The value of FLUTTER_STORAGE_BASE_URL() is not a valid host', 'Please see https://flutter.dev/community/china for an example of how to use it.'
)), )),
); );
}); });
...@@ -239,14 +213,15 @@ void main() { ...@@ -239,14 +213,15 @@ void main() {
group('specific os disabled', () { group('specific os disabled', () {
testWithoutContext('all http hosts are available - android disabled', () async { testWithoutContext('all http hosts are available - android disabled', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(operatingSystem: os), platform: FakePlatform(operatingSystem: os),
featureFlags: TestFeatureFlags(isAndroidEnabled: false), featureFlags: TestFeatureFlags(isAndroidEnabled: false),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kCloudHost), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kCocoaPods), method: HttpMethod.head),
FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head), FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDev), method: HttpMethod.head),
]), ]),
); );
...@@ -260,15 +235,16 @@ void main() { ...@@ -260,15 +235,16 @@ void main() {
testWithoutContext('all http hosts are available - iOS disabled', () async { testWithoutContext('all http hosts are available - iOS disabled', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final Platform platform = FakePlatform(operatingSystem: os); final Platform platform = FakePlatform(operatingSystem: os);
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: platform, platform: platform,
featureFlags: TestFeatureFlags(isIOSEnabled: false), featureFlags: TestFeatureFlags(isIOSEnabled: false),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kCloudHost), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head),
FakeRequest(Uri.parse(androidRequiredHttpHosts(platform)[0]), method: HttpMethod.head), FakeRequest(Uri.parse(kMaven), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDev), method: HttpMethod.head),
]), ]),
); );
...@@ -282,13 +258,14 @@ void main() { ...@@ -282,13 +258,14 @@ void main() {
testWithoutContext('all http hosts are available - android, iOS disabled', () async { testWithoutContext('all http hosts are available - android, iOS disabled', () async {
// Run the check for all operating systems one by one // Run the check for all operating systems one by one
for(final String os in osTested) { for (final String os in osTested) {
final HttpHostValidator httpHostValidator = HttpHostValidator( final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(operatingSystem: os), platform: FakePlatform(operatingSystem: os),
featureFlags: TestFeatureFlags(isAndroidEnabled: false, isIOSEnabled: false), featureFlags: TestFeatureFlags(isAndroidEnabled: false, isIOSEnabled: false),
httpClient: FakeHttpClient.list(<FakeRequest>[ httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kCloudHost), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head), FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head),
FakeRequest(Uri.parse(kPubDev), method: HttpMethod.head),
]), ]),
); );
...@@ -320,6 +297,7 @@ Handshake error in client (OS Error: ...@@ -320,6 +297,7 @@ Handshake error in client (OS Error:
responseError: const HandshakeException(handshakeMessage), responseError: const HandshakeException(handshakeMessage),
), ),
FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head), FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head),
FakeRequest(Uri.parse(kGitHub), method: HttpMethod.head),
]), ]),
); );
...@@ -335,24 +313,4 @@ Handshake error in client (OS Error: ...@@ -335,24 +313,4 @@ Handshake error in client (OS Error:
), ),
); );
}); });
testWithoutContext('Http host validator timeout message includes timeout duration.', () async {
final HttpHostValidator httpHostValidator = HttpHostValidator(
platform: FakePlatform(environment: kTestEnvironment),
featureFlags: TestFeatureFlags(isAndroidEnabled: false),
httpClient: FakeHttpClient.list(<FakeRequest>[
FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head, responseError: TimeoutException('Timeout error')),
FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head),
]),
);
// Run the validation check and get the results
final ValidationResult result = await httpHostValidator.validate();
// Timeout duration for tests is set to 1 second
expect(
result.messages,
contains(const ValidationMessage.error('HTTP host "$kTestEnvPubHost" is not reachable. Reason: Failed to connect to host in 1 second')),
);
});
} }
...@@ -497,10 +497,7 @@ void main() { ...@@ -497,10 +497,7 @@ void main() {
time.elapse(timeLapse); time.elapse(timeLapse);
List<String> lines = outputStdout(); List<String> lines = outputStdout();
expect( expect(lines.join(), '⣽\ba warning message.⣻');
lines.join(),
contains(warningMessage),
);
spinner.stop(); spinner.stop();
lines = outputStdout(); lines = outputStdout();
......
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