|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.Text.Json;
using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords;
using Microsoft.DotNet.Cli.Commands.Workload.List;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Configurer;
using Microsoft.Extensions.EnvironmentAbstractions;
using Microsoft.NET.Sdk.WorkloadManifestReader;
using NuGet.Common;
using NuGet.Packaging;
using NuGet.Versioning;
namespace Microsoft.DotNet.Cli.Commands.Workload.Install;
internal class WorkloadManifestUpdater : IWorkloadManifestUpdater
{
public static readonly string WorkloadSetManifestId = "Microsoft.NET.Workloads";
private readonly IReporter _reporter;
private readonly IWorkloadResolver _workloadResolver;
private readonly INuGetPackageDownloader _nugetPackageDownloader;
private readonly SdkFeatureBand _sdkFeatureBand;
private readonly string _userProfileDir;
private readonly PackageSourceLocation _packageSourceLocation;
private readonly Func<string, string> _getEnvironmentVariable;
private readonly IWorkloadInstallationRecordRepository _workloadRecordRepo;
private readonly IWorkloadManifestInstaller _workloadManifestInstaller;
private readonly bool _displayManifestUpdates;
public WorkloadManifestUpdater(IReporter reporter,
IWorkloadResolver workloadResolver,
INuGetPackageDownloader nugetPackageDownloader,
string userProfileDir,
IWorkloadInstallationRecordRepository workloadRecordRepo,
IWorkloadManifestInstaller workloadManifestInstaller,
PackageSourceLocation packageSourceLocation = null,
Func<string, string> getEnvironmentVariable = null,
bool displayManifestUpdates = true,
SdkFeatureBand? sdkFeatureBand = null)
{
_reporter = reporter;
_workloadResolver = workloadResolver;
_userProfileDir = userProfileDir;
_nugetPackageDownloader = nugetPackageDownloader;
_sdkFeatureBand = sdkFeatureBand ?? new SdkFeatureBand(_workloadResolver.GetSdkFeatureBand());
_packageSourceLocation = packageSourceLocation;
_getEnvironmentVariable = getEnvironmentVariable ?? Environment.GetEnvironmentVariable;
_workloadRecordRepo = workloadRecordRepo;
_workloadManifestInstaller = workloadManifestInstaller;
_displayManifestUpdates = displayManifestUpdates;
}
private static WorkloadManifestUpdater GetInstance(string userProfileDir)
{
var reporter = new NullReporter();
var dotnetPath = Path.GetDirectoryName(Environment.ProcessPath);
var sdkVersion = Product.Version;
var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(dotnetPath, sdkVersion, userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory));
var workloadResolver = WorkloadResolver.Create(workloadManifestProvider, dotnetPath, sdkVersion, userProfileDir);
var tempPackagesDir = new DirectoryPath(PathUtilities.CreateTempSubdirectory());
var nugetPackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(tempPackagesDir,
filePermissionSetter: null,
new FirstPartyNuGetPackageSigningVerifier(),
new NullLogger(),
reporter,
verifySignatures: SignCheck.IsDotNetSigned());
var installer = WorkloadInstallerFactory.GetWorkloadInstaller(reporter, new SdkFeatureBand(sdkVersion),
workloadResolver, VerbosityOptions.normal, userProfileDir, verifySignatures: false);
var workloadRecordRepo = installer.GetWorkloadInstallationRecordRepository();
return new WorkloadManifestUpdater(reporter, workloadResolver, nugetPackageDownloader, userProfileDir, workloadRecordRepo, installer);
}
public async Task UpdateAdvertisingManifestsAsync(bool includePreviews, bool useWorkloadSets = false, DirectoryPath? offlineCache = null)
{
if (useWorkloadSets)
{
await UpdateManifestWithVersionAsync("Microsoft.NET.Workloads", includePreviews, _sdkFeatureBand, null, offlineCache);
}
else
{
// this updates all the manifests
var manifests = _workloadResolver.GetInstalledManifests();
await Task.WhenAll(manifests.Select(manifest => UpdateAdvertisingManifestAsync(manifest, includePreviews, offlineCache))).ConfigureAwait(false);
WriteUpdatableWorkloadsFile();
}
}
public static async Task BackgroundUpdateAdvertisingManifestsAsync(string userProfileDir)
{
try
{
var manifestUpdater = GetInstance(userProfileDir);
await manifestUpdater.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
}
catch (Exception)
{
// Never surface messages on background updates
}
}
public async Task BackgroundUpdateAdvertisingManifestsWhenRequiredAsync()
{
if (!BackgroundUpdatesAreDisabled() &&
AdManifestSentinelIsDueForUpdate() &&
UpdatedAdManifestPackagesExistAsync().GetAwaiter().GetResult())
{
await UpdateAdvertisingManifestsAsync(false, ShouldUseWorkloadSetMode(_sdkFeatureBand, _userProfileDir));
var sentinelPath = GetAdvertisingManifestSentinelPath(_sdkFeatureBand);
if (File.Exists(sentinelPath))
{
File.SetLastAccessTime(sentinelPath, DateTime.Now);
}
else
{
File.Create(sentinelPath).Close();
}
}
}
public static bool ShouldUseWorkloadSetMode(SdkFeatureBand sdkFeatureBand, string dotnetDir)
{
string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(sdkFeatureBand, dotnetDir), "default.json");
var installStateContents = File.Exists(path) ? InstallStateContents.FromString(File.ReadAllText(path)) : new InstallStateContents();
return installStateContents.ShouldUseWorkloadSets();
}
private void WriteUpdatableWorkloadsFile()
{
var installedWorkloads = _workloadRecordRepo.GetInstalledWorkloads(_sdkFeatureBand);
var updatableWorkloads = GetUpdatableWorkloadsToAdvertise(installedWorkloads);
var filePath = GetAdvertisingWorkloadsFilePath(_sdkFeatureBand);
var jsonContent = JsonSerializer.Serialize(updatableWorkloads.Select(workload => workload.ToString()).ToArray());
if (Directory.Exists(Path.GetDirectoryName(filePath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
}
File.WriteAllText(filePath, jsonContent);
}
public void DeleteUpdatableWorkloadsFile()
{
var filePath = GetAdvertisingWorkloadsFilePath(_sdkFeatureBand);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
public static void AdvertiseWorkloadUpdates()
{
try
{
var backgroundUpdatesDisabled = bool.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.WORKLOAD_UPDATE_NOTIFY_DISABLE), out var disableEnvVar) && disableEnvVar;
SdkFeatureBand featureBand = new(Product.Version);
var adUpdatesFile = GetAdvertisingWorkloadsFilePath(CliFolderPathCalculator.DotnetUserProfileFolderPath, featureBand);
if (!backgroundUpdatesDisabled && File.Exists(adUpdatesFile))
{
var updatableWorkloads = JsonSerializer.Deserialize<string[]>(File.ReadAllText(adUpdatesFile));
if (updatableWorkloads != null && updatableWorkloads.Any())
{
Console.WriteLine();
Console.WriteLine(CliCommandStrings.WorkloadInstallWorkloadUpdatesAvailable);
}
}
}
catch (Exception)
{
// Never surface errors
}
}
public string GetAdvertisedWorkloadSetVersion()
{
var advertisedPath = GetAdvertisingManifestPath(_sdkFeatureBand, new ManifestId(WorkloadSetManifestId));
var workloadSetVersionFilePath = Path.Combine(advertisedPath, Constants.workloadSetVersionFileName);
if (File.Exists(workloadSetVersionFilePath))
{
return File.ReadAllText(workloadSetVersionFilePath);
}
return null;
}
public IEnumerable<ManifestUpdateWithWorkloads> CalculateManifestUpdates()
{
var currentManifestIds = GetInstalledManifestIds();
foreach (var manifestId in currentManifestIds)
{
var advertisingInfo = GetAdvertisingManifestVersionAndWorkloads(manifestId);
if (advertisingInfo == null)
{
continue;
}
var (installedVersion, installedBand) = GetInstalledManifestVersion(manifestId);
var ((adVersion, adBand), adWorkloads) = advertisingInfo.Value;
if (adVersion.CompareTo(installedVersion) > 0 && adBand.Equals(installedBand) ||
adBand.CompareTo(installedBand) > 0)
{
var update = new ManifestVersionUpdate(manifestId, adVersion, adBand.ToString());
yield return new(update, adWorkloads);
}
}
}
public IEnumerable<WorkloadId> GetUpdatableWorkloadsToAdvertise(IEnumerable<WorkloadId> installedWorkloads)
{
try
{
#if !DOT_NET_BUILD_FROM_SOURCE
if (OperatingSystem.IsWindows())
{
// Also advertise updates for workloads installed by Visual Studio
InstalledWorkloadsCollection installedVSWorkloads = new InstalledWorkloadsCollection();
VisualStudioWorkloads.GetInstalledWorkloads(_workloadResolver, installedVSWorkloads, _sdkFeatureBand);
installedWorkloads = [.. installedWorkloads.Concat(installedVSWorkloads.AsEnumerable().Select(kvp => new WorkloadId(kvp.Key))).Distinct()];
}
#endif
var overlayProvider = new TempDirectoryWorkloadManifestProvider(Path.Combine(_userProfileDir, "sdk-advertising", _sdkFeatureBand.ToString()), _sdkFeatureBand.ToString());
var advertisingManifestResolver = _workloadResolver.CreateOverlayResolver(overlayProvider);
return _workloadResolver.GetUpdatedWorkloads(advertisingManifestResolver, installedWorkloads);
}
catch
{
return [];
}
}
public IEnumerable<ManifestVersionUpdate> CalculateManifestRollbacks(string rollbackDefinitionFilePath, WorkloadHistoryRecorder recorder = null)
{
var currentManifestIds = GetInstalledManifestIds();
var manifestRollbacks = ParseRollbackDefinitionFile(rollbackDefinitionFilePath, _sdkFeatureBand);
if (recorder is not null)
{
recorder.HistoryRecord.RollbackFileContents = manifestRollbacks.ToDictionary(kvp => kvp.Id.ToString(), kvp => kvp.ManifestWithBand.Version + "/" + kvp.ManifestWithBand.Band);
}
var unrecognizedManifestIds = manifestRollbacks.Where(rollbackManifest => !currentManifestIds.Contains(rollbackManifest.Id));
if (unrecognizedManifestIds.Any())
{
_reporter.WriteLine(string.Format(CliCommandStrings.RollbackDefinitionContainsExtraneousManifestIds, rollbackDefinitionFilePath, string.Join(" ", unrecognizedManifestIds)).Yellow());
manifestRollbacks = manifestRollbacks.Where(rollbackManifest => currentManifestIds.Contains(rollbackManifest.Id));
}
return CalculateManifestRollbacks(manifestRollbacks);
}
private static IEnumerable<ManifestVersionUpdate> CalculateManifestRollbacks(IEnumerable<(ManifestId Id, ManifestVersionWithBand ManifestWithBand)> versionUpdates)
{
return versionUpdates.Select(manifest =>
{
var (id, (version, band)) = manifest;
return new ManifestVersionUpdate(id, version, band.ToString());
});
}
public async Task<IEnumerable<WorkloadDownload>> GetManifestPackageDownloadsAsync(bool includePreviews, SdkFeatureBand providedSdkFeatureBand, SdkFeatureBand installedSdkFeatureBand)
{
var downloads = new List<WorkloadDownload>();
foreach (var manifest in _workloadResolver.GetInstalledManifests())
{
try
{
PackageId? providedPackageId = null;
var fallbackFeatureBand = new SdkFeatureBand(manifest.ManifestFeatureBand);
// The bands should be checked in the order defined here.
SdkFeatureBand[] bands = [providedSdkFeatureBand, installedSdkFeatureBand, fallbackFeatureBand];
var success = false;
// Use Distinct to eliminate bands that are the same.
foreach (var band in bands.Distinct())
{
var packageId = _workloadManifestInstaller.GetManifestPackageId(new ManifestId(manifest.Id), band);
providedPackageId ??= packageId;
try
{
var latestVersion = await _nugetPackageDownloader.GetLatestPackageVersion(packageId, _packageSourceLocation, includePreviews);
success = true;
downloads.Add(new WorkloadDownload(manifest.Id, packageId.ToString(), latestVersion.ToString()));
break;
}
catch (NuGetPackageNotFoundException)
{
}
}
if (!success)
{
_reporter.WriteLine(CliCommandStrings.ManifestPackageUrlNotResolved, providedPackageId);
}
}
catch
{
_reporter.WriteLine(CliCommandStrings.ManifestPackageUrlNotResolved, manifest.Id);
}
}
return downloads;
}
private IEnumerable<ManifestId> GetInstalledManifestIds() => _workloadResolver.GetInstalledManifests().Select(manifest => new ManifestId(manifest.Id));
private async Task<bool> UpdateManifestWithVersionAsync(string id, bool includePreviews, SdkFeatureBand band, NuGetVersion packageVersion = null, DirectoryPath? offlineCache = null)
{
var manifestId = new ManifestId(id);
string packagePath = null;
try
{
var manifestPackageId = _workloadManifestInstaller.GetManifestPackageId(manifestId, band);
try
{
// If an offline cache is present, use that. Otherwise, try to acquire the package online.
packagePath = offlineCache != null ?
Directory.GetFiles(offlineCache.Value.Value)
.Where(path =>
path.EndsWith(".nupkg") &&
Path.GetFileName(path).StartsWith(manifestPackageId.ToString(), StringComparison.OrdinalIgnoreCase) &&
(packageVersion == null || path.Contains(packageVersion.ToString())))
.Max() :
await _nugetPackageDownloader.DownloadPackageAsync(manifestPackageId, packageVersion: packageVersion, packageSourceLocation: _packageSourceLocation, includePreview: includePreviews);
}
catch (NuGetPackageNotFoundException)
{
}
if (packagePath is null)
{
return false;
}
var adManifestPath = GetAdvertisingManifestPath(_sdkFeatureBand, manifestId);
await _workloadManifestInstaller.ExtractManifestAsync(packagePath, adManifestPath);
// add file that contains the advertised manifest feature band so GetAdvertisingManifestVersionAndWorkloads will use correct feature band, regardless of if rollback occurred or not
File.WriteAllText(Path.Combine(adManifestPath, "AdvertisedManifestFeatureBand.txt"), band.ToString());
if (id.Equals(WorkloadSetManifestId))
{
// Create version file later used as part of installing the workload set in the file-based installer and in the msi-based installer
using PackageArchiveReader packageReader = new(packagePath);
var downloadedPackageVersion = packageReader.NuspecReader.GetVersion();
if (packageVersion != null && !downloadedPackageVersion.Equals(packageVersion))
{
throw new NuGetPackageNotFoundException($"Requested workload version {packageVersion} of {id} but found version {downloadedPackageVersion} instead.");
}
var workloadSetVersion = WorkloadSetVersion.FromWorkloadSetPackageVersion(band, downloadedPackageVersion.ToString());
File.WriteAllText(Path.Combine(adManifestPath, Constants.workloadSetVersionFileName), workloadSetVersion);
}
if (_displayManifestUpdates)
{
_reporter.WriteLine(CliCommandStrings.AdManifestUpdated, manifestId);
}
return true;
}
catch (Exception e)
{
_reporter.WriteLine(CliCommandStrings.FailedAdManifestUpdate, manifestId, e.Message);
return false;
}
finally
{
if (!string.IsNullOrEmpty(packagePath) && File.Exists(packagePath) && (offlineCache == null || !offlineCache.HasValue))
{
File.Delete(packagePath);
}
if (!string.IsNullOrEmpty(packagePath) && (offlineCache == null || !offlineCache.HasValue))
{
var versionDir = Path.GetDirectoryName(packagePath);
if (Directory.Exists(versionDir) && !Directory.GetFileSystemEntries(versionDir).Any())
{
Directory.Delete(versionDir);
var idDir = Path.GetDirectoryName(versionDir);
if (Directory.Exists(idDir) && !Directory.GetFileSystemEntries(idDir).Any())
{
Directory.Delete(idDir);
}
}
}
}
}
private async Task UpdateAdvertisingManifestAsync(WorkloadManifestInfo manifest, bool includePreviews, DirectoryPath? offlineCache = null)
{
var fallbackFeatureBand = new SdkFeatureBand(manifest.ManifestFeatureBand);
// The bands should be checked in the order defined here.
SdkFeatureBand[] bands = [_sdkFeatureBand, fallbackFeatureBand];
foreach (var band in bands.Distinct())
{
if (await UpdateManifestWithVersionAsync(manifest.Id, includePreviews, band, null, offlineCache))
{
return;
}
}
_reporter.WriteLine(CliCommandStrings.AdManifestPackageDoesNotExist, manifest.Id);
}
private (ManifestVersionWithBand ManifestWithBand, WorkloadCollection Workloads)? GetAdvertisingManifestVersionAndWorkloads(ManifestId manifestId)
{
var manifestPath = Path.Combine(GetAdvertisingManifestPath(_sdkFeatureBand, manifestId), "WorkloadManifest.json");
if (!File.Exists(manifestPath))
{
return null;
}
using FileStream fsSource = new(manifestPath, FileMode.Open, FileAccess.Read);
var manifest = WorkloadManifestReader.ReadWorkloadManifest(manifestId.ToString(), fsSource, manifestPath);
// we need to know the feature band of the advertised manifest (read it from the AdvertisedManifestFeatureBand.txt file)
// if we don't find the file then use the current feature band
var adManifestFeatureBandPath = Path.Combine(GetAdvertisingManifestPath(_sdkFeatureBand, manifestId), "AdvertisedManifestFeatureBand.txt");
SdkFeatureBand adManifestFeatureBand = _sdkFeatureBand;
if (File.Exists(adManifestFeatureBandPath))
{
adManifestFeatureBand = new SdkFeatureBand(File.ReadAllText(adManifestFeatureBandPath));
}
ManifestVersionWithBand manifestWithBand = new(new ManifestVersion(manifest.Version), adManifestFeatureBand);
var workloads = manifest.Workloads.Values.OfType<WorkloadDefinition>().ToDictionary(w => w.Id);
return (manifestWithBand, workloads);
}
private ManifestVersionWithBand GetInstalledManifestVersion(ManifestId manifestId)
{
return new(new ManifestVersion(_workloadResolver.GetManifestVersion(manifestId.ToString())), new SdkFeatureBand(_workloadResolver.GetManifestFeatureBand(manifestId.ToString())));
}
private bool AdManifestSentinelIsDueForUpdate()
{
var sentinelPath = GetAdvertisingManifestSentinelPath(_sdkFeatureBand);
if (!int.TryParse(_getEnvironmentVariable(EnvironmentVariableNames.WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS), out int updateIntervalHours))
{
updateIntervalHours = 24;
}
if (File.Exists(sentinelPath))
{
var lastAccessTime = File.GetLastAccessTime(sentinelPath);
if (lastAccessTime.AddHours(updateIntervalHours) > DateTime.Now)
{
return false;
}
}
return true;
}
private async Task<bool> UpdatedAdManifestPackagesExistAsync()
{
var manifests = GetInstalledManifestIds();
// TODO: This doesn't seem to account for differing feature bands
var availableUpdates = await Task.WhenAll(manifests.Select(manifest => NewerManifestPackageExists(manifest))).ConfigureAwait(false);
return availableUpdates.Any();
}
private async Task<bool> NewerManifestPackageExists(ManifestId manifest)
{
try
{
var currentVersion = NuGetVersion.Parse(_workloadResolver.GetManifestVersion(manifest.ToString()));
var latestVersion = await _nugetPackageDownloader.GetLatestPackageVersion(_workloadManifestInstaller.GetManifestPackageId(manifest, _sdkFeatureBand));
return latestVersion > currentVersion;
}
catch (Exception)
{
return false;
}
}
public IEnumerable<ManifestVersionUpdate> CalculateManifestUpdatesForWorkloadSet(WorkloadSet workloadSet)
{
return CalculateManifestRollbacks(workloadSet.ManifestVersions.Select(kvp => (kvp.Key, new ManifestVersionWithBand(kvp.Value.Version, kvp.Value.FeatureBand))));
}
private static IEnumerable<(ManifestId Id, ManifestVersionWithBand ManifestWithBand)> ParseRollbackDefinitionFile(string rollbackDefinitionFilePath, SdkFeatureBand featureBand)
{
string fileContent;
if (Uri.TryCreate(rollbackDefinitionFilePath, UriKind.Absolute, out var rollbackUri) && !rollbackUri.IsFile)
{
using HttpClient httpClient = new();
fileContent = httpClient.GetStringAsync(rollbackDefinitionFilePath).Result;
}
else if (File.Exists(rollbackDefinitionFilePath))
{
fileContent = File.ReadAllText(rollbackDefinitionFilePath);
}
else
{
throw new ArgumentException(string.Format(CliCommandStrings.RollbackDefinitionFileDoesNotExist, rollbackDefinitionFilePath));
}
var versions = WorkloadSet.FromJson(fileContent, featureBand).ManifestVersions;
return versions.Select(kvp => (kvp.Key, new ManifestVersionWithBand(kvp.Value.Version, kvp.Value.FeatureBand)));
}
public IEnumerable<ManifestVersionUpdate> CalculateManifestUpdatesFromHistory(WorkloadHistoryState state)
{
return state.ManifestVersions.Select(
m => new ManifestVersionUpdate(
new ManifestId(m.Key),
new ManifestVersion(m.Value.Split('/')[0]),
m.Value.Split('/')[1]));
}
private bool BackgroundUpdatesAreDisabled() => bool.TryParse(_getEnvironmentVariable(EnvironmentVariableNames.WORKLOAD_UPDATE_NOTIFY_DISABLE), out var disableEnvVar) && disableEnvVar;
private string GetAdvertisingManifestSentinelPath(SdkFeatureBand featureBand) => Path.Combine(_userProfileDir, $".workloadAdvertisingManifestSentinel{featureBand}");
private string GetAdvertisingWorkloadsFilePath(SdkFeatureBand featureBand) => GetAdvertisingWorkloadsFilePath(_userProfileDir, featureBand);
private static string GetAdvertisingWorkloadsFilePath(string userProfileDir, SdkFeatureBand featureBand) => Path.Combine(userProfileDir, $".workloadAdvertisingUpdates{featureBand}");
private string GetAdvertisingManifestPath(SdkFeatureBand featureBand, ManifestId manifestId) => Path.Combine(_userProfileDir, "sdk-advertising", featureBand.ToString(), manifestId.ToString());
private record ManifestVersionWithBand(ManifestVersion Version, SdkFeatureBand Band);
}
|