[flutter_tools] Refactor checkVersionFreshness (#95056)
This commit is contained in:
committed by
GitHub
parent
7addb91364
commit
2d2cd1f5c0
@@ -278,3 +278,6 @@ FlutterProjectFactory get projectFactory {
|
||||
CustomDevicesConfig get customDevicesConfig => context.get<CustomDevicesConfig>()!;
|
||||
|
||||
PreRunValidator get preRunValidator => context.get<PreRunValidator>() ?? const NoOpPreRunValidator();
|
||||
|
||||
// TODO(fujino): Migrate to 'main' https://github.com/flutter/flutter/issues/95041
|
||||
const String kDefaultFrameworkChannel = 'master';
|
||||
|
||||
@@ -18,6 +18,7 @@ const String _unknownFrameworkVersion = '0.0.0-unknown';
|
||||
|
||||
/// The names of each channel/branch in order of increasing stability.
|
||||
enum Channel {
|
||||
// TODO(fujino): update to main https://github.com/flutter/flutter/issues/95041
|
||||
master,
|
||||
dev,
|
||||
beta,
|
||||
@@ -26,7 +27,7 @@ enum Channel {
|
||||
|
||||
// Beware: Keep order in accordance with stability
|
||||
const Set<String> kOfficialChannels = <String>{
|
||||
'master',
|
||||
globals.kDefaultFrameworkChannel,
|
||||
'dev',
|
||||
'beta',
|
||||
'stable',
|
||||
@@ -241,15 +242,15 @@ class FlutterVersion {
|
||||
}
|
||||
final DateTime? latestFlutterCommitDate = await _getLatestAvailableFlutterDate();
|
||||
|
||||
await checkVersionFreshness(
|
||||
this,
|
||||
return VersionFreshnessValidator(
|
||||
version: this,
|
||||
clock: _clock,
|
||||
localFrameworkCommitDate: localFrameworkCommitDate,
|
||||
latestFlutterCommitDate: latestFlutterCommitDate,
|
||||
logger: globals.logger,
|
||||
cache: globals.cache,
|
||||
pauseTime: timeToPauseToLetUserReadTheMessage,
|
||||
);
|
||||
pauseTime: VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage,
|
||||
).run();
|
||||
}
|
||||
|
||||
/// The name of the temporary git remote used to check for the latest
|
||||
@@ -361,13 +362,14 @@ class FlutterVersion {
|
||||
globals.cache.checkLockAcquired();
|
||||
final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(globals.cache, globals.logger);
|
||||
|
||||
final DateTime now = _clock.now();
|
||||
if (versionCheckStamp.lastTimeVersionWasChecked != null) {
|
||||
final Duration timeSinceLastCheck = _clock.now().difference(
|
||||
final Duration timeSinceLastCheck = now.difference(
|
||||
versionCheckStamp.lastTimeVersionWasChecked!,
|
||||
);
|
||||
|
||||
// Don't ping the server too often. Return cached value if it's fresh.
|
||||
if (timeSinceLastCheck < checkAgeConsideredUpToDate) {
|
||||
if (timeSinceLastCheck < VersionFreshnessValidator.checkAgeConsideredUpToDate) {
|
||||
return versionCheckStamp.lastKnownRemoteVersion;
|
||||
}
|
||||
}
|
||||
@@ -378,7 +380,7 @@ class FlutterVersion {
|
||||
await FlutterVersion.fetchRemoteFrameworkCommitDate(channel),
|
||||
);
|
||||
await versionCheckStamp.store(
|
||||
newTimeVersionWasChecked: _clock.now(),
|
||||
newTimeVersionWasChecked: now,
|
||||
newKnownRemoteVersion: remoteFrameworkCommitDate,
|
||||
);
|
||||
return remoteFrameworkCommitDate;
|
||||
@@ -390,7 +392,7 @@ class FlutterVersion {
|
||||
// Still update the timestamp to avoid us hitting the server on every single
|
||||
// command if for some reason we cannot connect (eg. we may be offline).
|
||||
await versionCheckStamp.store(
|
||||
newTimeVersionWasChecked: _clock.now(),
|
||||
newTimeVersionWasChecked: now,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -743,53 +745,152 @@ enum VersionCheckResult {
|
||||
newVersionAvailable,
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> checkVersionFreshness(FlutterVersion version, {
|
||||
required DateTime localFrameworkCommitDate,
|
||||
required DateTime? latestFlutterCommitDate,
|
||||
required SystemClock clock,
|
||||
required Cache cache,
|
||||
required Logger logger,
|
||||
Duration pauseTime = Duration.zero,
|
||||
}) async {
|
||||
// Don't perform update checks if we're not on an official channel.
|
||||
if (!kOfficialChannels.contains(version.channel)) {
|
||||
return;
|
||||
/// Determine whether or not the provided [version] is "fresh" and notify the user if appropriate.
|
||||
///
|
||||
/// To initiate the validation check, call [run].
|
||||
///
|
||||
/// We do not want to check with the upstream git remote for newer commits on
|
||||
/// every tool invocation, as this would significantly slow down running tool
|
||||
/// commands. Thus, the tool writes to the [VersionCheckStamp] every time that
|
||||
/// it actually has fetched commits from upstream, and this validator only
|
||||
/// checks again if it has been more than [checkAgeConsideredUpToDate] since the
|
||||
/// last fetch.
|
||||
///
|
||||
/// We do not want to notify users with "reasonably" fresh versions about new
|
||||
/// releases. The method [versionAgeConsideredUpToDate] defines a different
|
||||
/// duration of freshness for each channel. If [localFrameworkCommitDate] is
|
||||
/// newer than this duration, then we do not show the warning.
|
||||
///
|
||||
/// We do not want to annoy users who intentionally disregard the warning and
|
||||
/// choose not to upgrade. Thus, we only show the message if it has been more
|
||||
/// than [maxTimeSinceLastWarning] since the last time the user saw the warning.
|
||||
class VersionFreshnessValidator {
|
||||
VersionFreshnessValidator({
|
||||
required this.version,
|
||||
required this.localFrameworkCommitDate,
|
||||
required this.clock,
|
||||
required this.cache,
|
||||
required this.logger,
|
||||
this.latestFlutterCommitDate,
|
||||
this.pauseTime = Duration.zero,
|
||||
});
|
||||
|
||||
final FlutterVersion version;
|
||||
final DateTime localFrameworkCommitDate;
|
||||
final SystemClock clock;
|
||||
final Cache cache;
|
||||
final Logger logger;
|
||||
final Duration pauseTime;
|
||||
final DateTime? latestFlutterCommitDate;
|
||||
|
||||
late final DateTime now = clock.now();
|
||||
late final Duration frameworkAge = now.difference(localFrameworkCommitDate);
|
||||
|
||||
/// The amount of time we wait before pinging the server to check for the
|
||||
/// availability of a newer version of Flutter.
|
||||
@visibleForTesting
|
||||
static const Duration checkAgeConsideredUpToDate = Duration(days: 3);
|
||||
|
||||
/// The amount of time we wait between issuing a warning.
|
||||
///
|
||||
/// This is to avoid annoying users who are unable to upgrade right away.
|
||||
@visibleForTesting
|
||||
static const Duration maxTimeSinceLastWarning = Duration(days: 1);
|
||||
|
||||
/// The amount of time we pause for to let the user read the message about
|
||||
/// outdated Flutter installation.
|
||||
///
|
||||
/// This can be customized in tests to speed them up.
|
||||
@visibleForTesting
|
||||
static Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2);
|
||||
|
||||
// We show a warning if either we know there is a new remote version, or we
|
||||
// couldn't tell but the local version is outdated.
|
||||
@visibleForTesting
|
||||
bool canShowWarning(VersionCheckResult remoteVersionStatus) {
|
||||
final bool installationSeemsOutdated = frameworkAge > versionAgeConsideredUpToDate(version.channel);
|
||||
if (remoteVersionStatus == VersionCheckResult.newVersionAvailable) {
|
||||
return true;
|
||||
}
|
||||
if (!installationSeemsOutdated) {
|
||||
return false;
|
||||
}
|
||||
return remoteVersionStatus == VersionCheckResult.unknown;
|
||||
}
|
||||
|
||||
final Duration frameworkAge = clock.now().difference(localFrameworkCommitDate);
|
||||
final bool installationSeemsOutdated = frameworkAge > versionAgeConsideredUpToDate(version.channel);
|
||||
/// We warn the user if the age of their Flutter installation is greater than
|
||||
/// this duration. The durations are slightly longer than the expected release
|
||||
/// cadence for each channel, to give the user a grace period before they get
|
||||
/// notified.
|
||||
///
|
||||
/// For example, for the beta channel, this is set to eight weeks because
|
||||
/// beta releases happen approximately every month.
|
||||
@visibleForTesting
|
||||
static Duration versionAgeConsideredUpToDate(String channel) {
|
||||
switch (channel) {
|
||||
case 'stable':
|
||||
return const Duration(days: 365 ~/ 2); // Six months
|
||||
case 'beta':
|
||||
return const Duration(days: 7 * 8); // Eight weeks
|
||||
case 'dev':
|
||||
return const Duration(days: 7 * 4); // Four weeks
|
||||
default:
|
||||
return const Duration(days: 7 * 3); // Three weeks
|
||||
}
|
||||
}
|
||||
|
||||
// Get whether there's a newer version on the remote. This only goes
|
||||
// to the server if we haven't checked recently so won't happen on every
|
||||
// command.
|
||||
final VersionCheckResult remoteVersionStatus = latestFlutterCommitDate == null
|
||||
? VersionCheckResult.unknown
|
||||
: latestFlutterCommitDate.isAfter(localFrameworkCommitDate)
|
||||
? VersionCheckResult.newVersionAvailable
|
||||
: VersionCheckResult.versionIsCurrent;
|
||||
/// Execute validations and print warning to [logger] if necessary.
|
||||
Future<void> run() async {
|
||||
// Don't perform update checks if we're not on an official channel.
|
||||
if (!kOfficialChannels.contains(version.channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not load the stamp before the above server check as it may modify the stamp file.
|
||||
final VersionCheckStamp stamp = await VersionCheckStamp.load(cache, logger);
|
||||
final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2);
|
||||
final bool beenAWhileSinceWarningWasPrinted = clock.now().difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
|
||||
// Get whether there's a newer version on the remote. This only goes
|
||||
// to the server if we haven't checked recently so won't happen on every
|
||||
// command.
|
||||
final VersionCheckResult remoteVersionStatus;
|
||||
|
||||
// We show a warning if either we know there is a new remote version, or we couldn't tell but the local
|
||||
// version is outdated.
|
||||
final bool canShowWarning =
|
||||
remoteVersionStatus == VersionCheckResult.newVersionAvailable ||
|
||||
(remoteVersionStatus == VersionCheckResult.unknown &&
|
||||
installationSeemsOutdated);
|
||||
if (latestFlutterCommitDate == null) {
|
||||
remoteVersionStatus = VersionCheckResult.unknown;
|
||||
} else {
|
||||
if (latestFlutterCommitDate!.isAfter(localFrameworkCommitDate)) {
|
||||
remoteVersionStatus = VersionCheckResult.newVersionAvailable;
|
||||
} else {
|
||||
remoteVersionStatus = VersionCheckResult.versionIsCurrent;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not load the stamp before the above server check as it may modify the stamp file.
|
||||
final VersionCheckStamp stamp = await VersionCheckStamp.load(cache, logger);
|
||||
final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? clock.ago(maxTimeSinceLastWarning * 2);
|
||||
final bool beenAWhileSinceWarningWasPrinted = now.difference(lastTimeWarningWasPrinted) > maxTimeSinceLastWarning;
|
||||
if (!beenAWhileSinceWarningWasPrinted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool canShowWarningResult = canShowWarning(remoteVersionStatus);
|
||||
|
||||
if (!canShowWarningResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
// By this point, we should show the update message
|
||||
final String updateMessage;
|
||||
switch (remoteVersionStatus) {
|
||||
case VersionCheckResult.newVersionAvailable:
|
||||
updateMessage = newVersionAvailableMessage();
|
||||
break;
|
||||
case VersionCheckResult.versionIsCurrent:
|
||||
case VersionCheckResult.unknown:
|
||||
updateMessage = versionOutOfDateMessage(frameworkAge);
|
||||
break;
|
||||
}
|
||||
|
||||
if (beenAWhileSinceWarningWasPrinted && canShowWarning) {
|
||||
final String updateMessage =
|
||||
remoteVersionStatus == VersionCheckResult.newVersionAvailable
|
||||
? newVersionAvailableMessage()
|
||||
: versionOutOfDateMessage(frameworkAge);
|
||||
logger.printStatus(updateMessage, emphasis: true);
|
||||
await Future.wait<void>(<Future<void>>[
|
||||
stamp.store(
|
||||
newTimeWarningWasPrinted: clock.now(),
|
||||
newTimeWarningWasPrinted: now,
|
||||
cache: cache,
|
||||
),
|
||||
Future<void>.delayed(pauseTime),
|
||||
@@ -797,45 +898,6 @@ Future<void> checkVersionFreshness(FlutterVersion version, {
|
||||
}
|
||||
}
|
||||
|
||||
/// The amount of time we wait before pinging the server to check for the
|
||||
/// availability of a newer version of Flutter.
|
||||
@visibleForTesting
|
||||
const Duration checkAgeConsideredUpToDate = Duration(days: 3);
|
||||
|
||||
/// We warn the user if the age of their Flutter installation is greater than
|
||||
/// this duration. The durations are slightly longer than the expected release
|
||||
/// cadence for each channel, to give the user a grace period before they get
|
||||
/// notified.
|
||||
///
|
||||
/// For example, for the beta channel, this is set to five weeks because
|
||||
/// beta releases happen approximately every month.
|
||||
@visibleForTesting
|
||||
Duration versionAgeConsideredUpToDate(String channel) {
|
||||
switch (channel) {
|
||||
case 'stable':
|
||||
return const Duration(days: 365 ~/ 2); // Six months
|
||||
case 'beta':
|
||||
return const Duration(days: 7 * 8); // Eight weeks
|
||||
case 'dev':
|
||||
return const Duration(days: 7 * 4); // Four weeks
|
||||
default:
|
||||
return const Duration(days: 7 * 3); // Three weeks
|
||||
}
|
||||
}
|
||||
|
||||
/// The amount of time we wait between issuing a warning.
|
||||
///
|
||||
/// This is to avoid annoying users who are unable to upgrade right away.
|
||||
@visibleForTesting
|
||||
const Duration maxTimeSinceLastWarning = Duration(days: 1);
|
||||
|
||||
/// The amount of time we pause for to let the user read the message about
|
||||
/// outdated Flutter installation.
|
||||
///
|
||||
/// This can be customized in tests to speed them up.
|
||||
@visibleForTesting
|
||||
Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2);
|
||||
|
||||
@visibleForTesting
|
||||
String versionOutOfDateMessage(Duration frameworkAge) {
|
||||
String warning = 'WARNING: your installation of Flutter is ${frameworkAge.inDays} days old.';
|
||||
|
||||
@@ -20,8 +20,8 @@ import '../src/context.dart';
|
||||
import '../src/fake_process_manager.dart';
|
||||
|
||||
final SystemClock _testClock = SystemClock.fixed(DateTime(2015));
|
||||
final DateTime _stampUpToDate = _testClock.ago(checkAgeConsideredUpToDate ~/ 2);
|
||||
final DateTime _stampOutOfDate = _testClock.ago(checkAgeConsideredUpToDate * 2);
|
||||
final DateTime _stampUpToDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate ~/ 2);
|
||||
final DateTime _stampOutOfDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate * 2);
|
||||
|
||||
void main() {
|
||||
FakeCache cache;
|
||||
@@ -42,17 +42,17 @@ void main() {
|
||||
|
||||
for (final String channel in kOfficialChannels) {
|
||||
DateTime getChannelUpToDateVersion() {
|
||||
return _testClock.ago(versionAgeConsideredUpToDate(channel) ~/ 2);
|
||||
return _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate(channel) ~/ 2);
|
||||
}
|
||||
|
||||
DateTime getChannelOutOfDateVersion() {
|
||||
return _testClock.ago(versionAgeConsideredUpToDate(channel) * 2);
|
||||
return _testClock.ago(VersionFreshnessValidator.versionAgeConsideredUpToDate(channel) * 2);
|
||||
}
|
||||
|
||||
group('$FlutterVersion for $channel', () {
|
||||
setUpAll(() {
|
||||
Cache.disableLocking();
|
||||
timeToPauseToLetUserReadTheMessage = Duration.zero;
|
||||
VersionFreshnessValidator.timeToPauseToLetUserReadTheMessage = Duration.zero;
|
||||
});
|
||||
|
||||
testUsingContext('prints nothing when Flutter installation looks fresh', () async {
|
||||
@@ -151,14 +151,14 @@ void main() {
|
||||
);
|
||||
cache.versionStamp = json.encode(stamp);
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelOutOfDateVersion(),
|
||||
latestFlutterCommitDate: getChannelOutOfDateVersion(),
|
||||
);
|
||||
).run();
|
||||
|
||||
_expectVersionMessage('', logger);
|
||||
});
|
||||
@@ -172,14 +172,14 @@ void main() {
|
||||
);
|
||||
cache.versionStamp = json.encode(stamp);
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelOutOfDateVersion(),
|
||||
latestFlutterCommitDate: getChannelUpToDateVersion(),
|
||||
);
|
||||
).run();
|
||||
|
||||
_expectVersionMessage(newVersionAvailableMessage(), logger);
|
||||
expect(cache.setVersionStamp, true);
|
||||
@@ -195,14 +195,14 @@ void main() {
|
||||
);
|
||||
cache.versionStamp = json.encode(stamp);
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelOutOfDateVersion(),
|
||||
latestFlutterCommitDate: getChannelUpToDateVersion(),
|
||||
);
|
||||
).run();
|
||||
|
||||
_expectVersionMessage('', logger);
|
||||
});
|
||||
@@ -212,14 +212,14 @@ void main() {
|
||||
final BufferLogger logger = BufferLogger.test();
|
||||
cache.versionStamp = '{}';
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelOutOfDateVersion(),
|
||||
latestFlutterCommitDate: getChannelUpToDateVersion(),
|
||||
);
|
||||
).run();
|
||||
|
||||
_expectVersionMessage(newVersionAvailableMessage(), logger);
|
||||
expect(cache.setVersionStamp, true);
|
||||
@@ -234,14 +234,14 @@ void main() {
|
||||
);
|
||||
cache.versionStamp = json.encode(stamp);
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelOutOfDateVersion(),
|
||||
latestFlutterCommitDate: getChannelUpToDateVersion(),
|
||||
);
|
||||
).run();
|
||||
|
||||
_expectVersionMessage(newVersionAvailableMessage(), logger);
|
||||
});
|
||||
@@ -251,14 +251,14 @@ void main() {
|
||||
final BufferLogger logger = BufferLogger.test();
|
||||
cache.versionStamp = '{}';
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelUpToDateVersion(),
|
||||
latestFlutterCommitDate: null, // Failed to get remote version
|
||||
);
|
||||
// latestFlutterCommitDate defaults to null because we failed to get remote version
|
||||
).run();
|
||||
|
||||
_expectVersionMessage('', logger);
|
||||
});
|
||||
@@ -272,14 +272,14 @@ void main() {
|
||||
);
|
||||
cache.versionStamp = json.encode(stamp);
|
||||
|
||||
await checkVersionFreshness(
|
||||
flutterVersion,
|
||||
await VersionFreshnessValidator(
|
||||
version: flutterVersion,
|
||||
cache: cache,
|
||||
clock: _testClock,
|
||||
logger: logger,
|
||||
localFrameworkCommitDate: getChannelOutOfDateVersion(),
|
||||
latestFlutterCommitDate: null, // Failed to get remote version
|
||||
);
|
||||
// latestFlutterCommitDate defaults to null because we failed to get remote version
|
||||
).run();
|
||||
|
||||
_expectVersionMessage(versionOutOfDateMessage(_testClock.now().difference(getChannelOutOfDateVersion())), logger);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user