Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
cdba4ac
Support accessing install mode
Forgind Dec 28, 2023
3a54940
Simplify getting install mode
Forgind Jan 2, 2024
65ea81a
Update to use workload sets
Forgind Jan 4, 2024
02253d6
Update to properly install via workload set
Forgind Jan 5, 2024
63647b2
Undo List changes
Forgind Jan 6, 2024
347d0c9
Get real version
Forgind Jan 8, 2024
39f0ec7
Implement workload update --version
Forgind Jan 8, 2024
52af35d
Support rolling back workload set installation
Forgind Jan 8, 2024
2625649
Don't roll back for file-based
Forgind Jan 8, 2024
198e397
Small touchup
Forgind Jan 10, 2024
2a4a9f9
First stab at MSI-based install
Forgind Jan 10, 2024
b28ca38
Reimplement rolling back
Forgind Jan 10, 2024
0aca30e
Little fixups
Forgind Jan 10, 2024
4d1b0fc
Overwrite workload set if present
Forgind Jan 19, 2024
65e6c73
Fix garbage collecting workload sets
Forgind Jan 19, 2024
197b7f9
Find workload set before installing it
Forgind Jan 19, 2024
324d5e0
Support workload sets in list
Forgind Jan 22, 2024
d44a33d
Partial progress
Forgind Jan 23, 2024
60a387f
Avoid elevation if we will noop in MSI-based operations to adjust the…
Forgind Jan 19, 2024
402ff34
Finish adding to install state
Forgind Jan 23, 2024
5796ee5
Remove InstallStateReader
Forgind Jan 23, 2024
4b8ab17
Small fixes
Forgind Jan 24, 2024
2e0d6e5
More small fixes
Forgind Jan 25, 2024
0e4c5f1
Fix rollback behavior
Forgind Jan 25, 2024
3a60e0c
Update some tests
Forgind Jan 26, 2024
d47e404
Add test
Forgind Jan 26, 2024
ea939c7
Simplify workload set version finding in list
Forgind Feb 5, 2024
21a33a5
Couple comments
Forgind Feb 6, 2024
925239d
PR comments
Forgind Feb 6, 2024
1c69272
Address feedback
Forgind Feb 7, 2024
cb06526
Convert to and from package version
Forgind Mar 1, 2024
f21016d
Update version conversion logic
Forgind Mar 1, 2024
4155948
Add workload set update message
Forgind Mar 1, 2024
6324121
Merge branch 'release/8.0.3xx' of https://github.com/dotnet/sdk into …
Forgind Mar 1, 2024
8c8ce54
Delete duplicate method (?)
Forgind Mar 1, 2024
5bbcb0d
Fix method signature
Forgind Mar 1, 2024
5c09c2c
Fix test
Forgind Mar 1, 2024
b9f79ac
I'm dumb sometimes
Forgind Mar 4, 2024
e38c25a
Most PR comments
Forgind Mar 8, 2024
d0e0bcf
Fix transactions and other little things
Forgind Mar 8, 2024
9a34b4b
Permit multiple *.workloadset.json files
Forgind Mar 8, 2024
11c0d44
Fix transaction usage + update some tests
Forgind Mar 11, 2024
c7ec231
Correct my test
Forgind Mar 11, 2024
94255c9
Merge branch 'release/8.0.3xx' into sets-in-file-based
Forgind Mar 13, 2024
5944045
More feedback
Forgind Mar 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ public static class Constants
public static readonly string AnyRid = "any";

public static readonly string RestoreInteractiveOption = "--interactive";
public static readonly string workloadSetVersionFileName = "workloadVersion.txt";
}
}
24 changes: 24 additions & 0 deletions src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ public static bool TryDeleteDirectory(string directoryPath)
}
}

/// <summary>
/// Deletes the provided file. Then deletes the parent directory if empty
/// and continues to its parent until it fails. Returns whether it succeeded
/// in deleting the file it was intended to delete.
/// </summary>
public static bool DeleteFileAndEmptyParents(string path)
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: I think this would feel safer if it also took a "root" path where it wouldn't delete above that. There are a few places in FileBasedInstaller (mainly in the garbage collection I think) where this could simplify the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you clarify what error case you're trying to avoid? The two possible cases I can think of are:

  1. Deleting something you wanted
  2. Hitting an exception at some point because we don't have access to a directory

The second could happen, though wrapping it in a try/catch would presumably be just as good. (I think this is very unlikely unless the directory with the file to delete is already protected.) The first shouldn't happen.

Copy link
Member

Choose a reason for hiding this comment

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

I think under the dotnet/metadata directory there might be directories that we don't want to delete even if they are empty. It's not a big deal though.

