Commit 74c1be6f authored by Zachary Anderson's avatar Zachary Anderson Committed by Flutter GitHub Bot

[flutter_tool] Make base/net.dart context free (#49575)

parent 9980d6e5
......@@ -4,12 +4,14 @@
import 'dart:async';
import '../base/context.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import 'common.dart';
import 'file_system.dart';
import 'io.dart';
import 'logger.dart';
const int kNetworkProblemExitCode = 50;
......@@ -17,139 +19,160 @@ typedef HttpClientFactory = HttpClient Function();
typedef UrlTunneller = Future<String> Function(String url);
/// Download a file from the given URL.
///
/// If a destination file is not provided, returns the bytes.
///
/// If a destination file is provided, streams the bytes to that file and
/// returns an empty list.
///
/// If [maxAttempts] is exceeded, returns null.
Future<List<int>> fetchUrl(Uri url, {
int maxAttempts,
File destFile,
}) async {
int attempts = 0;
int durationSeconds = 1;
while (true) {
attempts += 1;
_MemoryIOSink memorySink;
IOSink sink;
if (destFile == null) {
memorySink = _MemoryIOSink();
sink = memorySink;
} else {
sink = destFile.openWrite();
}
class Net {
Net({
HttpClientFactory httpClientFactory,
@required Logger logger,
@required Platform platform,
}) :
_httpClientFactory = httpClientFactory,
_logger = logger,
_platform = platform;
final HttpClientFactory _httpClientFactory;
final Logger _logger;
final Platform _platform;
/// Download a file from the given URL.
///
/// If a destination file is not provided, returns the bytes.
///
/// If a destination file is provided, streams the bytes to that file and
/// returns an empty list.
///
/// If [maxAttempts] is exceeded, returns null.
Future<List<int>> fetchUrl(Uri url, {
int maxAttempts,
File destFile,
}) async {
int attempts = 0;
int durationSeconds = 1;
while (true) {
attempts += 1;
_MemoryIOSink memorySink;
IOSink sink;
if (destFile == null) {
memorySink = _MemoryIOSink();
sink = memorySink;
} else {
sink = destFile.openWrite();
}
final bool result = await _attempt(
url,
destSink: sink,
);
if (result) {
return memorySink?.writes?.takeBytes() ?? <int>[];
}
final bool result = await _attempt(
url,
destSink: sink,
);
if (result) {
return memorySink?.writes?.takeBytes() ?? <int>[];
}
if (maxAttempts != null && attempts >= maxAttempts) {
globals.printStatus('Download failed -- retry $attempts');
return null;
}
globals.printStatus('Download failed -- attempting retry $attempts in '
'$durationSeconds second${ durationSeconds == 1 ? "" : "s"}...');
await Future<void>.delayed(Duration(seconds: durationSeconds));
if (durationSeconds < 64) {
durationSeconds *= 2;
if (maxAttempts != null && attempts >= maxAttempts) {
_logger.printStatus('Download failed -- retry $attempts');
return null;
}
_logger.printStatus(
'Download failed -- attempting retry $attempts in '
'$durationSeconds second${ durationSeconds == 1 ? "" : "s"}...',
);
await Future<void>.delayed(Duration(seconds: durationSeconds));
if (durationSeconds < 64) {
durationSeconds *= 2;
}
}
}
}
/// Check if the given URL points to a valid endpoint.
Future<bool> doesRemoteFileExist(Uri url) async => await _attempt(url, onlyHeaders: true);
// Returns true on success and false on failure.
Future<bool> _attempt(Uri url, {
IOSink destSink,
bool onlyHeaders = false,
}) async {
assert(onlyHeaders || destSink != null);
globals.printTrace('Downloading: $url');
HttpClient httpClient;
if (context.get<HttpClientFactory>() != null) {
httpClient = context.get<HttpClientFactory>()();
} else {
httpClient = HttpClient();
}
HttpClientRequest request;
HttpClientResponse response;
try {
if (onlyHeaders) {
request = await httpClient.headUrl(url);
/// Check if the given URL points to a valid endpoint.
Future<bool> doesRemoteFileExist(Uri url) => _attempt(url, onlyHeaders: true);
// Returns true on success and false on failure.
Future<bool> _attempt(Uri url, {
IOSink destSink,
bool onlyHeaders = false,
}) async {
assert(onlyHeaders || destSink != null);
_logger.printTrace('Downloading: $url');
HttpClient httpClient;
if (_httpClientFactory != null) {
httpClient = _httpClientFactory();
} else {
request = await httpClient.getUrl(url);
}
response = await request.close();
} on ArgumentError catch (error) {
final String overrideUrl = globals.platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (overrideUrl != null && url.toString().contains(overrideUrl)) {
globals.printError(error.toString());
throwToolExit(
'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be '
'parsed as a valid url. Please see https://flutter.dev/community/china '
'for an example of how to use it.\n'
'Full URL: $url',
exitCode: kNetworkProblemExitCode,);
httpClient = HttpClient();
}
globals.printError(error.toString());
rethrow;
} on HandshakeException catch (error) {
globals.printTrace(error.toString());
throwToolExit(
'Could not authenticate download server. You may be experiencing a man-in-the-middle attack,\n'
'your network may be compromised, or you may have malware installed on your computer.\n'
'URL: $url',
exitCode: kNetworkProblemExitCode,
);
} on SocketException catch (error) {
globals.printTrace('Download error: $error');
return false;
} on HttpException catch (error) {
globals.printTrace('Download error: $error');
return false;
}
assert(response != null);
// If we're making a HEAD request, we're only checking to see if the URL is
// valid.
if (onlyHeaders) {
return response.statusCode == HttpStatus.ok;
}
if (response.statusCode != HttpStatus.ok) {
if (response.statusCode > 0 && response.statusCode < 500) {
HttpClientRequest request;
HttpClientResponse response;
try {
if (onlyHeaders) {
request = await httpClient.headUrl(url);
} else {
request = await httpClient.getUrl(url);
}
response = await request.close();
} on ArgumentError catch (error) {
final String overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (overrideUrl != null && url.toString().contains(overrideUrl)) {
_logger.printError(error.toString());
throwToolExit(
'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be '
'parsed as a valid url. Please see https://flutter.dev/community/china '
'for an example of how to use it.\n'
'Full URL: $url',
exitCode: kNetworkProblemExitCode,);
}
_logger.printError(error.toString());
rethrow;
} on HandshakeException catch (error) {
_logger.printTrace(error.toString());
throwToolExit(
'Download failed.\n'
'URL: $url\n'
'Error: ${response.statusCode} ${response.reasonPhrase}',
'Could not authenticate download server. You may be experiencing a man-in-the-middle attack,\n'
'your network may be compromised, or you may have malware installed on your computer.\n'
'URL: $url',
exitCode: kNetworkProblemExitCode,
);
} on SocketException catch (error) {
_logger.printTrace('Download error: $error');
return false;
} on HttpException catch (error) {
_logger.printTrace('Download error: $error');
return false;
}
assert(response != null);
// If we're making a HEAD request, we're only checking to see if the URL is
// valid.
if (onlyHeaders) {
return response.statusCode == HttpStatus.ok;
}
if (response.statusCode != HttpStatus.ok) {
if (response.statusCode > 0 && response.statusCode < 500) {
throwToolExit(
'Download failed.\n'
'URL: $url\n'
'Error: ${response.statusCode} ${response.reasonPhrase}',
exitCode: kNetworkProblemExitCode,
);
}
// 5xx errors are server errors and we can try again
_logger.printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
return false;
}
_logger.printTrace('Received response from server, collecting bytes...');
try {
assert(destSink != null);
await response.forEach(destSink.add);
return true;
} on IOException catch (error) {
_logger.printTrace('Download error: $error');
return false;
} finally {
await destSink?.flush();
await destSink?.close();
}
// 5xx errors are server errors and we can try again
globals.printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
return false;
}
globals.printTrace('Received response from server, collecting bytes...');
try {
assert(destSink != null);
await response.forEach(destSink.add);
return true;
} on IOException catch (error) {
globals.printTrace('Download error: $error');
return false;
} finally {
await destSink?.flush();
await destSink?.close();
}
}
/// An IOSink that collects whatever is written to it.
class _MemoryIOSink implements IOSink {
@override
......
......@@ -103,6 +103,9 @@ class Cache {
_fileSystem = fileSystem ?? globals.fs,
_platform = platform ?? globals.platform ,
_osUtils = osUtils ?? os {
// TODO(zra): Move to initializer list once logger and platform parameters
// are required.
_net = Net(logger: _logger, platform: _platform);
if (artifacts == null) {
_artifacts.add(MaterialFonts(this));
......@@ -135,6 +138,8 @@ class Cache {
final FileSystem _fileSystem;
final OperatingSystemUtils _osUtils;
Net _net;
static const List<String> _hostsBlockedInChina = <String> [
'storage.googleapis.com',
];
......@@ -386,7 +391,7 @@ class Cache {
final File cachedFile = _fileSystem.file(_fileSystem.path.join(serviceDir.path, url.pathSegments.last));
if (!cachedFile.existsSync()) {
try {
await _downloadFile(url, cachedFile);
await downloadFile(url, cachedFile);
} catch (e) {
throwToolExit('Failed to fetch third-party artifact $url: $e');
}
......@@ -439,6 +444,26 @@ class Cache {
this.includeAllPlatforms = includeAllPlatformsState;
return allAvailible;
}
/// Download a file from the given [url] and write it to [location].
Future<void> downloadFile(Uri url, File location) async {
_ensureExists(location.parent);
await _net.fetchUrl(url, destFile: location);
}
Future<bool> doesRemoteExist(String message, Uri url) async {
final Status status = globals.logger.startProgress(
message,
timeout: timeoutConfiguration.slowOperation,
);
bool exists;
try {
exists = await _net.doesRemoteFileExist(url);
} finally {
status.stop();
}
return exists;
}
}
/// Representation of a set of artifacts used by the tool.
......@@ -559,7 +584,7 @@ abstract class CachedArtifact extends ArtifactSet {
if (!verifier(tempFile)) {
final Status status = globals.logger.startProgress(message, timeout: timeoutConfiguration.slowOperation);
try {
await _downloadFile(url, tempFile);
await cache.downloadFile(url, tempFile);
status.stop();
} catch (exception) {
status.cancel();
......@@ -741,7 +766,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {
bool exists = false;
for (final String pkgName in getPackageDirs()) {
exists = await _doesRemoteExist('Checking package $pkgName is available...',
exists = await cache.doesRemoteExist('Checking package $pkgName is available...',
Uri.parse(url + pkgName + '.zip'));
if (!exists) {
return false;
......@@ -751,7 +776,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {
for (final List<String> toolsDir in getBinaryDirs()) {
final String cacheDir = toolsDir[0];
final String urlPath = toolsDir[1];
exists = await _doesRemoteExist('Checking $cacheDir tools are available...',
exists = await cache.doesRemoteExist('Checking $cacheDir tools are available...',
Uri.parse(url + urlPath));
if (!exists) {
return false;
......@@ -1305,19 +1330,6 @@ String flattenNameSubdirs(Uri url) {
return globals.fs.path.joinAll(convertedPieces);
}
/// Download a file from the given [url] and write it to [location].
Future<void> _downloadFile(Uri url, File location) async {
_ensureExists(location.parent);
await fetchUrl(url, destFile: location);
}
Future<bool> _doesRemoteExist(String message, Uri url) async {
final Status status = globals.logger.startProgress(message, timeout: timeoutConfiguration.slowOperation);
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()) {
......
......@@ -11,7 +11,9 @@ import '../android/android.dart' as android;
import '../android/android_sdk.dart' as android_sdk;
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/net.dart';
import '../base/os.dart';
import '../base/utils.dart';
......@@ -165,6 +167,14 @@ class CreateCommand extends FlutterCommand {
};
}
// Lazy-initialize the net utilities with values from the context.
Net _cachedNet;
Net get _net => _cachedNet ??= Net(
httpClientFactory: context.get<HttpClientFactory>() ?? () => HttpClient(),
logger: globals.logger,
platform: globals.platform,
);
// If it has a .metadata file with the project_type in it, use that.
// If it has an android dir and an android/app dir, it's a legacy app
// If it has an ios dir and an ios/Flutter dir, it's a legacy app
......@@ -229,13 +239,14 @@ class CreateCommand extends FlutterCommand {
'documentation and try again.');
}
return utf8.decode(await fetchUrl(Uri.https(_snippetsHost, 'snippets/$sampleId.dart')));
final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/$sampleId.dart');
return utf8.decode(await _net.fetchUrl(snippetsUri));
}
/// Fetches the samples index file from the Flutter docs website.
Future<String> _fetchSamplesIndexFromServer() async {
return utf8.decode(
await fetchUrl(Uri.https(_snippetsHost, 'snippets/index.json'), maxAttempts: 2));
final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json');
return utf8.decode(await _net.fetchUrl(snippetsUri, maxAttempts: 2));
}
/// Fetches the samples index file from the server and writes it to
......
......@@ -8,7 +8,9 @@ import 'dart:collection';
import 'package:meta/meta.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/net.dart';
import '../cache.dart';
......@@ -88,14 +90,27 @@ class UpdatePackagesCommand extends FlutterCommand {
@override
final bool hidden;
// Lazy-initialize the net utilities with values from the context.
Net _cachedNet;
Net get _net => _cachedNet ??= Net(
httpClientFactory: context.get<HttpClientFactory>() ?? () => HttpClient(),
logger: globals.logger,
platform: globals.platform,
);
Future<void> _downloadCoverageData() async {
final Status status = globals.logger.startProgress(
'Downloading lcov data for package:flutter...',
timeout: timeoutConfiguration.slowOperation,
);
final String urlBase = globals.platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com';
final List<int> data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info'));
final String coverageDir = globals.fs.path.join(Cache.flutterRoot, 'packages/flutter/coverage');
final Uri coverageUri = Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info');
final List<int> data = await _net.fetchUrl(coverageUri);
final String coverageDir = globals.fs.path.join(
Cache.flutterRoot,
'packages/flutter/coverage',
);
globals.fs.file(globals.fs.path.join(coverageDir, 'lcov.base.info'))
..createSync(recursive: true)
..writeAsBytesSync(data, flush: true);
......
......@@ -922,4 +922,13 @@ class FakeCache implements Cache {
@override
Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts) async {
}
@override
Future<void> downloadFile(Uri url, File location) async {
}
@override
Future<bool> doesRemoteExist(String message, Uri url) async {
return true;
}
}
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