Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
64e9830
Workload Repair should detect Corrupt Workload Sets
nagilson Jan 13, 2026
d95a5d7
improve workload set check
nagilson Jan 13, 2026
073c7e6
syntax errors
nagilson Jan 13, 2026
7a86443
Consider different install roots.
nagilson Jan 13, 2026
a7974e2
workload update also tested
nagilson Jan 13, 2026
a3959ba
move the mini corrupt wkl set repair logic into installing workload c…
nagilson Jan 13, 2026
40a576d
Revert "move the mini corrupt wkl set repair logic into installing wo…
nagilson Jan 13, 2026
549afce
Move repair corruption into corruption repairer so other commands are…
nagilson Jan 14, 2026
81fb328
Merge branch 'release/10.0.1xx' into nagilson-recover-manifests-on-in…
nagilson Jan 14, 2026
15be1af
use factory to set corruption repairer
nagilson Jan 14, 2026
6f38644
Merge branch 'nagilson-recover-manifests-on-install' of https://githu…
nagilson Jan 14, 2026
e91d74a
account for workload history recorder and other resolvers not being a…
nagilson Jan 14, 2026
5635e14
implement custom has missing manifest checks when less import context…
nagilson Jan 14, 2026
dcd04f7
use separate string since assembly resources are not easily shared
nagilson Jan 14, 2026
88fcd4e
don't refresh workload manifests and use workload set
nagilson Jan 15, 2026
1f1e0cb
remove extraneous comment
nagilson Jan 15, 2026
8fdbab3
Merge branch 'release/10.0.1xx' into nagilson-recover-manifests-on-in…
nagilson Jan 15, 2026
9687ee8
Merge branch 'release/10.0.1xx' into nagilson-recover-manifests-on-in…
nagilson Jan 15, 2026
514f5a0
Test fixes for the workload update changes
marcpopMSFT Jan 15, 2026
80aaaa0
Fix the new test to correctly update. This would previously had errored.
marcpopMSFT Jan 16, 2026
934a3b5
Switch teh test to directly call the new repair method. This doesn't …
marcpopMSFT Jan 16, 2026
f8688f8
Merge branch 'release/10.0.1xx' into nagilson-recover-manifests-on-in…
nagilson Jan 21, 2026
9f4429f
search all manifest roots and not just the sub manifest root
nagilson Jan 21, 2026
7be413a
Fix XLF
nagilson Jan 21, 2026
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
58 changes: 31 additions & 27 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -2724,4 +2724,8 @@ Proceed?</value>
<data name="DurationColon" xml:space="preserve">
<value>duration:</value>
</data>
<data name="WorkloadSetHasMissingManifests" xml:space="preserve">
<value>Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this.</value>
<comment>{0} is the workload set version. {Locked="dotnet workload repair"}</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.Commands.Workload.Config;
using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords;
using Microsoft.DotNet.Cli.Extensions;
Expand Down Expand Up @@ -642,7 +643,7 @@ public IEnumerable<WorkloadHistoryRecord> GetWorkloadHistoryRecords(string sdkFe
public void Shutdown()
{
// Perform any additional cleanup here that's intended to run at the end of the command, regardless
// of success or failure. For file based installs, there shouldn't be any additional work to
// of success or failure. For file based installs, there shouldn't be any additional work to
// perform.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable disable

using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
Expand Down Expand Up @@ -48,7 +49,7 @@ public static IInstaller GetWorkloadInstaller(

userProfileDir ??= CliFolderPathCalculator.DotnetUserProfileFolderPath;

return new FileBasedInstaller(
var installer = new FileBasedInstaller(
reporter,
sdkFeatureBand,
workloadResolver,
Expand All @@ -59,6 +60,25 @@ public static IInstaller GetWorkloadInstaller(
verbosity: verbosity,
packageSourceLocation: packageSourceLocation,
restoreActionConfig: restoreActionConfig);

// Attach corruption repairer to recover from corrupt workload sets
if (nugetPackageDownloader is not null &&
workloadResolver?.GetWorkloadManifestProvider() is SdkDirectoryWorkloadManifestProvider sdkProvider &&
sdkProvider.CorruptionRepairer is null)
{
sdkProvider.CorruptionRepairer = new WorkloadManifestCorruptionRepairer(
reporter,
installer,
workloadResolver,
sdkFeatureBand,
dotnetDir,
userProfileDir,
nugetPackageDownloader,
packageSourceLocation,
verbosity);
}

return installer;
}

private static bool CanWriteToDotnetRoot(string dotnetDir = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public override int Execute()
{
Reporter.WriteLine();

var workloadIds = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(new SdkFeatureBand(_sdkVersion));
var sdkFeatureBand = new SdkFeatureBand(_sdkVersion);
var workloadIds = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(sdkFeatureBand);

if (!workloadIds.Any())
{
Expand All @@ -79,7 +80,7 @@ public override int Execute()

Reporter.WriteLine(string.Format(CliCommandStrings.RepairingWorkloads, string.Join(" ", workloadIds)));

ReinstallWorkloadsBasedOnCurrentManifests(workloadIds, new SdkFeatureBand(_sdkVersion));
ReinstallWorkloadsBasedOnCurrentManifests(workloadIds, sdkFeatureBand);

WorkloadInstallCommand.TryRunGarbageCollection(_workloadInstaller, Reporter, Verbosity, workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion));

Expand All @@ -105,4 +106,5 @@ private void ReinstallWorkloadsBasedOnCurrentManifests(IEnumerable<WorkloadId> w
{
_workloadInstaller.RepairWorkloads(workloadIds, sdkFeatureBand);
}

}
4 changes: 4 additions & 0 deletions src/Cli/dotnet/Commands/Workload/WorkloadHistoryRecorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public void Run(Action workloadAction)
private WorkloadHistoryState GetWorkloadState()
{
var resolver = _workloadResolverFunc();
if (resolver.GetWorkloadManifestProvider() is SdkDirectoryWorkloadManifestProvider sdkProvider)
{
sdkProvider.CorruptionFailureMode = ManifestCorruptionFailureMode.Ignore;
}
var currentWorkloadVersion = resolver.GetWorkloadVersion().Version;
return new WorkloadHistoryState()
{
Expand Down
121 changes: 121 additions & 0 deletions src/Cli/dotnet/Commands/Workload/WorkloadManifestCorruptionRepairer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Commands.Workload.Install;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.NET.Sdk.WorkloadManifestReader;

namespace Microsoft.DotNet.Cli.Commands.Workload;

internal sealed class WorkloadManifestCorruptionRepairer : IWorkloadManifestCorruptionRepairer
{
private readonly IReporter _reporter;
private readonly IInstaller _workloadInstaller;
private readonly IWorkloadResolver _workloadResolver;
private readonly SdkFeatureBand _sdkFeatureBand;
private readonly string _dotnetPath;
private readonly string _userProfileDir;
private readonly INuGetPackageDownloader? _packageDownloader;
private readonly PackageSourceLocation? _packageSourceLocation;
private readonly VerbosityOptions _verbosity;

private bool _checked;

public WorkloadManifestCorruptionRepairer(
IReporter reporter,
IInstaller workloadInstaller,
IWorkloadResolver workloadResolver,
SdkFeatureBand sdkFeatureBand,
string dotnetPath,
string userProfileDir,
INuGetPackageDownloader? packageDownloader,
PackageSourceLocation? packageSourceLocation,
VerbosityOptions verbosity)
{
_reporter = reporter ?? NullReporter.Instance;
_workloadInstaller = workloadInstaller;
_workloadResolver = workloadResolver;
_sdkFeatureBand = sdkFeatureBand;
_dotnetPath = dotnetPath;
_userProfileDir = userProfileDir;
_packageDownloader = packageDownloader;
_packageSourceLocation = packageSourceLocation;
_verbosity = verbosity;
}

public void EnsureManifestsHealthy(ManifestCorruptionFailureMode failureMode)
{
if (_checked)
{
return;
}

_checked = true;

if (failureMode == ManifestCorruptionFailureMode.Ignore)
{
return;
}

// Get the workload set directly from the provider - it was already resolved during construction
// and doesn't require reading the install state file again
var provider = _workloadResolver.GetWorkloadManifestProvider() as SdkDirectoryWorkloadManifestProvider;
var workloadSet = provider?.ResolvedWorkloadSet;

if (workloadSet is null)
{
// No workload set is being used
return;
}

if (!provider?.HasMissingManifests(workloadSet) ?? true)
{
return;
}

if (failureMode == ManifestCorruptionFailureMode.Throw)
{
throw new InvalidOperationException(string.Format(CliCommandStrings.WorkloadSetHasMissingManifests, workloadSet.Version));
}

_reporter.WriteLine($"Repairing workload set {workloadSet.Version}...");
CliTransaction.RunNew(context => RepairCorruptWorkloadSet(context, workloadSet));
}



private void RepairCorruptWorkloadSet(ITransactionContext context, WorkloadSet workloadSet)
{
var manifestUpdates = CreateManifestUpdatesFromWorkloadSet(workloadSet);

foreach (var manifestUpdate in manifestUpdates)
{
_workloadInstaller.InstallWorkloadManifest(manifestUpdate, context);
}

}

[MemberNotNull(nameof(_packageDownloader))]
private IEnumerable<ManifestVersionUpdate> CreateManifestUpdatesFromWorkloadSet(WorkloadSet workloadSet)
{
if (_packageDownloader is null)
{
throw new InvalidOperationException("Package downloader is required to repair workload manifests.");
}

var manifestUpdater = new WorkloadManifestUpdater(
_reporter,
_workloadResolver,
_packageDownloader,
_userProfileDir,
_workloadInstaller.GetWorkloadInstallationRecordRepository(),
_workloadInstaller,
_packageSourceLocation,
displayManifestUpdates: _verbosity >= VerbosityOptions.detailed);

return manifestUpdater.CalculateManifestUpdatesForWorkloadSet(workloadSet);
}
}
5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading