Skip to content
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
309aea6
add telemetry - initial phase
nagilson Jan 30, 2026
b1db5c0
progress reporters also report telemetry for larger tasks
nagilson Jan 30, 2026
6a1e7c4
--info has telemetry and use custom App I for now
nagilson Jan 30, 2026
b62dd81
failures should be properly recorded
nagilson Jan 30, 2026
91e7ad1
catch more detail than just 'exception' for failure
nagilson Jan 30, 2026
4335250
share accepted channel values
nagilson Jan 30, 2026
19942d9
specific error for invalid versions
nagilson Jan 30, 2026
dc384ec
add version sanitization tests
nagilson Jan 30, 2026
b95c06f
use slnf so tests are found by test explorer in code
nagilson Jan 30, 2026
fb9e749
include more specific error details + dev tag for telem
nagilson Jan 30, 2026
6689644
first run notice + library hook guidance + tests
nagilson Jan 30, 2026
dc49cd3
base implementation - user error/uncontrolled failure vs our product…
nagilson Jan 30, 2026
28d30a1
consider more failures product failures
nagilson Jan 30, 2026
ff0c333
initial telemetry dashboard
nagilson Jan 31, 2026
1d88b21
throw some more specific error types
nagilson Jan 31, 2026
d47f984
collect further insights that will drive decisions
nagilson Jan 31, 2026
1105401
error categories should be presentt
nagilson Jan 31, 2026
c92b39c
filter metric should be correct
nagilson Jan 31, 2026
6d0ec37
don't track data we don't need or want
nagilson Jan 31, 2026
9df5a81
Include line number for error
nagilson Feb 2, 2026
1977c58
Consolidate logic which generates paths for local dotnetup storage.
nagilson Feb 2, 2026
0941794
Align with CLI existing code for telemetry
nagilson Feb 2, 2026
a237d49
Add telemetry notice document
nagilson Feb 2, 2026
60ac35e
PR Feedback round 1
nagilson Feb 2, 2026
765cc87
Demo project for libraries to attach to dotnetup
nagilson Feb 2, 2026
7d825d6
url sanitization
nagilson Feb 2, 2026
3acca2f
Don't show entire stack trace + better version err
nagilson Feb 2, 2026
378bb8e
Don't fail with lock error
nagilson Feb 2, 2026
359dbfe
Merge remote-tracking branch 'upstream/release/dnup' into nagilson-do…
nagilson Feb 3, 2026
80da391
--format json for info but still have custom output option
nagilson Feb 3, 2026
3a32472
merge and add telem for runtime cmd
nagilson Feb 9, 2026
36fb6a9
Dashboard now includes runtime metric
nagilson Feb 9, 2026
f7cb435
Some failing tests due to concurrency
nagilson Feb 9, 2026
191dbc1
Consider that CI machines may set NOLOGO
nagilson Feb 9, 2026
9297c6b
Try to avoid breaking fullframework build
nagilson Feb 9, 2026
ced1c2d
Allow custom time frame optoin on workbook
nagilson Feb 9, 2026
4f3f1b6
be aware that install path source may be global.json informed
nagilson Feb 10, 2026
e7f575e
Record sha for dev builds
nagilson Feb 10, 2026
a22bd28
Track and block attempts to install to admin location
nagilson Feb 10, 2026
7eca6f4
Consider fetching more network failure info
nagilson Feb 10, 2026
4b88adf
fix merge conflicts, improve path resolver, muxer telemetry moved
nagilson Feb 19, 2026
ba2c7ea
work for other powershell installs besides pwsh
nagilson Feb 19, 2026
041ed4a
pr feedback
nagilson Feb 19, 2026
83e4775
PR Feedback - Separate Error Mapping
nagilson Feb 19, 2026
a3bf658
don't assume sdk install anymore, reduce dead code / duplicate impls
nagilson Feb 19, 2026
6f679d0
fix ambiguity
nagilson Feb 19, 2026
0de4847
expect proper mapping to errors in win test
nagilson Feb 19, 2026
1d408c9
nologo in test to prevent first run output
nagilson Feb 20, 2026
04b4dbf
Merge remote-tracking branch 'origin/release/dnup' into nagilson-dotn…
nagilson Feb 20, 2026
f9078bf
pr feedback round 1 - simpler changes
nagilson Feb 21, 2026
964f167
hard code tags, consolidate mapping
nagilson Feb 21, 2026
9018884
simplify stack trace collection
nagilson Feb 21, 2026
a857a84
PR Feedback - Reduce exception parsing, fix workbook, error telemetry
nagilson Feb 23, 2026
760c689
restore-toolset merge fix
nagilson Feb 23, 2026
705bea0
convert to else if chain for clearer code
nagilson Feb 23, 2026
1f3d599
Instruct on how to run dotnetup for telemetry testing
nagilson Feb 23, 2026
a2dd601
workbook improvements
nagilson Feb 23, 2026
2d3273b
catch unauthorized exceptions and handle them differently as product …
nagilson Feb 23, 2026
0a545aa
improve telemetry notice
nagilson Feb 23, 2026
9e280f5
Simplify prerelease version chk
nagilson Feb 23, 2026
290ffb6
PR Feedback - clean up unused, shared code
nagilson Feb 23, 2026
bc12fe4
fix merge
nagilson Feb 23, 2026
dba6bff
bug fix for preview version with extra '.'
nagilson Feb 23, 2026
db052de
Prerelease version validation fix
nagilson Feb 23, 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
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ dotnetup:
- `dotnet build d:\sdk\src\Installer\dotnetup\dotnetup.csproj`
- `dotnet test d:\sdk\test\dotnetup.Tests\dotnetup.Tests.csproj`
- Do not run `dotnet build` from within the dotnetup directory as restore may fail.
- When running dotnetup directly (e.g. `dotnet run`), use the repo-local dogfood dotnet instance:
- `d:\sdk\.dotnet\dotnet run --project d:\sdk\src\Installer\dotnetup\dotnetup.csproj -- <args>`

