diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart index d2d73f6df0..cef2d2ae2e 100644 --- a/packages/flutter_tools/lib/src/globals.dart +++ b/packages/flutter_tools/lib/src/globals.dart @@ -278,3 +278,6 @@ FlutterProjectFactory get projectFactory { CustomDevicesConfig get customDevicesConfig => context.get()!; PreRunValidator get preRunValidator => context.get() ?? const NoOpPreRunValidator(); + +// TODO(fujino): Migrate to 'main' https://github.com/flutter/flutter/issues/95041 +const String kDefaultFrameworkChannel = 'master'; diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 15a648d3b0..ec86fc41e1 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -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 kOfficialChannels = { - '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 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 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(>[ stamp.store( - newTimeWarningWasPrinted: clock.now(), + newTimeWarningWasPrinted: now, cache: cache, ), Future.delayed(pauseTime), @@ -797,45 +898,6 @@ Future 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.'; diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index cbcaecf782..af66ab95b7 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -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); });