Unverified Commit b1a23299 authored by Ben Konyi's avatar Ben Konyi Committed by GitHub

Added '--check-for-remote-artifacts' option for Flutter Doctor. (#23543)

* Added '--check-for-remote-artifacts' option for Flutter Doctor.

This option takes a Flutter engine revision and issues HEAD requests to
determine whether or not artifacts for the provided engine revision are
available from cloud storage. This functionality is needed for the Dart
SDK autoroller to avoid creating a PR to roll the engine into the
framework while artifacts haven't finished building, which would cause
Cirrus tests to fail.
parent d8bf21a3
......@@ -29,7 +29,11 @@ Future<List<int>> fetchUrl(Uri url) async {
}
}
Future<List<int>> _attempt(Uri url) async {
/// Check if the given URL points to a valid endpoint.
Future<bool> doesRemoteFileExist(Uri url) async =>
(await _attempt(url, onlyHeaders: true)) != null;
Future<List<int>> _attempt(Uri url, {bool onlyHeaders = false}) async {
printTrace('Downloading: $url');
HttpClient httpClient;
if (context[HttpClientFactory] != null) {
......@@ -39,7 +43,11 @@ Future<List<int>> _attempt(Uri url) async {
}
HttpClientRequest request;
try {
request = await httpClient.getUrl(url);
if (onlyHeaders) {
request = await httpClient.headUrl(url);
} else {
request = await httpClient.getUrl(url);
}
} on HandshakeException catch (error) {
printTrace(error.toString());
throwToolExit(
......@@ -53,6 +61,11 @@ Future<List<int>> _attempt(Uri url) async {
return null;
}
final HttpClientResponse response = await request.close();
// If we're making a HEAD request, we're only checking to see if the URL is
// valid.
if (onlyHeaders) {
return (response.statusCode == 200) ? <int>[] : null;
}
if (response.statusCode != 200) {
if (response.statusCode > 0 && response.statusCode < 500) {
throwToolExit(
......
......@@ -516,6 +516,43 @@ class FlutterEngine extends CachedArtifact {
}
}
Future<bool> areRemoteArtifactsAvailable({String engineVersion,
bool includeAllPlatforms = true}) async {
final bool includeAllPlatformsState = cache.includeAllPlatforms;
cache.includeAllPlatforms = includeAllPlatforms;
Future<bool> checkForArtifacts(String engineVersion) async {
engineVersion ??= version;
final String url = '$_storageBaseUrl/flutter_infra/flutter/$engineVersion/';
bool exists = false;
for (String pkgName in _getPackageDirs()) {
exists = await _doesRemoteExist('Checking package $pkgName is available...',
Uri.parse(url + pkgName + '.zip'));
if (!exists) {
return false;
}
}
for (List<String> toolsDir in _getBinaryDirs()) {
final String cacheDir = toolsDir[0];
final String urlPath = toolsDir[1];
exists = await _doesRemoteExist('Checking $cacheDir tools are available...',
Uri.parse(url + urlPath));
if (!exists) {
return false;
}
}
return true;
}
final bool result = await checkForArtifacts(engineVersion);
cache.includeAllPlatforms = includeAllPlatformsState;
return result;
}
void _makeFilesExecutable(Directory dir) {
for (FileSystemEntity entity in dir.listSync()) {
if (entity is File) {
......@@ -582,6 +619,13 @@ Future<void> _downloadFile(Uri url, File location) async {
location.writeAsBytesSync(fileBytes, flush: true);
}
Future<bool> _doesRemoteExist(String message, Uri url) async {
final Status status = logger.startProgress(message, expectSlowOperation: true);
final bool exists = await doesRemoteFileExist(url);
status.stop();
return exists;
}
/// Create the given [directory] and parents, as necessary.
void _ensureExists(Directory directory) {
if (!directory.existsSync())
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import '../base/common.dart';
import '../doctor.dart';
import '../runner/flutter_command.dart';
......@@ -14,6 +15,11 @@ class DoctorCommand extends FlutterCommand {
negatable: false,
help: 'Run the Android SDK manager tool to accept the SDK\'s licenses.',
);
argParser.addOption('check-for-remote-artifacts',
hide: !verbose,
help: 'Used to determine if Flutter engine artifacts for all platforms '
'are available for download.',
valueHelp: 'engine revision git hash',);
}
final bool verbose;
......@@ -26,6 +32,19 @@ class DoctorCommand extends FlutterCommand {
@override
Future<FlutterCommandResult> runCommand() async {
if (argResults.wasParsed('check-for-remote-artifacts')) {
final String engineRevision = argResults['check-for-remote-artifacts'];
if (engineRevision.startsWith(RegExp(r'[a-f0-9]{1,40}'))) {
final bool success = await doctor.checkRemoteArtifacts(engineRevision);
if (!success) {
throwToolExit('Artifacts for engine $engineRevision are missing or are '
'not yet available.', exitCode: 1);
}
} else {
throwToolExit('Remote artifact revision $engineRevision is not a valid '
'git hash.');
}
}
final bool success = await doctor.diagnose(androidLicenses: argResults['android-licenses'], verbose: verbose);
return FlutterCommandResult(success ? ExitStatus.success : ExitStatus.warning);
}
......
......@@ -164,6 +164,12 @@ class Doctor {
return buffer.toString();
}
Future<bool> checkRemoteArtifacts(String engineRevision) async {
final Cache cache = Cache();
final FlutterEngine engine = FlutterEngine(cache);
return await engine.areRemoteArtifactsAvailable(engineVersion: engineRevision);
}
/// Print information about the state of installed tooling.
Future<bool> diagnose({ bool androidLicenses = false, bool verbose = true }) async {
if (androidLicenses)
......
......@@ -103,6 +103,30 @@ void main() {
const io.HandshakeException('test exception handling'),
),
});
testUsingContext('remote file non-existant', () async {
final Uri invalid = Uri.parse('http://example.invalid/');
final bool result = await doesRemoteFileExist(invalid);
expect(result, false);
}, overrides: <Type, Generator>{
HttpClientFactory: () => () => MockHttpClient(404),
});
testUsingContext('remote file server error', () async {
final Uri valid = Uri.parse('http://example.valid/');
final bool result = await doesRemoteFileExist(valid);
expect(result, false);
}, overrides: <Type, Generator>{
HttpClientFactory: () => () => MockHttpClient(500),
});
testUsingContext('remote file exists', () async {
final Uri valid = Uri.parse('http://example.valid/');
final bool result = await doesRemoteFileExist(valid);
expect(result, true);
}, overrides: <Type, Generator>{
HttpClientFactory: () => () => MockHttpClient(200),
});
}
class MockHttpClientThrowing implements io.HttpClient {
......@@ -131,6 +155,11 @@ class MockHttpClient implements io.HttpClient {
return MockHttpClientRequest(statusCode);
}
@override
Future<io.HttpClientRequest> headUrl(Uri url) async {
return MockHttpClientRequest(statusCode);
}
@override
dynamic noSuchMethod(Invocation invocation) {
throw 'io.HttpClient - $invocation';
......
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