Output Considerations:
- When considering how output should look, solicit advice from baronfel.
Expand Down
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@
<PackageVersion Include="Spectre.Console" Version="0.53.0" />
<PackageVersion Include="System.CodeDom" Version="$(SystemCodeDomPackageVersion)" />
<PackageVersion Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />

<!-- OpenTelemetry packages for dotnetup telemetry -->
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.3.0" />
<PackageVersion Include="OpenTelemetry" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageVersion Include="System.CommandLine.NamingConventionBinder" Version="$(SystemCommandLineNamingConventionBinderVersion)" />
<PackageVersion Include="System.ComponentModel.Composition" Version="$(SystemComponentModelCompositionPackageVersion)" />
<PackageVersion Include="System.Composition.AttributedModel" Version="$(SystemCompositionAttributedModelPackageVersion)" />
Expand Down
110 changes: 110 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/DotnetInstallException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Dotnet.Installation;

/// <summary>
/// Error codes for .NET installation failures.
/// </summary>
public enum DotnetInstallErrorCode
{
/// <summary>Unknown error.</summary>
Unknown,

/// <summary>The requested version was not found in the releases index.</summary>
VersionNotFound,

/// <summary>The requested release was not found.</summary>
ReleaseNotFound,

/// <summary>No matching file was found for the platform/architecture.</summary>
NoMatchingReleaseFileForPlatform,

/// <summary>Failed to download the archive.</summary>
DownloadFailed,

/// <summary>Archive hash verification failed.</summary>
HashMismatch,

/// <summary>Failed to extract the archive.</summary>
ExtractionFailed,

/// <summary>The channel or version format is invalid.</summary>
InvalidChannel,

/// <summary>Network connectivity issue.</summary>
NetworkError,

/// <summary>Insufficient permissions.</summary>
PermissionDenied,

/// <summary>Disk space issue.</summary>
DiskFull,

/// <summary>Failed to fetch the releases manifest from Microsoft servers.</summary>
ManifestFetchFailed,

/// <summary>Failed to parse the releases manifest (invalid JSON or schema).</summary>
ManifestParseFailed,

/// <summary>The archive file is corrupted or truncated.</summary>
ArchiveCorrupted,

/// <summary>Another installation process is already running.</summary>
InstallationLocked,

/// <summary>Failed to read/write the dotnetup installation manifest.</summary>
LocalManifestError,

/// <summary>The dotnetup installation manifest is corrupted.</summary>
LocalManifestCorrupted,
}

