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