{
if (!File.Exists(path))
{
return false;
}

File.Delete(path);
var dir = Path.GetDirectoryName(path);

while (!Directory.EnumerateFileSystemEntries(dir).Any())
{
Directory.Delete(dir);
dir = Path.GetDirectoryName(dir);
}

return !File.Exists(path);
}

/// <summary>
/// Returns childItem relative to directory, with Path.DirectorySeparatorChar as separator
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,21 @@ public InstallResponseMessage SendUpdateWorkloadModeRequest(SdkFeatureBand sdkFe
UseWorkloadSets = newMode,
});
}

/// <summary>
/// Send an <see cref="InstallRequestMessage"/> to adjust the workload set version used for installing and updating workloads
/// </summary>
/// <param name="sdkFeatureBand">The SDK feature band of the install state file to write</param>
/// <param name="newVersion">The workload set version</param>
/// <returns></returns>
public InstallResponseMessage SendUpdateWorkloadSetRequest(SdkFeatureBand sdkFeatureBand, string newVersion)
{
return Send(new InstallRequestMessage
{
RequestType = InstallRequestType.AdjustWorkloadSetVersion,
SdkFeatureBand = sdkFeatureBand.ToString(),
WorkloadSetVersion = newVersion,
});
}
}
}
8 changes: 8 additions & 0 deletions src/Cli/dotnet/Installer/Windows/InstallRequestMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ public bool UseWorkloadSets
get; set;
}

/// <summary>
/// The workload set version
/// </summary>
public string WorkloadSetVersion
{
get; set;
}

/// <summary>
/// Converts a deserialized array of bytes into an <see cref="InstallRequestMessage"/>.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Cli/dotnet/Installer/Windows/InstallRequestType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,10 @@ public enum InstallRequestType
/// Changes the workload mode
/// </summary>
AdjustWorkloadMode,

/// <summary>
/// Changes the workload set version
/// </summary>
AdjustWorkloadSetVersion,
}
}
53 changes: 53 additions & 0 deletions src/Cli/dotnet/commands/InstallingWorkloadCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal abstract class InstallingWorkloadCommand : WorkloadCommandBase
protected readonly SdkFeatureBand _sdkFeatureBand;
protected readonly ReleaseVersion _targetSdkVersion;
protected readonly string _fromRollbackDefinition;
protected string _workloadSetVersion;
protected readonly PackageSourceLocation _packageSourceLocation;
protected readonly IWorkloadResolverFactory _workloadResolverFactory;
protected IWorkloadResolver _workloadResolver;
Expand Down Expand Up @@ -96,6 +97,53 @@ protected static Dictionary<string, string> GetInstallStateContents(IEnumerable<
manifestVersionUpdates.Select(update => new WorkloadManifestInfo(update.ManifestId.ToString(), update.NewVersion.ToString(), /* We don't actually use the directory here */ string.Empty, update.NewFeatureBand))
).ToDictionaryForJson();

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.UseWorkloadSets ?? false;
}

protected IEnumerable<ManifestVersionUpdate> HandleWorkloadUpdateFromVersion(ITransactionContext context, DirectoryPath? offlineCache)
{
// Ensure workload set mode is set to 'workloadset'
// Do not skip checking the mode first, as setting it triggers
// an admin authorization popup for MSI-based installs.
if (!ShouldUseWorkloadSetMode(_sdkFeatureBand, _dotnetPath))
{
_workloadInstaller.UpdateInstallMode(_sdkFeatureBand, true);
}

_workloadManifestUpdater.DownloadWorkloadSet(_workloadSetVersion, offlineCache);
return InstallWorkloadSet(context);
}

public IEnumerable<ManifestVersionUpdate> InstallWorkloadSet(ITransactionContext context)
{
var advertisingPackagePath = Path.Combine(_userProfileDir, "sdk-advertising", _sdkFeatureBand.ToString(), "microsoft.net.workloads");
if (File.Exists(Path.Combine(advertisingPackagePath, Constants.workloadSetVersionFileName)))
{
// This file isn't created in tests.
PrintWorkloadSetTransition(File.ReadAllText(Path.Combine(advertisingPackagePath, Constants.workloadSetVersionFileName)));
}
var workloadSetPath = _workloadInstaller.InstallWorkloadSet(context, advertisingPackagePath);
var files = Directory.EnumerateFiles(workloadSetPath, "*.workloadset.json");
return _workloadManifestUpdater.ParseRollbackDefinitionFiles(files);
}

private void PrintWorkloadSetTransition(string newVersion)
{
var currentVersion = _workloadResolver.GetWorkloadVersion();
if (currentVersion == null)
{
Reporter.WriteLine(string.Format(Strings.NewWorkloadSet, newVersion));
}
else
{
Reporter.WriteLine(string.Format(Strings.WorkloadSetUpgrade, currentVersion, newVersion));
}
}

