diff --git a/.vsts-ci.yml b/.vsts-ci.yml
index 0177d78daa08..be64ed44ecc6 100644
--- a/.vsts-ci.yml
+++ b/.vsts-ci.yml
@@ -5,7 +5,7 @@ trigger:
branches:
include:
- main
- - release/9.0.1xx
+ - release/9.0.2xx
- internal/release/*
- exp/*
diff --git a/NuGet.config b/NuGet.config
index a6f37020b08f..7f7ab1a0ca1d 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -4,6 +4,8 @@
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 7ed68812bb6a..b34c13b0965a 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -660,14 +660,14 @@
aren't shipping, or those extensions packages depend on aspnetcore packages that won't ship. However, given the cost
of maintaining this coherency path is high. This being toolset means that aspire is responsible for its own coherency.
-->
-
+
https://github.com/dotnet/aspire
- b88ce9e7cb0430fb0b4e2d018f13694a4c733289
+ 137e8dcae0a7b22c05f48c4e7a5d36fe3f00a8d7
-
+
https://github.com/dotnet/aspire
- b88ce9e7cb0430fb0b4e2d018f13694a4c733289
+ 137e8dcae0a7b22c05f48c4e7a5d36fe3f00a8d7
diff --git a/eng/Versions.props b/eng/Versions.props
index 2dfbb229c183..cb1b7edfd783 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -362,7 +362,7 @@
8.0.100
- 9.0.0-preview.4.24456.4
+ 8.2.1
9.0.100-preview.6
9.0.0-preview.6.24327.7
34.99.0-preview.6.340
diff --git a/sdk.sln b/sdk.sln
index 0b5a7846a285..9e6140637b8b 100644
--- a/sdk.sln
+++ b/sdk.sln
@@ -510,6 +510,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.WebTools.AspireSe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Sdk.Compilers.Toolset", "src\Microsoft.Net.Sdk.Compilers.Toolset\Microsoft.Net.Sdk.Compilers.Toolset.csproj", "{FA579C03-2EB4-4D47-88EE-BFF339E96FAF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.WebTools.AspireService.Tests", "test\Microsoft.WebTools.AspireService.Tests\Microsoft.WebTools.AspireService.Tests.csproj", "{1F0B4B3C-DC88-4740-B04F-1707102E9930}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -968,6 +970,10 @@ Global
{FA579C03-2EB4-4D47-88EE-BFF339E96FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA579C03-2EB4-4D47-88EE-BFF339E96FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA579C03-2EB4-4D47-88EE-BFF339E96FAF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F0B4B3C-DC88-4740-B04F-1707102E9930}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1146,6 +1152,7 @@ Global
{19014C60-F87C-4CC7-AC0F-C41B6126EBCE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{94C8526E-DCC2-442F-9868-3DD0BA2688BE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{FA579C03-2EB4-4D47-88EE-BFF339E96FAF} = {22AB674F-ED91-4FBC-BFEE-8A1E82F9F05E}
+ {1F0B4B3C-DC88-4740-B04F-1707102E9930} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}
@@ -1153,6 +1160,7 @@ Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5
+ src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{1f0b4b3c-dc88-4740-b04f-1707102e9930}*SharedItemsImports = 5
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{a9103b98-d888-4260-8a05-fa36f640698a}*SharedItemsImports = 5
diff --git a/src/BuiltInTools/AspireService/AspireServerService.cs b/src/BuiltInTools/AspireService/AspireServerService.cs
index d777ce951154..3b620131f73f 100644
--- a/src/BuiltInTools/AspireService/AspireServerService.cs
+++ b/src/BuiltInTools/AspireService/AspireServerService.cs
@@ -1,18 +1,12 @@
// 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.Diagnostics;
using System.Net;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
-using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
diff --git a/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs b/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs
index 189a88c48ed5..a0b0f7766d48 100644
--- a/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs
+++ b/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs
@@ -1,7 +1,6 @@
// 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.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@@ -35,7 +34,11 @@ public static X509Certificate2 GenerateCert()
// The file will be automatically generated by the following call and disposed when the returned cert is disposed.
using (cert)
{
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), password: null, X509KeyStorageFlags.UserKeySet);
+#else
return new X509Certificate2(cert.Export(X509ContentType.Pfx), "", X509KeyStorageFlags.UserKeySet);
+#endif
}
}
else
diff --git a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
index 62182db3b0bd..50301fdbf15c 100644
--- a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
+++ b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
@@ -82,7 +82,7 @@ private UpdateHandlerActions GetMetadataUpdateHandlerActions()
var handlerActions = new UpdateHandlerActions();
foreach (var assembly in sortedAssemblies)
{
- foreach (var attr in assembly.GetCustomAttributesData())
+ foreach (var attr in TryGetCustomAttributesData(assembly))
{
// Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
// define their own copy without having to cross-compile.
@@ -106,6 +106,25 @@ private UpdateHandlerActions GetMetadataUpdateHandlerActions()
return handlerActions;
}
+ private IList TryGetCustomAttributesData(Assembly assembly)
+ {
+ try
+ {
+ return assembly.GetCustomAttributesData();
+ }
+ catch (Exception e)
+ {
+ // In cross-platform scenarios, such as debugging in VS through WSL, Roslyn
+ // runs on Windows, and the agent runs on Linux. Assemblies accessible to Windows
+ // may not be available or loaded on linux (such as WPF's assemblies).
+ // In such case, we can ignore the assemblies and continue enumerating handlers for
+ // the rest of the assemblies of current domain.
+ _log($"'{assembly.FullName}' is not loaded ({e.Message})");
+
+ return new List();
+ }
+ }
+
internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handlerType)
{
bool methodFound = false;
diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
index 0941b5b89621..1968f7d21aa3 100644
--- a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
+++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
@@ -26,9 +26,11 @@ public static void Initialize()
// When launching the application process dotnet-watch sets Hot Reload environment variables via CLI environment directives (dotnet [env:X=Y] run).
// Currently, the CLI parser sets the env variables to the dotnet.exe process itself, rather then to the target process.
// This may cause the dotnet.exe process to connect to the named pipe and break it for the target process.
- if (Path.ChangeExtension(processPath, ".exe") != Path.ChangeExtension(s_targetProcessPath, ".exe"))
+ var processExe = Path.ChangeExtension(processPath, ".exe");
+ var expectedExe = Path.ChangeExtension(s_targetProcessPath, ".exe");
+ if (!string.Equals(processExe, expectedExe, Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
- Log($"Ignoring process '{processPath}', expecting '{s_targetProcessPath}'");
+ Log($"Ignoring process '{processExe}', expecting '{expectedExe}'");
return;
}
diff --git a/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
index 785219c3879e..d2f57ce7eb34 100644
--- a/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
@@ -27,7 +27,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken)
var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment();
- FileItem? changedFile = null;
+ ChangedFile? changedFile = null;
var buildEvaluator = new BuildEvaluator(Context, RootFileSetFactory);
await using var browserConnector = new BrowserConnector(Context);
@@ -86,7 +86,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken)
var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, processExitedSource: null, combinedCancellationSource.Token);
- Task fileSetTask;
+ Task fileSetTask;
Task finishedTask;
while (true)
@@ -94,9 +94,9 @@ public override async Task WatchAsync(CancellationToken cancellationToken)
fileSetTask = fileSetWatcher.GetChangedFileAsync(startedWatching: null, combinedCancellationSource.Token);
finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);
- if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result is FileItem fileItem)
+ if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result.HasValue)
{
- if (await staticFileHandler.HandleFileChangesAsync([fileItem], combinedCancellationSource.Token))
+ if (await staticFileHandler.HandleFileChangesAsync([fileSetTask.Result.Value], combinedCancellationSource.Token))
{
// We're able to handle the file change event without doing a full-rebuild.
continue;
@@ -131,7 +131,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken)
Debug.Assert(finishedTask == fileSetTask);
changedFile = fileSetTask.Result;
Debug.Assert(changedFile != null, "ChangedFile should only be null when cancelled");
- Context.Reporter.Output($"File changed: {changedFile.Value.FilePath}");
+ Context.Reporter.Output($"File changed: {changedFile.Value.Item.FilePath}");
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/FileItem.cs b/src/BuiltInTools/dotnet-watch/FileItem.cs
index 71779898168a..7fc91cd6fa1c 100644
--- a/src/BuiltInTools/dotnet-watch/FileItem.cs
+++ b/src/BuiltInTools/dotnet-watch/FileItem.cs
@@ -1,9 +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 Microsoft.DotNet.Watcher.Internal;
+
namespace Microsoft.DotNet.Watcher
{
- internal readonly struct FileItem
+ internal readonly record struct FileItem
{
public string FilePath { get; init; }
@@ -14,7 +16,7 @@ internal readonly struct FileItem
public string? StaticWebAssetPath { get; init; }
- public bool IsNewFile { get; init; }
+ public ChangeKind Change { get; init; }
public bool IsStaticFile => StaticWebAssetPath != null;
}
diff --git a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
index ad7599378ee2..0025fbbbcbf5 100644
--- a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
+++ b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
@@ -46,7 +46,7 @@ public IReadOnlyList GetProcessArguments(int iteration)
return [context.RootProjectOptions.Command, .. context.RootProjectOptions.CommandArguments];
}
- public async ValueTask EvaluateAsync(FileItem? changedFile, CancellationToken cancellationToken)
+ public async ValueTask EvaluateAsync(ChangedFile? changedFile, CancellationToken cancellationToken)
{
if (context.EnvironmentOptions.SuppressMSBuildIncrementalism)
{
@@ -54,7 +54,7 @@ public async ValueTask EvaluateAsync(FileItem? changedFile, Ca
return _evaluationResult = await CreateEvaluationResult(cancellationToken);
}
- if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile))
+ if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile?.Item))
{
RequiresRevaluation = true;
}
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs
index 17e4c6d27cd8..09871a21655c 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs
@@ -1,20 +1,15 @@
// 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.Collections.Immutable;
using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MSBuild;
-using Microsoft.Extensions.Tools.Internal;
-using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Text;
+using Microsoft.DotNet.Watcher.Internal;
+using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools;
@@ -106,13 +101,24 @@ ImmutableArray MapDocuments(ProjectId mappedProjectId, IReadOnlyLi
}).ToImmutableArray();
}
- public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken)
+ public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken)
{
var updatedSolution = CurrentSolution;
- foreach (var changedFile in changedFiles)
+ var documentsToRemove = new List();
+
+ foreach (var (changedFile, change) in changedFiles)
{
+ // when a file is added we reevaluate the project:
+ Debug.Assert(change != ChangeKind.Add);
+
var documentIds = updatedSolution.GetDocumentIdsWithFilePath(changedFile.FilePath);
+ if (change == ChangeKind.Delete)
+ {
+ documentsToRemove.AddRange(documentIds);
+ continue;
+ }
+
foreach (var documentId in documentIds)
{
var textDocument = updatedSolution.GetDocument(documentId)
@@ -140,9 +146,17 @@ public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles
}
}
+ updatedSolution = RemoveDocuments(updatedSolution, documentsToRemove);
+
await ReportSolutionFilesAsync(SetCurrentSolution(updatedSolution), cancellationToken);
}
+ private static Solution RemoveDocuments(Solution solution, IEnumerable ids)
+ => solution
+ .RemoveDocuments(ids.Where(id => solution.GetDocument(id) != null).ToImmutableArray())
+ .RemoveAdditionalDocuments(ids.Where(id => solution.GetAdditionalDocument(id) != null).ToImmutableArray())
+ .RemoveAnalyzerConfigDocuments(ids.Where(id => solution.GetAnalyzerConfigDocument(id) != null).ToImmutableArray());
+
private static async ValueTask GetSourceTextAsync(string filePath, CancellationToken cancellationToken)
{
var zeroLengthRetryPerformed = false;
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
index dfe625075227..3a6961854987 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
@@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics;
using Microsoft.Build.Graph;
+using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools
@@ -13,14 +14,14 @@ internal sealed class ScopedCssFileHandler(IReporter reporter, ProjectNodeMap pr
{
private const string BuildTargetName = "GenerateComputedBuildStaticWebAssets";
- public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken)
+ public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken)
{
var projectsToRefresh = new HashSet();
var hasApplicableFiles = false;
for (int i = 0; i < files.Count; i++)
{
- var file = files[i];
+ var file = files[i].Item;
if (!file.FilePath.EndsWith(".razor.css", StringComparison.Ordinal) &&
!file.FilePath.EndsWith(".cshtml.css", StringComparison.Ordinal))
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
index f8d9b8bffd12..ce53bc8a7ce3 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
@@ -7,6 +7,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.StackTraceExplorer;
+using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools
@@ -18,13 +19,13 @@ internal sealed class StaticFileHandler(IReporter reporter, ProjectNodeMap proje
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
- public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken)
+ public async ValueTask HandleFileChangesAsync(IReadOnlyList files, CancellationToken cancellationToken)
{
var allFilesHandled = true;
var refreshRequests = new Dictionary>();
for (int i = 0; i < files.Count; i++)
{
- var file = files[i];
+ var file = files[i].Item;
if (file.StaticWebAssetPath is null)
{
diff --git a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
index ee09b3c257df..24b543ebd9b3 100644
--- a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
@@ -74,7 +74,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
HotReloadFileSetWatcher? fileSetWatcher = null;
EvaluationResult? evaluationResult = null;
RunningProject? rootRunningProject = null;
- Task? fileSetWatcherTask = null;
+ Task? fileSetWatcherTask = null;
try
{
@@ -178,7 +178,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
// When a new file is added we need to run design-time build to find out
// what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.).
// We don't need to rebuild and restart the application though.
- if (changedFiles.Any(f => f.IsNewFile))
+ if (changedFiles.Any(f => f.Change is ChangeKind.Add))
{
Context.Reporter.Verbose("File addition triggered re-evaluation.");
@@ -195,9 +195,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
// update files in the change set with new evaluation info:
for (int i = 0; i < changedFiles.Length; i++)
{
- if (evaluationResult.Files.TryGetValue(changedFiles[i].FilePath, out var evaluatedFile))
+ if (evaluationResult.Files.TryGetValue(changedFiles[i].Item.FilePath, out var evaluatedFile))
{
- changedFiles[i] = evaluatedFile;
+ changedFiles[i] = changedFiles[i] with { Item = evaluatedFile };
}
}
@@ -336,24 +336,43 @@ await Task.WhenAll(
}
}
- private void ReportFileChanges(IReadOnlyList fileItems)
+ private void ReportFileChanges(IReadOnlyList changedFiles)
{
- Report(added: true);
- Report(added: false);
+ Report(kind: ChangeKind.Add);
+ Report(kind: ChangeKind.Update);
+ Report(kind: ChangeKind.Delete);
- void Report(bool added)
+ void Report(ChangeKind kind)
{
- var items = fileItems.Where(item => item.IsNewFile == added).ToArray();
+ var items = changedFiles.Where(item => item.Change == kind).ToArray();
if (items is not [])
{
- Context.Reporter.Output(GetMessage(items, added));
+ Context.Reporter.Output(GetMessage(items, kind));
}
}
- string GetMessage(IReadOnlyList items, bool added)
- => items is [var item]
- ? (added ? "File added: " : "File changed: ") + GetRelativeFilePath(item.FilePath)
- : (added ? "Files added: " : "Files changed: ") + string.Join(", ", items.Select(f => GetRelativeFilePath(f.FilePath)));
+ string GetMessage(IReadOnlyList items, ChangeKind kind)
+ => items is [{Item: var item }]
+ ? GetSingularMessage(kind) + ": " + GetRelativeFilePath(item.FilePath)
+ : GetPluralMessage(kind) + ": " + string.Join(", ", items.Select(f => GetRelativeFilePath(f.Item.FilePath)));
+
+ static string GetSingularMessage(ChangeKind kind)
+ => kind switch
+ {
+ ChangeKind.Update => "File updated",
+ ChangeKind.Add => "File added",
+ ChangeKind.Delete => "File deleted",
+ _ => throw new InvalidOperationException()
+ };
+
+ static string GetPluralMessage(ChangeKind kind)
+ => kind switch
+ {
+ ChangeKind.Update => "Files updated",
+ ChangeKind.Add => "Files added",
+ ChangeKind.Delete => "Files deleted",
+ _ => throw new InvalidOperationException()
+ };
}
private async ValueTask EvaluateRootProjectAsync(CancellationToken cancellationToken)
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
index b844f8da4fa8..3a339bf63fb1 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
@@ -10,7 +10,7 @@ internal sealed class FileWatcher(IReadOnlyDictionary fileSet,
private readonly Dictionary _watchers = [];
private bool _disposed;
- public event Action? OnFileChange;
+ public event Action? OnFileChange;
public void Dispose()
{
@@ -73,9 +73,9 @@ private void WatcherErrorHandler(object? sender, Exception error)
}
}
- private void WatcherChangedHandler(object? sender, (string changedPath, bool newFile) args)
+ private void WatcherChangedHandler(object? sender, (string changedPath, ChangeKind kind) args)
{
- OnFileChange?.Invoke(args.changedPath, args.newFile);
+ OnFileChange?.Invoke(args.changedPath, args.kind);
}
private void DisposeWatcher(string directory)
@@ -101,22 +101,22 @@ private void EnsureNotDisposed()
private static string EnsureTrailingSlash(string path)
=> (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path;
- public async Task GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken)
+ public async Task GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken)
{
StartWatching();
- var fileChangedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var fileChangedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));
- void FileChangedCallback(string path, bool newFile)
+ void FileChangedCallback(string path, ChangeKind kind)
{
if (fileSet.TryGetValue(path, out var fileItem))
{
- fileChangedSource.TrySetResult(fileItem);
+ fileChangedSource.TrySetResult(new ChangedFile(fileItem, kind));
}
}
- FileItem? changedFile;
+ ChangedFile? changedFile;
OnFileChange += FileChangedCallback;
try
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs
new file mode 100644
index 000000000000..5fef3b698624
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs
@@ -0,0 +1,13 @@
+// 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.Watcher.Internal;
+
+internal enum ChangeKind
+{
+ Update,
+ Add,
+ Delete
+}
+
+internal readonly record struct ChangedFile(FileItem Item, ChangeKind Change);
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs
index ca622206e933..7040fe1a0763 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs
@@ -33,7 +33,7 @@ internal DotnetFileWatcher(string watchedDirectory, Func? OnFileChange;
+ public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
public event EventHandler? OnError;
@@ -78,10 +78,10 @@ private void WatcherRenameHandler(object sender, RenamedEventArgs e)
return;
}
- Logger?.Invoke("Rename");
+ Logger?.Invoke($"Renamed '{e.OldFullPath}' to '{e.FullPath}'.");
- NotifyChange(e.OldFullPath, newFile: false);
- NotifyChange(e.FullPath, newFile: true);
+ NotifyChange(e.OldFullPath, ChangeKind.Delete);
+ NotifyChange(e.FullPath, ChangeKind.Add);
if (Directory.Exists(e.FullPath))
{
@@ -89,12 +89,23 @@ private void WatcherRenameHandler(object sender, RenamedEventArgs e)
{
// Calculated previous path of this moved item.
var oldLocation = Path.Combine(e.OldFullPath, newLocation.Substring(e.FullPath.Length + 1));
- NotifyChange(oldLocation, newFile: false);
- NotifyChange(newLocation, newFile: true);
+ NotifyChange(oldLocation, ChangeKind.Delete);
+ NotifyChange(newLocation, ChangeKind.Add);
}
}
}
+ private void WatcherDeletedHandler(object sender, FileSystemEventArgs e)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ Logger?.Invoke($"Deleted '{e.FullPath}'.");
+ NotifyChange(e.FullPath, ChangeKind.Delete);
+ }
+
private void WatcherChangeHandler(object sender, FileSystemEventArgs e)
{
if (_disposed)
@@ -102,8 +113,8 @@ private void WatcherChangeHandler(object sender, FileSystemEventArgs e)
return;
}
- Logger?.Invoke("Change");
- NotifyChange(e.FullPath, newFile: false);
+ Logger?.Invoke($"Updated '{e.FullPath}'.");
+ NotifyChange(e.FullPath, ChangeKind.Update);
}
private void WatcherAddedHandler(object sender, FileSystemEventArgs e)
@@ -113,14 +124,14 @@ private void WatcherAddedHandler(object sender, FileSystemEventArgs e)
return;
}
- Logger?.Invoke("Added");
- NotifyChange(e.FullPath, newFile: true);
+ Logger?.Invoke($"Added '{e.FullPath}'.");
+ NotifyChange(e.FullPath, ChangeKind.Add);
}
- private void NotifyChange(string fullPath, bool newFile)
+ private void NotifyChange(string fullPath, ChangeKind kind)
{
// Only report file changes
- OnFileChange?.Invoke(this, (fullPath, newFile));
+ OnFileChange?.Invoke(this, (fullPath, kind));
}
private void CreateFileSystemWatcher()
@@ -140,7 +151,7 @@ private void CreateFileSystemWatcher()
_fileSystemWatcher.IncludeSubdirectories = true;
_fileSystemWatcher.Created += WatcherAddedHandler;
- _fileSystemWatcher.Deleted += WatcherChangeHandler;
+ _fileSystemWatcher.Deleted += WatcherDeletedHandler;
_fileSystemWatcher.Changed += WatcherChangeHandler;
_fileSystemWatcher.Renamed += WatcherRenameHandler;
_fileSystemWatcher.Error += WatcherErrorHandler;
@@ -156,7 +167,7 @@ private void DisposeInnerWatcher()
_fileSystemWatcher.EnableRaisingEvents = false;
_fileSystemWatcher.Created -= WatcherAddedHandler;
- _fileSystemWatcher.Deleted -= WatcherChangeHandler;
+ _fileSystemWatcher.Deleted -= WatcherDeletedHandler;
_fileSystemWatcher.Changed -= WatcherChangeHandler;
_fileSystemWatcher.Renamed -= WatcherRenameHandler;
_fileSystemWatcher.Error -= WatcherErrorHandler;
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs
index f75c7e3e1481..ebdef49913ff 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs
@@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Watcher.Internal
{
internal interface IFileSystemWatcher : IDisposable
{
- event EventHandler<(string filePath, bool newFile)> OnFileChange;
+ event EventHandler<(string filePath, ChangeKind kind)> OnFileChange;
event EventHandler OnError;
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs
index da7630b71eb8..2df2004ee84b 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs
@@ -15,7 +15,7 @@ internal class PollingFileWatcher : IFileSystemWatcher
private Dictionary _knownEntities = new();
private Dictionary _tempDictionary = new();
- private Dictionary _changes = new();
+ private Dictionary _changes = new();
private Thread _pollingThread;
private bool _raiseEvents;
@@ -40,7 +40,7 @@ public PollingFileWatcher(string watchedDirectory)
_pollingThread.Start();
}
- public event EventHandler<(string filePath, bool newFile)>? OnFileChange;
+ public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
#pragma warning disable CS0067 // not used
public event EventHandler? OnError;
@@ -106,8 +106,8 @@ private void CheckForChangedFiles()
if (!_knownEntities.ContainsKey(fullFilePath))
{
- // New file
- RecordChange(f, isNewFile: true);
+ // New file or directory
+ RecordChange(f, ChangeKind.Add);
}
else
{
@@ -115,10 +115,11 @@ private void CheckForChangedFiles()
try
{
- if (fileMeta.FileInfo.LastWriteTime != f.LastWriteTime)
+ if (!fileMeta.FileInfo.Attributes.HasFlag(FileAttributes.Directory) &&
+ fileMeta.FileInfo.LastWriteTime != f.LastWriteTime)
{
// File changed
- RecordChange(f, isNewFile: false);
+ RecordChange(f, ChangeKind.Update);
}
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, foundAgain: true);
@@ -136,8 +137,8 @@ private void CheckForChangedFiles()
{
if (!file.Value.FoundAgain)
{
- // File deleted
- RecordChange(file.Value.FileInfo, isNewFile: false);
+ // File or directory deleted
+ RecordChange(file.Value.FileInfo, ChangeKind.Delete);
}
}
@@ -148,7 +149,7 @@ private void CheckForChangedFiles()
_tempDictionary.Clear();
}
- private void RecordChange(FileSystemInfo fileInfo, bool isNewFile)
+ private void RecordChange(FileSystemInfo fileInfo, ChangeKind kind)
{
if (_changes.ContainsKey(fileInfo.FullName) ||
fileInfo.FullName.Equals(_watchedDirectory.FullName, StringComparison.Ordinal))
@@ -156,18 +157,15 @@ private void RecordChange(FileSystemInfo fileInfo, bool isNewFile)
return;
}
- _changes.Add(fileInfo.FullName, isNewFile);
+ _changes.Add(fileInfo.FullName, kind);
- if (fileInfo.FullName != _watchedDirectory.FullName)
+ if (fileInfo is FileInfo { Directory: { } directory })
{
- if (fileInfo is FileInfo { Directory: { } directory })
- {
- RecordChange(directory, isNewFile: false);
- }
- else if (fileInfo is DirectoryInfo { Parent: { } parent })
- {
- RecordChange(parent, isNewFile: false);
- }
+ RecordChange(directory, ChangeKind.Update);
+ }
+ else if (fileInfo is DirectoryInfo { Parent: { } parent })
+ {
+ RecordChange(parent, ChangeKind.Update);
}
}
@@ -202,14 +200,14 @@ private static void ForeachEntityInDirectory(DirectoryInfo dirInfo, Action fileSet, DateTime buildCompletionTime, IReporter reporter) : IDisposable
{
private static readonly TimeSpan s_debounceInterval = TimeSpan.FromMilliseconds(50);
+ private static readonly DateTime s_fileNotExistFileTime = DateTime.FromFileTime(0);
private readonly FileWatcher _fileWatcher = new(fileSet, reporter);
private readonly object _changedFilesLock = new();
- private readonly ConcurrentDictionary _changedFiles = new(StringComparer.Ordinal);
+ private readonly ConcurrentDictionary _changedFiles = new(StringComparer.Ordinal);
- private TaskCompletionSource? _tcs;
+ private TaskCompletionSource? _tcs;
private bool _initialized;
private bool _disposed;
@@ -65,7 +67,7 @@ private void EnsureInitialized()
continue;
}
- FileItem[] changedFiles;
+ ChangedFile[] changedFiles;
lock (_changedFilesLock)
{
changedFiles = _changedFiles.Values.ToArray();
@@ -82,7 +84,7 @@ private void EnsureInitialized()
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Default);
- void FileChangedCallback(string path, bool newFile)
+ void FileChangedCallback(string path, ChangeKind kind)
{
// only handle file changes:
if (Directory.Exists(path))
@@ -90,43 +92,66 @@ void FileChangedCallback(string path, bool newFile)
return;
}
- try
+ if (kind != ChangeKind.Delete)
{
- // TODO: Deleted files will be ignored https://github.com/dotnet/sdk/issues/42535
-
- // Do not report changes to files that happened during build:
- var creationTime = File.GetCreationTimeUtc(path);
- var writeTime = File.GetLastWriteTimeUtc(path);
- if (creationTime.Ticks < buildCompletionTime.Ticks && writeTime.Ticks < buildCompletionTime.Ticks)
+ try
+ {
+ // Do not report changes to files that happened during build:
+ var creationTime = File.GetCreationTimeUtc(path);
+ var writeTime = File.GetLastWriteTimeUtc(path);
+
+ if (creationTime == s_fileNotExistFileTime || writeTime == s_fileNotExistFileTime)
+ {
+ // file might have been deleted since we received the event
+ kind = ChangeKind.Delete;
+ }
+ else if (creationTime.Ticks < buildCompletionTime.Ticks && writeTime.Ticks < buildCompletionTime.Ticks)
+ {
+ reporter.Verbose(
+ $"Ignoring file change during build: {kind} '{path}' " +
+ $"(created {FormatTimestamp(creationTime)} and written {FormatTimestamp(writeTime)} before {FormatTimestamp(buildCompletionTime)}).");
+
+ return;
+ }
+ else if (writeTime > creationTime)
+ {
+ reporter.Verbose($"File change: {kind} '{path}' (written {FormatTimestamp(writeTime)} after {FormatTimestamp(buildCompletionTime)}).");
+ }
+ else
+ {
+ reporter.Verbose($"File change: {kind} '{path}' (created {FormatTimestamp(creationTime)} after {FormatTimestamp(buildCompletionTime)}).");
+ }
+ }
+ catch (Exception e)
{
- reporter.Verbose($"Ignoring file updated during build: '{path}' ({FormatTimestamp(creationTime)},{FormatTimestamp(writeTime)} < {FormatTimestamp(buildCompletionTime)}).");
+ reporter.Verbose($"Ignoring file '{path}' due to access error: {e.Message}.");
return;
}
}
- catch (Exception e)
+
+ if (kind == ChangeKind.Delete)
{
- reporter.Verbose($"Ignoring file '{path}' due to access error: {e.Message}.");
- return;
+ reporter.Verbose($"File '{path}' deleted after {FormatTimestamp(buildCompletionTime)}.");
}
- if (newFile)
+ if (kind == ChangeKind.Add)
{
lock (_changedFilesLock)
{
- _changedFiles.TryAdd(path, new FileItem { FilePath = path, IsNewFile = newFile });
+ _changedFiles.TryAdd(path, new ChangedFile(new FileItem { FilePath = path }, kind));
}
}
else if (fileSet.TryGetValue(path, out var fileItem))
{
lock (_changedFilesLock)
{
- _changedFiles.TryAdd(path, fileItem);
+ _changedFiles.TryAdd(path, new ChangedFile(fileItem, kind));
}
}
}
}
- public Task GetChangedFilesAsync(CancellationToken cancellationToken, bool forceWaitForNewUpdate = false)
+ public Task GetChangedFilesAsync(CancellationToken cancellationToken, bool forceWaitForNewUpdate = false)
{
EnsureInitialized();
@@ -142,6 +167,6 @@ void FileChangedCallback(string path, bool newFile)
}
internal static string FormatTimestamp(DateTime time)
- => time.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
+ => time.ToString("HH:mm:ss.fffffff");
}
}
diff --git a/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs b/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs
index b324abf9122e..8684564e03db 100644
--- a/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs
+++ b/src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs
@@ -3,12 +3,18 @@
using System.Reflection;
using Microsoft.DotNet.Cli;
+using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.CommandFactory;
+using NuGet.Configuration;
namespace Microsoft.DotNet.BuildServer
{
internal class VBCSCompilerServer : IBuildServer
{
+ private static readonly string s_toolsetPackageName = "microsoft.net.sdk.compilers.toolset";
+ private static readonly string s_vbcsCompilerExeFileName = "VBCSCompiler.exe";
+ private static readonly string s_shutdownArg = "-shutdown";
+
internal static readonly string VBCSCompilerPath = Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"Roslyn",
@@ -28,18 +34,46 @@ public VBCSCompilerServer(ICommandFactory commandFactory = null)
public void Shutdown()
{
- var command = _commandFactory
- .Create("exec", new[] { VBCSCompilerPath, "-shutdown" })
- .CaptureStdOut()
- .CaptureStdErr();
+ List errors = null;
+
+ // Shutdown the compiler from the SDK.
+ execute(_commandFactory.Create("exec", [VBCSCompilerPath, s_shutdownArg]), ref errors);
+
+ // Shutdown toolset compilers.
+ var nuGetPackageRoot = SettingsUtility.GetGlobalPackagesFolder(Settings.LoadDefaultSettings(root: null));
+ var toolsetPackageDirectory = Path.Join(nuGetPackageRoot, s_toolsetPackageName);
+ if (Directory.Exists(toolsetPackageDirectory))
+ {
+ foreach (var versionDirectory in Directory.EnumerateDirectories(toolsetPackageDirectory))
+ {
+ var vbcsCompilerPath = Path.Join(versionDirectory, s_vbcsCompilerExeFileName);
+ if (File.Exists(vbcsCompilerPath))
+ {
+ execute(CommandFactoryUsingResolver.Create(vbcsCompilerPath, [s_shutdownArg]), ref errors);
+ }
+ }
+ }
- var result = command.Execute();
- if (result.ExitCode != 0)
+ if (errors?.Count > 0)
{
throw new BuildServerException(
string.Format(
LocalizableStrings.ShutdownCommandFailed,
- result.StdErr));
+ string.Join(Environment.NewLine, errors)));
+ }
+
+ static void execute(ICommand command, ref List errors)
+ {
+ command = command
+ .CaptureStdOut()
+ .CaptureStdErr();
+
+ var result = command.Execute();
+ if (result.ExitCode != 0)
+ {
+ errors ??= new List();
+ errors.Add(result.StdErr);
+ }
}
}
}
diff --git a/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs b/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs
index 0dc61ea54241..ba9e244e77b2 100644
--- a/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs
+++ b/src/Cli/dotnet/Installer/Windows/InstallMessageDispatcher.cs
@@ -224,7 +224,7 @@ public InstallResponseMessage SendGetGlobalJsonWorkloadSetVersionsRequest(SdkFea
{
return Send(new InstallRequestMessage
{
- RequestType = InstallRequestType.RecordWorkloadSetInGlobalJson,
+ RequestType = InstallRequestType.GetGlobalJsonWorkloadSetVersions,
SdkFeatureBand = sdkFeatureBand.ToString(),
});
}
diff --git a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs
index 2ce33f5d8158..42b25a77fc10 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs
+++ b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs
@@ -16,7 +16,8 @@ Task DownloadPackageAsync(PackageId packageId,
bool includePreview = false,
bool? includeUnlisted = null,
DirectoryPath? downloadFolder = null,
- PackageSourceMapping packageSourceMapping = null);
+ PackageSourceMapping packageSourceMapping = null,
+ bool isTool = false);
Task GetPackageUrl(PackageId packageId,
NuGetVersion packageVersion = null,
diff --git a/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx b/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx
index dc604eb2bc79..b526943144d6 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx
+++ b/src/Cli/dotnet/NugetPackageDownloader/LocalizableStrings.resx
@@ -126,6 +126,9 @@
{0} is not found in NuGet feeds {1}.
+
+ Package {0} is not a .NET tool.
+
Downloading {0} version {1} failed.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs
index 262d6cd2fc29..a861993465c8 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs
+++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs
@@ -86,19 +86,45 @@ public async Task DownloadPackageAsync(PackageId packageId,
bool includePreview = false,
bool? includeUnlisted = null,
DirectoryPath? downloadFolder = null,
- PackageSourceMapping packageSourceMapping = null)
+ PackageSourceMapping packageSourceMapping = null,
+ bool isTool = false)
{
CancellationToken cancellationToken = CancellationToken.None;
(var source, var resolvedPackageVersion) = await GetPackageSourceAndVersion(packageId, packageVersion,
packageSourceLocation, includePreview, includeUnlisted ?? packageVersion is not null, packageSourceMapping).ConfigureAwait(false);
- FindPackageByIdResource resource = null;
SourceRepository repository = GetSourceRepository(source);
- resource = await repository.GetResourceAsync(cancellationToken)
- .ConfigureAwait(false);
+ // TODO: Fix this to use the PackageSearchResourceV3 once https://github.com/NuGet/NuGet.Client/pull/5991 is completed.
+ if (isTool && await repository.GetResourceAsync().ConfigureAwait(false) is ServiceIndexResourceV3 serviceIndex)
+ {
+ // See https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec
+ var uri = serviceIndex.GetServiceEntries("PackageBaseAddress/3.0.0")[0].Uri;
+ var queryUri = uri + $"{packageId}/{packageVersion}/{packageId}.nuspec";
+
+ using HttpClient client = new(new HttpClientHandler() { CheckCertificateRevocationList = true });
+ using HttpResponseMessage response = await client.GetAsync(queryUri).ConfigureAwait(false);
+
+ if (response.IsSuccessStatusCode)
+ {
+ string nuspec = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+
+ XDocument doc = XDocument.Parse(nuspec);
+
+ if (!ToolPackageInstance.IsToolPackage(doc))
+ {
+ throw new GracefulException(string.Format(LocalizableStrings.NotATool, packageId));
+ }
+ }
+ else
+ {
+ throw new GracefulException(string.Format(LocalizableStrings.NotATool, packageId));
+ }
+ }
+ FindPackageByIdResource resource = await repository.GetResourceAsync(cancellationToken)
+ .ConfigureAwait(false);
if (resource == null)
{
throw new NuGetPackageNotFoundException(
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf
index e5b5123b171a..d87b1db20e13 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.cs.xlf
@@ -39,6 +39,11 @@
Balíček {0} se nenašel v informačních kanálech NuGet {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf
index b0f68edf07a9..b0d18b15fa40 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.de.xlf
@@ -39,6 +39,11 @@
"{0}" wurde in NuGet-Feeds "{1}" nicht gefunden.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf
index 2261022a005b..c77bb97e1a70 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.es.xlf
@@ -39,6 +39,11 @@
No se encuentra {0} en las fuentes de NuGet {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf
index c7a5d791edc3..61391544e180 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.fr.xlf
@@ -39,6 +39,11 @@
{0} est introuvable dans les flux NuGet {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf
index dafa443d72db..2f79a93f75a4 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.it.xlf
@@ -39,6 +39,11 @@
{0} non è stato trovato nei feed NuGet {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf
index fc1e0d3848e2..d89d1cdc10c5 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ja.xlf
@@ -39,6 +39,11 @@
{0} が NuGet フィード {1} に見つかりません。
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf
index 0530ae8622f5..eb7f0181a5fe 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ko.xlf
@@ -39,6 +39,11 @@
NuGet 피드 {1} 에서 {0}을(를) 찾을 수 없습니다.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf
index 2513fb391d78..2774f9237e84 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pl.xlf
@@ -39,6 +39,11 @@
Nie znaleziono pakietu {0} w kanałach informacyjnych NuGet {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf
index beea5f9cf625..018fb56a4b33 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.pt-BR.xlf
@@ -39,6 +39,11 @@
{0} não foi encontrado no NuGet feeds {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf
index 870e9957d7b5..6d8ab98474ba 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.ru.xlf
@@ -39,6 +39,11 @@
{0} не найдено в веб-каналах NuGet {1}.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf
index 01b1300840a1..d90e0756b192 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.tr.xlf
@@ -39,6 +39,11 @@
{0}, {1} NuGet akışlarında bulunamadı.
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf
index 2dea90b8c612..578284141d94 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hans.xlf
@@ -39,6 +39,11 @@
在 NuGet 源 {1} 中找不到 {0}。
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf
index 46a8c60a1b69..6f05220c3a7b 100644
--- a/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf
+++ b/src/Cli/dotnet/NugetPackageDownloader/xlf/LocalizableStrings.zh-Hant.xlf
@@ -39,6 +39,11 @@
在 NuGet 摘要 {1} 中找不到 {0}"。
+
+ Package {0} is not a .NET tool.
+ Package {0} is not a .NET tool.
+
+
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
Skipping signature verification for NuGet package "{0}" because it comes from a source that does not require signature validation.
diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs
index 360e530e3b49..2bb88a290745 100644
--- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs
+++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs
@@ -38,8 +38,8 @@ private static string GetCachedDeviceId()
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- // Get device Id from Windows registry
- using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools"))
+ // Get device Id from Windows registry matching the OS architecture
+ using (var key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools"))
{
deviceId = key?.GetValue("deviceid") as string;
}
@@ -80,10 +80,16 @@ private static void CacheDeviceId(string deviceId)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- // Cache device Id in Windows registry
- using (var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools"))
+ // Cache device Id in Windows registry matching the OS architecture
+ using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
{
- key.SetValue("deviceid", deviceId);
+ using(var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools"))
+ {
+ if (key != null)
+ {
+ key.SetValue("deviceid", deviceId);
+ }
+ }
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
diff --git a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs
index 3189520dcf89..5173997199a0 100644
--- a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs
+++ b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs
@@ -130,13 +130,21 @@ public IToolPackage InstallPackage(PackageLocation packageLocation, PackageId pa
{
DownloadAndExtractPackage(packageId, nugetPackageDownloader, toolDownloadDir.Value, packageVersion, packageSourceLocation, includeUnlisted: givenSpecificVersion).GetAwaiter().GetResult();
}
- else if(isGlobalTool)
+ else
{
- throw new ToolPackageException(
- string.Format(
- CommonLocalizableStrings.ToolPackageConflictPackageId,
- packageId,
- packageVersion.ToNormalizedString()));
+ if (!ToolPackageInstance.IsToolPackage(package.Nuspec.Xml))
+ {
+ throw new GracefulException(string.Format(NuGetPackageDownloader.LocalizableStrings.NotATool, packageId));
+ }
+
+ if (isGlobalTool)
+ {
+ throw new ToolPackageException(
+ string.Format(
+ CommonLocalizableStrings.ToolPackageConflictPackageId,
+ packageId,
+ packageVersion.ToNormalizedString()));
+ }
}
CreateAssetFile(packageId, packageVersion, toolDownloadDir, assetFileDirectory, _runtimeJsonPath, targetFramework);
@@ -277,7 +285,7 @@ private static async Task DownloadAndExtractPackage(
bool includeUnlisted = false
)
{
- var packagePath = await nugetPackageDownloader.DownloadPackageAsync(packageId, packageVersion, packageSourceLocation, includeUnlisted: includeUnlisted).ConfigureAwait(false);
+ var packagePath = await nugetPackageDownloader.DownloadPackageAsync(packageId, packageVersion, packageSourceLocation, includeUnlisted: includeUnlisted, isTool: true).ConfigureAwait(false);
// look for package on disk and read the version
NuGetVersion version;
diff --git a/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs b/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs
index 4bcd76b82bdc..b9299943926e 100644
--- a/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs
+++ b/src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs
@@ -23,6 +23,17 @@ public static ToolPackageInstance CreateFromAssetFile(PackageId id, DirectoryPat
return new ToolPackageInstance(id, version, packageDirectory, assetsJsonParentDirectory);
}
+
+ ///
+ /// Validates that the nuspec XML represents a .NET tool package.
+ ///
+ /// The nuspec XML to check.
+ /// if the nuspec represents a .NET tool package; otherwise, .
+ public static bool IsToolPackage(XDocument nuspec) =>
+ nuspec.Root.Descendants().Where(
+ e => e.Name.LocalName == "packageType" &&
+ e.Attributes().Where(a => a.Name.LocalName == "name" && a.Value == "DotnetTool").Any()).Any();
+
private const string PackagedShimsDirectoryConvention = "shims";
public IEnumerable Warnings => _toolConfiguration.Value.Warnings;
diff --git a/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs b/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs
index 18dc4a50ce22..4f2af8f52567 100644
--- a/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs
+++ b/src/Cli/dotnet/commands/dotnet-msbuild/MSBuildLogger.cs
@@ -18,6 +18,9 @@ public sealed class MSBuildLogger : INodeLogger
internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval";
internal const string BuildTelemetryEventName = "build";
internal const string LoggingConfigurationTelemetryEventName = "loggingConfiguration";
+ internal const string BuildcheckAcquisitionFailureEventName = "buildcheck/acquisitionfailure";
+ internal const string BuildcheckRunEventName = "buildcheck/run";
+ internal const string BuildcheckRuleStatsEventName = "buildcheck/rule";
internal const string SdkTaskBaseCatchExceptionTelemetryEventName = "taskBaseCatchException";
internal const string PublishPropertiesTelemetryEventName = "PublishProperties";
@@ -138,6 +141,21 @@ internal static void FormatAndSend(ITelemetry telemetry, TelemetryEventArgs args
toBeHashed: Array.Empty(),
toBeMeasured: new[] { "FileLoggersCount" });
break;
+ case BuildcheckAcquisitionFailureEventName:
+ TrackEvent(telemetry, $"msbuild/{BuildcheckAcquisitionFailureEventName}", args.Properties,
+ toBeHashed: new[] { "AssemblyName", "ExceptionType", "ExceptionMessage" },
+ toBeMeasured: Array.Empty());
+ break;
+ case BuildcheckRunEventName:
+ TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties,
+ toBeHashed: Array.Empty(),
+ toBeMeasured: new[] { "TotalRuntimeInMilliseconds" });
+ break;
+ case BuildcheckRuleStatsEventName:
+ TrackEvent(telemetry, $"msbuild/{BuildcheckRuleStatsEventName}", args.Properties,
+ toBeHashed: new[] { "RuleId", "CheckFriendlyName" },
+ toBeMeasured: new[] { "TotalRuntimeInMilliseconds" });
+ break;
// Pass through events that don't need special handling
case SdkTaskBaseCatchExceptionTelemetryEventName:
case PublishPropertiesTelemetryEventName:
diff --git a/src/Cli/dotnet/commands/dotnet-run/Program.cs b/src/Cli/dotnet/commands/dotnet-run/Program.cs
index 8399465d5ced..cefc00ba84b6 100644
--- a/src/Cli/dotnet/commands/dotnet-run/Program.cs
+++ b/src/Cli/dotnet/commands/dotnet-run/Program.cs
@@ -24,6 +24,35 @@ public static RunCommand FromParseResult(ParseResult parseResult)
parseResult = ModifyParseResultForShorthandProjectOption(parseResult);
}
+ // if the application arguments contain any binlog args then we need to remove them from the application arguments and apply
+ // them to the restore args.
+ // this is because we can't model the binlog command structure in MSbuild in the System.CommandLine parser, but we need
+ // bl information to synchronize the restore and build logger configurations
+ var applicationArguments = parseResult.GetValue(RunCommandParser.ApplicationArguments).ToList();
+
+ var binlogArgs = new List();
+ var nonBinLogArgs = new List();
+ foreach (var arg in applicationArguments)
+ {
+
+ if (arg.StartsWith("/bl:") || arg.Equals("/bl")
+ || arg.StartsWith("--binaryLogger:") || arg.Equals("--binaryLogger")
+ || arg.StartsWith("-bl:") || arg.Equals("-bl"))
+ {
+ binlogArgs.Add(arg);
+ }
+ else
+ {
+ nonBinLogArgs.Add(arg);
+ }
+ }
+
+ var restoreArgs = parseResult.OptionValuesToBeForwarded(RunCommandParser.GetCommand()).ToList();
+ if (binlogArgs.Count > 0)
+ {
+ restoreArgs.AddRange(binlogArgs);
+ }
+
var command = new RunCommand(
noBuild: parseResult.HasOption(RunCommandParser.NoBuildOption),
projectFileOrDirectory: parseResult.GetValue(RunCommandParser.ProjectOption),
@@ -32,8 +61,8 @@ public static RunCommand FromParseResult(ParseResult parseResult)
noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption),
interactive: parseResult.HasOption(RunCommandParser.InteractiveOption),
verbosity: parseResult.HasOption(CommonOptions.VerbosityOption) ? parseResult.GetValue(CommonOptions.VerbosityOption) : null,
- restoreArgs: parseResult.OptionValuesToBeForwarded(RunCommandParser.GetCommand()).ToArray(),
- args: parseResult.GetValue(RunCommandParser.ApplicationArguments)
+ restoreArgs: restoreArgs.ToArray(),
+ args: nonBinLogArgs.ToArray()
);
return command;
diff --git a/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs b/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs
index e7b154267041..56e10007691a 100644
--- a/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs
+++ b/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs
@@ -4,6 +4,7 @@
#nullable enable
using System.Reflection;
+using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
@@ -225,15 +226,16 @@ private string[] GetRestoreArguments(IEnumerable cliRestoreArgs)
private ICommand GetTargetCommand()
{
- // TODO for MSBuild usage here: need to sync loggers (primarily binlog) used with this evaluation
- var project = EvaluateProject(ProjectFileFullPath, RestoreArgs);
+ FacadeLogger? logger = DetermineBinlogger(RestoreArgs);
+ var project = EvaluateProject(ProjectFileFullPath, RestoreArgs, logger);
ValidatePreconditions(project);
- InvokeRunArgumentsTarget(project, RestoreArgs, Verbosity);
+ InvokeRunArgumentsTarget(project, RestoreArgs, Verbosity, logger);
+ logger?.ReallyShutdown();
var runProperties = ReadRunPropertiesFromProject(project, Args);
var command = CreateCommandFromRunProperties(project, runProperties);
return command;
- static ProjectInstance EvaluateProject(string projectFilePath, string[] restoreArgs)
+ static ProjectInstance EvaluateProject(string projectFilePath, string[] restoreArgs, ILogger? binaryLogger)
{
var globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
@@ -251,8 +253,8 @@ static ProjectInstance EvaluateProject(string projectFilePath, string[] restoreA
globalProperties[key] = string.Join(";", values);
}
}
- var project = new ProjectInstance(projectFilePath, globalProperties, null);
- return project;
+ var collection = new ProjectCollection(globalProperties: globalProperties, loggers: binaryLogger is null ? null : [binaryLogger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
+ return collection.LoadProject(projectFilePath).CreateProjectInstance();
}
static void ValidatePreconditions(ProjectInstance project)
@@ -329,34 +331,152 @@ static ICommand CreateCommandFromRunProperties(ProjectInstance project, RunPrope
return command;
}
- static void InvokeRunArgumentsTarget(ProjectInstance project, string[] restoreArgs, VerbosityOptions? verbosity)
+ static void InvokeRunArgumentsTarget(ProjectInstance project, string[] restoreArgs, VerbosityOptions? verbosity, FacadeLogger? binaryLogger)
{
// if the restoreArgs contain a `-bl` then let's probe it
List loggersForBuild = [
MakeTerminalLogger(verbosity)
];
- if (restoreArgs.FirstOrDefault(arg => arg.StartsWith("-bl", StringComparison.OrdinalIgnoreCase)) is string blArg)
+ if (binaryLogger is not null)
+ {
+ loggersForBuild.Add(binaryLogger);
+ }
+
+ if (!project.Build([ComputeRunArgumentsTarget], loggers: loggersForBuild, remoteLoggers: null, out var _targetOutputs))
+ {
+ throw new GracefulException(LocalizableStrings.RunCommandEvaluationExceptionBuildFailed, ComputeRunArgumentsTarget);
+ }
+ }
+
+ static FacadeLogger? DetermineBinlogger(string[] restoreArgs)
+ {
+
+ List binaryLoggers = new();
+
+ foreach (var blArg in restoreArgs.Where(arg => arg.StartsWith("-bl", StringComparison.OrdinalIgnoreCase)))
{
if (blArg.Contains(':'))
{
// split and forward args
var split = blArg.Split(':', 2);
- loggersForBuild.Add(new BinaryLogger { Parameters = split[1] });
+ var filename = split[1];
+ if (filename.EndsWith(".binlog"))
+ {
+ filename = filename.Substring(0, filename.Length - ".binlog".Length);
+ filename = filename + "-dotnet-run" + ".binlog";
+ }
+ binaryLoggers.Add(new BinaryLogger { Parameters = filename });
}
else
{
- // just the defaults
- loggersForBuild.Add(new BinaryLogger { Parameters = "{}.binlog" });
+ // the same name will be used for the build and run-restore-exec steps, so we need to make sure they don't conflict
+ var filename = "msbuild-dotnet-run" + ".binlog";
+ binaryLoggers.Add(new BinaryLogger { Parameters = filename });
}
- };
+ }
- if (!project.Build([ComputeRunArgumentsTarget], loggers: loggersForBuild, remoteLoggers: null, out var _targetOutputs))
+ // this binaryLogger needs to be used for both evaluation and execution, so we need to only call it with a single IEventSource across
+ // both of those phases.
+ // We need a custom logger to handle this, because the MSBuild API for evaluation and execution calls logger Initialize and Shutdown methods, so will not allow us to do this.
+ if (binaryLoggers.Count > 0)
{
- throw new GracefulException(LocalizableStrings.RunCommandEvaluationExceptionBuildFailed, ComputeRunArgumentsTarget);
+ var fakeLogger = ConfigureDispatcher(binaryLoggers);
+
+ return fakeLogger;
+ }
+ return null;
+ }
+
+ static FacadeLogger ConfigureDispatcher(List binaryLoggers)
+ {
+ var dispatcher = new PersistentDispatcher(binaryLoggers);
+ return new FacadeLogger(dispatcher);
+ }
+ }
+
+ ///
+ /// This class acts as a wrapper around the BinaryLogger, to allow us to keep the BinaryLogger alive across multiple phases of the build.
+ /// The methods here are stubs so that the real binarylogger sees that we support these functionalities.
+ /// We need to ensure that the child logger is Initialized and Shutdown only once, so this fake event source
+ /// acts as a buffer. We'll provide this dispatcher to another fake logger, and that logger will
+ /// bind to this dispatcher to foward events from the actual build to the binary logger through this dispatcher.
+ ///
+ ///
+ private class PersistentDispatcher : EventArgsDispatcher, IEventSource4
+ {
+ private List innerLoggers;
+
+ public PersistentDispatcher(List innerLoggers)
+ {
+ this.innerLoggers = innerLoggers;
+ foreach (var logger in innerLoggers)
+ {
+ logger.Initialize(this);
+ }
+ }
+ public event TelemetryEventHandler TelemetryLogged { add { } remove { } }
+
+ public void IncludeEvaluationMetaprojects() { }
+ public void IncludeEvaluationProfiles() { }
+ public void IncludeEvaluationPropertiesAndItems() { }
+ public void IncludeTaskInputs() { }
+
+ public void Destroy()
+ {
+ foreach (var innerLogger in innerLoggers)
+ {
+ innerLogger.Shutdown();
}
}
}
+ ///
+ /// This logger acts as a forwarder to the provided dispatcher, so that multiple different build engine operations
+ /// can be forwarded to the shared binary logger held by the dispatcher.
+ /// We opt into lots of data to ensure that we can forward all events to the binary logger.
+ ///
+ ///
+ private class FacadeLogger(PersistentDispatcher dispatcher) : ILogger
+ {
+ public PersistentDispatcher Dispatcher => dispatcher;
+
+ public LoggerVerbosity Verbosity { get => LoggerVerbosity.Diagnostic; set { } }
+ public string? Parameters { get => ""; set { } }
+
+ public void Initialize(IEventSource eventSource)
+ {
+ if (eventSource is IEventSource3 eventSource3)
+ {
+ eventSource3.IncludeEvaluationMetaprojects();
+ dispatcher.IncludeEvaluationMetaprojects();
+
+ eventSource3.IncludeEvaluationProfiles();
+ dispatcher.IncludeEvaluationProfiles();
+
+ eventSource3.IncludeTaskInputs();
+ dispatcher.IncludeTaskInputs();
+ }
+
+ eventSource.AnyEventRaised += (sender, args) => dispatcher.Dispatch(args);
+
+ if (eventSource is IEventSource4 eventSource4)
+ {
+ eventSource4.IncludeEvaluationPropertiesAndItems();
+ dispatcher.IncludeEvaluationPropertiesAndItems();
+ }
+ }
+
+ public void ReallyShutdown()
+ {
+ dispatcher.Destroy();
+ }
+
+ // we don't do anything on shutdown, because we want to keep the dispatcher alive for the next phase
+ public void Shutdown()
+ {
+ }
+ }
+
static ILogger MakeTerminalLogger(VerbosityOptions? verbosity)
{
var msbuildVerbosity = ToLoggerVerbosity(verbosity);
diff --git a/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs b/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs
index 1499142be4c0..42b9dd56417e 100644
--- a/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs
+++ b/src/Cli/dotnet/commands/dotnet-tool/update/ToolUpdateCommand.cs
@@ -8,6 +8,7 @@
using Microsoft.DotNet.ToolManifest;
using Microsoft.DotNet.ToolPackage;
using Microsoft.DotNet.Tools.Tool.Common;
+using Microsoft.Extensions.EnvironmentAbstractions;
using CreateShellShimRepository = Microsoft.DotNet.Tools.Tool.Install.CreateShellShimRepository;
namespace Microsoft.DotNet.Tools.Tool.Update
@@ -43,16 +44,17 @@ public ToolUpdateCommand(
localToolsResolverCache,
reporter);
+ _global = result.GetValue(ToolInstallCommandParser.GlobalOption);
+ _toolPath = result.GetValue(ToolInstallCommandParser.ToolPathOption);
+ DirectoryPath? location = string.IsNullOrWhiteSpace(_toolPath) ? null : new DirectoryPath(_toolPath);
_toolUpdateGlobalOrToolPathCommand =
toolUpdateGlobalOrToolPathCommand
?? new ToolUpdateGlobalOrToolPathCommand(
result,
createToolPackageStoreDownloaderUninstaller,
createShellShimRepository,
- reporter);
-
- _global = result.GetValue(ToolInstallCommandParser.GlobalOption);
- _toolPath = result.GetValue(ToolInstallCommandParser.ToolPathOption);
+ reporter,
+ ToolPackageFactory.CreateToolPackageStoreQuery(location));
}
diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs
index dc36affa9eda..f50e0551e9ca 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs
+++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs
@@ -39,7 +39,10 @@ internal static string GetWorkloadsVersion(WorkloadInfoHelper workloadInfoHelper
{
workloadInfoHelper ??= new WorkloadInfoHelper(false);
- return workloadInfoHelper.ManifestProvider.GetWorkloadVersion().Version;
+ var versionInfo = workloadInfoHelper.ManifestProvider.GetWorkloadVersion();
+
+ // The explicit space here is intentional, as it's easy to miss in localization and crucial for parsing
+ return versionInfo.Version + (versionInfo.VersionNotInstalledMessage is not null ? ' ' + Workloads.Workload.List.LocalizableStrings.WorkloadVersionNotInstalledShort : string.Empty);
}
internal static void ShowWorkloadsInfo(ParseResult parseResult = null, WorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null, string dotnetDir = null, bool showVersion = true)
@@ -57,46 +60,68 @@ internal static void ShowWorkloadsInfo(ParseResult parseResult = null, WorkloadI
InstalledWorkloadsCollection installedWorkloads = workloadInfoHelper.AddInstalledVsWorkloads(installedList);
string dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
+
+
+ var versionInfo = workloadInfoHelper.ManifestProvider.GetWorkloadVersion();
+
+ void WriteUpdateModeAndAnyError(string indent = "")
+ {
+ var useWorkloadSets = InstallStateContents.FromPath(Path.Combine(WorkloadInstallType.GetInstallStateFolder(workloadInfoHelper._currentSdkFeatureBand, workloadInfoHelper.UserLocalPath), "default.json")).UseWorkloadSets;
+ var workloadSetsString = useWorkloadSets == true ? "workload sets" : "loose manifests";
+ reporter.WriteLine(indent + string.Format(CommonStrings.WorkloadManifestInstallationConfiguration, workloadSetsString));
+
+ var additionalMessage = versionInfo.VersionNotInstalledMessage ?? versionInfo.UpdateModeMessage;
+ if (additionalMessage != null)
+ {
+ reporter.WriteLine(indent + additionalMessage);
+ }
+ }
+
if (showVersion)
{
- reporter.WriteLine($" Workload version: {workloadInfoHelper.ManifestProvider.GetWorkloadVersion().Version}");
+ reporter.WriteLine($" Workload version: {GetWorkloadsVersion()}");
+
+ WriteUpdateModeAndAnyError(indent: " ");
+ reporter.WriteLine();
}
- var useWorkloadSets = InstallStateContents.FromPath(Path.Combine(WorkloadInstallType.GetInstallStateFolder(workloadInfoHelper._currentSdkFeatureBand, workloadInfoHelper.UserLocalPath), "default.json")).UseWorkloadSets;
- var workloadSetsString = useWorkloadSets == true ? "workload sets" : "loose manifests";
- reporter.WriteLine(string.Format(CommonStrings.WorkloadManifestInstallationConfiguration, workloadSetsString));
-
if (installedWorkloads.Count == 0)
{
reporter.WriteLine(CommonStrings.NoWorkloadsInstalledInfoWarning);
- return;
}
+ else
+ {
+ var manifestInfoDict = workloadInfoHelper.WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase);
- var manifestInfoDict = workloadInfoHelper.WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase);
+ foreach (var workload in installedWorkloads.AsEnumerable())
+ {
+ var workloadManifest = workloadInfoHelper.WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workload.Key));
+ var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand;
- foreach (var workload in installedWorkloads.AsEnumerable())
- {
- var workloadManifest = workloadInfoHelper.WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workload.Key));
- var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand;
+ const int align = 10;
+ const string separator = " ";
- const int align = 10;
- const string separator = " ";
+ reporter.WriteLine($" [{workload.Key}]");
- reporter.WriteLine($" [{workload.Key}]");
+ reporter.Write($"{separator}{CommonStrings.WorkloadSourceColumn}:");
+ reporter.WriteLine($" {workload.Value,align}");
- reporter.Write($"{separator}{CommonStrings.WorkloadSourceColumn}:");
- reporter.WriteLine($" {workload.Value,align}");
+ reporter.Write($"{separator}{CommonStrings.WorkloadManifestVersionColumn}:");
+ reporter.WriteLine($" {workloadManifest.Version + '/' + workloadFeatureBand,align}");
- reporter.Write($"{separator}{CommonStrings.WorkloadManifestVersionColumn}:");
- reporter.WriteLine($" {workloadManifest.Version + '/' + workloadFeatureBand,align}");
+ reporter.Write($"{separator}{CommonStrings.WorkloadManifestPathColumn}:");
+ reporter.WriteLine($" {workloadManifest.ManifestPath,align}");
- reporter.Write($"{separator}{CommonStrings.WorkloadManifestPathColumn}:");
- reporter.WriteLine($" {workloadManifest.ManifestPath,align}");
+ reporter.Write($"{separator}{CommonStrings.WorkloadInstallTypeColumn}:");
+ reporter.WriteLine($" {WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(Utils.Product.Version), dotnetPath).ToString(),align}"
+ );
+ reporter.WriteLine("");
+ }
+ }
- reporter.Write($"{separator}{CommonStrings.WorkloadInstallTypeColumn}:");
- reporter.WriteLine($" {WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(Utils.Product.Version), dotnetPath).ToString(),align}"
- );
- reporter.WriteLine("");
+ if (!showVersion)
+ {
+ WriteUpdateModeAndAnyError();
}
}
diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs
index ac0e1e358a36..270284bce9b4 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs
+++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadHistoryRecorder.cs
@@ -54,8 +54,7 @@ public void Run(Action workloadAction)
private WorkloadHistoryState GetWorkloadState()
{
var resolver = _workloadResolverFunc();
- var currentWorkloadVersion = resolver.GetWorkloadVersion();
- var versionString = currentWorkloadVersion.WorkloadInstallType == WorkloadVersion.Type.LooseManifest ? string.Empty : currentWorkloadVersion.Version;
+ var currentWorkloadVersion = resolver.GetWorkloadVersion().Version;
return new WorkloadHistoryState()
{
ManifestVersions = resolver.GetInstalledManifests().ToDictionary(manifest => manifest.Id.ToString(), manifest => $"{manifest.Version}/{manifest.ManifestFeatureBand}"),
@@ -63,9 +62,9 @@ private WorkloadHistoryState GetWorkloadState()
.GetInstalledWorkloads(new SdkFeatureBand(_workloadResolver.GetSdkFeatureBand()))
.Select(id => id.ToString())
.ToList(),
- WorkloadSetVersion = versionString
+ WorkloadSetVersion = currentWorkloadVersion
};
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs b/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs
index 0ff7756e7637..d709227d2292 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs
+++ b/src/Cli/dotnet/commands/dotnet-workload/install/FileBasedInstaller.cs
@@ -48,7 +48,8 @@ public FileBasedInstaller(IReporter reporter,
string tempDirPath = null,
VerbosityOptions verbosity = VerbosityOptions.normal,
PackageSourceLocation packageSourceLocation = null,
- RestoreActionConfig restoreActionConfig = null)
+ RestoreActionConfig restoreActionConfig = null,
+ VerbosityOptions nugetPackageDownloaderVerbosity = VerbosityOptions.normal)
{
_userProfileDir = userProfileDir;
_dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
@@ -58,7 +59,8 @@ public FileBasedInstaller(IReporter reporter,
_nugetPackageDownloader = nugetPackageDownloader ??
new NuGetPackageDownloader(_tempPackagesDir, filePermissionSetter: null,
new FirstPartyNuGetPackageSigningVerifier(), logger,
- restoreActionConfig: _restoreActionConfig);
+ restoreActionConfig: _restoreActionConfig,
+ verbosityOptions: nugetPackageDownloaderVerbosity);
bool userLocal = WorkloadFileBasedInstall.IsUserLocal(_dotnetDir, sdkFeatureBand.ToString());
_workloadRootDir = userLocal ? _userProfileDir : _dotnetDir;
_workloadMetadataDir = Path.Combine(_workloadRootDir, "metadata", "workloads");
@@ -92,9 +94,8 @@ IEnumerable GetPacksInWorkloads(IEnumerable workloadIds)
public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null)
{
- SdkFeatureBand workloadSetFeatureBand;
- string workloadSetPackageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out workloadSetFeatureBand);
- var workloadSetPackageId = GetManifestPackageId(new ManifestId("Microsoft.NET.Workloads"), workloadSetFeatureBand);
+ string workloadSetPackageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out SdkFeatureBand workloadSetFeatureBand);
+ var workloadSetPackageId = GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand);
var workloadSetPath = Path.Combine(_workloadRootDir, "sdk-manifests", _sdkFeatureBand.ToString(), "workloadsets", workloadSetVersion);
@@ -119,6 +120,18 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo
return WorkloadSet.FromWorkloadSetFolder(workloadSetPath, workloadSetVersion, _sdkFeatureBand);
}
+ public WorkloadSet GetWorkloadSetContents(string workloadSetVersion) => GetWorkloadSetContentsAsync(workloadSetVersion).GetAwaiter().GetResult();
+
+ public async Task GetWorkloadSetContentsAsync(string workloadSetVersion)
+ {
+ string workloadSetPackageVersion = WorkloadSet.WorkloadSetVersionToWorkloadSetPackageVersion(workloadSetVersion, out var workloadSetFeatureBand);
+ var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand),
+ new NuGetVersion(workloadSetPackageVersion), _packageSourceLocation);
+ var tempExtractionDir = Path.Combine(_tempPackagesDir.Value, $"{WorkloadManifestUpdater.WorkloadSetManifestId}-{workloadSetPackageVersion}-extracted");
+ await ExtractManifestAsync(packagePath, tempExtractionDir);
+ return WorkloadSet.FromWorkloadSetFolder(tempExtractionDir, workloadSetVersion, _sdkFeatureBand);
+ }
+
public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, ITransactionContext transactionContext, DirectoryPath? offlineCache = null)
{
var packInfos = GetPacksInWorkloads(workloadIds);
diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs
index a80755c6af41..3f3e984d6eac 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs
+++ b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadInstallCommand.cs
@@ -20,6 +20,7 @@ internal class WorkloadInstallCommand : InstallingWorkloadCommand
{
private bool _skipManifestUpdate;
private readonly IReadOnlyCollection _workloadIds;
+ private readonly bool _shouldShutdownInstaller;
public bool IsRunningRestore { get; set; }
@@ -45,6 +46,7 @@ public WorkloadInstallCommand(
WorkloadInstallerFactory.GetWorkloadInstaller(resolvedReporter, _sdkFeatureBand,
_workloadResolver, Verbosity, _userProfileDir, VerifySignatures, PackageDownloader, _dotnetPath, TempDirectoryPath,
_packageSourceLocation, RestoreActionConfiguration, elevationRequired: !_printDownloadLinkOnly && string.IsNullOrWhiteSpace(_downloadToCacheOption));
+ _shouldShutdownInstaller = _workloadInstallerFromConstructor != null;
_workloadManifestUpdater = _workloadManifestUpdaterFromConstructor ?? new WorkloadManifestUpdater(resolvedReporter, _workloadResolver, PackageDownloader, _userProfileDir,
_workloadInstaller.GetWorkloadInstallationRecordRepository(), _workloadInstaller, _packageSourceLocation, displayManifestUpdates: Verbosity.IsDetailedOrDiagnostic());
@@ -120,7 +122,9 @@ public override int Execute()
throw new GracefulException(string.Format(LocalizableStrings.CannotCombineSkipManifestAndVersion,
WorkloadInstallCommandParser.SkipManifestUpdateOption.Name, InstallingWorkloadCommandParser.VersionOption.Name), isUserError: true);
}
- else if (_skipManifestUpdate && SpecifiedWorkloadSetVersionInGlobalJson)
+ else if ((_skipManifestUpdate && SpecifiedWorkloadSetVersionInGlobalJson) &&
+ !IsRunningRestore) // When running restore, we first update workloads, then query the projects to figure out what workloads should be installed, then run the install command.
+ // When we run the install command we set skipManifestUpdate to true as an optimization to avoid trying to update twice
{
throw new GracefulException(string.Format(LocalizableStrings.CannotUseSkipManifestWithGlobalJsonWorkloadVersion,
WorkloadInstallCommandParser.SkipManifestUpdateOption.Name, _globalJsonPath), isUserError: true);
@@ -146,14 +150,21 @@ public override int Execute()
}
catch (Exception e)
{
- _workloadInstaller.Shutdown();
+ if (_shouldShutdownInstaller)
+ {
+ _workloadInstaller.Shutdown();
+ }
// Don't show entire stack trace
throw new GracefulException(string.Format(LocalizableStrings.WorkloadInstallationFailed, e.Message), e, isUserError: false);
}
}
- _workloadInstaller.Shutdown();
+ if (_shouldShutdownInstaller)
+ {
+ _workloadInstaller.Shutdown();
+ }
+
return _workloadInstaller.ExitCode;
}
diff --git a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs
index b0e420496375..5f86698b72df 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs
+++ b/src/Cli/dotnet/commands/dotnet-workload/install/WorkloadManifestUpdater.cs
@@ -20,7 +20,7 @@ namespace Microsoft.DotNet.Workloads.Workload.Install
{
internal class WorkloadManifestUpdater : IWorkloadManifestUpdater
{
- readonly string WorkloadSetManifestId = "Microsoft.NET.Workloads";
+ public static readonly string WorkloadSetManifestId = "Microsoft.NET.Workloads";
private readonly IReporter _reporter;
private readonly IWorkloadResolver _workloadResolver;
diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx
index 4ba629fee8d5..e82e8b1e6a75 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx
+++ b/src/Cli/dotnet/commands/dotnet-workload/list/LocalizableStrings.resx
@@ -134,6 +134,9 @@
Found workload version {0} pinned in the global.json file at {1}.
+
+ (not installed)
+
Found workload version {0} pinned in the global.json file at {1}, but it was not installed. Running `dotnet workload install`, `dotnet workload update`, or `dotnet workload restore` may fix this.
diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf
index 722830ef8eb3..2a900b4f2dc2 100644
--- a/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf
+++ b/src/Cli/dotnet/commands/dotnet-workload/list/xlf/LocalizableStrings.cs.xlf
@@ -32,6 +32,11 @@
Aktualizace jsou k dispozici pro následující úlohy: {0}. Pokud chcete získat nejnovější verzi, spusťte aktualizaci úlohy dotnet (`dotnet workload update`).
{Locked="dotnet workload update"}
+
+ (not installed)
+ (not installed)
+
+