Skip to content

Commit 2e3a93c

Browse files
authored
Introduce Workload Clean: Garbage Collection Component (#30266)
2 parents 6629a7b + 70dff83 commit 2e3a93c

Some content is hidden

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

46 files changed

+1234
-168
lines changed

src/Cli/dotnet/commands/InstallingWorkloadCommand.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ protected async Task<List<WorkloadDownload>> GetDownloads(IEnumerable<WorkloadId
162162

163163
protected IEnumerable<WorkloadId> GetInstalledWorkloads(bool fromPreviousSdk)
164164
{
165-
//var currentFeatureBand = new SdkFeatureBand(_installedFeatureBand.ToString());
166165
if (fromPreviousSdk)
167166
{
168167
var priorFeatureBands = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetFeatureBandsWithInstallationRecords()

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.CommandLine;
67
using System.CommandLine.Parsing;
@@ -29,11 +30,11 @@ public static Command GetCommand()
2930
return Command;
3031
}
3132

32-
internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null)
33+
internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null, string dotnetDir = null)
3334
{
3435
if(workloadInfoHelper != null)
3536
{
36-
workloadInfoHelper ??= new WorkloadInfoHelper(parseResult.HasOption(SharedOptions.InteractiveOption));
37+
workloadInfoHelper ??= new WorkloadInfoHelper(parseResult != null ? parseResult.HasOption(SharedOptions.InteractiveOption) : false);
3738
}
3839
else
3940
{
@@ -42,6 +43,7 @@ internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkload
4243
IEnumerable<WorkloadId> installedList = workloadInfoHelper.InstalledSdkWorkloadIds;
4344
InstalledWorkloadsCollection installedWorkloads = workloadInfoHelper.AddInstalledVsWorkloads(installedList);
4445
reporter ??= Cli.Utils.Reporter.Output;
46+
string dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
4547

4648
if (!installedList.Any())
4749
{
@@ -73,7 +75,7 @@ internal static void ShowWorkloadsInfo(ParseResult parseResult = null, IWorkload
7375
reporter.WriteLine($" {workloadManifest.ManifestPath,align}");
7476

7577
reporter.Write($"{separator}{CommonStrings.WorkloadInstallTypeColumn}:");
76-
reporter.WriteLine($" {WorkloadInstallerFactory.GetWorkloadInstallType(new SdkFeatureBand(workloadFeatureBand), workloadManifest.ManifestPath).ToString(),align}"
78+
reporter.WriteLine($" {WorkloadInstallerFactory.GetWorkloadInstallType(new SdkFeatureBand(workloadFeatureBand), dotnetPath),align}"
7779
);
7880
}
7981
}
@@ -100,6 +102,7 @@ private static Command ConstructCommand()
100102
command.AddCommand(WorkloadUninstallCommandParser.GetCommand());
101103
command.AddCommand(WorkloadRepairCommandParser.GetCommand());
102104
command.AddCommand(WorkloadRestoreCommandParser.GetCommand());
105+
command.AddCommand(WorkloadCleanCommandParser.GetCommand());
103106
command.AddCommand(WorkloadElevateCommandParser.GetCommand());
104107

105108
command.SetHandler((parseResult) => ProcessArgs(parseResult));

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ public InstalledWorkloadsCollection AddInstalledVsWorkloads(IEnumerable<Workload
7777
#if !DOT_NET_BUILD_FROM_SOURCE
7878
if (OperatingSystem.IsWindows())
7979
{
80-
VisualStudioWorkloads.GetInstalledWorkloads(WorkloadResolver, _currentSdkFeatureBand,
81-
installedWorkloads);
80+
VisualStudioWorkloads.GetInstalledWorkloads(WorkloadResolver, installedWorkloads);
8281
}
8382
#endif
8483
return installedWorkloads;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<root>
3+
<!--
4+
Microsoft ResX Schema
5+
6+
Version 2.0
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
11+
associated with the data types.
12+
13+
Example:
14+
15+
... ado.net/XML headers & schema ...
16+
<resheader name="resmimetype">text/microsoft-resx</resheader>
17+
<resheader name="version">2.0</resheader>
18+
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
19+
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
20+
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
21+
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
22+
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
23+
<value>[base64 mime encoded serialized .NET Framework object]</value>
24+
</data>
25+
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
26+
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
27+
<comment>This is a comment</comment>
28+
</data>
29+
30+
There are any number of "resheader" rows that contain simple
31+
name/value pairs.
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
37+
mimetype set.
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
41+
extensible. For a given mimetype the value must be set accordingly:
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
45+
read any of the formats listed below.
46+
47+
mimetype: application/x-microsoft.net.object.binary.base64
48+
value : The object must be serialized with
49+
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
50+
: and then encoded with base64 encoding.
51+
52+
mimetype: application/x-microsoft.net.object.soap.base64
53+
value : The object must be serialized with
54+
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
55+
: and then encoded with base64 encoding.
56+
57+
mimetype: application/x-microsoft.net.object.bytearray.base64
58+
value : The object must be serialized into a byte array
59+
: using a System.ComponentModel.TypeConverter
60+
: and then encoded with base64 encoding.
61+
-->
62+
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
63+
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
64+
<xsd:element name="root" msdata:IsDataSet="true">
65+
<xsd:complexType>
66+
<xsd:choice maxOccurs="unbounded">
67+
<xsd:element name="metadata">
68+
<xsd:complexType>
69+
<xsd:sequence>
70+
<xsd:element name="value" type="xsd:string" minOccurs="0" />
71+
</xsd:sequence>
72+
<xsd:attribute name="name" use="required" type="xsd:string" />
73+
<xsd:attribute name="type" type="xsd:string" />
74+
<xsd:attribute name="mimetype" type="xsd:string" />
75+
<xsd:attribute ref="xml:space" />
76+
</xsd:complexType>
77+
</xsd:element>
78+
<xsd:element name="assembly">
79+
<xsd:complexType>
80+
<xsd:attribute name="alias" type="xsd:string" />
81+
<xsd:attribute name="name" type="xsd:string" />
82+
</xsd:complexType>
83+
</xsd:element>
84+
<xsd:element name="data">
85+
<xsd:complexType>
86+
<xsd:sequence>
87+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
88+
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
89+
</xsd:sequence>
90+
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
91+
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
92+
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
93+
<xsd:attribute ref="xml:space" />
94+
</xsd:complexType>
95+
</xsd:element>
96+
<xsd:element name="resheader">
97+
<xsd:complexType>
98+
<xsd:sequence>
99+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
100+
</xsd:sequence>
101+
<xsd:attribute name="name" type="xsd:string" use="required" />
102+
</xsd:complexType>
103+
</xsd:element>
104+
</xsd:choice>
105+
</xsd:complexType>
106+
</xsd:element>
107+
</xsd:schema>
108+
<resheader name="resmimetype">
109+
<value>text/microsoft-resx</value>
110+
</resheader>
111+
<resheader name="version">
112+
<value>2.0</value>
113+
</resheader>
114+
<resheader name="reader">
115+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
116+
</resheader>
117+
<resheader name="writer">
118+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119+
</resheader>
120+
<data name="CleanAllOptionDescription" xml:space="preserve">
121+
<value>Causes clean to remove and uninstall all workload components from all SDK versions.</value>
122+
</data>
123+
<data name="CommandDescription" xml:space="preserve">
124+
<value>Removes workload components that may have been left behind from previous updates and uninstallations.</value>
125+
</data>
126+
<data name="VSWorkloadNotRemoved" xml:space="preserve">
127+
<value>Workload '{0}' was not removed because it is installed and managed by Visual Studio: '{1}'. Please uninstall this workload using the Visual Studio Installer to fully remove it.</value>
128+
</data>
129+
<data name="InvalidWorkloadProcessPath" xml:space="preserve">
130+
<value>The path '{0}' of the process is the root path of the drive, which is not allowed, or it is invalid/inaccessible. Please run dotnet in a valid and available path.</value>
131+
</data>
132+
<data name="CannotAnalyzeVSWorkloadBand" xml:space="preserve">
133+
<value>Workloads managed by Visual Studio must be uninstalled using the Visual Studio Installer. For the version of Visual Studio managing the SDK '{0}', we could not display workloads to uninstall. This is likely because '{0}' uses a different dotnet root path or custom user profile directory from the current running SDK.
134+
Paths searched: '{1}', '{2}'.</value>
135+
</data>
136+
</root>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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+
using System;
4+
using System.Collections.Generic;
5+
using System.CommandLine;
6+
using System.IO;
7+
using Microsoft.Deployment.DotNet.Releases;
8+
using Microsoft.DotNet.Cli;
9+
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
10+
using Microsoft.DotNet.Cli.Utils;
11+
using Microsoft.DotNet.Configurer;
12+
using Microsoft.DotNet.Installer.Windows;
13+
using Microsoft.DotNet.Workloads.Workload.Install;
14+
using Microsoft.DotNet.Workloads.Workload.Install.InstallRecord;
15+
using Microsoft.DotNet.Workloads.Workload.List;
16+
using Microsoft.NET.Sdk.WorkloadManifestReader;
17+
18+
#nullable enable
19+
20+
namespace Microsoft.DotNet.Workloads.Workload.Clean
21+
{
22+
internal class WorkloadCleanCommand : WorkloadCommandBase
23+
{
24+
private readonly bool _cleanAll;
25+
26+
private string? _dotnetPath;
27+
private string _userProfileDir;
28+
29+
private readonly ReleaseVersion _sdkVersion;
30+
private readonly IInstaller _workloadInstaller;
31+
private readonly IWorkloadResolver _workloadResolver;
32+
33+
public WorkloadCleanCommand(
34+
ParseResult parseResult,
35+
IReporter? reporter = null,
36+
IWorkloadResolver? workloadResolver = null,
37+
string? dotnetDir = null,
38+
string? version = null,
39+
string? userProfileDir = null
40+
) : base(parseResult, reporter: reporter)
41+
{
42+
_cleanAll = parseResult.GetValue(WorkloadCleanCommandParser.CleanAllOption);
43+
44+
_dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
45+
if (_dotnetPath == null)
46+
{
47+
throw new GracefulException(String.Format(LocalizableStrings.InvalidWorkloadProcessPath, Environment.ProcessPath ?? "null"));
48+
}
49+
50+
_userProfileDir = userProfileDir ?? CliFolderPathCalculator.DotnetUserProfileFolderPath;
51+
52+
_sdkVersion = WorkloadOptionsExtensions.GetValidatedSdkVersion(parseResult.GetValue(WorkloadUninstallCommandParser.VersionOption), version, _dotnetPath, userProfileDir, true);
53+
var sdkFeatureBand = new SdkFeatureBand(_sdkVersion);
54+
55+
var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(_dotnetPath, _sdkVersion.ToString(), _userProfileDir);
56+
_workloadResolver = workloadResolver ?? WorkloadResolver.Create(workloadManifestProvider, _dotnetPath, _sdkVersion.ToString(), _userProfileDir);
57+
_workloadInstaller = WorkloadInstallerFactory.GetWorkloadInstaller(Reporter, sdkFeatureBand, _workloadResolver, Verbosity, _userProfileDir, VerifySignatures, PackageDownloader, _dotnetPath);
58+
59+
}
60+
61+
public override int Execute()
62+
{
63+
ExecuteGarbageCollection();
64+
return 0;
65+
}
66+
67+
private void ExecuteGarbageCollection()
68+
{
69+
_workloadInstaller.GarbageCollectInstalledWorkloadPacks(cleanAllPacks: _cleanAll);
70+
DisplayUninstallableVSWorkloads();
71+
}
72+
73+
/// <summary>
74+
/// Print VS Workloads with the same machine arch which can't be uninstalled through the SDK CLI to increase user awareness that they must uninstall via VS.
75+
/// </summary>
76+
private void DisplayUninstallableVSWorkloads()
77+
{
78+
#if !DOT_NET_BUILD_FROM_SOURCE
79+
// We don't want to print MSI related content in a file-based installation.
80+
if (!(_workloadInstaller.GetType() == typeof(NetSdkMsiInstallerClient)))
81+
{
82+
return;
83+
}
84+
85+
if (OperatingSystem.IsWindows())
86+
{
87+
// All VS Workloads should have a corresponding MSI based SDK. This means we can pull all of the VS SDK feature bands using MSI/VS related registry keys.
88+
var installedSDKVersionsWithPotentialVSRecords = MsiInstallerBase.GetInstalledSdkVersions();
89+
HashSet<string> vsWorkloadUninstallWarnings = new();
90+
91+
string defaultDotnetWinPath = MsiInstallerBase.GetDotNetHome();
92+
foreach (string sdkVersion in installedSDKVersionsWithPotentialVSRecords)
93+
{
94+
try
95+
{
96+
#pragma warning disable CS8604 // We error in the constructor if the dotnet path is null.
97+
98+
// We don't know if the dotnet installation for the other bands is in a different directory than the current dotnet; check the default directory if it isn't.
99+
var bandedDotnetPath = Path.Exists(Path.Combine(_dotnetPath, "sdk", sdkVersion)) ? _dotnetPath : defaultDotnetWinPath;
100+
101+
if (!Path.Exists(bandedDotnetPath))
102+
{
103+
Reporter.WriteLine(AnsiColorExtensions.Yellow(string.Format(LocalizableStrings.CannotAnalyzeVSWorkloadBand, sdkVersion, _dotnetPath, defaultDotnetWinPath)));
104+
continue;
105+
}
106+
107+
var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(bandedDotnetPath, sdkVersion, _userProfileDir);
108+
var bandedResolver = WorkloadResolver.Create(workloadManifestProvider, bandedDotnetPath, sdkVersion.ToString(), _userProfileDir);
109+
#pragma warning restore CS8604
110+
111+
InstalledWorkloadsCollection vsWorkloads = new();
112+
VisualStudioWorkloads.GetInstalledWorkloads(bandedResolver, vsWorkloads, _cleanAll ? null : new SdkFeatureBand(sdkVersion));
113+
foreach (var vsWorkload in vsWorkloads.AsEnumerable())
114+
{
115+
vsWorkloadUninstallWarnings.Add(string.Format(LocalizableStrings.VSWorkloadNotRemoved, $"{vsWorkload.Key}", $"{vsWorkload.Value}"));
116+
}
117+
}
118+
catch (WorkloadManifestException ex)
119+
{
120+
// Limitation: We don't know the dotnetPath of the other feature bands when making the manifestProvider and resolvers.
121+
// This can cause the manifest resolver to fail as it may look for manifests in an invalid path.
122+
// It can theoretically be customized, but that is not currently supported for workloads with VS.
123+
Reporter.WriteLine(AnsiColorExtensions.Yellow(string.Format(LocalizableStrings.CannotAnalyzeVSWorkloadBand, sdkVersion, _dotnetPath, defaultDotnetWinPath)));
124+
Cli.Utils.Reporter.Verbose.WriteLine(ex.Message);
125+
}
126+
}
127+
128+
foreach (string warning in vsWorkloadUninstallWarnings)
129+
{
130+
Reporter.WriteLine(warning.Yellow());
131+
}
132+
}
133+
#endif
134+
}
135+
}
136+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
using System.CommandLine;
4+
using Microsoft.DotNet.Workloads.Workload.Clean;
5+
using LocalizableStrings = Microsoft.DotNet.Workloads.Workload.Clean.LocalizableStrings;
6+
7+
namespace Microsoft.DotNet.Cli
8+
{
9+
internal static class WorkloadCleanCommandParser
10+
{
11+
12+
public static readonly Option<bool> CleanAllOption = new Option<bool>("--all", LocalizableStrings.CleanAllOptionDescription);
13+
14+
private static readonly Command Command = ConstructCommand();
15+
16+
public static Command GetCommand()
17+
{
18+
return Command;
19+
}
20+
21+
private static Command ConstructCommand()
22+
{
23+
Command command = new Command("clean", LocalizableStrings.CommandDescription);
24+
25+
command.AddOption(CleanAllOption);
26+
27+
command.SetHandler((parseResult) => new WorkloadCleanCommand(parseResult).Execute());
28+
29+
return command;
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)