/// <summary>
/// Exception thrown when a .NET installation operation fails.
/// </summary>
public class DotnetInstallException : Exception
{
/// <summary>
/// Gets the error code for this exception.
/// </summary>
public DotnetInstallErrorCode ErrorCode { get; }

/// <summary>
/// Gets the version that was being installed, if applicable.
/// </summary>
public string? Version { get; }

/// <summary>
/// Gets the component being installed (SDK, Runtime, etc.).
/// </summary>
public string? Component { get; }

public DotnetInstallException(DotnetInstallErrorCode errorCode, string message)
: base(message)
{
ErrorCode = errorCode;
}

public DotnetInstallException(DotnetInstallErrorCode errorCode, string message, Exception innerException)
: base(message, innerException)
{
ErrorCode = errorCode;
}

public DotnetInstallException(DotnetInstallErrorCode errorCode, string message, string? version = null, string? component = null)
: base(message)
{
ErrorCode = errorCode;
Version = version;
Component = component;
}

public DotnetInstallException(DotnetInstallErrorCode errorCode, string message, Exception innerException, string? version = null, string? component = null)
: base(message, innerException)
{
ErrorCode = errorCode;
Version = version;
Component = component;
}
}
48 changes: 34 additions & 14 deletions src/Installer/Microsoft.Dotnet.Installation/IProgressTarget.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.Dotnet.Installation;

public interface IProgressTarget
{
public IProgressReporter CreateProgressReporter();
IProgressReporter CreateProgressReporter();
}

public interface IProgressReporter : IDisposable
{
public IProgressTask AddTask(string description, double maxValue);
IProgressTask AddTask(string description, double maxValue);

/// <summary>
/// Adds a task with telemetry activity tracking.
/// </summary>
/// <param name="activityName">The name for the telemetry activity (e.g., "download", "extract").</param>
/// <param name="description">The user-visible description.</param>
/// <param name="maxValue">The maximum progress value.</param>
IProgressTask AddTask(string activityName, string description, double maxValue)
=> AddTask(description, maxValue); // Default: no telemetry
}

public interface IProgressTask
Expand All @@ -23,23 +28,38 @@ public interface IProgressTask
double Value { get; set; }
double MaxValue { get; set; }

/// <summary>
/// Sets a telemetry tag on the underlying activity (if any).
/// </summary>
void SetTag(string key, object? value) { }

/// <summary>
/// Records an error on the underlying activity (if any).
/// </summary>
void RecordError(Exception ex) { }

/// <summary>
/// Marks the task as successfully completed.
/// </summary>
void Complete() { }
}

public class NullProgressTarget : IProgressTarget
{
public IProgressReporter CreateProgressReporter() => new NullProgressReporter();
class NullProgressReporter : IProgressReporter

private sealed class NullProgressReporter : IProgressReporter
{
public void Dispose()
{
}
public void Dispose() { }

public IProgressTask AddTask(string description, double maxValue)
{
return new NullProgressTask(description);
}
=> new NullProgressTask(description);

public IProgressTask AddTask(string activityName, string description, double maxValue)
=> new NullProgressTask(description);
}
class NullProgressTask : IProgressTask

private sealed class NullProgressTask : IProgressTask
{
public NullProgressTask(string description)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ namespace Microsoft.Dotnet.Installation.Internal;

internal class ChannelVersionResolver
{
/// <summary>
/// Channel keyword for the latest stable release.
/// </summary>
public const string LatestChannel = "latest";

/// <summary>
/// Channel keyword for the latest preview release.
/// </summary>
public const string PreviewChannel = "preview";

/// <summary>
/// Channel keyword for the latest Long Term Support (LTS) release.
/// </summary>
public const string LtsChannel = "lts";

/// <summary>
/// Channel keyword for the latest Standard Term Support (STS) release.
/// </summary>
public const string StsChannel = "sts";

/// <summary>
/// Known channel keywords that are always valid.
/// </summary>
public static readonly IReadOnlyList<string> KnownChannelKeywords = [LatestChannel, PreviewChannel, LtsChannel, StsChannel];

/// <summary>
/// Maximum reasonable major version number. .NET versions are currently single-digit;
/// anything above 99 is clearly invalid input (e.g., typos, random numbers).
/// </summary>
internal const int MaxReasonableMajorVersion = 99;

private ReleaseManifest _releaseManifest = new();

public ChannelVersionResolver()
Expand All @@ -25,7 +56,7 @@ public ChannelVersionResolver(ReleaseManifest releaseManifest)
public IEnumerable<string> GetSupportedChannels(bool includeFeatureBands = true)
{
var productIndex = _releaseManifest.GetReleasesIndex();
return ["latest", "preview", "lts", "sts",
return [..KnownChannelKeywords,
..productIndex
.Where(p => p.IsSupported)
.OrderByDescending(p => p.LatestReleaseVersion)
Expand Down Expand Up @@ -57,6 +88,78 @@ static IEnumerable<string> GetChannelsForProduct(Product product, bool includeFe
return GetLatestVersionForChannel(installRequest.Channel, installRequest.Component);
}

/// <summary>
/// Checks if a channel string looks like a valid .NET version/channel format.
/// This is a preliminary validation before attempting resolution.
/// </summary>
/// <param name="channel">The channel string to validate</param>
/// <returns>True if the format appears valid, false if clearly invalid</returns>
public static bool IsValidChannelFormat(string channel)
{
if (string.IsNullOrWhiteSpace(channel))
{
return false;
}

// Known keywords are always valid
if (KnownChannelKeywords.Any(k => string.Equals(k, channel, StringComparison.OrdinalIgnoreCase)))
{
return true;
}

// Try to parse as a version-like string
var parts = channel.Split('.');
if (parts.Length == 0 || parts.Length > 4)
{
return false;
}

// First part must be a valid major version
if (!int.TryParse(parts[0], out var major) || major < 0 || major > MaxReasonableMajorVersion)
{
return false;
}

// If there are more parts, validate them
if (parts.Length >= 2)
{
if (!int.TryParse(parts[1], out var minor) || minor < 0)
{
return false;
}
}

if (parts.Length >= 3)
{
var patch = parts[2];
if (string.IsNullOrEmpty(patch))
{
return false;
}

// Allow either:
// - a fully specified numeric patch (e.g., "103"), or
// - a feature band pattern with a numeric prefix and "xx" suffix (e.g., "1xx", "101xx").
if (patch.EndsWith("xx", StringComparison.OrdinalIgnoreCase))
{
var prefix = patch.Substring(0, patch.Length - 2);
if (prefix.Length == 0 || !int.TryParse(prefix, out _))
{
return false;
}
}
else
{
if (!int.TryParse(patch, out var numericPatch) || numericPatch < 0)
{
return false;
}
}
}

return true;
}

/// <summary>
/// Parses a version channel string into its components.
/// </summary>
Expand Down Expand Up @@ -97,18 +200,18 @@ static IEnumerable<string> GetChannelsForProduct(Product product, bool includeFe
/// <returns>Latest fully specified version string, or null if not found</returns>
public ReleaseVersion? GetLatestVersionForChannel(UpdateChannel channel, InstallComponent component)
{
if (string.Equals(channel.Name, "lts", StringComparison.OrdinalIgnoreCase) || string.Equals(channel.Name, "sts", StringComparison.OrdinalIgnoreCase))
if (string.Equals(channel.Name, LtsChannel, StringComparison.OrdinalIgnoreCase) || string.Equals(channel.Name, StsChannel, StringComparison.OrdinalIgnoreCase))
{
var releaseType = string.Equals(channel.Name, "lts", StringComparison.OrdinalIgnoreCase) ? ReleaseType.LTS : ReleaseType.STS;
var releaseType = string.Equals(channel.Name, LtsChannel, StringComparison.OrdinalIgnoreCase) ? ReleaseType.LTS : ReleaseType.STS;
var productIndex = _releaseManifest.GetReleasesIndex();
return GetLatestVersionByReleaseType(productIndex, releaseType, component);
}
else if (string.Equals(channel.Name, "preview", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(channel.Name, PreviewChannel, StringComparison.OrdinalIgnoreCase))
{
var productIndex = _releaseManifest.GetReleasesIndex();
return GetLatestPreviewVersion(productIndex, component);
}
else if (string.Equals(channel.Name, "latest", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(channel.Name, LatestChannel, StringComparison.OrdinalIgnoreCase))
{
var productIndex = _releaseManifest.GetReleasesIndex();
return GetLatestActiveVersion(productIndex, component);
Expand Down
Loading
Loading