Skip to content

Commit 9b24fc9

Browse files
authored
Add quality metrics for dotnetup (#52792)
2 parents 1265011 + db052de commit 9b24fc9

File tree

78 files changed

+5115
-282
lines changed

Some content is hidden

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

78 files changed

+5115
-282
lines changed

.github/copilot-instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ dotnetup:
3434
- `dotnet build d:\sdk\src\Installer\dotnetup\dotnetup.csproj`
3535
- `dotnet test d:\sdk\test\dotnetup.Tests\dotnetup.Tests.csproj`
3636
- Do not run `dotnet build` from within the dotnetup directory as restore may fail.
37+
- When running dotnetup directly (e.g. `dotnet run`), use the repo-local dogfood dotnet instance:
38+
- `d:\sdk\.dotnet\dotnet run --project d:\sdk\src\Installer\dotnetup\dotnetup.csproj -- <args>`
3739

3840
Output Considerations:
3941
- When considering how output should look, solicit advice from baronfel.

Directory.Packages.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@
118118
<PackageVersion Include="Spectre.Console" Version="0.53.0" />
119119
<PackageVersion Include="System.CodeDom" Version="$(SystemCodeDomPackageVersion)" />
120120
<PackageVersion Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
121+
122+
<!-- OpenTelemetry packages for dotnetup telemetry -->
123+
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.3.0" />
124+
<PackageVersion Include="OpenTelemetry" Version="1.9.0" />
125+
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
121126
<PackageVersion Include="System.CommandLine.NamingConventionBinder" Version="$(SystemCommandLineNamingConventionBinderVersion)" />
122127
<PackageVersion Include="System.ComponentModel.Composition" Version="$(SystemComponentModelCompositionPackageVersion)" />
123128
<PackageVersion Include="System.Composition.AttributedModel" Version="$(SystemCompositionAttributedModelPackageVersion)" />
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.Dotnet.Installation;
5+
6+
/// <summary>
7+
/// Error codes for .NET installation failures.
8+
/// </summary>
9+
public enum DotnetInstallErrorCode
10+
{
11+
/// <summary>Unknown error.</summary>
12+
Unknown,
13+
14+
/// <summary>The requested version was not found in the releases index.</summary>
15+
VersionNotFound,
16+
17+
/// <summary>The requested release was not found.</summary>
18+
ReleaseNotFound,
19+
20+
/// <summary>No matching file was found for the platform/architecture.</summary>
21+
NoMatchingReleaseFileForPlatform,
22+
23+
/// <summary>Failed to download the archive.</summary>
24+
DownloadFailed,
25+
26+
/// <summary>Archive hash verification failed.</summary>
27+
HashMismatch,
28+
29+
/// <summary>Failed to extract the archive.</summary>
30+
ExtractionFailed,
31+
32+
/// <summary>The channel or version format is invalid.</summary>
33+
InvalidChannel,
34+
35+
/// <summary>Network connectivity issue.</summary>
36+
NetworkError,
37+
38+
/// <summary>Insufficient permissions.</summary>
39+
PermissionDenied,
40+
41+
/// <summary>Disk space issue.</summary>
42+
DiskFull,
43+
44+
/// <summary>Failed to fetch the releases manifest from Microsoft servers.</summary>
45+
ManifestFetchFailed,
46+
47+
/// <summary>Failed to parse the releases manifest (invalid JSON or schema).</summary>
48+
ManifestParseFailed,
49+
50+
/// <summary>The archive file is corrupted or truncated.</summary>
51+
ArchiveCorrupted,
52+
53+
/// <summary>Another installation process is already running.</summary>
54+
InstallationLocked,
55+
56+
/// <summary>Failed to read/write the dotnetup installation manifest.</summary>
57+
LocalManifestError,
58+
59+
/// <summary>The dotnetup installation manifest is corrupted.</summary>
60+
LocalManifestCorrupted,
61+
}
62+
63+
/// <summary>
64+
/// Exception thrown when a .NET installation operation fails.
65+
/// </summary>
66+
public class DotnetInstallException : Exception
67+
{
68+
/// <summary>
69+
/// Gets the error code for this exception.
70+
/// </summary>
71+
public DotnetInstallErrorCode ErrorCode { get; }
72+
73+
/// <summary>
74+
/// Gets the version that was being installed, if applicable.
75+
/// </summary>
76+
public string? Version { get; }
77+
78+
/// <summary>
79+
/// Gets the component being installed (SDK, Runtime, etc.).
80+
/// </summary>
81+
public string? Component { get; }
82+
83+
public DotnetInstallException(DotnetInstallErrorCode errorCode, string message)
84+
: base(message)
85+
{
86+
ErrorCode = errorCode;
87+
}
88+
89+
public DotnetInstallException(DotnetInstallErrorCode errorCode, string message, Exception innerException)
90+
: base(message, innerException)
91+
{
92+
ErrorCode = errorCode;
93+
}
94+
95+
public DotnetInstallException(DotnetInstallErrorCode errorCode, string message, string? version = null, string? component = null)
96+
: base(message)
97+
{
98+
ErrorCode = errorCode;
99+
Version = version;
100+
Component = component;
101+
}
102+
103+
public DotnetInstallException(DotnetInstallErrorCode errorCode, string message, Exception innerException, string? version = null, string? component = null)
104+
: base(message, innerException)
105+
{
106+
ErrorCode = errorCode;
107+
Version = version;
108+
Component = component;
109+
}
110+
}

src/Installer/Microsoft.Dotnet.Installation/IProgressTarget.cs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,38 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Text;
7-
84
namespace Microsoft.Dotnet.Installation;
95

106
public interface IProgressTarget
117
{
12-
public IProgressReporter CreateProgressReporter();
8+
IProgressReporter CreateProgressReporter();
139
}
1410

1511
public interface IProgressReporter : IDisposable
1612
{
17-
public IProgressTask AddTask(string description, double maxValue);
13+
IProgressTask AddTask(string description, double maxValue);
1814
}
1915

2016
public interface IProgressTask
2117
{
2218
string Description { get; set; }
2319
double Value { get; set; }
2420
double MaxValue { get; set; }
25-
26-
2721
}
2822

2923
public class NullProgressTarget : IProgressTarget
3024
{
3125
public IProgressReporter CreateProgressReporter() => new NullProgressReporter();
32-
class NullProgressReporter : IProgressReporter
26+
27+
private sealed class NullProgressReporter : IProgressReporter
3328
{
34-
public void Dispose()
35-
{
36-
}
29+
public void Dispose() { }
30+
3731
public IProgressTask AddTask(string description, double maxValue)
38-
{
39-
return new NullProgressTask(description);
40-
}
32+
=> new NullProgressTask(description);
4133
}
42-
class NullProgressTask : IProgressTask
34+
35+
private sealed class NullProgressTask : IProgressTask
4336
{
4437
public NullProgressTask(string description)
4538
{

src/Installer/Microsoft.Dotnet.Installation/Internal/ChannelVersionResolver.cs

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,37 @@ namespace Microsoft.Dotnet.Installation.Internal;
1010

1111
internal class ChannelVersionResolver
1212
{
13+
/// <summary>
14+
/// Channel keyword for the latest stable release.
15+
/// </summary>
16+
public const string LatestChannel = "latest";
17+
18+
/// <summary>
19+
/// Channel keyword for the latest preview release.
20+
/// </summary>
21+
public const string PreviewChannel = "preview";
22+
23+
/// <summary>
24+
/// Channel keyword for the latest Long Term Support (LTS) release.
25+
/// </summary>
26+
public const string LtsChannel = "lts";
27+
28+
/// <summary>
29+
/// Channel keyword for the latest Standard Term Support (STS) release.
30+
/// </summary>
31+
public const string StsChannel = "sts";
32+
33+
/// <summary>
34+
/// Known channel keywords that are always valid.
35+
/// </summary>
36+
public static readonly IReadOnlyList<string> KnownChannelKeywords = [LatestChannel, PreviewChannel, LtsChannel, StsChannel];
37+
38+
/// <summary>
39+
/// Maximum reasonable major version number. .NET versions are currently single-digit;
40+
/// anything above 99 is clearly invalid input (e.g., typos, random numbers).
41+
/// </summary>
42+
internal const int MaxReasonableMajorVersion = 99;
43+
1344
private ReleaseManifest _releaseManifest = new();
1445

1546
public ChannelVersionResolver()
@@ -25,7 +56,7 @@ public ChannelVersionResolver(ReleaseManifest releaseManifest)
2556
public IEnumerable<string> GetSupportedChannels(bool includeFeatureBands = true)
2657
{
2758
var productIndex = _releaseManifest.GetReleasesIndex();
28-
return ["latest", "preview", "lts", "sts",
59+
return [..KnownChannelKeywords,
2960
..productIndex
3061
.Where(p => p.IsSupported)
3162
.OrderByDescending(p => p.LatestReleaseVersion)
@@ -57,6 +88,89 @@ static IEnumerable<string> GetChannelsForProduct(Product product, bool includeFe
5788
return GetLatestVersionForChannel(installRequest.Channel, installRequest.Component);
5889
}
5990

91+
/// <summary>
92+
/// Checks if a channel string looks like a valid .NET version/channel format.
93+
/// This is a preliminary validation before attempting resolution.
94+
/// </summary>
95+
/// <param name="channel">The channel string to validate</param>
96+
/// <returns>True if the format appears valid, false if clearly invalid</returns>
97+
public static bool IsValidChannelFormat(string channel)
98+
{
99+
if (string.IsNullOrWhiteSpace(channel))
100+
{
101+
return false;
102+
}
103+
104+
// Known keywords are always valid
105+
if (KnownChannelKeywords.Any(k => string.Equals(k, channel, StringComparison.OrdinalIgnoreCase)))
106+
{
107+
return true;
108+
}
109+
110+
// Check for prerelease suffix (e.g., "10.0.100-preview.1.32640")
111+
var dashIndex = channel.IndexOf('-');
112+
var hasPrerelease = dashIndex >= 0;
113+
var versionPart = hasPrerelease ? channel.Substring(0, dashIndex) : channel;
114+
115+
// Try to parse as a version-like string
116+
var parts = versionPart.Split('.');
117+
if (parts.Length == 0 || parts.Length > 4)
118+
{
119+
return false;
120+
}
121+
122+
// First part must be a valid major version
123+
if (!int.TryParse(parts[0], out var major) || major < 0 || major > MaxReasonableMajorVersion)
124+
{
125+
return false;
126+
}
127+
128+
// If there are more parts, validate them
129+
if (parts.Length >= 2)
130+
{
131+
if (!int.TryParse(parts[1], out var minor) || minor < 0)
132+
{
133+
return false;
134+
}
135+
}
136+
137+
if (parts.Length >= 3)
138+
{
139+
var patch = parts[2];
140+
if (string.IsNullOrEmpty(patch))
141+
{
142+
return false;
143+
}
144+
145+
// Allow either:
146+
// - a fully specified numeric patch (e.g., "103"), optionally with a prerelease suffix, or
147+
// - a feature band pattern with a numeric prefix and "xx" suffix (e.g., "1xx", "101xx"),
148+
// but NOT with a prerelease suffix (wildcards with prerelease not supported).
149+
if (patch.EndsWith("xx", StringComparison.OrdinalIgnoreCase))
150+
{
151+
if (hasPrerelease)
152+
{
153+
return false;
154+
}
155+
156+
var prefix = patch.Substring(0, patch.Length - 2);
157+
if (prefix.Length == 0 || !int.TryParse(prefix, out _))
158+
{
159+
return false;
160+
}
161+
}
162+
else
163+
{
164+
if (!int.TryParse(patch, out var numericPatch) || numericPatch < 0)
165+
{
166+
return false;
167+
}
168+
}
169+
}
170+
171+
return true;
172+
}
173+
60174
/// <summary>
61175
/// Parses a version channel string into its components.
62176
/// </summary>
@@ -97,18 +211,18 @@ static IEnumerable<string> GetChannelsForProduct(Product product, bool includeFe
97211
/// <returns>Latest fully specified version string, or null if not found</returns>
98212
public ReleaseVersion? GetLatestVersionForChannel(UpdateChannel channel, InstallComponent component)
99213
{
100-
if (string.Equals(channel.Name, "lts", StringComparison.OrdinalIgnoreCase) || string.Equals(channel.Name, "sts", StringComparison.OrdinalIgnoreCase))
214+
if (string.Equals(channel.Name, LtsChannel, StringComparison.OrdinalIgnoreCase) || string.Equals(channel.Name, StsChannel, StringComparison.OrdinalIgnoreCase))
101215
{
102-
var releaseType = string.Equals(channel.Name, "lts", StringComparison.OrdinalIgnoreCase) ? ReleaseType.LTS : ReleaseType.STS;
216+
var releaseType = string.Equals(channel.Name, LtsChannel, StringComparison.OrdinalIgnoreCase) ? ReleaseType.LTS : ReleaseType.STS;
103217
var productIndex = _releaseManifest.GetReleasesIndex();
104218
return GetLatestVersionByReleaseType(productIndex, releaseType, component);
105219
}
106-
else if (string.Equals(channel.Name, "preview", StringComparison.OrdinalIgnoreCase))
220+
else if (string.Equals(channel.Name, PreviewChannel, StringComparison.OrdinalIgnoreCase))
107221
{
108222
var productIndex = _releaseManifest.GetReleasesIndex();
109223
return GetLatestPreviewVersion(productIndex, component);
110224
}
111-
else if (string.Equals(channel.Name, "latest", StringComparison.OrdinalIgnoreCase))
225+
else if (string.Equals(channel.Name, LatestChannel, StringComparison.OrdinalIgnoreCase))
112226
{
113227
var productIndex = _releaseManifest.GetReleasesIndex();
114228
return GetLatestActiveVersion(productIndex, component);

0 commit comments

Comments
 (0)