Support in-tool notification for version upgrade (#396)

* support version upgrade notification

* update the migration guides link

* remove check by user

* update warning message

* move warning logic to UpgradeNotificationHelper

* update string match and newline character

* rename FrequencyService methods

---------

Co-authored-by: NanxiangLiu <33285578+Nickcandy@users.noreply.github.com>
This commit is contained in:
Yan Xu 2023-07-24 20:54:28 +08:00 коммит произвёл GitHub
Родитель 4956ccdc93
Коммит dae2d4660c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 313 добавлений и 1 удалений

Просмотреть файл

@ -28,5 +28,6 @@ namespace Microsoft.Azure.PowerShell.Common.Config
public const string DisplayBreakingChangeWarning = "DisplayBreakingChangeWarning";
public const string EnableDataCollection = "EnableDataCollection";
public const string EnableTestCoverage = "EnableTestCoverage";
public const string CheckForUpgrade = "CheckForUpgrade";
}
}

Просмотреть файл

@ -16,6 +16,7 @@ using Microsoft.Azure.Commands.Common.Authentication;
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
using Microsoft.Azure.PowerShell.Common.Config;
using Microsoft.Azure.PowerShell.Common.Share.Survey;
using Microsoft.Azure.PowerShell.Common.Share.UpgradeNotification;
using Microsoft.Azure.ServiceManagement.Common.Models;
using Microsoft.WindowsAzure.Commands.Common;
using Microsoft.WindowsAzure.Commands.Common.CustomAttributes;
@ -400,7 +401,8 @@ namespace Microsoft.WindowsAzure.Commands.Utilities.Common
protected override void EndProcessing()
{
WriteEndProcessingRecommendation();
WriteWarningMessageForVersionUpgrade();
if (MetricHelper.IsCalledByUser()
&& SurveyHelper.GetInstance().ShouldPromptAzSurvey()
&& (AzureSession.Instance.TryGetComponent<IConfigManager>(nameof(IConfigManager), out var configManager)
@ -436,6 +438,13 @@ namespace Microsoft.WindowsAzure.Commands.Utilities.Common
}
}
private void WriteWarningMessageForVersionUpgrade()
{
AzureSession.Instance.TryGetComponent<IConfigManager>(nameof(IConfigManager), out var configManager);
AzureSession.Instance.TryGetComponent<IFrequencyService>(nameof(IFrequencyService), out var frequencyService);
UpgradeNotificationHelper.GetInstance().WriteWarningMessageForVersionUpgrade(this, _qosEvent, configManager, frequencyService);
}
protected string CurrentPath()
{
// SessionState is only available within PowerShell so default to

Просмотреть файл

@ -0,0 +1,37 @@
using System;
namespace Microsoft.WindowsAzure.Commands.Common
{
/// <summary>
/// Interface for a service that manages the frequency of business logic execution based on configured feature flags.
/// </summary>
public interface IFrequencyService
{
/// <summary>
/// Checks if the specified feature is enabled and if it's time to run the business logic based on the feature's frequency.
/// If both conditions are met, it runs the specified business action.
/// </summary>
/// <param name="featureName">The name of the feature to check.</param>
/// <param name="businessCheck">A function that returns true if the business logic should be executed.</param>
/// <param name="business">An action to execute if the business logic should be executed.</param>
void TryRun(string featureName, Func<bool> businessCheck, Action business);
/// <summary>
/// Registers a feature with the specified name and frequency to the service.
/// </summary>
/// <param name="featureName">The name of the feature to add.</param>
/// <param name="frequency">The frequency at which the business logic should be executed for the feature.</param>
void Register(string featureName, TimeSpan frequency);
/// <summary>
/// Registers the specified feature to the service's per-PSsession registry.
/// </summary>
/// <param name="featureName">The name of the feature to add.</param>
void RegisterInSession(string featureName);
/// <summary>
/// Saves the current state of the service to persistent storage.
/// </summary>
void Save();
}
}

Просмотреть файл

@ -322,6 +322,8 @@ namespace Microsoft.WindowsAzure.Commands.Common
eventProperties.Add("duration", qos.Duration.ToString("c"));
eventProperties.Add("InternalCalledCmdlets", MetricHelper.InternalCalledCmdlets);
eventProperties.Add("InstallationId", MetricHelper.InstallationId);
eventProperties.Add("upgrade-notification-checked", qos.HigherVersionsChecked.ToString());
eventProperties.Add("upgrade-notification-prompted", qos.UpgradeNotificationPrompted.ToString());
if (!string.IsNullOrWhiteSpace(SharedVariable.PredictorCorrelationId))
{
eventProperties.Add("predictor-correlation-id", SharedVariable.PredictorCorrelationId);
@ -456,6 +458,7 @@ namespace Microsoft.WindowsAzure.Commands.Common
{
eventProperties.Add("OutputToPipeline", qos.OutputToPipeline.Value.ToString());
}
foreach (var key in qos.CustomProperties.Keys)
{
eventProperties[key] = qos.CustomProperties[key];
@ -607,6 +610,8 @@ public class AzurePSQoSEvent
public string SubscriptionId { get; set; }
public string TenantId { get; set; }
public bool SurveyPrompted { get; set; }
public bool HigherVersionsChecked { get; set; }
public bool UpgradeNotificationPrompted { get; set; }
/// <summary>
/// Appear in certain resource creation commands like New-AzVM. See RegionalRecommender (PS repo).

Просмотреть файл

@ -0,0 +1,260 @@
using Microsoft.Azure.PowerShell.Common.Config;
using Microsoft.WindowsAzure.Commands.Common;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Threading;
namespace Microsoft.Azure.PowerShell.Common.Share.UpgradeNotification
{
public class UpgradeNotificationHelper
{
private const string AZPSMigrationGuideLink = "https://go.microsoft.com/fwlink/?linkid=2241373";
private const string FrequencyKeyForUpgradeNotification = "VersionUpgradeNotification";
private static TimeSpan FrequencyTimeSpanForUpgradeNotification = TimeSpan.FromDays(30);
private const string FrequencyKeyForUpgradeCheck = "VersionUpgradeCheck";
private static TimeSpan FrequencyTimeSpanForUpgradeCheck = TimeSpan.FromDays(2);
//temp record file for az module versions
private static string AzVersionCacheFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".Azure", "AzModuleVerions.json");
private bool hasNotified { get; set; }
private Dictionary<string, string> versionDict = null;
private static UpgradeNotificationHelper _instance;
private UpgradeNotificationHelper()
{
try
{
// load temp record file to versionDict
if (File.Exists(AzVersionCacheFile))
{
using (StreamReader sr = new StreamReader(new FileStream(AzVersionCacheFile, FileMode.Open, FileAccess.Read, FileShare.None)))
{
versionDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(sr.ReadToEnd());
}
}
}
catch (Exception)
{
versionDict = null;
}
}
public static UpgradeNotificationHelper GetInstance()
{
if (_instance == null)
{
_instance = new UpgradeNotificationHelper();
}
return _instance;
}
public void WriteWarningMessageForVersionUpgrade(Microsoft.WindowsAzure.Commands.Utilities.Common.AzurePSCmdlet cmdlet, AzurePSQoSEvent _qosEvent, IConfigManager configManager, IFrequencyService frequencyService) {
_qosEvent.HigherVersionsChecked = false;
_qosEvent.UpgradeNotificationPrompted = false;
try
{
//disabled by az config, skip
if (configManager!=null&& configManager.GetConfigValue<bool>(ConfigKeysForCommon.CheckForUpgrade).Equals(false))
{
return;
}
//has done check this session, skip
if (hasNotified)
{
return;
}
//register verion check and upgrade notification in frequency service
if (frequencyService == null) {
return;
}
frequencyService.Register(FrequencyKeyForUpgradeCheck, FrequencyTimeSpanForUpgradeCheck);
frequencyService.Register(FrequencyKeyForUpgradeNotification, FrequencyTimeSpanForUpgradeNotification);
string checkModuleName = "Az";
string checkModuleCurrentVersion = _qosEvent.AzVersion;
string upgradeModuleNames = "Az";
if ("0.0.0".Equals(_qosEvent.AzVersion))
{
checkModuleName = _qosEvent.ModuleName;
checkModuleCurrentVersion = _qosEvent.ModuleVersion;
upgradeModuleNames = "Az.*";
}
//refresh az module versions if necessary
frequencyService.TryRun(FrequencyKeyForUpgradeCheck, () => true, () =>
{
Thread loadHigherVersionsThread = new Thread(new ThreadStart(() =>
{
_qosEvent.HigherVersionsChecked = true;
try
{
//no lock for this method, may skip some notifications, it's expected.
RefreshVersionInfo(upgradeModuleNames);
}
catch (Exception)
{
//do nothing
}
}));
loadHigherVersionsThread.Start();
});
bool shouldPrintWarningMsg = HasHigherVersion(checkModuleName, checkModuleCurrentVersion);
//prompt warning message for upgrade if necessary
frequencyService.TryRun(FrequencyKeyForUpgradeNotification, () => shouldPrintWarningMsg, () =>
{
_qosEvent.UpgradeNotificationPrompted = true;
hasNotified = true;
string latestModuleVersion = GetModuleLatestVersion(checkModuleName);
string updateModuleCmdletName = GetCmdletForUpdateModule();
string warningMsg = $"You're using {checkModuleName} version {checkModuleCurrentVersion}. The latest version of {checkModuleName} is {latestModuleVersion}. Upgrade your Az modules using the following commands:{Environment.NewLine}";
warningMsg += $" {updateModuleCmdletName} {upgradeModuleNames} -WhatIf -- Simulate updating your Az modules.{Environment.NewLine}";
warningMsg += $" {updateModuleCmdletName} {upgradeModuleNames} -- Update your Az modules.{Environment.NewLine}";
if ("Az".Equals(checkModuleName) && GetInstance().HasHigherMajorVersion(checkModuleName, checkModuleCurrentVersion))
{
warningMsg += $"There will be breaking changes from {checkModuleCurrentVersion} to {latestModuleVersion}. Open {AZPSMigrationGuideLink} and check the details.{Environment.NewLine}";
}
cmdlet.WriteWarning(warningMsg);
});
}
catch (Exception ex)
{
cmdlet.WriteDebug($"Failed to write warning message for version upgrade due to '{ex.Message}'.");
}
}
private void RefreshVersionInfo(string loadModuleNames)
{
this.versionDict = LoadHigherAzVersions(loadModuleNames);
if (!VersionsAreFreshed())
{
return;
}
string content = JsonConvert.SerializeObject(this.versionDict);
using (StreamWriter sw = new StreamWriter(new FileStream(AzVersionCacheFile, FileMode.Create, FileAccess.Write, FileShare.None)))
{
sw.Write(content);
}
}
private bool VersionsAreFreshed()
{
return versionDict != null && versionDict.Count > 0;
}
private string GetModuleLatestVersion(string moduleName)
{
string defaultVersion = "0.0.0";
if (!VersionsAreFreshed())
{
return defaultVersion;
}
return versionDict.ContainsKey(moduleName) ? versionDict[moduleName] : defaultVersion;
}
private bool HasHigherVersion(string moduleName, string currentVersion)
{
if (!VersionsAreFreshed())
{
return false;
}
try
{
Version currentVersionValue = Version.Parse(currentVersion);
Version latestVersionValue = Version.Parse(versionDict[moduleName]);
return latestVersionValue > currentVersionValue;
}
catch (Exception)
{
return false;
}
}
private bool HasHigherMajorVersion(string moduleName, string currentVersion)
{
if (!VersionsAreFreshed())
{
return false;
}
try
{
Version currentVersionValue = Version.Parse(currentVersion);
Version latestVersionValue = Version.Parse(versionDict[moduleName]);
return latestVersionValue.Major > currentVersionValue.Major;
}
catch (Exception)
{
return false;
}
}
private static Dictionary<string, string> LoadHigherAzVersions(string moduleName)
{
Dictionary<string, string> versionDict = new Dictionary<string, string>();
string findModuleCmdlet = GetCmdletForFindModule();
findModuleCmdlet += " -Name " + moduleName + " | Select-Object Name, Version";
var outputs = ExecutePSScript<PSObject>(findModuleCmdlet);
foreach (PSObject obj in outputs)
{
versionDict[obj.Properties["Name"].Value.ToString()] = obj.Properties["Version"].Value.ToString();
}
return versionDict;
}
private static string GetCmdletForUpdateModule()
{
if (ExecutePSScript<PSObject>("Get-Command -Name Update-PSResource").Count > 0)
{
return "Update-PSResource";
}
else
{
return "Update-Module";
}
}
private static string GetCmdletForFindModule()
{
if (ExecutePSScript<PSObject>("Get-Command -Name Find-PSResource").Count > 0)
{
return "Find-PSResource -Repository PSGallery -Type Module";
}
else
{
return "Find-Module -Repository PSGallery";
}
}
// This method is copied from CmdletExtensions.ExecuteScript. But it'll run with NewRunspace, ignore the warning or error message.
private static List<T> ExecutePSScript<T>(string contents)
{
List<T> output = new List<T>();
using (System.Management.Automation.PowerShell powershell = System.Management.Automation.PowerShell.Create(RunspaceMode.NewRunspace))
{
powershell.AddScript(contents);
Collection<T> result = powershell.Invoke<T>();
if (result != null && result.Count > 0)
{
output.AddRange(result);
}
}
return output;
}
}
}