protected async Task<List<WorkloadDownload>> GetDownloads(IEnumerable<WorkloadId> workloadIds, bool skipManifestUpdate, bool includePreview, string downloadFolder = null)
{
List<WorkloadDownload> ret = new();
Expand Down Expand Up @@ -202,6 +250,11 @@ internal static class InstallingWorkloadCommandParser
Hidden = true
};

public static readonly CliOption<string> WorkloadSetVersionOption = new("--version")
{
Description = Strings.WorkloadSetVersionOptionDescription
};

public static readonly CliOption<bool> PrintDownloadLinkOnlyOption = new("--print-download-link-only")
{
Description = Strings.PrintDownloadLinkOnlyDescription,
Expand Down
14 changes: 11 additions & 3 deletions src/Cli/dotnet/commands/dotnet-workload/InstallStateContents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;

#pragma warning disable CS8632

namespace Microsoft.DotNet.Workloads.Workload
{
internal class InstallStateContents
Expand All @@ -12,17 +14,21 @@ internal class InstallStateContents
public bool? UseWorkloadSets { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string> Manifests { get; set; }
public Dictionary<string, string>? Manifests { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WorkloadVersion { get; set; }

private static readonly JsonSerializerOptions s_options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
AllowTrailingCommas = true,
};

public static InstallStateContents FromString(string contents)
{
return JsonSerializer.Deserialize<InstallStateContents>(contents, s_options);
return JsonSerializer.Deserialize<InstallStateContents>(contents, s_options) ?? new InstallStateContents();
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason for this change? Will the JsonSerializer ever return null from Deserialize?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it would if the string is null or something like that. That should never happen in our case, but it was making nullable unhappy, and this was an easy way to make that error go away.

}

public static InstallStateContents FromPath(string path)
Expand All @@ -35,4 +41,6 @@ public override string ToString()
return JsonSerializer.Serialize<InstallStateContents>(this, s_options);
}
}
}
}

#pragma warning restore CS8632
7 changes: 4 additions & 3 deletions src/Cli/dotnet/commands/dotnet-workload/WorkloadInfoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal class WorkloadInfoHelper : IWorkloadInfoHelper
{
public readonly SdkFeatureBand _currentSdkFeatureBand;
private readonly string _targetSdkVersion;
public string DotnetPath { get; }

public WorkloadInfoHelper(
bool isInteractive,
Expand All @@ -30,20 +31,20 @@ public WorkloadInfoHelper(
string userProfileDir = null,
IWorkloadResolver workloadResolver = null)
{
string dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
DotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
ReleaseVersion currentSdkReleaseVersion = new(currentSdkVersion ?? Product.Version);
_currentSdkFeatureBand = new SdkFeatureBand(currentSdkReleaseVersion);

_targetSdkVersion = targetSdkVersion;
userProfileDir ??= CliFolderPathCalculator.DotnetUserProfileFolderPath;
ManifestProvider =
new SdkDirectoryWorkloadManifestProvider(dotnetPath,
new SdkDirectoryWorkloadManifestProvider(DotnetPath,
string.IsNullOrWhiteSpace(_targetSdkVersion)
? currentSdkReleaseVersion.ToString()
: _targetSdkVersion,
userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory));
WorkloadResolver = workloadResolver ?? NET.Sdk.WorkloadManifestReader.WorkloadResolver.Create(
ManifestProvider, dotnetPath,
ManifestProvider, DotnetPath,
currentSdkReleaseVersion.ToString(), userProfileDir);

var restoreConfig = new RestoreActionConfig(Interactive: isInteractive);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public override int Execute()

private void ExecuteGarbageCollection()
{
_workloadInstaller.GarbageCollect(workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion),
_workloadInstaller.GarbageCollect(workloadVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadVersion),
cleanAllPacks: _cleanAll);

DisplayUninstallableVSWorkloads();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
using NuGet.Common;
using NuGet.Versioning;
using static Microsoft.NET.Sdk.WorkloadManifestReader.WorkloadResolver;

using PathUtility = Microsoft.DotNet.Tools.Common.PathUtility;

namespace Microsoft.DotNet.Workloads.Workload.Install
{
Expand Down Expand Up @@ -85,6 +85,31 @@ IEnumerable<PackInfo> GetPacksInWorkloads(IEnumerable<WorkloadId> workloadIds)
return packs;
}

public string InstallWorkloadSet(ITransactionContext context, string advertisingPackagePath)
{
var workloadVersion = File.ReadAllText(Path.Combine(advertisingPackagePath, Constants.workloadSetVersionFileName));
var workloadSetPath = Path.Combine(_dotnetDir, "sdk-manifests", _sdkFeatureBand.ToString(), "workloadsets", workloadVersion);
context.Run(
action: () =>
{
Directory.CreateDirectory(workloadSetPath);

foreach (var file in Directory.EnumerateFiles(advertisingPackagePath))
{
File.Copy(file, Path.Combine(workloadSetPath, Path.GetFileName(file)), overwrite: true);
}
},
rollback: () =>
{
foreach (var file in Directory.EnumerateFiles(workloadSetPath))
{
PathUtility.DeleteFileAndEmptyParents(file);
}
});

return workloadSetPath;
}

public void InstallWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, ITransactionContext transactionContext, DirectoryPath? offlineCache = null)
{
var packInfos = GetPacksInWorkloads(workloadIds);
Expand Down Expand Up @@ -452,6 +477,15 @@ public void GarbageCollect(Func<string, IWorkloadResolver> getResolverForWorkloa

}

public void AdjustWorkloadSetInInstallState(SdkFeatureBand sdkFeatureBand, string workloadVersion)
{
string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkFeatureBand, _dotnetDir), "default.json");
Directory.CreateDirectory(Path.GetDirectoryName(path));
var installStateContents = InstallStateContents.FromPath(path);
installStateContents.WorkloadVersion = workloadVersion;
File.WriteAllText(path, installStateContents.ToString());
}

public void RemoveManifestsFromInstallState(SdkFeatureBand sdkFeatureBand)
{
string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(_sdkFeatureBand, _dotnetDir), "default.json");
Expand Down Expand Up @@ -509,7 +543,14 @@ public void Shutdown()

public PackageId GetManifestPackageId(ManifestId manifestId, SdkFeatureBand featureBand)
{
return new PackageId($"{manifestId}.Manifest-{featureBand}");
if (manifestId.ToString().Equals("Microsoft.NET.Workloads", StringComparison.OrdinalIgnoreCase))
{
return new PackageId($"{manifestId}.{featureBand}");
}
else
{
return new PackageId($"{manifestId}.Manifest-{featureBand}");
}
}

public async Task ExtractManifestAsync(string nupkgPath, string targetPath)
Expand Down
4 changes: 4 additions & 0 deletions src/Cli/dotnet/commands/dotnet-workload/install/IInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal interface IInstaller : IWorkloadManifestInstaller
{
int ExitCode { get; }

string InstallWorkloadSet(ITransactionContext context, string advertisingPackagePath);

void InstallWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, ITransactionContext transactionContext, DirectoryPath? offlineCache = null);

void RepairWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, DirectoryPath? offlineCache = null);
Expand All @@ -25,6 +27,8 @@ internal interface IInstaller : IWorkloadManifestInstaller

