[flutter_tools] cache result of BotDetector in persistent tool state (#52325)

The Azure bot detection can take up to a second to determine if a client is/isn't a bot. To prevent this from slowing down all flutter commands, we can cache the results in the persistent tool state - since we don't expect the same client id to ever become a bot or stop being a bot
This commit is contained in:
Jonah Williams 2020-03-10 11:35:52 -07:00 committed by GitHub
parent 377879825e
commit 2133343a29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 10 deletions

View file

@ -35,8 +35,6 @@ Future<int> run(
String flutterVersion, String flutterVersion,
Map<Type, Generator> overrides, Map<Type, Generator> overrides,
}) async { }) async {
reportCrashes ??= !await globals.isRunningOnBot;
if (muteCommandLogging) { if (muteCommandLogging) {
// Remove the verbose option; for help and doctor, users don't need to see // Remove the verbose option; for help and doctor, users don't need to see
// verbose logs. // verbose logs.
@ -48,6 +46,8 @@ Future<int> run(
commands.forEach(runner.addCommand); commands.forEach(runner.addCommand);
return runInContext<int>(() async { return runInContext<int>(() async {
reportCrashes ??= !await globals.isRunningOnBot;
// Initialize the system locale. // Initialize the system locale.
final String systemLocale = await intl_standalone.findSystemLocale(); final String systemLocale = await intl_standalone.findSystemLocale();
intl.Intl.defaultLocale = intl.Intl.verifiedLocale( intl.Intl.defaultLocale = intl.Intl.verifiedLocale(

View file

@ -5,6 +5,7 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import '../persistent_tool_state.dart';
import 'io.dart'; import 'io.dart';
import 'net.dart'; import 'net.dart';
@ -12,20 +13,21 @@ class BotDetector {
BotDetector({ BotDetector({
@required HttpClientFactory httpClientFactory, @required HttpClientFactory httpClientFactory,
@required Platform platform, @required Platform platform,
@required PersistentToolState persistentToolState,
}) : }) :
_platform = platform, _platform = platform,
_azureDetector = AzureDetector( _azureDetector = AzureDetector(
httpClientFactory: httpClientFactory, httpClientFactory: httpClientFactory,
); ),
_persistentToolState = persistentToolState;
final Platform _platform; final Platform _platform;
final AzureDetector _azureDetector; final AzureDetector _azureDetector;
final PersistentToolState _persistentToolState;
bool _isRunningOnBot;
Future<bool> get isRunningOnBot async { Future<bool> get isRunningOnBot async {
if (_isRunningOnBot != null) { if (_persistentToolState.isRunningOnBot != null) {
return _isRunningOnBot; return _persistentToolState.isRunningOnBot;
} }
if ( if (
// Explicitly stated to not be a bot. // Explicitly stated to not be a bot.
@ -36,10 +38,10 @@ class BotDetector {
// When set, GA logs to a local file (normally for tests) so we don't need to filter. // When set, GA logs to a local file (normally for tests) so we don't need to filter.
|| _platform.environment.containsKey('FLUTTER_ANALYTICS_LOG_FILE') || _platform.environment.containsKey('FLUTTER_ANALYTICS_LOG_FILE')
) { ) {
return _isRunningOnBot = false; return _persistentToolState.isRunningOnBot = false;
} }
return _isRunningOnBot = _platform.environment['BOT'] == 'true' return _persistentToolState.isRunningOnBot = _platform.environment['BOT'] == 'true'
// https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
|| _platform.environment['TRAVIS'] == 'true' || _platform.environment['TRAVIS'] == 'true'
@ -94,7 +96,7 @@ class AzureDetector {
return _isRunningOnAzure; return _isRunningOnAzure;
} }
final HttpClient client = _httpClientFactory() final HttpClient client = _httpClientFactory()
..connectionTimeout = const Duration(seconds: 1); ..connectionTimeout = const Duration(milliseconds: 250);
try { try {
final HttpClientRequest request = await client.getUrl( final HttpClientRequest request = await client.getUrl(
Uri.parse(_serviceUrl), Uri.parse(_serviceUrl),

View file

@ -80,6 +80,7 @@ XCDevice get xcdevice => context.get<XCDevice>();
final BotDetector _defaultBotDetector = BotDetector( final BotDetector _defaultBotDetector = BotDetector(
httpClientFactory: context.get<HttpClientFactory>() ?? () => HttpClient(), httpClientFactory: context.get<HttpClientFactory>() ?? () => HttpClient(),
platform: platform, platform: platform,
persistentToolState: persistentToolState,
); );
BotDetector get botDetector => context.get<BotDetector>() ?? _defaultBotDetector; BotDetector get botDetector => context.get<BotDetector>() ?? _defaultBotDetector;

View file

@ -46,6 +46,9 @@ abstract class PersistentToolState {
/// Update the last active version for a given [channel]. /// Update the last active version for a given [channel].
void updateLastActiveVersion(String fullGitHash, Channel channel); void updateLastActiveVersion(String fullGitHash, Channel channel);
/// Whether this client was already determined to be or not be a bot.
bool isRunningOnBot;
} }
class _DefaultPersistentToolState implements PersistentToolState { class _DefaultPersistentToolState implements PersistentToolState {
@ -78,6 +81,7 @@ class _DefaultPersistentToolState implements PersistentToolState {
Channel.beta: 'last-active-beta-version', Channel.beta: 'last-active-beta-version',
Channel.stable: 'last-active-stable-version' Channel.stable: 'last-active-stable-version'
}; };
static const String _kBotKey = 'is-bot';
final Config _config; final Config _config;
@ -108,4 +112,10 @@ class _DefaultPersistentToolState implements PersistentToolState {
String _versionKeyFor(Channel channel) { String _versionKeyFor(Channel channel) {
return _lastActiveVersionKeys[channel]; return _lastActiveVersionKeys[channel];
} }
@override
bool get isRunningOnBot => _config.getValue(_kBotKey) as bool;
@override
set isRunningOnBot(bool value) => _config.setValue(_kBotKey, value);
} }

View file

@ -2,8 +2,11 @@
// 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 'package:file/memory.dart';
import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/bot_detector.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
@ -18,6 +21,7 @@ void main() {
MockHttpClientRequest mockHttpClientRequest; MockHttpClientRequest mockHttpClientRequest;
MockHttpHeaders mockHttpHeaders; MockHttpHeaders mockHttpHeaders;
BotDetector botDetector; BotDetector botDetector;
PersistentToolState persistentToolState;
setUp(() { setUp(() {
fakePlatform = FakePlatform()..environment = <String, String>{}; fakePlatform = FakePlatform()..environment = <String, String>{};
@ -25,9 +29,14 @@ void main() {
mockHttpClient = MockHttpClient(); mockHttpClient = MockHttpClient();
mockHttpClientRequest = MockHttpClientRequest(); mockHttpClientRequest = MockHttpClientRequest();
mockHttpHeaders = MockHttpHeaders(); mockHttpHeaders = MockHttpHeaders();
persistentToolState = PersistentToolState.test(
directory: MemoryFileSystem.test().currentDirectory,
logger: BufferLogger.test(),
);
botDetector = BotDetector( botDetector = BotDetector(
platform: fakePlatform, platform: fakePlatform,
httpClientFactory: () => mockHttpClient, httpClientFactory: () => mockHttpClient,
persistentToolState: persistentToolState,
); );
}); });
@ -35,13 +44,17 @@ void main() {
testWithoutContext('returns false unconditionally if BOT=false is set', () async { testWithoutContext('returns false unconditionally if BOT=false is set', () async {
fakePlatform.environment['BOT'] = 'false'; fakePlatform.environment['BOT'] = 'false';
fakePlatform.environment['TRAVIS'] = 'true'; fakePlatform.environment['TRAVIS'] = 'true';
expect(await botDetector.isRunningOnBot, isFalse); expect(await botDetector.isRunningOnBot, isFalse);
expect(persistentToolState.isRunningOnBot, isFalse);
}); });
testWithoutContext('returns false unconditionally if FLUTTER_HOST is set', () async { testWithoutContext('returns false unconditionally if FLUTTER_HOST is set', () async {
fakePlatform.environment['FLUTTER_HOST'] = 'foo'; fakePlatform.environment['FLUTTER_HOST'] = 'foo';
fakePlatform.environment['TRAVIS'] = 'true'; fakePlatform.environment['TRAVIS'] = 'true';
expect(await botDetector.isRunningOnBot, isFalse); expect(await botDetector.isRunningOnBot, isFalse);
expect(persistentToolState.isRunningOnBot, isFalse);
}); });
testWithoutContext('returns false with and without a terminal attached', () async { testWithoutContext('returns false with and without a terminal attached', () async {
@ -52,12 +65,14 @@ void main() {
expect(await botDetector.isRunningOnBot, isFalse); expect(await botDetector.isRunningOnBot, isFalse);
mockStdio.stdout.hasTerminal = false; mockStdio.stdout.hasTerminal = false;
expect(await botDetector.isRunningOnBot, isFalse); expect(await botDetector.isRunningOnBot, isFalse);
expect(persistentToolState.isRunningOnBot, isFalse);
}); });
testWithoutContext('can test analytics outputs on bots when outputting to a file', () async { testWithoutContext('can test analytics outputs on bots when outputting to a file', () async {
fakePlatform.environment['TRAVIS'] = 'true'; fakePlatform.environment['TRAVIS'] = 'true';
fakePlatform.environment['FLUTTER_ANALYTICS_LOG_FILE'] = '/some/file'; fakePlatform.environment['FLUTTER_ANALYTICS_LOG_FILE'] = '/some/file';
expect(await botDetector.isRunningOnBot, isFalse); expect(await botDetector.isRunningOnBot, isFalse);
expect(persistentToolState.isRunningOnBot, isFalse);
}); });
testWithoutContext('returns true when azure metadata is reachable', () async { testWithoutContext('returns true when azure metadata is reachable', () async {
@ -65,12 +80,31 @@ void main() {
return Future<HttpClientRequest>.value(mockHttpClientRequest); return Future<HttpClientRequest>.value(mockHttpClientRequest);
}); });
when(mockHttpClientRequest.headers).thenReturn(mockHttpHeaders); when(mockHttpClientRequest.headers).thenReturn(mockHttpHeaders);
expect(await botDetector.isRunningOnBot, isTrue); expect(await botDetector.isRunningOnBot, isTrue);
expect(persistentToolState.isRunningOnBot, isTrue);
});
testWithoutContext('caches azure bot detection results across instances', () async {
when(mockHttpClient.getUrl(any)).thenAnswer((_) {
return Future<HttpClientRequest>.value(mockHttpClientRequest);
});
when(mockHttpClientRequest.headers).thenReturn(mockHttpHeaders);
expect(await botDetector.isRunningOnBot, isTrue);
expect(await BotDetector(
platform: fakePlatform,
httpClientFactory: () => mockHttpClient,
persistentToolState: persistentToolState,
).isRunningOnBot, isTrue);
verify(mockHttpClient.getUrl(any)).called(1);
}); });
testWithoutContext('returns true when running on borg', () async { testWithoutContext('returns true when running on borg', () async {
fakePlatform.environment['BORG_ALLOC_DIR'] = 'true'; fakePlatform.environment['BORG_ALLOC_DIR'] = 'true';
expect(await botDetector.isRunningOnBot, isTrue); expect(await botDetector.isRunningOnBot, isTrue);
expect(persistentToolState.isRunningOnBot, isTrue);
}); });
}); });
}); });
@ -94,6 +128,7 @@ void main() {
when(mockHttpClient.getUrl(any)).thenAnswer((_) { when(mockHttpClient.getUrl(any)).thenAnswer((_) {
throw const SocketException('HTTP connection timed out'); throw const SocketException('HTTP connection timed out');
}); });
expect(await azureDetector.isRunningOnAzure, isFalse); expect(await azureDetector.isRunningOnAzure, isFalse);
}); });
@ -102,6 +137,7 @@ void main() {
return Future<HttpClientRequest>.value(mockHttpClientRequest); return Future<HttpClientRequest>.value(mockHttpClientRequest);
}); });
when(mockHttpClientRequest.headers).thenReturn(mockHttpHeaders); when(mockHttpClientRequest.headers).thenReturn(mockHttpHeaders);
expect(await azureDetector.isRunningOnAzure, isTrue); expect(await azureDetector.isRunningOnAzure, isTrue);
}); });
}); });