Skip to content

Commit 77d21f6

Browse files
authored
Workload History (#42020)
This builds off of #34946 and #36809. It has three primary effects: 1. Every time a user runs a command that modifies the workload state (such as install or update), the global workload state is recorded before and after the command is executed and written to a 'workload history record' on disk. 2. If a user runs the new `dotnet workload history` command, the workload history records are read in, organized, and displayed as a table, potentially including 'unlogged changes' if the workloads had changed outside the command. 3. Users can now use the `--from-history` option on `dotnet workload update` to select a workload history record and install the set of manifests that were installed globally at that time. Note that for all these changes, effects are global. Workload history records may notice a global.json, but it is not considered when updating to a particular state, and update --from-history fails if a global.json is present.
1 parent e08a921 commit 77d21f6

File tree

56 files changed

+2037
-190
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2037
-190
lines changed

src/Cli/dotnet/Installer/Windows/InstallRequestType.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ public enum InstallRequestType
5656
DeleteWorkloadInstallationRecord,
5757

5858
/// <summary>
59-
/// Creates an install state file.
59+
/// Adds manifests to the install state file.
6060
/// </summary>
6161
SaveInstallStateManifestVersions,
6262

6363
/// <summary>
64-
/// Removes an install state file.
64+
/// Removes manifests from an install state file.
6565
/// </summary>
6666
RemoveManifestsFromInstallStateFile,
6767

src/Cli/dotnet/commands/InstallingWorkloadCommand.cs

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
using System.Text.Json.Nodes;
1111
using Microsoft.Deployment.DotNet.Releases;
1212
using Microsoft.DotNet.Cli;
13+
using Microsoft.DotNet.Cli.Commands.DotNetWorkloads;
1314
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
1415
using Microsoft.DotNet.Cli.Utils;
1516
using Microsoft.DotNet.ToolPackage;
17+
using Microsoft.DotNet.Workloads.Workload.History;
1618
using Microsoft.DotNet.Workloads.Workload.Install;
1719
using Microsoft.DotNet.Workloads.Workload.Update;
1820
using Microsoft.Extensions.EnvironmentAbstractions;
@@ -24,6 +26,7 @@ namespace Microsoft.DotNet.Workloads.Workload
2426
{
2527
internal abstract class InstallingWorkloadCommand : WorkloadCommandBase
2628
{
29+
protected readonly string[] _arguments;
2730
protected readonly bool _printDownloadLinkOnly;
2831
protected readonly string _fromCacheOption;
2932
protected readonly bool _includePreviews;
@@ -36,6 +39,8 @@ internal abstract class InstallingWorkloadCommand : WorkloadCommandBase
3639
protected readonly SdkFeatureBand _sdkFeatureBand;
3740
protected readonly ReleaseVersion _targetSdkVersion;
3841
protected readonly string _fromRollbackDefinition;
42+
protected int _fromHistorySpecified;
43+
protected bool _historyManifestOnlyOption;
3944
protected string _workloadSetVersionFromCommandLine;
4045
protected string _globalJsonPath;
4146
protected string _workloadSetVersionFromGlobalJson;
@@ -46,10 +51,37 @@ internal abstract class InstallingWorkloadCommand : WorkloadCommandBase
4651
protected readonly IWorkloadManifestUpdater _workloadManifestUpdaterFromConstructor;
4752
protected IInstaller _workloadInstaller;
4853
protected IWorkloadManifestUpdater _workloadManifestUpdater;
54+
private WorkloadHistoryState _workloadHistoryRecord;
4955

5056
protected bool UseRollback => !string.IsNullOrWhiteSpace(_fromRollbackDefinition);
57+
protected bool FromHistory => _fromHistorySpecified != 0;
5158
protected bool SpecifiedWorkloadSetVersionOnCommandLine => !string.IsNullOrWhiteSpace(_workloadSetVersionFromCommandLine);
5259
protected bool SpecifiedWorkloadSetVersionInGlobalJson => !string.IsNullOrWhiteSpace(_workloadSetVersionFromGlobalJson);
60+
protected WorkloadHistoryState _WorkloadHistoryRecord
61+
{
62+
get
63+
{
64+
if (_workloadHistoryRecord is null && FromHistory)
65+
{
66+
var workloadHistoryRecords = _workloadInstaller.GetWorkloadHistoryRecords(_sdkFeatureBand.ToString()).OrderBy(r => r.TimeStarted).ToList();
67+
if (workloadHistoryRecords.Count == 0)
68+
{
69+
throw new GracefulException(Update.LocalizableStrings.NoWorkloadHistoryRecords, isUserError: true);
70+
}
71+
72+
var displayRecords = WorkloadHistoryDisplay.ProcessWorkloadHistoryRecords(workloadHistoryRecords, out _);
73+
74+
if (_fromHistorySpecified < 1 || _fromHistorySpecified > displayRecords.Count)
75+
{
76+
throw new GracefulException(Update.LocalizableStrings.WorkloadHistoryRecordInvalidIdValue, isUserError: true);
77+
}
78+
79+
_workloadHistoryRecord = displayRecords[_fromHistorySpecified - 1].HistoryState;
80+
}
81+
82+
return _workloadHistoryRecord;
83+
}
84+
}
5385

5486
public InstallingWorkloadCommand(
5587
ParseResult parseResult,
@@ -61,6 +93,7 @@ public InstallingWorkloadCommand(
6193
string tempDirPath)
6294
: base(parseResult, reporter: reporter, tempDirPath: tempDirPath, nugetPackageDownloader: nugetPackageDownloader)
6395
{
96+
_arguments = parseResult.GetArguments();
6497
_printDownloadLinkOnly = parseResult.GetValue(InstallingWorkloadCommandParser.PrintDownloadLinkOnlyOption);
6598
_fromCacheOption = parseResult.GetValue(InstallingWorkloadCommandParser.FromCacheOption);
6699
_includePreviews = parseResult.GetValue(InstallingWorkloadCommandParser.IncludePreviewOption);
@@ -105,19 +138,24 @@ public InstallingWorkloadCommand(
105138
_globalJsonPath = SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory);
106139
_workloadSetVersionFromGlobalJson = SdkDirectoryWorkloadManifestProvider.GlobalJsonReader.GetWorkloadVersionFromGlobalJson(_globalJsonPath);
107140

108-
if (SpecifiedWorkloadSetVersionInGlobalJson && (SpecifiedWorkloadSetVersionOnCommandLine || UseRollback))
141+
if (SpecifiedWorkloadSetVersionInGlobalJson && (SpecifiedWorkloadSetVersionOnCommandLine || UseRollback || FromHistory))
109142
{
110143
throw new GracefulException(string.Format(Strings.CannotSpecifyVersionOnCommandLineAndInGlobalJson, _globalJsonPath), isUserError: true);
111144
}
112-
113-
if (SpecifiedWorkloadSetVersionOnCommandLine && UseRollback)
145+
else if (SpecifiedWorkloadSetVersionOnCommandLine && UseRollback)
114146
{
115147
throw new GracefulException(string.Format(Update.LocalizableStrings.CannotCombineOptions,
116148
InstallingWorkloadCommandParser.FromRollbackFileOption.Name,
117149
InstallingWorkloadCommandParser.WorkloadSetVersionOption.Name), isUserError: true);
118150
}
151+
else if (SpecifiedWorkloadSetVersionOnCommandLine && FromHistory)
152+
{
153+
throw new GracefulException(string.Format(Update.LocalizableStrings.CannotCombineOptions,
154+
InstallingWorkloadCommandParser.WorkloadSetVersionOption.Name,
155+
WorkloadUpdateCommandParser.FromHistoryOption.Name), isUserError: true);
156+
}
119157

120-
// At this point, at most one of SpecifiedWorkloadSetVersionOnCommandLine, UseRollback, and SpecifiedWorkloadSetVersionInGlobalJson is true
158+
// At this point, at most one of SpecifiedWorkloadSetVersionOnCommandLine, UseRollback, FromHistory, and SpecifiedWorkloadSetVersionInGlobalJson is true
121159
}
122160

123161
protected static Dictionary<string, string> GetInstallStateContents(IEnumerable<ManifestVersionUpdate> manifestVersionUpdates) =>
@@ -141,10 +179,16 @@ public static bool ShouldUseWorkloadSetMode(SdkFeatureBand sdkFeatureBand, strin
141179
return GetCurrentInstallState(sdkFeatureBand, dotnetDir).UseWorkloadSets ?? false;
142180
}
143181

144-
protected void UpdateWorkloadManifests(ITransactionContext context, DirectoryPath? offlineCache)
182+
protected void UpdateWorkloadManifests(WorkloadHistoryRecorder recorder, ITransactionContext context, DirectoryPath? offlineCache)
145183
{
146-
var updateToLatestWorkloadSet = ShouldUseWorkloadSetMode(_sdkFeatureBand, _workloadRootDir);
147-
if (UseRollback && updateToLatestWorkloadSet)
184+
var updateToLatestWorkloadSet = ShouldUseWorkloadSetMode(_sdkFeatureBand, _workloadRootDir) && !SpecifiedWorkloadSetVersionInGlobalJson;
185+
if (FromHistory && !string.IsNullOrWhiteSpace(_WorkloadHistoryRecord.WorkloadSetVersion))
186+
{
187+
// This is essentially the same as updating to a specific workload set version, and we're now past the error check,
188+
// so we can just use the same code path.
189+
_workloadSetVersionFromCommandLine = _WorkloadHistoryRecord.WorkloadSetVersion;
190+
}
191+
else if ((UseRollback || FromHistory) && updateToLatestWorkloadSet)
148192
{
149193
// Rollback files are only for loose manifests. Update the mode to be loose manifests.
150194
Reporter.WriteLine(Update.LocalizableStrings.UpdateFromRollbackSwitchesModeToLooseManifests);
@@ -163,10 +207,15 @@ protected void UpdateWorkloadManifests(ITransactionContext context, DirectoryPat
163207
{
164208
_workloadInstaller.UpdateInstallMode(_sdkFeatureBand, true);
165209
}
210+
211+
if (SpecifiedWorkloadSetVersionInGlobalJson)
212+
{
213+
recorder.HistoryRecord.GlobalJsonVersion = _workloadSetVersionFromGlobalJson;
214+
}
166215
}
167216

168217
string resolvedWorkloadSetVersion = _workloadSetVersionFromGlobalJson ??_workloadSetVersionFromCommandLine;
169-
if (string.IsNullOrWhiteSpace(resolvedWorkloadSetVersion) && !UseRollback)
218+
if (string.IsNullOrWhiteSpace(resolvedWorkloadSetVersion) && !UseRollback && !FromHistory)
170219
{
171220
_workloadManifestUpdater.UpdateAdvertisingManifestsAsync(_includePreviews, updateToLatestWorkloadSet, offlineCache).Wait();
172221
if (updateToLatestWorkloadSet)
@@ -188,7 +237,8 @@ protected void UpdateWorkloadManifests(ITransactionContext context, DirectoryPat
188237
}
189238
else
190239
{
191-
manifestsToUpdate = UseRollback ? _workloadManifestUpdater.CalculateManifestRollbacks(_fromRollbackDefinition) :
240+
manifestsToUpdate = UseRollback ? _workloadManifestUpdater.CalculateManifestRollbacks(_fromRollbackDefinition, recorder) :
241+
FromHistory ? _workloadManifestUpdater.CalculateManifestUpdatesFromHistory(_WorkloadHistoryRecord) :
192242
_workloadManifestUpdater.CalculateManifestUpdates().Select(m => m.ManifestUpdate);
193243
}
194244

@@ -204,9 +254,10 @@ protected void UpdateWorkloadManifests(ITransactionContext context, DirectoryPat
204254

205255
if (!SpecifiedWorkloadSetVersionInGlobalJson)
206256
{
207-
if (UseRollback)
257+
if (UseRollback || (FromHistory && string.IsNullOrWhiteSpace(_WorkloadHistoryRecord.WorkloadSetVersion)))
208258
{
209259
_workloadInstaller.SaveInstallStateManifestVersions(_sdkFeatureBand, GetInstallStateContents(manifestsToUpdate));
260+
_workloadInstaller.AdjustWorkloadSetInInstallState(_sdkFeatureBand, null);
210261
}
211262
else if (SpecifiedWorkloadSetVersionOnCommandLine)
212263
{
@@ -253,17 +304,12 @@ protected void UpdateWorkloadManifests(ITransactionContext context, DirectoryPat
253304

254305
private IEnumerable<ManifestVersionUpdate> InstallWorkloadSet(ITransactionContext context, string workloadSetVersion)
255306
{
256-
PrintWorkloadSetTransition(workloadSetVersion);
307+
Reporter.WriteLine(string.Format(Strings.NewWorkloadSet, workloadSetVersion));
257308
var workloadSet = _workloadInstaller.InstallWorkloadSet(context, workloadSetVersion);
258309

259310
return _workloadManifestUpdater.CalculateManifestUpdatesForWorkloadSet(workloadSet);
260311
}
261312

262-
private void PrintWorkloadSetTransition(string newVersion)
263-
{
264-
Reporter.WriteLine(string.Format(Strings.NewWorkloadSet, newVersion));
265-
}
266-
267313
protected async Task<List<WorkloadDownload>> GetDownloads(IEnumerable<WorkloadId> workloadIds, bool skipManifestUpdate, bool includePreview, string downloadFolder = null,
268314
IReporter reporter = null, INuGetPackageDownloader packageDownloader = null)
269315
{

src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ private static CliCommand ConstructCommand()
130130
command.Subcommands.Add(WorkloadCleanCommandParser.GetCommand());
131131
command.Subcommands.Add(WorkloadElevateCommandParser.GetCommand());
132132
command.Subcommands.Add(WorkloadConfigCommandParser.GetCommand());
133+
command.Subcommands.Add(WorkloadHistoryCommandParser.GetCommand());
133134

134135
command.Validators.Add(commandResult =>
135136
{
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.DotNet.Cli.Utils;
5+
using Microsoft.DotNet.Workloads.Workload.History;
6+
7+
namespace Microsoft.DotNet.Cli.Commands.DotNetWorkloads
8+
{
9+
internal static class WorkloadHistoryDisplay
10+
{
11+
public class DisplayRecord
12+
{
13+
public int? ID { get; set; }
14+
public DateTimeOffset? TimeStarted { get; set; }
15+
public string Command { get; set; }
16+
public List<string> Workloads { get; set; }
17+
public string GlobalJsonVersion { get; set; }
18+
19+
public WorkloadHistoryState HistoryState { get; set; }
20+
}
21+
22+
public static List<DisplayRecord> ProcessWorkloadHistoryRecords(IEnumerable<WorkloadHistoryRecord> historyRecords, out bool unknownRecordsPresent)
23+
{
24+
List<DisplayRecord> displayRecords = new();
25+
unknownRecordsPresent = false;
26+
27+
int currentId = 2;
28+
29+
foreach (var historyRecord in historyRecords.OrderBy(r => r.TimeStarted))
30+
{
31+
if (displayRecords.Any() && !historyRecord.StateBeforeCommand.Equals(displayRecords.Last()?.HistoryState))
32+
{
33+
// Workload state changed without history record being written
34+
var unknownDisplayRecord = new DisplayRecord()
35+
{
36+
Command = "Unlogged Changes",
37+
ID = currentId,
38+
TimeStarted = null,
39+
HistoryState = historyRecord.StateBeforeCommand
40+
};
41+
42+
currentId++;
43+
displayRecords.Add(unknownDisplayRecord);
44+
unknownRecordsPresent = true;
45+
}
46+
47+
displayRecords.Add(new DisplayRecord()
48+
{
49+
ID = currentId,
50+
TimeStarted = historyRecord.TimeStarted,
51+
Command = historyRecord.CommandName,
52+
GlobalJsonVersion = historyRecord.GlobalJsonVersion,
53+
HistoryState = historyRecord.StateAfterCommand
54+
});
55+
56+
currentId++;
57+
}
58+
59+
if (displayRecords.Count > 0)
60+
{
61+
displayRecords.Insert(0, new DisplayRecord()
62+
{
63+
TimeStarted = DateTimeOffset.MinValue,
64+
ID = 1,
65+
Command = "InitialState",
66+
HistoryState = historyRecords.First().StateBeforeCommand
67+
});
68+
}
69+
70+
return displayRecords;
71+
}
72+
}
73+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.DotNet.Workloads.Workload.History
5+
{
6+
internal class WorkloadHistoryRecord
7+
{
8+
public DateTimeOffset TimeStarted { get; set; }
9+
public DateTimeOffset TimeCompleted { get; set; }
10+
11+
public string CommandName { get; set; }
12+
13+
public Dictionary<string, string> RollbackFileContents { get; set; }
14+
15+
public string[] CommandLineArgs { get; set; }
16+
17+
public bool Succeeded { get; set; }
18+
19+
public string ErrorMessage { get; set; }
20+
21+
public WorkloadHistoryState StateBeforeCommand { get; set; }
22+
23+
public WorkloadHistoryState StateAfterCommand { get; set; }
24+
25+
public string GlobalJsonVersion { get; set; }
26+
}
27+
28+
internal class WorkloadHistoryState
29+
{
30+
public Dictionary<string, string> ManifestVersions { get; set; }
31+
32+
public string WorkloadSetVersion { get; set; }
33+
34+
public List<string> InstalledWorkloads { get; set; }
35+
36+
public bool Equals(WorkloadHistoryState other)
37+
{
38+
if (ManifestVersions.Count != other.ManifestVersions.Count)
39+
{
40+
return false;
41+
}
42+
43+
foreach (var manifestId in ManifestVersions.Keys)
44+
{
45+
if (!other.ManifestVersions.TryGetValue(manifestId, out string otherManifestVersion) ||
46+
ManifestVersions[manifestId] != otherManifestVersion)
47+
{
48+
return false;
49+
}
50+
}
51+
52+
if ((WorkloadSetVersion is not null && !WorkloadSetVersion.Equals(other.WorkloadSetVersion)) ||
53+
(WorkloadSetVersion is null && other.WorkloadSetVersion is not null))
54+
{
55+
return false;
56+
}
57+
58+
return new HashSet<string>(InstalledWorkloads).SetEquals(other.InstalledWorkloads);
59+
}
60+
61+
public override bool Equals(object other)
62+
{
63+
if (other is WorkloadHistoryState otherState)
64+
{
65+
return Equals(otherState);
66+
}
67+
68+
return false;
69+
}
70+
71+
public override int GetHashCode()
72+
{
73+
HashCode hc = new HashCode();
74+
foreach (var kvp in ManifestVersions)
75+
{
76+
hc.Add(kvp.Key);
77+
hc.Add(kvp.Value);
78+
}
79+
80+
hc.Add(WorkloadSetVersion);
81+
82+
foreach (var workload in InstalledWorkloads)
83+
{
84+
hc.Add(workload);
85+
}
86+
87+
return hc.ToHashCode();
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)