IEnumerable<WorkloadDownload> GetDownloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, bool includeInstalledItems);

void AdjustWorkloadSetInInstallState(SdkFeatureBand sdkFeatureBand, string workloadVersion);

/// <summary>
/// Replace the workload resolver used by this installer. Typically used to call <see cref="GetDownloads(IEnumerable{WorkloadId}, SdkFeatureBand, bool)"/>
/// for a set of workload manifests that isn't currently installed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ namespace Microsoft.DotNet.Workloads.Workload.Install
{
internal interface IWorkloadManifestUpdater
{
Task UpdateAdvertisingManifestsAsync(bool includePreviews, DirectoryPath? offlineCache = null);
Task UpdateAdvertisingManifestsAsync(bool includePreviews, bool useWorkloadSets = false, DirectoryPath? offlineCache = null);

Task BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();

IEnumerable<ManifestUpdateWithWorkloads> CalculateManifestUpdates();

IEnumerable<ManifestVersionUpdate> CalculateManifestRollbacks(string rollbackDefinitionFilePath);
IEnumerable<ManifestVersionUpdate> ParseRollbackDefinitionFiles(IEnumerable<string> files);

Task<IEnumerable<WorkloadDownload>> GetManifestPackageDownloadsAsync(bool includePreviews, SdkFeatureBand providedSdkFeatureBand, SdkFeatureBand installedSdkFeatureBand);

IEnumerable<WorkloadId> GetUpdatableWorkloadsToAdvertise(IEnumerable<WorkloadId> installedWorkloads);

void DeleteUpdatableWorkloadsFile();

void DownloadWorkloadSet(string version, DirectoryPath? offlineCache);
}

internal record ManifestUpdateWithWorkloads(ManifestVersionUpdate ManifestUpdate, WorkloadCollection Workloads);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,13 @@
<data name="ManifestMsiNotFoundInNuGetPackage" xml:space="preserve">
<value>Manifest MSI not found in NuGet package {0}</value>
</data>
<data name="WorkloadSetVersionOptionDescription" xml:space="preserve">
<value>Update to the specified workload version.</value>
</data>
<data name="NewWorkloadSet" xml:space="preserve">
<value>Installing workload version {0}.</value>
</data>
<data name="WorkloadSetUpgrade" xml:space="preserve">
<value>Updating workload version from {0} to {1}.</value>
</data>
</root>
Loading