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:
Родитель
4956ccdc93
Коммит
dae2d4660c
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче