Skip to content

Conversation

@Forgind
Copy link
Contributor

@Forgind Forgind commented Dec 29, 2023

This PR should enable installing and updating via workload sets. In particular, if you have mode set to workload sets (and don't use a rollback file), it will now look for the latest workload set (or latest stable if you don't specify to use previews) and install/update to that rather than updating manifests individually.

It now also supports dotnet workload update --version x where it will update the workload set version x.

Finally, it now will display the workload set it is using if it's set to use workload sets in the install state file when the user runs dotnet workload list.

All functionality was manually tested at some point, and there's a new unit test.

Causes the dotnet workload update/install/repair commands to use a new GetWorkloadInstallMode method to decide whether to use workload sets or not. If they decide to use workload sets, they calculate manifest updates by reading from a dictionary that is not implemented.
@ghost ghost added Area-Workloads untriaged Request triage from a team member labels Dec 29, 2023
@Forgind Forgind changed the base branch from main to release/8.0.2xx December 29, 2023 00:27
_shutdown = true;
}

public PackageId GetManifestPackageId(ManifestId manifestId, SdkFeatureBand featureBand)
Copy link
Member

Choose a reason for hiding this comment

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

This is misleading, I'd suggest having two separate methods for manifest and workload set package, even though there similar - it will make it much easier to understand 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.

I'll admit you're right that it's a little misleading. I'm trying to balance making things clearer vs. reusing more code—since this method is called in the middle of where I need it, I'd have to either put a random if/else in the middle of that method (that is, move this up one stack frame) or copy most of the method into a separate method just for workload sets. I'd rather not lose any updates that come in the future, which is why I architected it this way. If you feel strongly, though, I can change that.

Copy link
Member

Choose a reason for hiding this comment

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

Do you still need an if/else in the calling method? I see that this method is called on line 285 with the workload set package ID hardcoded, so it could just call a different method directly there. Are there other places where you're calling it and you don't know whether it's being called for a workload set or a normal manifest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In GetManifestPackageDownloadsAsync and UpdateManifestWithVersionAsync, we shouldn't know whether it's a workload set manifest ID or something else.

@Forgind Forgind requested a review from vijayrkn as a code owner March 1, 2024 20:50
@Forgind Forgind changed the base branch from release/8.0.2xx to release/8.0.3xx March 1, 2024 20:50
Forgind added 3 commits March 1, 2024 13:51
(and this time, build to make sure that's it)
Version is not written into the install state by default anymore, so this writes it into a temporary "versionFile" (analogous to packageVersion.txt) and verifies that it's updated to the right workload set version.
@Forgind
Copy link
Contributor Author

Forgind commented Mar 4, 2024

Windows_NT Build_Release and Darwin Build_Release both failed due to CanBuildProjectWithAPackageReferenceWithMultipleAliases. I didn't bother rerunning them.

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

It looks like this PR is feature-complete for merging, thanks.

Some feedback (which is also covered in PR comments on the code):

  • Terminology: We should use "workload version" instead of "workload set" or "workload set version" when possible in user-facing messages.
  • Resolver changes / workload set filenames: I sent an email about this. I think we want to revert most of the SdkDirectoryWorkloadManifestProvider changes here and re-generate the workload set packages with different .json filenames.
  • Pinning: We shouldn't write the workload set version to the install state file unless --version was specified (see my PR code comment for more details)
  • Transactions: The CliTransaction class isn't being used correctly, which I think means it wouldn't actually run the rollback actions.

Follow-up items (ie things to do in subsequent PRs):

  • Offline cache: We support a --download-to-cache parameter to download all required packages locally, and a --from-cache parameter to use that to do updates / installs without network access. It looks like those aren't supported for workload sets here. Those parameters are hidden and a lot lower priority, so we can add support for them as a follow-up item (if at all). FYI @marcpopMSFT @baronfel
  • Add support for dotnet workload restore with workload set version specified in global.json, as well as root tracking, garbage collection, etc.
  • Check for available workload set updates and notify user about them (see WriteUpdatableWorkloadsFile)
  • Tests: I'd like to see a lot more workload set tests. I think the VM testing infrastructure I'm working on will be great for this. I also have some ideas for file-based end-to-end workload tests, but probably that's a lot longer term.
  • ManifestVersionUpdate class: This isn't directly related to this PR or to workload sets, but I noticed it when reviewing the code. Do we need to represent the existing/old manifest version in ManifestVersionUpdate anymore? Manifests are now side-by-side so it's probably not needed.

We had also previously discussed how the APIs here are passing around information such as the workload set version via a path to the package in the advertising manifests. I still don't think that this is the best design. If we are worried about downloading packages twice, I took a look and for MSI-based installs we don't actually do so because the MSI installer manages its own cache of downloaded packages. If the double download is actually an issue we could do the same type of caching for the file-based installer.

However, I don't want to block on changing this design, so we can check it in as is and possibly refactor it later.

/// 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.

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 GetInstallStateMode(SdkFeatureBand sdkFeatureBand, string dotnetDir)
Copy link
Member

Choose a reason for hiding this comment

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

Returning a bool from a method that is named to return a "mode" doesn't make clear what the value of the boolean means. Perhaps this should be named ShouldUseWorkloadSetMode.

using PackageArchiveReader packageReader = new(packagePath);
var downloadedPackageVersion = packageReader.NuspecReader.GetVersion();
var correctedPackageVersion = WorkloadSetPackageVersionToWorkloadSetVersion(_sdkFeatureBand, downloadedPackageVersion.ToString());
File.WriteAllText(Path.Combine(adManifestPath, "packageVersion.txt"), correctedPackageVersion);
Copy link
Member

Choose a reason for hiding this comment

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

It looks like the contents of this file are the workload set version, not the package version, so the file should be named accordingly:

Suggested change
File.WriteAllText(Path.Combine(adManifestPath, "packageVersion.txt"), correctedPackageVersion);
File.WriteAllText(Path.Combine(adManifestPath, "workloadSetVersion.txt"), correctedPackageVersion);

Also, it might be good to create a constant so we don't have to hard code this file name in multiple places. There are various path components in the workload code that we don't do this for, but it might be a good idea for something with mixed case which is more likely to end up being inconsistent and only fail on case-sensitive file systems.

Comment on lines 100 to 103
if (advertisingPackagePath is not null)
{
PathUtility.DeleteFileAndEmptyParents(advertisingPackagePath);
}
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem like the right rollback action here. The rollback here should undo what was done in the corresponding action, so it should delete the destination files that were copied, not the source. If the source needs to be deleted that should probably be done elsewhere.

ExecutePackage(msi, plannedAction, msiPackageId);

// Update the reference count against the MSI.
UpdateDependent(InstallRequestType.AddDependent, msi.Manifest.ProviderKeyName, _dependent);
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 you only want to add the dependent if you installed the package, if you uninstalled it then you probably want to remove the dependent. @joeloff, does that seem right?

{
WorkloadSet? workloadSet = null;
foreach (var jsonFile in Directory.GetFiles(workloadSetDirectory, "*.workloadset.json"))
var jsonFile = Path.Combine(workloadSetDirectory, "workloadset.json");
Copy link
Member

Choose a reason for hiding this comment

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

I sent an email about this. Probably we don't want to change this logic, as it would mean we'd have to update the MSBuild SDK Resolver in Visual Studio for it to work with workload sets. Keeping the current logic (ie reverting the change here) does probably mean the current workload set packages won't work and will have to be regenerated.

case JsonTokenType.PropertyName:
var sdkPropName = reader.GetString();
if (string.Equals("workloadVersion", sdkPropName, StringComparison.OrdinalIgnoreCase))
if (string.Equals("workloadSetVersion", sdkPropName, StringComparison.OrdinalIgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

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

What's the reason for this change? The spec specifies workloadVersion as the property name in the global.json file. If we were to make this change, this would be another case where we'd have to update the resolver in VS for it to work there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My understanding was that we didn't have workload set version support for global.json yet, so I didn't think of it as a breaking change yet and preferred the more descriptive name. Both because it sounds like the current consensus is to use "workload version" instead of "workload set version" and because of what you said about updating VS, I don't think this is a good idea anymore, so I'll revert it.

}

[Fact]
public void WorkloadSetCanIncludeMultipleJsonFiles()
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should change the resolver behavior, so I think we should keep these tests.

var resolverFactory = new MockWorkloadResolverFactory(Path.Combine(Path.GetTempPath(), "dotnetTestPath"), versionNumber, workloadResolver, "userProfileDir");
var updateCommand = new WorkloadUpdateCommand(Parser.Instance.Parse("dotnet workload update"), Reporter.Output, resolverFactory, workloadInstaller, nugetPackageDownloader, workloadManifestUpdater);

var installStatePath = Path.Combine(Path.GetTempPath(), "dotnetTestPath", "metadata", "workloads", versionNumber, "InstallState", "default.json");
Copy link
Member

Choose a reason for hiding this comment

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

I would avoid using the temp path for tests. I know we do have some tests that do it, but I think it's a lot better to call _testAssetsManager.CreateTestDirectory(identifier: upgrade.ToString(). Then you don't have to delete the directory and can investigate it after the test runs.

// this updates all the manifests
var manifests = _workloadResolver.GetInstalledManifests();
await Task.WhenAll(manifests.Select(manifest => UpdateAdvertisingManifestAsync(manifest, includePreviews, offlineCache))).ConfigureAwait(false);
WriteUpdatableWorkloadsFile();
Copy link
Member

Choose a reason for hiding this comment

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

This file is used for a message we print out on dotnet commands letting people know that a workload update is available. We should do a similar thing for workload sets.

However, it doesn't need to be in this PR, we can do that as a follow-up.

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

This is quite close. I had a few comments, the most important to change before merging is the name of the workloadVersion property in global.json.

installer.SaveInstallStateManifestVersions(sdkFeatureBand, GetInstallStateContents(manifestsToUpdate));
}

installer.AdjustWorkloadSetInInstallState(sdkFeatureBand, string.IsNullOrWhiteSpace(_workloadSetVersion) ? null : workloadSetVersion);
Copy link
Member

Choose a reason for hiding this comment

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

It seems a bit confusing that this uses two different values: _workloadSetVersion and workloadSetVersion. Is there any case where _workloadSetVersion would be set, but workloadVersion would have a different value? I think not, so I think this can use _workloadSetVersion in both cases. If not, then this should definitely have a comment explaining the difference.

Probably it would be good to have a comment anyway explaining why this uses _workloadSetVersion and not workloadSetVersion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only way I think _workloadSetVersion could be set, and it would be different from workloadSetVersion is if the user requests version x, and we reach out to NuGet and ask for x, and it returns version y instead. I don't think that's possible today—I think if NuGet can't find the right version, it would error rather than return the wrong version—so I think you're right that I can switch to just using _workloadSetVersion and delete workloadSetVersion. I should be able to do something similar in WorkloadUpdateCommand.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think you want to delete workloadSetVersion entirely, just don't use it on this line. If there's no pinned workload set version and no version was specified on the command line, but the install state is opted into workload sets, then _workloadSetVersion would be null, but workloadSetVersion would be the latest workload set version that was found, and should be installed / used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. We'd still install that version, but we don't actually need to keep track of it in Workload(Update/Install)Command.

Comment on lines 318 to 319
.Where(path => path.EndsWith(".nupkg") && Path.GetFileName(path).StartsWith(manifestPackageId.ToString(), StringComparison.OrdinalIgnoreCase))
.Max() :
Copy link
Member

Choose a reason for hiding this comment

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

This looks like the correct logic if packageVersion is not specified. If it is specified, I think this logic needs to take the version into account to return the package version specified instead of the latest version in the cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was how it was before...I'll admit I thought that was a little odd but figured it must not matter too much since we hadn't fixed it for other manifests? But I can change the logic for this.

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 the difference is that previously it would always look for the latest version of a package, it didn't have the capability of specifying a specific version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mmm...I think you're right. I'd thought there was something that handled versions previously, but I don't know what I was thinking of.

}

private IEnumerable<(ManifestId Id, ManifestVersionWithBand ManifestWithBand)> ParseRollbackDefinitionFile(string rollbackDefinitionFilePath)
public IEnumerable<ManifestVersionUpdate> ParseRollbackDefinitionFiles(IEnumerable<string> rollbackFilePaths)
Copy link
Member

Choose a reason for hiding this comment

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

SdkDirectoryWorkloadManifestProvider.GetAvailableWorkloadSets has logic that does the same thing as this method. Could this be factored so there's only one implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They don't look like they do the same thing. That method finds all installed workload sets and returns the ones that match this feature band. This takes in paths to workload sets and parses them into a set of manifest version updates. It's basically "what happens after you call SdkDirectoryWorkloadManifestProvider.GetAvailableWorkloadSets"

Copy link
Member

Choose a reason for hiding this comment

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

The methods don't do the same thing, but inside the loop in GetAvailableWorkloadSets it seemed like it was doing the same thing (aggregating multiple json files into a single workload set).

fullSet = fullSet.DistinctBy<(ManifestId, ManifestVersionWithBand), ManifestId>(update => update.Item1).ToList();
if (size != fullSet.Count)
{
throw new Exception();
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't throw an exception with no details here. It looks like this is probably supposed to catch the case where the same manifest ID appears in multiple json files in the workload set package. If so, ideally the message would include information about which ID was duplicated. However, since in practice we don't expect multiple json files in the package, it's not a big deal. But we still shouldn't throw an Exception without a message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch; that was supposed to have a real exception, but then I forgot to add that part 🙂

{
// Rollback files are only for loose manifests. Update the mode to be loose manifests.
Reporter.WriteLine(LocalizableStrings.UpdateFromRollbackSwitchesModeToLooseManifests);
_workloadInstaller.UpdateInstallMode(_sdkFeatureBand, false);
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be run inside the transaction, and rolled back if the update fails.

_workloadInstaller.RemoveManifestsFromInstallState(sdkFeatureBand);
}

_workloadInstaller.AdjustWorkloadSetInInstallState(sdkFeatureBand, string.IsNullOrWhiteSpace(_workloadSetVersion) ? null : workloadSetVersion);
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 maybe I got confused here by the difference between _workloadSetVersion and workloadSetVersion. I commented on the same code in WorkloadInstallCommand, I think it would be easier to understand if this line was consistent about using _workloadSetVersion, and it would be good to have a comment explaining why it does so.

return workloads;
}

private void RunInNewTransaction(Action<ITransactionContext> a)
Copy link
Member

Choose a reason for hiding this comment

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

Is this the exact same code as in WorkloadInstallCommand? If so, should this be moved up to the base class?

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 actually had the exact same thought. The only issue is that the strings are separate, so this is an annoyingly big change. I do think it would be cleaner to do it that way, though...

public Dictionary<string, string>? Manifests { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WorkloadSetVersion { get; set; }
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 I mentioned this when we were discussing the PR, but I may have forgotten to include it in the PR comments: The property name in the global.json file should be workloadVersion (though it looks like the tests weren't consistent with the casing, so probably we should be case-insensitive).

So this property should be named WorkloadVersion, or I think you could also use a Json attribute to change the name used when reading / writing json.

"""
{
"workloadVersion": "8.0.201"
"workloadSetVersion": "8.0.201"
Copy link
Member

Choose a reason for hiding this comment

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

As mentioned in InstallStateContents, the JSON property should be workloadVersion, so these changes in the tests should be reverted.

return $"{nugetVersion.Major}.{nugetVersion.Patch}.{patch}{release}";
}

public static string WorkloadSetPackageVersionToWorkloadSetVersion(SdkFeatureBand sdkFeatureBand, string packageVersion)
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this wasn't addressed yet, it would be good to sync the implementations with arcade, especially since there are tests covering arcade's version of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Workloads untriaged Request triage from a team member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants