Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/Aspire.Cli/Aspire.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<Compile Include="$(SharedDir)CircularBuffer.cs" Link="Utils\CircularBuffer.cs" />
<Compile Include="$(SharedDir)StringComparers.cs" Link="StringComparers.cs" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting\Backchannel\BackchannelDataTypes.cs" Link="Backchannel\CliBackchannelDataTypes.cs" />
<Compile Include="$(SharedDir)PackageUpdateHelpers.cs" Link="Utils\PackageUpdateHelpers.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Semver;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;

namespace Aspire.Cli.Commands;

Expand Down
5 changes: 4 additions & 1 deletion src/Aspire.Cli/Commands/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.Diagnostics;
using Aspire.Cli.Configuration;
using Aspire.Cli.Utils;

Expand Down Expand Up @@ -29,7 +30,9 @@ protected BaseCommand(string name, string description, IFeatures features, ICliU
// but we'll only wait so long before we get details back about updates
// being available (it should already be in the cache for longer running
// commands and some commands will opt out entirely)
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var cts = !Debugger.IsAttached
? new CancellationTokenSource(TimeSpan.FromSeconds(10))
: new CancellationTokenSource();
await updateNotifier.NotifyIfUpdateAvailableAsync(currentDirectory, cancellationToken: cts.Token);
}
catch
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Aspire.Cli.Templating;
using Aspire.Cli.Utils;
using Spectre.Console;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;

namespace Aspire.Cli.Commands;

internal sealed class NewCommand : BaseCommand
Expand Down
41 changes: 4 additions & 37 deletions src/Aspire.Cli/DotNetCliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
using Aspire.Cli.Resources;
using Aspire.Cli.Telemetry;
using Aspire.Hosting;
using Aspire.Shared;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;

namespace Aspire.Cli;

Expand Down Expand Up @@ -716,52 +718,17 @@ public async Task<int> AddPackageAsync(FileInfo projectFilePath, string packageN
return (ExitCodeConstants.FailedToAddPackage, null);
}

var foundPackages = new List<NuGetPackage>();
try
{
using var document = JsonDocument.Parse(stdout);

var searchResultsArray = document.RootElement.GetProperty("searchResult");

foreach (var sourceResult in searchResultsArray.EnumerateArray())
{
var source = sourceResult.GetProperty("sourceName").GetString();
var sourcePackagesArray = sourceResult.GetProperty("packages");

foreach (var packageResult in sourcePackagesArray.EnumerateArray())
{
var id = packageResult.GetProperty("id").GetString();

// var version = prerelease switch {
// true => packageResult.GetProperty("version").GetString(),
// false => packageResult.GetProperty("latestVersion").GetString()
// };

var version = packageResult.GetProperty("latestVersion").GetString();

foundPackages.Add(new NuGetPackage
{
Id = id!,
Version = version!,
Source = source!
});
}
}
var foundPackages = PackageUpdateHelpers.ParsePackageSearchResults(stdout);
return (result, foundPackages.ToArray());
}
catch (JsonException ex)
{
logger.LogError($"Failed to read JSON returned by the package search. {ex.Message}");
return (ExitCodeConstants.FailedToAddPackage, null);
}

return (result, foundPackages.ToArray());
}
}
}

internal class NuGetPackage
{
public string Id { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
}
1 change: 1 addition & 0 deletions src/Aspire.Cli/NuGet/NuGetPackageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Aspire.Cli.Telemetry;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;

namespace Aspire.Cli.NuGet;

Expand Down
64 changes: 4 additions & 60 deletions src/Aspire.Cli/Utils/CliUpdateNotifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Aspire.Cli.Interaction;
using Aspire.Cli.NuGet;
using Aspire.Shared;
using Microsoft.Extensions.Logging;
using Semver;

Expand Down Expand Up @@ -31,7 +32,7 @@ public async Task NotifyIfUpdateAvailableAsync(DirectoryInfo workingDirectory, C
}

var availablePackages = await nuGetPackageCache.GetCliPackagesAsync(workingDirectory, prerelease: true, source: null, cancellationToken);
var newerVersion = GetNewerVersion(currentVersion, availablePackages);
var newerVersion = PackageUpdateHelpers.GetNewerVersion(currentVersion, availablePackages);

if (newerVersion is not null)
{
Expand All @@ -46,63 +47,6 @@ public async Task NotifyIfUpdateAvailableAsync(DirectoryInfo workingDirectory, C

protected virtual SemVersion? GetCurrentVersion()
{
try
{
var versionString = VersionHelper.GetDefaultTemplateVersion();
// Remove any build metadata (e.g., +sha.12345) for comparison
var cleanVersionString = versionString.Split('+')[0];
return SemVersion.Parse(cleanVersionString, SemVersionStyles.Strict);
}
catch
{
return null;
}
return PackageUpdateHelpers.GetCurrentPackageVersion();
}

private static SemVersion? GetNewerVersion(SemVersion currentVersion, IEnumerable<NuGetPackage> availablePackages)
{
SemVersion? newestStable = null;
SemVersion? newestPrerelease = null;

foreach (var package in availablePackages)
{
if (SemVersion.TryParse(package.Version, SemVersionStyles.Strict, out var version))
{
if (version.IsPrerelease)
{
newestPrerelease = newestPrerelease is null || SemVersion.PrecedenceComparer.Compare(version, newestPrerelease) > 0 ? version : newestPrerelease;
}
else
{
newestStable = newestStable is null || SemVersion.PrecedenceComparer.Compare(version, newestStable) > 0 ? version : newestStable;
}
}
}

// Apply notification rules
if (currentVersion.IsPrerelease)
{
// Rule 1: If using a prerelease version where the version is lower than the latest stable version, prompt to upgrade
if (newestStable is not null && SemVersion.PrecedenceComparer.Compare(currentVersion, newestStable) < 0)
{
return newestStable;
}

// Rule 2: If using a prerelease version and there is a newer prerelease version, prompt to upgrade
if (newestPrerelease is not null && SemVersion.PrecedenceComparer.Compare(currentVersion, newestPrerelease) < 0)
{
return newestPrerelease;
}
}
else
{
// Rule 3: If using a stable version and there is a newer stable version, prompt to upgrade
if (newestStable is not null && SemVersion.PrecedenceComparer.Compare(currentVersion, newestStable) < 0)
{
return newestStable;
}
}

return null;
}
}
}
10 changes: 2 additions & 8 deletions src/Aspire.Cli/Utils/VersionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Cli.Resources;
using Aspire.Shared;

namespace Aspire.Cli.Utils;

internal static class VersionHelper
{
public static string GetDefaultTemplateVersion()
{
// Write some code that gets the informational assembly version of the current assembly and returns it as a string.
var assembly = typeof(VersionHelper).Assembly;
var informationalVersion = assembly
.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
.OfType<System.Reflection.AssemblyInformationalVersionAttribute>()
.FirstOrDefault()?.InformationalVersion;

return informationalVersion ?? throw new InvalidOperationException(ErrorStrings.UnableToRetrieveAssemblyVersion);
return PackageUpdateHelpers.GetCurrentAssemblyVersion() ?? throw new InvalidOperationException(ErrorStrings.UnableToRetrieveAssemblyVersion);
}
}
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Aspire.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Compile Include="$(SharedDir)LaunchProfiles\*.cs" />
<Compile Include="$(SharedDir)PortAllocator.cs" Link="Publishing\PortAllocator.cs" />
<Compile Include="$(SharedDir)OverloadResolutionPriorityAttribute.cs" Link="Utils\OverloadResolutionPriorityAttribute.cs" />
<Compile Include="$(SharedDir)PackageUpdateHelpers.cs" Link="Utils\PackageUpdateHelpers.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Services.AddHostedService<DistributedApplicationLifecycle>();
_innerBuilder.Services.AddHostedService<DistributedApplicationRunner>();
_innerBuilder.Services.AddHostedService<VersionCheckService>();
_innerBuilder.Services.AddSingleton<IVersionFetcher, VersionFetcher>();
_innerBuilder.Services.AddSingleton<IPackageFetcher, PackageFetcher>();
_innerBuilder.Services.AddSingleton(options);
_innerBuilder.Services.AddSingleton<ResourceNotificationService>();
_innerBuilder.Services.AddSingleton<ResourceLoggerService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Semver;
using Aspire.Shared;

namespace Aspire.Hosting.VersionChecking;

internal interface IVersionFetcher
internal interface IPackageFetcher
{
Task<SemVersion?> TryFetchLatestVersionAsync(string appHostDirectory, CancellationToken cancellationToken);
Task<List<NuGetPackage>> TryFetchPackagesAsync(string appHostDirectory, CancellationToken cancellationToken);
}
6 changes: 0 additions & 6 deletions src/Aspire.Hosting/VersionChecking/Package.cs

This file was deleted.

69 changes: 69 additions & 0 deletions src/Aspire.Hosting/VersionChecking/PackageFetcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Aspire.Hosting.Dcp.Process;
using Aspire.Shared;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting.VersionChecking;

internal sealed class PackageFetcher : IPackageFetcher
{
public const string PackageId = "Aspire.Hosting.AppHost";

// Limit the number of packages fetched per search to avoid overwhelming the output. This should never happen unless there is a bug in the API.
// Package search returns the latest version per source and few packages will match "Aspire.Hosting.AppHost" search string.
private const int SearchPageSize = 1000;

private readonly ILogger<PackageFetcher> _logger;

public PackageFetcher(ILogger<PackageFetcher> logger)
{
_logger = logger;
}

public async Task<List<NuGetPackage>> TryFetchPackagesAsync(string appHostDirectory, CancellationToken cancellationToken)
{
var outputJson = new StringBuilder();
var spec = new ProcessSpec("dotnet")
{
Arguments = $"package search {PackageId} --format json --prerelease --take {SearchPageSize}",
ThrowOnNonZeroReturnCode = false,
InheritEnv = true,
OnOutputData = output =>
{
outputJson.Append(output);
_logger.LogDebug("dotnet (stdout): {Output}", output);
},
OnErrorData = error =>
{
_logger.LogDebug("dotnet (stderr): {Error}", error);
},
WorkingDirectory = appHostDirectory
};

_logger.LogDebug("Running dotnet CLI to check for latest version of {PackageId} with arguments: {ArgumentList}", PackageId, spec.Arguments);
var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);

await using (processDisposable)
{
var processResult = await pendingProcessResult
.WaitAsync(cancellationToken)
.ConfigureAwait(false);

if (processResult.ExitCode != 0)
{
_logger.LogDebug("The dotnet CLI call to check for latest version failed with exit code {ExitCode}.", processResult.ExitCode);
return [];
}
}

// Filter packages to only consider "Aspire.Hosting.AppHost".
// Although the CLI command 'dotnet package search Aspire.Hosting.AppHost --format json'
// should already limit results according to NuGet search syntax
// (https://learn.microsoft.com/en-us/nuget/consume-packages/finding-and-choosing-packages#search-syntax),
// we add this extra check for robustness in case the CLI output includes unexpected packages.
return PackageUpdateHelpers.ParsePackageSearchResults(outputJson.ToString(), PackageId);
}
}
Loading
Loading