Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ public static class ConfigKeysForCommon
public const string DisplayBreakingChangeWarning = "DisplayBreakingChangeWarning";
public const string EnableDataCollection = "EnableDataCollection";
public const string EnableTestCoverage = "EnableTestCoverage";
public const string CheckForUpgrade = "CheckForUpgrade";
}
}
11 changes: 10 additions & 1 deletion src/Common/AzurePSCmdlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand Down Expand Up @@ -400,7 +401,8 @@ private void WriteBreakingChangeOrPreviewMessage()
protected override void EndProcessing()
{
WriteEndProcessingRecommendation();

WriteWarningMessageForVersionUpgrade();

if (MetricHelper.IsCalledByUser()
&& SurveyHelper.GetInstance().ShouldPromptAzSurvey()
&& (AzureSession.Instance.TryGetComponent<IConfigManager>(nameof(IConfigManager), out var configManager)
Expand Down Expand Up @@ -436,6 +438,13 @@ private void WriteEndProcessingRecommendation()
}
}

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
Expand Down
37 changes: 37 additions & 0 deletions src/Common/IFrequencyService.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
5 changes: 5 additions & 0 deletions src/Common/MetricHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ private void PopulatePropertiesFromQos(AzurePSQoSEvent qos, IDictionary<string,
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);
Expand Down Expand Up @@ -456,6 +458,7 @@ private void PopulatePropertiesFromQos(AzurePSQoSEvent qos, IDictionary<string,
{
eventProperties.Add("OutputToPipeline", qos.OutputToPipeline.Value.ToString());
}

foreach (var key in qos.CustomProperties.Keys)
{
eventProperties[key] = qos.CustomProperties[key];
Expand Down Expand Up @@ -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).
Expand Down
260 changes: 260 additions & 0 deletions src/Common/UpgradeNotification/UpgradeNotificationHelper.cs
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +251 to +253
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check $LASTEXITCODE before adding result to output, here is an example.

}
}

return output;
}
}
}