diff --git a/.merge_file_JsDg5L b/.merge_file_JsDg5L new file mode 100644 index 000000000000..8ced53fdb48f --- /dev/null +++ b/.merge_file_JsDg5L @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.CommandLine; +using Microsoft.DotNet.Cli.Commands.MSBuild; +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Store; + +public class StoreCommand : MSBuildForwardingApp +{ + private StoreCommand(IEnumerable msbuildArgs, string msbuildPath = null) + : base(msbuildArgs, msbuildPath) + { + } + + public static StoreCommand FromArgs(string[] args, string msbuildPath = null) + { + var result = Parser.Parse(["dotnet", "store", ..args]); + return FromParseResult(result, msbuildPath); + } + + public static StoreCommand FromParseResult(ParseResult result, string msbuildPath = null) + { + List msbuildArgs = ["--target:ComposeStore"]; + + result.ShowHelpOrErrorIfAppropriate(); + + if (!result.HasOption(StoreCommandParser.ManifestOption)) + { + throw new GracefulException(CliCommandStrings.SpecifyManifests); + } + + msbuildArgs.AddRange(result.OptionValuesToBeForwarded(StoreCommandParser.GetCommand())); + + msbuildArgs.AddRange(result.GetValue(StoreCommandParser.Argument) ?? []); + + return new StoreCommand(msbuildArgs, msbuildPath); + } + + public static int Run(ParseResult parseResult) + { + parseResult.HandleDebugSwitch(); + + return FromParseResult(parseResult).Execute(); + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b60c4c4b99f5..23f942a1452c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "dotnet.testWindow.disableAutoDiscovery": true, "dotnet.testWindow.disableBuildOnRun": true, + "dotnet.enableWorkspaceBasedDevelopment": false, "dotnet.defaultSolution": "cli.slnf", "files.associations": { "*.slnf": "json", diff --git a/.vsts-ci.yml b/.vsts-ci.yml index f7d2eaefcfe8..74adc33363ac 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -104,7 +104,7 @@ extends: publishTaskPrefix: 1ES. populateInternalRuntimeVariables: true runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) - locBranch: release/10.0.2xx + locBranch: release/10.0.3xx # WORKAROUND: BinSkim requires the folder exist prior to scanning. preSteps: - powershell: New-Item -ItemType Directory -Path $(Build.SourcesDirectory)/artifacts/bin -Force diff --git a/Directory.Packages.props b/Directory.Packages.props index e7a1a781ae05..a048eb169421 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -56,6 +56,7 @@ + diff --git a/TemplateEngine.code-workspace b/TemplateEngine.code-workspace new file mode 100644 index 000000000000..a0cf99055cd9 --- /dev/null +++ b/TemplateEngine.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "TemplateEngine.slnf" + } + } diff --git a/build/RunTestsOnHelix.sh b/build/RunTestsOnHelix.sh old mode 100755 new mode 100644 diff --git a/cli.code-workspace b/cli.code-workspace new file mode 100644 index 000000000000..4e3fcba684cd --- /dev/null +++ b/cli.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "cli.slnf" + } + } diff --git a/cli.slnf b/cli.slnf index a300800156bf..630b3b217cc1 100644 --- a/cli.slnf +++ b/cli.slnf @@ -9,7 +9,8 @@ "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj", "test\\dotnet.Tests\\dotnet.Tests.csproj", "test\\Microsoft.DotNet.Cli.Utils.Tests\\Microsoft.DotNet.Cli.Utils.Tests.csproj", - "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj" + "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", + "src\\Cli\\Microsoft.DotNet.Cli.Definitions\\Microsoft.DotNet.Cli.Definitions.csproj" ] } } diff --git a/containers.code-workspace b/containers.code-workspace new file mode 100644 index 000000000000..b7441acdeb5b --- /dev/null +++ b/containers.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "containers.slnf" + } + } diff --git a/documentation/specs/dotnet-run-for-maui.md b/documentation/specs/dotnet-run-for-maui.md index 6ee879a91010..22697c37d544 100644 --- a/documentation/specs/dotnet-run-for-maui.md +++ b/documentation/specs/dotnet-run-for-maui.md @@ -82,6 +82,7 @@ devices`, or `xcrun devicectl list devices`._ `ComputeAvailableDevices` MSBuild target. * `build`: unchanged, but is passed `-p:Device`. + Environment variables from `-e` are passed as `@(RuntimeEnvironmentVariable)` items. * `deploy` @@ -91,10 +92,13 @@ devices`, or `xcrun devicectl list devices`._ * Call the MSBuild target, passing in the identifier for the selected `-p:Device` global MSBuild property. + * Environment variables from `-e` are passed as `@(RuntimeEnvironmentVariable)` items. + * This step needs to run, even with `--no-build`, as you may have selected a different device. * `ComputeRunArguments`: unchanged, but is passed `-p:Device`. + Environment variables from `-e` are passed as `@(RuntimeEnvironmentVariable)` items. * `run`: unchanged. `ComputeRunArguments` should have set a valid `$(RunCommand)` and `$(RunArguments)` using the value supplied by @@ -139,6 +143,103 @@ A new `--device` switch will: * The iOS and Android workloads will know how to interpret `$(Device)` to select an appropriate device, emulator, or simulator. +## Environment Variables + +The `dotnet run` command supports passing environment variables via the +`-e` or `--environment` option: + +```dotnetcli +dotnet run -e FOO=BAR -e ANOTHER=VALUE +``` + +These environment variables are: + +1. **Passed to the running application** - as process environment + variables when the app is launched. + +2. **Passed to MSBuild during build, deploy, and ComputeRunArguments** - + as `@(RuntimeEnvironmentVariable)` items that workloads can consume. + **This behavior is opt-in**: projects must declare the `RuntimeEnvironmentVariableSupport` + project capability to receive these items. + +```xml + + + + +``` + +This allows workloads (iOS, Android, etc.) to access environment +variables during the `build`, `DeployToDevice`, and `ComputeRunArguments` target execution. + +### Opting In + +To receive environment variables as MSBuild items, projects must opt in by declaring +the `RuntimeEnvironmentVariableSupport` project capability: + +```xml + + + +``` + +Mobile workloads (iOS, Android, etc.) should declare this capability in their SDK targets +so that all projects using those workloads automatically opt in. + +Workloads can consume these items in their MSBuild targets: + +```xml + + + + +``` + +### Implementation Details + +For the **build step**, which uses out-of-process MSBuild via `dotnet build`, +environment variables are injected by creating a temporary `.props` file. +The file is created in the project's `$(IntermediateOutputPath)` directory +(e.g., `obj/Debug/net11.0-android/dotnet-run-env.props`). The path is +obtained from the project evaluation performed during target framework and +device selection. If `IntermediateOutputPath` is not available, the file +falls back to the `obj/` directory. + +The file is passed to MSBuild via the `CustomBeforeMicrosoftCommonProps` property, +ensuring the items are available early in evaluation. +The temporary file is automatically deleted after the build completes. + +The generated props file looks like: + +```xml + + + + + + +``` + +For the **deploy step** (`DeployToDevice` target) and +**ComputeRunArguments target**, which use in-process MSBuild, +environment variables are added directly as +`@(RuntimeEnvironmentVariable)` items to the `ProjectInstance` before +invoking the target. + +## Binary Logs for Device Selection + +When using `-bl` with `dotnet run`, all MSBuild operations are logged to a single +binlog file: device selection, build, deploy, and run argument computation. + +File naming for `dotnet run` binlogs: + +* `-bl:filename.binlog` creates `filename-dotnet-run.binlog` +* `-bl` creates `msbuild-dotnet-run.binlog` + +Note: The build step may also create `msbuild.binlog` separately. Use +`--no-build` with `-bl` to only capture run-specific MSBuild +operations. + ## What about Launch Profiles? The iOS and Android workloads ignore all diff --git a/eng/Version.Details.props b/eng/Version.Details.props index eb4e191bbef7..304cd05ad70e 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -142,6 +142,8 @@ This file should be imported by eng/Versions.props 11.0.0-preview.3.26152.106 2.1.0 + + 11.0.0-preview.3.26152.106 2.2.0-preview.26154.1 4.2.0-preview.26154.1 @@ -285,6 +287,8 @@ This file should be imported by eng/Versions.props $(SystemWindowsExtensionsPackageVersion) $(NETStandardLibraryRefPackageVersion) + + $(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion) $(MicrosoftTestingPlatformPackageVersion) $(MSTestPackageVersion) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 14e500c082b3..2af70bc94bdb 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -527,6 +527,10 @@ https://github.com/dotnet/dotnet 5507d7a2f05bb6c073a055ead6ce1c4bbe396cda + + https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet + 4c0aa722933ea491006247bbc0a484fa3c28cd14 + diff --git a/eng/Versions.props b/eng/Versions.props index 018d909d3aaa..c907943111cc 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -77,6 +77,7 @@ 9.0.0 2.0.0-preview.1.24427.4 9.0.0 + 9.0.0 4.5.1 9.0.0 4.5.5 diff --git a/sdk.code-workspace b/sdk.code-workspace new file mode 100644 index 000000000000..22494f2d1d58 --- /dev/null +++ b/sdk.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "sdk.slnx" + } + } diff --git a/source-build.code-workspace b/source-build.code-workspace new file mode 100644 index 000000000000..55d94da909d0 --- /dev/null +++ b/source-build.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "source-build.slnf" + } + } diff --git a/src/BuiltInTools/HotReloadClient/ApplyStatus.cs b/src/BuiltInTools/HotReloadClient/ApplyStatus.cs deleted file mode 100644 index 1131aeaabef5..000000000000 --- a/src/BuiltInTools/HotReloadClient/ApplyStatus.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -namespace Microsoft.DotNet.HotReload; - -internal enum ApplyStatus -{ - /// - /// Failed to apply updates. - /// - Failed = 0, - - /// - /// All requested updates have been applied successfully. - /// - AllChangesApplied = 1, - - /// - /// Succeeded aplying changes, but some updates were not applicable to the target process because of required capabilities. - /// - SomeChangesApplied = 2, - - /// - /// No updates were applicable to the target process because of required capabilities. - /// - NoChangesApplied = 3, -} diff --git a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs index 3cacff6b82c2..1642f63f8b82 100644 --- a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs @@ -12,6 +12,7 @@ using System.IO; using System.IO.Pipes; using System.Linq; +using System.Net.Sockets; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -99,12 +100,13 @@ private void ReportPipeReadException(Exception e, string responseType, Cancellat { // Don't report a warning when cancelled or the pipe has been disposed. The process has terminated or the host is shutting down in that case. // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. - if (e is ObjectDisposedException or EndOfStreamException || cancellationToken.IsCancellationRequested) + // On Unix named pipes can also throw SocketException with ErrorCode 125 (Operation canceled) when disposed. + if (e is ObjectDisposedException or EndOfStreamException or SocketException { ErrorCode: 125 } || cancellationToken.IsCancellationRequested) { return; } - Logger.LogError("Failed to read {ResponseType} from the pipe: {Message}", responseType, e.Message); + Logger.LogError("Failed to read {ResponseType} from the pipe: {Exception}", responseType, e.ToString()); } private async Task ListenForResponsesAsync(CancellationToken cancellationToken) @@ -175,54 +177,43 @@ public override Task> GetUpdateCapabilitiesAsync(Cancella private ResponseLoggingLevel ResponseLoggingLevel => Logger.IsEnabled(LogLevel.Debug) ? ResponseLoggingLevel.Verbose : ResponseLoggingLevel.WarningsAndErrors; - public override async Task ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) + public async override Task> ApplyManagedCodeUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken) { RequireReadyForUpdates(); if (_managedCodeUpdateFailedOrCancelled) { Logger.LogDebug("Previous changes failed to apply. Further changes are not applied to this process."); - return ApplyStatus.Failed; + return Task.FromResult(false); } var applicableUpdates = await FilterApplicableUpdatesAsync(updates, cancellationToken); if (applicableUpdates.Count == 0) { Logger.LogDebug("No updates applicable to this process"); - return ApplyStatus.NoChangesApplied; + return Task.FromResult(true); } var request = new ManagedCodeUpdateRequest(ToRuntimeUpdates(applicableUpdates), ResponseLoggingLevel); - var success = false; - try - { - success = await SendAndReceiveUpdateAsync(request, isProcessSuspended, cancellationToken); - } - finally + // Only cancel apply operation when the process exits: + var updateCompletionTask = QueueUpdateBatchRequest(request, applyOperationCancellationToken); + + return CompleteApplyOperationAsync(); + + async Task CompleteApplyOperationAsync() { - if (!success) + if (await updateCompletionTask) { - // Don't report a warning when cancelled. The process has terminated or the host is shutting down in that case. - // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. - if (!cancellationToken.IsCancellationRequested) - { - Logger.LogWarning("Further changes won't be applied to this process."); - } - - _managedCodeUpdateFailedOrCancelled = true; - DisposePipe(); + return true; } - } - if (success) - { - Logger.Log(LogEvents.UpdatesApplied, applicableUpdates.Count, updates.Length); - } + Logger.LogWarning("Further changes won't be applied to this process."); + _managedCodeUpdateFailedOrCancelled = true; + DisposePipe(); - return - !success ? ApplyStatus.Failed : - (applicableUpdates.Count < updates.Length) ? ApplyStatus.SomeChangesApplied : ApplyStatus.AllChangesApplied; + return false; + } static ImmutableArray ToRuntimeUpdates(IEnumerable updates) => [.. updates.Select(static update => new RuntimeManagedCodeUpdate(update.ModuleId, @@ -232,19 +223,17 @@ static ImmutableArray ToRuntimeUpdates(IEnumerable ApplyStaticAssetUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) + public override async Task> ApplyStaticAssetUpdatesAsync(ImmutableArray updates, CancellationToken processExitedCancellationToken, CancellationToken cancellationToken) { if (!enableStaticAssetUpdates) { // The client has no concept of static assets. - return ApplyStatus.AllChangesApplied; + return Task.FromResult(true); } RequireReadyForUpdates(); - var appliedUpdateCount = 0; - - foreach (var update in updates) + var completionTasks = updates.Select(update => { var request = new StaticAssetUpdateRequest( new RuntimeStaticAssetUpdate( @@ -256,72 +245,37 @@ public async override Task ApplyStaticAssetUpdatesAsync(ImmutableAr Logger.LogDebug("Sending static file update request for asset '{Url}'.", update.RelativePath); - var success = await SendAndReceiveUpdateAsync(request, isProcessSuspended, cancellationToken); - if (success) - { - appliedUpdateCount++; - } - } + // Only cancel apply operation when the process exits: + return QueueUpdateBatchRequest(request, processExitedCancellationToken); + }); - Logger.Log(LogEvents.UpdatesApplied, appliedUpdateCount, updates.Length); + return CompleteApplyOperationAsync(); - return - (appliedUpdateCount == 0) ? ApplyStatus.Failed : - (appliedUpdateCount < updates.Length) ? ApplyStatus.SomeChangesApplied : ApplyStatus.AllChangesApplied; + async Task CompleteApplyOperationAsync() + { + var results = await Task.WhenAll(completionTasks); + return results.All(isSuccess => isSuccess); + } } - private ValueTask SendAndReceiveUpdateAsync(TRequest request, bool isProcessSuspended, CancellationToken cancellationToken) + private Task QueueUpdateBatchRequest(TRequest request, CancellationToken applyOperationCancellationToken) where TRequest : IUpdateRequest { - // Should not initialized: + // Not initialized: Debug.Assert(_pipe != null); - return SendAndReceiveUpdateAsync( - send: SendAndReceiveAsync, - isProcessSuspended, - suspendedResult: true, - cancellationToken); - - async ValueTask SendAndReceiveAsync(int batchId, CancellationToken cancellationToken) - { - Logger.LogDebug("Sending update batch #{UpdateId}", batchId); - - try - { - await WriteRequestAsync(cancellationToken); - - if (await ReceiveUpdateResponseAsync(cancellationToken)) - { - Logger.LogDebug("Update batch #{UpdateId} completed.", batchId); - return true; - } - - Logger.LogDebug("Update batch #{UpdateId} failed.", batchId); - } - catch (Exception e) + return QueueUpdateBatch( + sendAndReceive: async batchId => { - // Don't report an error when cancelled. The process has terminated or the host is shutting down in that case. - // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. - if (cancellationToken.IsCancellationRequested) - { - Logger.LogDebug("Update batch #{UpdateId} canceled.", batchId); - } - else - { - Logger.LogError("Update batch #{UpdateId} failed with error: {Message}", batchId, e.Message); - Logger.LogDebug("Update batch #{UpdateId} exception stack trace: {StackTrace}", batchId, e.StackTrace); - } - } - - return false; - } - - async ValueTask WriteRequestAsync(CancellationToken cancellationToken) - { - await _pipe.WriteAsync((byte)request.Type, cancellationToken); - await request.WriteAsync(_pipe, cancellationToken); - await _pipe.FlushAsync(cancellationToken); - } + await _pipe.WriteAsync((byte)request.Type, applyOperationCancellationToken); + await request.WriteAsync(_pipe, applyOperationCancellationToken); + await _pipe.FlushAsync(applyOperationCancellationToken); + + var success = await ReceiveUpdateResponseAsync(applyOperationCancellationToken); + Logger.Log(success ? LogEvents.UpdateBatchCompleted : LogEvents.UpdateBatchFailed, batchId); + return success; + }, + applyOperationCancellationToken); } private async ValueTask ReceiveUpdateResponseAsync(CancellationToken cancellationToken) diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClient.cs b/src/BuiltInTools/HotReloadClient/HotReloadClient.cs index e8bccc3ee90e..6e45c8c76d78 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadClient.cs @@ -69,16 +69,18 @@ protected static ImmutableArray AddImplicitCapabilities(IEnumerable> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken); /// - /// Applies managed code updates to the target process. + /// Returns a task that applies managed code updates to the target process. /// - /// Cancellation token. The cancellation should trigger on process terminatation. - public abstract Task ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken); + /// The token used to cancel creation of the apply task. + /// The token to be used to cancel the apply operation. Should trigger on process terminatation. + public abstract Task> ApplyManagedCodeUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken); /// - /// Applies static asset updates to the target process. + /// Returns a task that applies static asset updates to the target process. /// - /// Cancellation token. The cancellation should trigger on process terminatation. - public abstract Task ApplyStaticAssetUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken); + /// The token used to cancel creation of the apply task. + /// The token to be used to cancel the apply operation. Should trigger on process terminatation. + public abstract Task> ApplyStaticAssetUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken); /// /// Notifies the agent that the initial set of updates has been applied and the user code in the process can start executing. @@ -133,13 +135,13 @@ protected async Task> FilterApplicable return applicableUpdates; } - protected async ValueTask SendAndReceiveUpdateAsync( - Func> send, - bool isProcessSuspended, - TResult suspendedResult, - CancellationToken cancellationToken) - where TResult : struct + /// + /// Queues a batch of updates to be applied in the target process. + /// + protected Task QueueUpdateBatch(Func> sendAndReceive, CancellationToken applyOperationCancellationToken) { + var completionSource = new TaskCompletionSource(); + var batchId = _updateBatchId++; Task previous; @@ -147,19 +149,31 @@ protected async ValueTask SendAndReceiveUpdateAsync( { previous = _pendingUpdates; - if (isProcessSuspended) + _pendingUpdates = Task.Run(async () => { - _pendingUpdates = Task.Run(async () => - { - await previous; - _ = await send(batchId, cancellationToken); - }, cancellationToken); + await previous; - return suspendedResult; - } + try + { + Logger.Log(LogEvents.SendingUpdateBatch, batchId); + completionSource.SetResult(await sendAndReceive(batchId)); + } + catch (OperationCanceledException) + { + // Don't report an error when cancelled. The process has terminated or the host is shutting down in that case. + // Best effort: There is an inherent race condition due to time between the process exiting and the cancellation token triggering. + Logger.Log(LogEvents.UpdateBatchCanceled, batchId); + completionSource.SetCanceled(); + } + catch (Exception e) + { + Logger.Log(LogEvents.UpdateBatchFailedWithError, batchId, e.Message); + Logger.Log(LogEvents.UpdateBatchExceptionStackTrace, batchId, e.StackTrace ?? ""); + completionSource.SetResult(false); + } + }, applyOperationCancellationToken); } - await previous; - return await send(batchId, cancellationToken); + return completionSource.Task; } } diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs index 35b99dc1e437..1b02eac9de48 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadClients.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadClients.cs @@ -112,61 +112,22 @@ public async ValueTask> GetUpdateCapabilitiesAsync(Cancel } /// Cancellation token. The cancellation should trigger on process terminatation. - /// True if the updates are initial updates applied automatically when a process starts. - public async ValueTask ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, bool isInitial, CancellationToken cancellationToken) + public async Task ApplyManagedCodeUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken) { - var anyFailure = false; + // Apply to all processes. + // The module the change is for does not need to be loaded to any of the processes, yet we still consider it successful if the application does not fail. + // In each process we store the deltas for application when/if the module is loaded to the process later. + // An error is only reported if the delta application fails, which would be a bug either in the runtime (applying valid delta incorrectly), + // the compiler (producing wrong delta), or rude edit detection (the change shouldn't have been allowed). - if (clients is [var (singleClient, _)]) - { - anyFailure = await singleClient.ApplyManagedCodeUpdatesAsync(updates, isProcessSuspended, cancellationToken) == ApplyStatus.Failed; - } - else - { - // Apply to all processes. - // The module the change is for does not need to be loaded to any of the processes, yet we still consider it successful if the application does not fail. - // In each process we store the deltas for application when/if the module is loaded to the process later. - // An error is only reported if the delta application fails, which would be a bug either in the runtime (applying valid delta incorrectly), - // the compiler (producing wrong delta), or rude edit detection (the change shouldn't have been allowed). - - var results = await Task.WhenAll(clients.Select(c => c.client.ApplyManagedCodeUpdatesAsync(updates, isProcessSuspended, cancellationToken))); - - var index = 0; - foreach (var status in results) - { - var (client, name) = clients[index++]; + var applyTasks = await Task.WhenAll(clients.Select(c => c.client.ApplyManagedCodeUpdatesAsync(updates, applyOperationCancellationToken, cancellationToken))); - switch (status) - { - case ApplyStatus.Failed: - anyFailure = true; - break; + return CompleteApplyOperationAsync(); - case ApplyStatus.AllChangesApplied: - break; - - case ApplyStatus.SomeChangesApplied: - client.Logger.LogWarning("Some changes not applied to {Name} because they are not supported by the runtime.", name); - break; - - case ApplyStatus.NoChangesApplied: - client.Logger.LogWarning("No changes applied to {Name} because they are not supported by the runtime.", name); - break; - } - } - } - - if (!anyFailure) + async Task CompleteApplyOperationAsync() { - // Only report status for updates made directly by the user, not for initial updates. - if (!isInitial) - { - // all clients share the same loggers, pick any: - var logger = clients[0].client.Logger; - logger.Log(LogEvents.HotReloadSucceeded); - } - - if (browserRefreshServer != null) + var results = await Task.WhenAll(applyTasks); + if (browserRefreshServer != null && results.All(isSuccess => isSuccess)) { await browserRefreshServer.RefreshBrowserAsync(cancellationToken); } @@ -187,45 +148,38 @@ public async ValueTask InitialUpdatesAppliedAsync(CancellationToken cancellation } /// Cancellation token. The cancellation should trigger on process terminatation. - public async Task ApplyStaticAssetUpdatesAsync(IEnumerable assets, CancellationToken cancellationToken) + public async Task ApplyStaticAssetUpdatesAsync(IEnumerable assets, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken) { if (browserRefreshServer != null) { - await browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.RelativeUrl), cancellationToken); + return browserRefreshServer.UpdateStaticAssetsAsync(assets.Select(static a => a.RelativeUrl), applyOperationCancellationToken).AsTask(); } - else - { - var updates = new List(); - foreach (var asset in assets) + var updates = new List(); + + foreach (var asset in assets) + { + try { - try - { - ClientLogger.LogDebug("Loading asset '{Url}' from '{Path}'.", asset.RelativeUrl, asset.FilePath); - updates.Add(await HotReloadStaticAssetUpdate.CreateAsync(asset, cancellationToken)); - } - catch (Exception e) when (e is not OperationCanceledException) - { - ClientLogger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message); - continue; - } + ClientLogger.LogDebug("Loading asset '{Url}' from '{Path}'.", asset.RelativeUrl, asset.FilePath); + updates.Add(await HotReloadStaticAssetUpdate.CreateAsync(asset, cancellationToken)); + } + catch (Exception e) when (e is not OperationCanceledException) + { + ClientLogger.LogError("Failed to read file {FilePath}: {Message}", asset.FilePath, e.Message); + continue; } - - await ApplyStaticAssetUpdatesAsync([.. updates], isProcessSuspended: false, cancellationToken); } + + return await ApplyStaticAssetUpdatesAsync([.. updates], applyOperationCancellationToken, cancellationToken); } /// Cancellation token. The cancellation should trigger on process terminatation. - public async ValueTask ApplyStaticAssetUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) + public async ValueTask ApplyStaticAssetUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken) { - if (clients is [var (singleClient, _)]) - { - await singleClient.ApplyStaticAssetUpdatesAsync(updates, isProcessSuspended, cancellationToken); - } - else - { - await Task.WhenAll(clients.Select(c => c.client.ApplyStaticAssetUpdatesAsync(updates, isProcessSuspended, cancellationToken))); - } + var applyTasks = await Task.WhenAll(clients.Select(c => c.client.ApplyStaticAssetUpdatesAsync(updates, applyOperationCancellationToken, cancellationToken))); + + return Task.WhenAll(applyTasks); } /// Cancellation token. The cancellation should trigger on process terminatation. diff --git a/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs b/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs index 7be92781e01c..8b43cb4bad29 100644 --- a/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs +++ b/src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs @@ -20,9 +20,13 @@ private static LogEvent Create(LogLevel level, string message) public static void Log(this ILogger logger, LogEvent logEvent, params object[] args) => logger.Log(logEvent.Level, logEvent.Id, logEvent.Message, args); - public static readonly LogEvent UpdatesApplied = Create(LogLevel.Debug, "Updates applied: {0} out of {1}."); - public static readonly LogEvent Capabilities = Create(LogLevel.Debug, "Capabilities: '{1}'."); - public static readonly LogEvent HotReloadSucceeded = Create(LogLevel.Information, "Hot reload succeeded."); + public static readonly LogEvent SendingUpdateBatch = Create(LogLevel.Debug, "Sending update batch #{0}"); + public static readonly LogEvent UpdateBatchCompleted = Create(LogLevel.Debug, "Update batch #{0} completed."); + public static readonly LogEvent UpdateBatchFailed = Create(LogLevel.Debug, "Update batch #{0} failed."); + public static readonly LogEvent UpdateBatchCanceled = Create(LogLevel.Debug, "Update batch #{0} canceled."); + public static readonly LogEvent UpdateBatchFailedWithError = Create(LogLevel.Debug, "Update batch #{0} failed with error: {1}"); + public static readonly LogEvent UpdateBatchExceptionStackTrace = Create(LogLevel.Debug, "Update batch #{0} exception stack trace: {1}"); + public static readonly LogEvent Capabilities = Create(LogLevel.Debug, "Capabilities: '{0}'."); public static readonly LogEvent RefreshingBrowser = Create(LogLevel.Debug, "Refreshing browser."); public static readonly LogEvent ReloadingBrowser = Create(LogLevel.Debug, "Reloading browser."); public static readonly LogEvent SendingWaitMessage = Create(LogLevel.Debug, "Sending wait message."); diff --git a/src/BuiltInTools/HotReloadClient/Microsoft.DotNet.HotReload.Client.Package.csproj b/src/BuiltInTools/HotReloadClient/Microsoft.DotNet.HotReload.Client.Package.csproj index ba105a785823..362b369e9eed 100644 --- a/src/BuiltInTools/HotReloadClient/Microsoft.DotNet.HotReload.Client.Package.csproj +++ b/src/BuiltInTools/HotReloadClient/Microsoft.DotNet.HotReload.Client.Package.csproj @@ -39,6 +39,7 @@ + diff --git a/src/BuiltInTools/HotReloadClient/Utilities/ResponseAction.cs b/src/BuiltInTools/HotReloadClient/Utilities/ResponseFunc.cs similarity index 70% rename from src/BuiltInTools/HotReloadClient/Utilities/ResponseAction.cs rename to src/BuiltInTools/HotReloadClient/Utilities/ResponseFunc.cs index 64a2cbcb7183..160bce801a6e 100644 --- a/src/BuiltInTools/HotReloadClient/Utilities/ResponseAction.cs +++ b/src/BuiltInTools/HotReloadClient/Utilities/ResponseFunc.cs @@ -4,9 +4,11 @@ #nullable enable using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.HotReload; // Workaround for ReadOnlySpan not working as a generic parameter on .NET Framework -public delegate void ResponseAction(ReadOnlySpan data, ILogger logger); +public delegate TResult ResponseFunc(ReadOnlySpan data, ILogger logger); diff --git a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs index d39ea6c0f519..79c5d957265b 100644 --- a/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs +++ b/src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs @@ -239,16 +239,20 @@ public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken) return SendAsync(JsonWaitRequest.Message, cancellationToken); } - private ValueTask SendAsync(ReadOnlyMemory messageBytes, CancellationToken cancellationToken) - => SendAndReceiveAsync(request: _ => messageBytes, response: null, cancellationToken); + private async ValueTask SendAsync(ReadOnlyMemory messageBytes, CancellationToken cancellationToken) + { + await SendAndReceiveAsync, VoidResult>(request: _ => messageBytes, response: null, cancellationToken); + } - public async ValueTask SendAndReceiveAsync( + public async ValueTask SendAndReceiveAsync( Func? request, - ResponseAction? response, + ResponseFunc? response, CancellationToken cancellationToken) + where TResult : struct { var responded = false; var openConnections = GetOpenBrowserConnections(); + var result = default(TResult?); foreach (var connection in openConnections) { @@ -263,7 +267,7 @@ public async ValueTask SendAndReceiveAsync( } } - if (response != null && !await connection.TryReceiveMessageAsync(response, cancellationToken)) + if (response != null && (result = await connection.TryReceiveMessageAsync(response, cancellationToken)) == null) { continue; } @@ -281,6 +285,7 @@ public async ValueTask SendAndReceiveAsync( } DisposeClosedBrowserConnections(); + return result; } public ValueTask RefreshBrowserAsync(CancellationToken cancellationToken) diff --git a/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs b/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs index 498c26110089..74bfabc268d9 100644 --- a/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs +++ b/src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs @@ -68,7 +68,8 @@ internal async ValueTask TrySendMessageAsync(ReadOnlyMemory messageB return true; } - internal async ValueTask TryReceiveMessageAsync(ResponseAction receiver, CancellationToken cancellationToken) + internal async ValueTask TryReceiveMessageAsync(ResponseFunc receiver, CancellationToken cancellationToken) + where TResponseResult : struct { var writer = new ArrayBufferWriter(initialCapacity: 1024); @@ -88,12 +89,12 @@ internal async ValueTask TryReceiveMessageAsync(ResponseAction receiver, C catch (Exception e) when (e is not OperationCanceledException) { ServerLogger.LogDebug("Failed to receive response: {Message}", e.Message); - return false; + return null; } if (result.MessageType == WebSocketMessageType.Close) { - return false; + return null; } writer.Advance(result.Count); @@ -103,7 +104,6 @@ internal async ValueTask TryReceiveMessageAsync(ResponseAction receiver, C } } - receiver(writer.WrittenSpan, AgentLogger); - return true; + return receiver(writer.WrittenSpan, AgentLogger); } } diff --git a/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs index d4a165e0793a..dee0f34ed354 100644 --- a/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs @@ -91,18 +91,18 @@ public override async Task WaitForConnectionEstablishedAsync(CancellationToken c public override Task> GetUpdateCapabilitiesAsync(CancellationToken cancellationToken) => Task.FromResult(_capabilities); - public override async Task ApplyManagedCodeUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) + public override async Task> ApplyManagedCodeUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken) { var applicableUpdates = await FilterApplicableUpdatesAsync(updates, cancellationToken); if (applicableUpdates.Count == 0) { - return ApplyStatus.NoChangesApplied; + return Task.FromResult(true); } // When testing abstract away the browser and pretend all changes have been applied: if (suppressBrowserRequestsForTesting) { - return ApplyStatus.AllChangesApplied; + return Task.FromResult(true); } // Make sure to send the same update to all browsers, the only difference is the shared secret. @@ -117,59 +117,42 @@ public override async Task ApplyManagedCodeUpdatesAsync(ImmutableAr var loggingLevel = Logger.IsEnabled(LogLevel.Debug) ? ResponseLoggingLevel.Verbose : ResponseLoggingLevel.WarningsAndErrors; - var (anySuccess, anyFailure) = await SendAndReceiveUpdateAsync( - send: SendAndReceiveAsync, - isProcessSuspended, - suspendedResult: (anySuccess: true, anyFailure: false), - cancellationToken); - // If no browser is connected we assume the changes have been applied. // If at least one browser suceeds we consider the changes successfully applied. // TODO: // The refresh server should remember the deltas and apply them to browsers connected in future. // Currently the changes are remembered on the dev server and sent over there from the browser. // If no browser is connected the changes are not sent though. - return (!anySuccess && anyFailure) ? ApplyStatus.Failed : (applicableUpdates.Count < updates.Length) ? ApplyStatus.SomeChangesApplied : ApplyStatus.AllChangesApplied; - async ValueTask<(bool anySuccess, bool anyFailure)> SendAndReceiveAsync(int batchId, CancellationToken cancellationToken) - { - Logger.LogDebug("Sending update batch #{UpdateId}", batchId); - - var anySuccess = false; - var anyFailure = false; - - await browserRefreshServer.SendAndReceiveAsync( - request: sharedSecret => new JsonApplyManagedCodeUpdatesRequest - { - SharedSecret = sharedSecret, - UpdateId = batchId, - Deltas = deltas, - ResponseLoggingLevel = (int)loggingLevel - }, - response: new ResponseAction((value, logger) => - { - if (ReceiveUpdateResponseAsync(value, logger)) + return QueueUpdateBatch( + sendAndReceive: async batchId => + { + var result = await browserRefreshServer.SendAndReceiveAsync( + request: sharedSecret => new JsonApplyManagedCodeUpdatesRequest { - Logger.LogDebug("Update batch #{UpdateId} completed.", batchId); - anySuccess = true; - } - else + SharedSecret = sharedSecret, + UpdateId = batchId, + Deltas = deltas, + ResponseLoggingLevel = (int)loggingLevel + }, + response: new ResponseFunc((value, logger) => { - Logger.LogDebug("Update batch #{UpdateId} failed.", batchId); - anyFailure = true; - } - }), - cancellationToken); - - return (anySuccess, anyFailure); - } + var success = ReceiveUpdateResponse(value, logger); + Logger.Log(success ? LogEvents.UpdateBatchCompleted : LogEvents.UpdateBatchFailed, batchId); + return success; + }), + applyOperationCancellationToken); + + return result ?? false; + }, + applyOperationCancellationToken); } - public override Task ApplyStaticAssetUpdatesAsync(ImmutableArray updates, bool isProcessSuspended, CancellationToken cancellationToken) + public override Task> ApplyStaticAssetUpdatesAsync(ImmutableArray updates, CancellationToken applyOperationCancellationToken, CancellationToken cancellationToken) // static asset updates are handled by browser refresh server: - => Task.FromResult(ApplyStatus.NoChangesApplied); + => Task.FromResult(Task.FromResult(true)); - private static bool ReceiveUpdateResponseAsync(ReadOnlySpan value, ILogger logger) + private static bool ReceiveUpdateResponse(ReadOnlySpan value, ILogger logger) { var data = AbstractBrowserRefreshServer.DeserializeJson(value); diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs index 0bae06178441..72fadfe6f024 100644 --- a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs @@ -46,7 +46,7 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti var console = new PhysicalConsole(TestFlags.None); var reporter = new ConsoleReporter(console, suppressEmojis: false); var environmentOptions = EnvironmentOptions.FromEnvironment(muxerPath); - var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true)); + var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout()); var loggerFactory = new LoggerFactory(reporter, globalOptions.LogLevel); var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName); diff --git a/src/BuiltInTools/Watch/Build/BuildNames.cs b/src/BuiltInTools/Watch/Build/BuildNames.cs index 0b2b8d0ccd00..a2d0b790ce0d 100644 --- a/src/BuiltInTools/Watch/Build/BuildNames.cs +++ b/src/BuiltInTools/Watch/Build/BuildNames.cs @@ -22,6 +22,7 @@ internal static class PropertyNames public const string DesignTimeBuild = nameof(DesignTimeBuild); public const string SkipCompilerExecution = nameof(SkipCompilerExecution); public const string ProvideCommandLineArgs = nameof(ProvideCommandLineArgs); + public const string NonExistentFile = nameof(NonExistentFile); } internal static class ItemNames diff --git a/src/BuiltInTools/Watch/Build/EvaluationResult.cs b/src/BuiltInTools/Watch/Build/EvaluationResult.cs index 829f527774e0..cc0566e49b39 100644 --- a/src/BuiltInTools/Watch/Build/EvaluationResult.cs +++ b/src/BuiltInTools/Watch/Build/EvaluationResult.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.Build.Execution; using Microsoft.Build.Graph; using Microsoft.DotNet.HotReload; @@ -51,7 +52,9 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera .SetItem(PropertyNames.DotNetWatchBuild, "true") .SetItem(PropertyNames.DesignTimeBuild, "true") .SetItem(PropertyNames.SkipCompilerExecution, "true") - .SetItem(PropertyNames.ProvideCommandLineArgs, "true"); + .SetItem(PropertyNames.ProvideCommandLineArgs, "true") + // this will force CoreCompile task to execute and return command line args even if all inputs and outputs are up to date: + .SetItem(PropertyNames.NonExistentFile, "__NonExistentSubDir__\\__NonExistentFile__"); } /// @@ -126,6 +129,9 @@ public static ImmutableDictionary GetGlobalBuildOptions(IEnumera } } + // command line args items should be available: + Debug.Assert(Path.GetExtension(projectInstance.FullPath) != ".csproj" || projectInstance.GetItems("CscCommandLineArgs").Any()); + var projectPath = projectInstance.FullPath; var projectDirectory = Path.GetDirectoryName(projectPath)!; diff --git a/src/BuiltInTools/Watch/Build/FileItem.cs b/src/BuiltInTools/Watch/Build/FileItem.cs index bd6719f2ca00..3d3c4fd6ffb2 100644 --- a/src/BuiltInTools/Watch/Build/FileItem.cs +++ b/src/BuiltInTools/Watch/Build/FileItem.cs @@ -1,7 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Watch { internal readonly record struct FileItem diff --git a/src/BuiltInTools/Watch/Build/ProjectGraphFactory.cs b/src/BuiltInTools/Watch/Build/ProjectGraphFactory.cs index 520bd4b8f972..56ffe6560b0e 100644 --- a/src/BuiltInTools/Watch/Build/ProjectGraphFactory.cs +++ b/src/BuiltInTools/Watch/Build/ProjectGraphFactory.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; @@ -29,7 +29,7 @@ internal sealed class ProjectGraphFactory(ImmutableDictionary gl reuseProjectRootElementCache: true); /// - /// Tries to create a project graph by running the build evaluation phase on the . + /// Tries to create a project graph by running the build evaluation phase on the . /// public ProjectGraph? TryLoadProjectGraph( string rootProjectFile, diff --git a/src/BuiltInTools/Watch/Build/ProjectNodeMap.cs b/src/BuiltInTools/Watch/Build/ProjectNodeMap.cs index a916346da9cc..69e2e5a0440b 100644 --- a/src/BuiltInTools/Watch/Build/ProjectNodeMap.cs +++ b/src/BuiltInTools/Watch/Build/ProjectNodeMap.cs @@ -1,7 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using Microsoft.Build.Graph; using Microsoft.Extensions.Logging; diff --git a/src/BuiltInTools/Watch/Context/DotNetWatchContext.cs b/src/BuiltInTools/Watch/Context/DotNetWatchContext.cs index 591b6e283302..f7caada6824c 100644 --- a/src/BuiltInTools/Watch/Context/DotNetWatchContext.cs +++ b/src/BuiltInTools/Watch/Context/DotNetWatchContext.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 Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch diff --git a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs index 61eac70ca07d..0d133c351d78 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs @@ -63,7 +63,7 @@ internal sealed record EnvironmentOptions( TestOutput: EnvironmentVariables.TestOutputDir ); - public TimeSpan GetProcessCleanupTimeout(bool isHotReloadEnabled) + public TimeSpan GetProcessCleanupTimeout() // Allow sufficient time for the process to exit gracefully and release resources (e.g., network ports). => ProcessCleanupTimeout ?? TimeSpan.FromSeconds(5); diff --git a/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs index 3f5bff195e92..1e445f5e9cb9 100644 --- a/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs +++ b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs @@ -74,7 +74,7 @@ private void Watch(IEnumerable filePaths, bool containingDirectories, bo from path in filePaths group path by PathUtilities.EnsureTrailingSlash(PathUtilities.NormalizeDirectorySeparators(Path.GetDirectoryName(path)!)) into g - select (g.Key, containingDirectories ? [] : g.Select(path => Path.GetFileName(path)).ToImmutableHashSet(PathUtilities.OSSpecificPathComparer)); + select (g.Key, containingDirectories ? [] : g.Select(Path.GetFileName).ToImmutableHashSet(PathUtilities.OSSpecificPathComparer)); foreach (var (directory, fileNames) in filesByDirectory) { diff --git a/src/BuiltInTools/Watch/FileWatcher/PollingDirectoryWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/PollingDirectoryWatcher.cs index 143881c65f7e..89a77475aa77 100644 --- a/src/BuiltInTools/Watch/FileWatcher/PollingDirectoryWatcher.cs +++ b/src/BuiltInTools/Watch/FileWatcher/PollingDirectoryWatcher.cs @@ -19,7 +19,7 @@ internal sealed class PollingDirectoryWatcher : DirectoryWatcher private Dictionary _snapshotBuilder = new(PathUtilities.OSSpecificPathComparer); private readonly Dictionary _changesBuilder = new(PathUtilities.OSSpecificPathComparer); - private Thread _pollingThread; + private readonly Thread _pollingThread; private bool _raiseEvents; private volatile bool _disposed; @@ -88,10 +88,7 @@ private void CaptureInitialSnapshot() { Debug.Assert(_currentSnapshot.Count == 0); - ForeachEntityInDirectory(_watchedDirectory, (filePath, writeTime) => - { - _currentSnapshot.Add(filePath, writeTime); - }); + ForeachEntityInDirectory(_watchedDirectory, _currentSnapshot.Add); } private void CheckForChangedFiles() diff --git a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs index a8f73dbb48df..360d49ebf8ae 100644 --- a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; @@ -23,7 +23,6 @@ internal sealed class CompilationHandler : IDisposable /// Lock to synchronize: /// /// - /// /// private readonly object _runningProjectsAndUpdatesGuard = new(); @@ -181,7 +180,10 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) var updatesToApply = _previousUpdates.Skip(appliedUpdateCount).ToImmutableArray(); if (updatesToApply.Any()) { - await clients.ApplyManagedCodeUpdatesAsync(ToManagedCodeUpdates(updatesToApply), isProcessSuspended: false, isInitial: true, processCommunicationCancellationToken); + await await clients.ApplyManagedCodeUpdatesAsync( + ToManagedCodeUpdates(updatesToApply), + applyOperationCancellationToken: processExitedSource.Token, + cancellationToken: processCommunicationCancellationToken); } appliedUpdateCount += updatesToApply.Length; @@ -366,7 +368,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C return (updates.ProjectUpdates, projectsToRebuild, projectsToRedeploy, projectsToRestart); } - public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) + public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, Stopwatch stopwatch, CancellationToken cancellationToken) { Debug.Assert(!updates.IsEmpty); @@ -383,18 +385,43 @@ public async ValueTask ApplyUpdatesAsync(ImmutableArray // Apply changes to all running projects, even if they do not have a static project dependency on any project that changed. // The process may load any of the binaries using MEF or some other runtime dependency loader. - await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationToken) => + var applyTasks = new List(); + + foreach (var (_, projects) in projectsToUpdate) { - try + foreach (var runningProject in projects) { - using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken); - await runningProject.Clients.ApplyManagedCodeUpdatesAsync(ToManagedCodeUpdates(updates), isProcessSuspended: false, isInitial: false, processCommunicationCancellationSource.Token); + // Only cancel applying updates when the process exits. Canceling disables further updates since the state of the runtime becomes unknown. + var applyTask = await runningProject.Clients.ApplyManagedCodeUpdatesAsync( + ToManagedCodeUpdates(updates), + applyOperationCancellationToken: runningProject.ProcessExitedCancellationToken, + cancellationToken); + + applyTasks.Add(runningProject.CompleteApplyOperationAsync(applyTask)); } - catch (OperationCanceledException) when (runningProject.ProcessExitedCancellationToken.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + } + + // fire and forget: + _ = CompleteApplyOperationAsync(applyTasks, stopwatch, MessageDescriptor.ManagedCodeChangesApplied); + } + + private async Task CompleteApplyOperationAsync(IEnumerable applyTasks, Stopwatch stopwatch, MessageDescriptor message) + { + try + { + await Task.WhenAll(applyTasks); + + _context.Logger.Log(message, stopwatch.ElapsedMilliseconds); + } + catch (Exception e) + { + // Handle all exceptions since this is a fire-and-forget task. + + if (e is not OperationCanceledException) { - runningProject.Clients.ClientLogger.Log(MessageDescriptor.HotReloadCanceledProcessExited); + _context.Logger.LogError("Failed to apply updates: {Exception}", e.ToString()); } - }, cancellationToken); + } } private static RunningProject? GetCorrespondingRunningProject(Project project, ImmutableDictionary> runningProjects) @@ -556,12 +583,13 @@ static MessageDescriptor GetMessageDescriptor(Diagnostic diagnostic, bool verbos private static readonly string[] s_targets = [TargetNames.GenerateComputedBuildStaticWebAssets, TargetNames.ResolveReferencedProjectsStaticWebAssets]; private static bool HasScopedCssTargets(ProjectInstance projectInstance) - => s_targets.All(t => projectInstance.Targets.ContainsKey(t)); + => s_targets.All(projectInstance.Targets.ContainsKey); public async ValueTask HandleStaticAssetChangesAsync( IReadOnlyList files, ProjectNodeMap projectMap, IReadOnlyDictionary manifests, + Stopwatch stopwatch, CancellationToken cancellationToken) { var assets = new Dictionary>(); @@ -697,30 +725,36 @@ public async ValueTask HandleStaticAssetChangesAsync( } } - var tasks = assets.Select(async entry => - { - var (applicationProjectInstance, instanceAssets) = entry; + // Creating apply tasks involves reading static assets from disk. Parallelize this IO. + var applyTaskProducers = new List>(); + foreach (var (applicationProjectInstance, instanceAssets) in assets) + { if (failedApplicationProjectInstances?.Contains(applicationProjectInstance) == true) { - return; + continue; } if (!TryGetRunningProject(applicationProjectInstance.FullPath, out var runningProjects)) { - return; + continue; } foreach (var runningProject in runningProjects) { - using var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(runningProject.ProcessExitedCancellationToken, cancellationToken); - await runningProject.Clients.ApplyStaticAssetUpdatesAsync(instanceAssets.Values, processCommunicationCancellationSource.Token); + // Only cancel applying updates when the process exits. Canceling in-progress static asset update might be ok, + // but for consistency with managed code updates we only cancel when the process exits. + applyTaskProducers.Add(runningProject.Clients.ApplyStaticAssetUpdatesAsync( + instanceAssets.Values, + applyOperationCancellationToken: runningProject.ProcessExitedCancellationToken, + cancellationToken)); } - }); + } - await Task.WhenAll(tasks).WaitAsync(cancellationToken); + var applyTasks = await Task.WhenAll(applyTaskProducers); - Logger.Log(MessageDescriptor.StaticAssetsReloaded); + // fire and forget: + _ = CompleteApplyOperationAsync(applyTasks, stopwatch, MessageDescriptor.StaticAssetsChangesApplied); } /// diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs index ab85d94c08f0..93cde1a44a06 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; @@ -145,7 +145,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) } // Cancel iteration as soon as the root process exits, so that we don't spent time loading solution, etc. when the process is already dead. - rootRunningProject.ProcessExitedCancellationToken.Register(() => iterationCancellationSource.Cancel()); + rootRunningProject.ProcessExitedCancellationToken.Register(iterationCancellationSource.Cancel); if (shutdownCancellationToken.IsCancellationRequested) { @@ -261,7 +261,7 @@ void FileChangedCallback(ChangedPath change) var stopwatch = Stopwatch.StartNew(); HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.StaticHandler); - await compilationHandler.HandleStaticAssetChangesAsync(changedFiles, projectMap, evaluationResult.StaticWebAssetsManifests, iterationCancellationToken); + await compilationHandler.HandleStaticAssetChangesAsync(changedFiles, projectMap, evaluationResult.StaticWebAssetsManifests, stopwatch, iterationCancellationToken); HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.StaticHandler); HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler); @@ -378,7 +378,7 @@ void FileChangedCallback(ChangedPath change) // so that updated code doesn't attempt to access the dependency before it has been deployed. if (!managedCodeUpdates.IsEmpty) { - await compilationHandler.ApplyUpdatesAsync(managedCodeUpdates, iterationCancellationToken); + await compilationHandler.ApplyUpdatesAsync(managedCodeUpdates, stopwatch, iterationCancellationToken); } if (!projectsToRestart.IsEmpty) @@ -394,8 +394,6 @@ await Task.WhenAll( _context.Logger.Log(MessageDescriptor.ProjectsRestarted, projectsToRestart.Length); } - _context.Logger.Log(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds); - async Task> CaptureChangedFilesSnapshot(ImmutableArray rebuiltProjects) { var changedPaths = Interlocked.Exchange(ref changedFilesAccumulator, []); @@ -476,7 +474,7 @@ async Task> CaptureChangedFilesSnapshot(ImmutableArr foreach (var file in changedFiles) { - if (file.Item.ContainingProjectPaths.All(containingProjectPath => rebuiltProjectPaths.Contains(containingProjectPath))) + if (file.Item.ContainingProjectPaths.All(rebuiltProjectPaths.Contains)) { newChangedFiles.Add(file); } @@ -738,7 +736,7 @@ private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatche fileWatcher.WatchContainingDirectories([_context.RootProjectOptions.ProjectPath], includeSubdirectories: true); _ = await fileWatcher.WaitForFileChangeAsync( - acceptChange: change => AcceptChange(change), + acceptChange: AcceptChange, startedWatching: () => _context.Logger.Log(MessageDescriptor.WaitingForFileChangeBeforeRestarting), cancellationToken); } diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs index f14b000bb34c..1fa1a691b624 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.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.Diagnostics.Tracing; namespace Microsoft.DotNet.Watch diff --git a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs index 2fc7e54a9b34..1581a57f3c66 100644 --- a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs +++ b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; using Microsoft.CodeAnalysis.Host.Mef; @@ -76,7 +77,7 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok continue; } - newSolution = HotReloadService.WithProjectInfo(newSolution, ProjectInfo.Create( + newSolution = HotReloadService.WithProjectInfo(newSolution, WithChecksumAlgorithm(ProjectInfo.Create( oldProjectId, newProjectInfo.Version, newProjectInfo.Name, @@ -93,7 +94,8 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok MapDocuments(oldProjectId, newProjectInfo.AdditionalDocuments), isSubmission: false, hostObjectType: null, - outputRefFilePath: newProjectInfo.OutputRefFilePath) + outputRefFilePath: newProjectInfo.OutputRefFilePath), + GetChecksumAlgorithm(newProjectInfo)) .WithAnalyzerConfigDocuments(MapDocuments(oldProjectId, newProjectInfo.AnalyzerConfigDocuments)) .WithCompilationOutputInfo(newProjectInfo.CompilationOutputInfo)); } @@ -122,6 +124,20 @@ ImmutableArray MapDocuments(ProjectId mappedProjectId, IReadOnlyLi }).ToImmutableArray(); } + // TODO: remove + // workaround for https://github.com/dotnet/roslyn/pull/82051 + + private static MethodInfo? s_withChecksumAlgorithm; + private static PropertyInfo? s_getChecksumAlgorithm; + + private static ProjectInfo WithChecksumAlgorithm(ProjectInfo info, SourceHashAlgorithm algorithm) + => (ProjectInfo)(s_withChecksumAlgorithm ??= typeof(ProjectInfo).GetMethod("WithChecksumAlgorithm", BindingFlags.NonPublic | BindingFlags.Instance)!) + .Invoke(info, [algorithm])!; + + private static SourceHashAlgorithm GetChecksumAlgorithm(ProjectInfo info) + => (SourceHashAlgorithm)(s_getChecksumAlgorithm ??= typeof(ProjectInfo).GetProperty("ChecksumAlgorithm", BindingFlags.NonPublic | BindingFlags.Instance)!) + .GetValue(info)!; + public async ValueTask UpdateFileContentAsync(IEnumerable changedFiles, CancellationToken cancellationToken) { var updatedSolution = CurrentSolution; diff --git a/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj b/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj index 6b1847b3f788..10081bbd08ba 100644 --- a/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj +++ b/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj @@ -13,9 +13,9 @@ MicrosoftAspNetCore @@ -444,7 +444,7 @@ This is equivalent to deleting project.assets.json. The max number of test modules that can run in parallel. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. + Build failed with exit code: {0}. Specify either the project, solution, directory, or test modules option. @@ -1908,6 +1908,9 @@ Your project targets multiple frameworks. Specify which framework to run using ' The --solution-folder and --in-root options cannot be used together; use only one of the options. + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution Folder(s) @@ -2691,4 +2694,11 @@ Proceed? duration: + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + {0} is the workload set version. {Locked="dotnet workload repair"} + + + No test projects were found. + diff --git a/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs b/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs index f1b14ac9eeda..564dc3ceaedc 100644 --- a/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs +++ b/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs @@ -19,7 +19,7 @@ public static int Run(ParseResult parseResult) public static int RunWithReporter(string[] args, IReporter reporter) { - var result = Parser.Parse(["dotnet", "complete", ..args]); + var result = Parser.Parse(["dotnet", "complete", .. args]); return RunWithReporter(result, reporter); } diff --git a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs index 7bf9f15236a3..c6a6d87ff9df 100644 --- a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs +++ b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs @@ -26,7 +26,7 @@ public static int Run(ParseResult parseResult) public static void ProcessInputAndSendTelemetry(string[] args, ITelemetry telemetry) { - var result = Parser.Parse(["dotnet", "internal-reportinstallsuccess", ..args]); + var result = Parser.Parse(["dotnet", "internal-reportinstallsuccess", .. args]); ProcessInputAndSendTelemetry(result, telemetry); } diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs index ff5d7e4a6066..57a922005fc2 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs @@ -25,7 +25,7 @@ public class MSBuildCommand( { public static MSBuildCommand FromArgs(string[] args, string? msbuildPath = null) { - var result = Parser.Parse(["dotnet", "msbuild", ..args]); + var result = Parser.Parse(["dotnet", "msbuild", .. args]); return FromParseResult(result, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index bba916eb775f..ab7d0ba0542b 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -25,7 +25,7 @@ public class PackCommand( { public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "pack", ..args]); + var parseResult = Parser.Parse(["dotnet", "pack", .. args]); return FromParseResult(parseResult, msbuildPath); } @@ -97,14 +97,14 @@ public static int RunPackCommand(ParseResult parseResult) if (args.Count != 1) { - Console.Error.WriteLine(CliStrings.PackCmd_OneNuspecAllowed); + Console.Error.WriteLine(CliStrings.PackCmd_OneNuspecAllowed); return 1; } var nuspecPath = args[0]; var packArgs = new PackArgs() - { + { Logger = new NuGetConsoleLogger(), Exclude = new List(), OutputDirectory = parseResult.GetValue(definition.OutputOption), diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index db75885e6da0..bb68394da143 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -22,7 +22,7 @@ private PublishCommand( public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "publish", ..args]); + var parseResult = Parser.Parse(["dotnet", "publish", .. args]); return FromParseResult(parseResult); } diff --git a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs index a79d38066986..2743d1ffe048 100644 --- a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs +++ b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs @@ -13,7 +13,7 @@ public static class RestoreCommand { public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var result = Parser.Parse(["dotnet", "restore", ..args]); + var result = Parser.Parse(["dotnet", "restore", .. args]); return FromParseResult(result, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs b/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs index bf75c95e16a6..f87701357182 100644 --- a/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs +++ b/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs @@ -37,7 +37,7 @@ public RestoringCommand( string? msbuildPath = null, string? userProfileDir = null, bool? advertiseWorkloadUpdates = null) - : base(GetCommandArguments(msbuildArgs, noRestore), msbuildPath) + : base(GetCommandArguments(msbuildArgs, noRestore), msbuildPath) { userProfileDir = CliFolderPathCalculator.DotnetUserProfileFolderPath; Task.Run(() => WorkloadManifestUpdater.BackgroundUpdateAdvertisingManifestsAsync(userProfileDir)); @@ -118,13 +118,13 @@ private static MSBuildArgs GetCommandArguments( ReadOnlyDictionary restoreProperties = msbuildArgs.GlobalProperties? .Where(kvp => !IsPropertyExcludedFromRestore(kvp.Key))? - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase) is { } filteredList ? new(filteredList): ReadOnlyDictionary.Empty; + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase) is { } filteredList ? new(filteredList) : ReadOnlyDictionary.Empty; var restoreMSBuildArgs = MSBuildArgs.FromProperties(RestoreOptimizationProperties) .CloneWithAdditionalTargets("Restore") .CloneWithExplicitArgs([.. newArgumentsToAdd, .. existingArgumentsToForward]) .CloneWithAdditionalProperties(restoreProperties); - if (msbuildArgs.Verbosity is {} verbosity) + if (msbuildArgs.Verbosity is { } verbosity) { restoreMSBuildArgs = restoreMSBuildArgs.CloneWithVerbosity(verbosity); } @@ -171,7 +171,7 @@ private static bool HasPropertyToExcludeFromRestore(MSBuildArgs msbuildArgs) private static readonly List FlagsThatTriggerSilentSeparateRestore = [.. ComputeFlags(FlagsThatTriggerSilentRestore)]; - private static readonly List PropertiesToExcludeFromSeparateRestore = [ .. PropertiesToExcludeFromRestore ]; + private static readonly List PropertiesToExcludeFromSeparateRestore = [.. PropertiesToExcludeFromRestore]; /// /// We investigate the arguments we're about to send to a separate restore call and filter out diff --git a/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs new file mode 100644 index 000000000000..7c51ec53f605 --- /dev/null +++ b/src/Cli/dotnet/Commands/Run/EnvironmentVariablesToMSBuild.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; +using System.Xml; +using Microsoft.Build.Execution; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Commands.Run; + +/// +/// Provides utilities for passing environment variables to MSBuild as items. +/// Environment variables specified via dotnet run -e NAME=VALUE are passed +/// as <RuntimeEnvironmentVariable Include="NAME" Value="VALUE" /> items. +/// +internal static class EnvironmentVariablesToMSBuild +{ + private const string PropsFileName = "dotnet-run-env.props"; + + /// + /// Adds environment variables as MSBuild items to a ProjectInstance. + /// Use this for in-process MSBuild operations (e.g., DeployToDevice target). + /// + /// The MSBuild project instance to add items to. + /// The environment variables to add. + public static void AddAsItems(ProjectInstance projectInstance, IReadOnlyDictionary environmentVariables) + { + foreach (var (name, value) in environmentVariables) + { + projectInstance.AddItem(Constants.RuntimeEnvironmentVariable, name, new Dictionary + { + ["Value"] = value + }); + } + } + + /// + /// Creates a temporary .props file containing environment variables as MSBuild items. + /// Use this for out-of-process MSBuild operations where you need to inject items via + /// CustomBeforeMicrosoftCommonProps property. + /// + /// The full path to the project file. If null or empty, returns null. + /// The environment variables to include. + /// + /// Optional intermediate output path where the file will be created. + /// If null or empty, defaults to "obj" subdirectory of the project directory. + /// + /// The full path to the created props file, or null if no environment variables were specified or projectFilePath is null. + public static string? CreatePropsFile(string? projectFilePath, IReadOnlyDictionary environmentVariables, string? intermediateOutputPath = null) + { + if (string.IsNullOrEmpty(projectFilePath) || environmentVariables.Count == 0) + { + return null; + } + + string projectDirectory = Path.GetDirectoryName(projectFilePath) ?? ""; + + // Normalize path separators - MSBuild may return paths with backslashes on non-Windows + string normalized = intermediateOutputPath?.Replace('\\', Path.DirectorySeparatorChar) ?? ""; + string objDir = string.IsNullOrEmpty(normalized) + ? Path.Combine(projectDirectory, Constants.ObjDirectoryName) + : Path.IsPathRooted(normalized) + ? normalized + : Path.Combine(projectDirectory, normalized); + Directory.CreateDirectory(objDir); + + // Ensure we return a full path for MSBuild property usage + string propsFilePath = Path.GetFullPath(Path.Combine(objDir, PropsFileName)); + using (var stream = File.Create(propsFilePath)) + { + WritePropsFileContent(stream, environmentVariables); + } + + return propsFilePath; + } + + /// + /// Deletes the temporary environment variables props file if it exists. + /// + /// The path to the props file to delete. + public static void DeletePropsFile(string? propsFilePath) + { + if (propsFilePath is not null && File.Exists(propsFilePath)) + { + try + { + File.Delete(propsFilePath); + } + catch (Exception ex) + { + // Best effort cleanup - don't fail the build if we can't delete the temp file + Reporter.Verbose.WriteLine($"Failed to delete temporary props file '{propsFilePath}': {ex.Message}"); + } + } + } + + /// + /// Adds the props file property to the MSBuild arguments. + /// This uses CustomBeforeMicrosoftCommonProps to inject the props file early in evaluation. + /// + /// The base MSBuild arguments. + /// The path to the props file (from ). + /// The MSBuild arguments with the props file property added, or the original args if propsFilePath is null. + public static MSBuildArgs AddPropsFileToArgs(MSBuildArgs msbuildArgs, string? propsFilePath) + { + if (propsFilePath is null) + { + return msbuildArgs; + } + + // Add the props file via CustomBeforeMicrosoftCommonProps. + // This ensures the items are available early in evaluation, similar to how we add items + // directly to ProjectInstance for in-process target invocations. + var additionalProperties = new ReadOnlyDictionary(new Dictionary + { + [Constants.CustomBeforeMicrosoftCommonProps] = propsFilePath + }); + + return msbuildArgs.CloneWithAdditionalProperties(additionalProperties); + } + + /// + /// Writes the content of the .props file containing environment variables as items. + /// + private static void WritePropsFileContent(Stream stream, IReadOnlyDictionary environmentVariables) + { + using var writer = XmlWriter.Create(stream, new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true + }); + + writer.WriteStartElement("Project"); + writer.WriteStartElement("ItemGroup"); + + foreach (var (name, value) in environmentVariables) + { + writer.WriteStartElement(Constants.RuntimeEnvironmentVariable); + writer.WriteAttributeString("Include", name); + writer.WriteAttributeString("Value", value); + writer.WriteEndElement(); + } + + writer.WriteEndElement(); // ItemGroup + writer.WriteEndElement(); // Project + } +} diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 06292bb8b4f9..c750000bf5a9 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -156,7 +156,7 @@ public int Execute() { // Pre-run evaluation: Handle target framework and device selection for project-based scenarios using var selector = ProjectFileFullPath is not null - ? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, logger) + ? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, EnvironmentVariables, logger) : null; if (selector is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(selector)) { @@ -186,7 +186,7 @@ public int Execute() Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding); } - EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder); + EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder, selector?.IntermediateOutputPath, selector?.HasRuntimeEnvironmentVariableSupport ?? false); } else if (EntryPointFileFullPath is not null && launchProfileParseResult.Profile is not ExecutableLaunchProfile) { @@ -472,7 +472,7 @@ internal LaunchProfileParseResult ReadLaunchProfileSettings() return LaunchSettings.ReadProfileSettingsFromFile(launchSettingsPath, LaunchProfile); } - private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder) + private void EnsureProjectIsBuilt(out Func? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder, string? intermediateOutputPath, bool hasRuntimeEnvironmentVariableSupport) { int buildResult; if (EntryPointFileFullPath is not null) @@ -489,11 +489,28 @@ private void EnsureProjectIsBuilt(out Func? projectFactory = null; cachedRunProperties = null; projectBuilder = null; - buildResult = new RestoringCommand( - MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]), - NoRestore || _restoreDoneForDeviceSelection, - advertiseWorkloadUpdates: false - ).Execute(); + + // Create temporary props file for environment variables only if the project has opted in. + // This avoids invalidating incremental builds for projects that don't consume the items. + // Use IntermediateOutputPath from earlier project evaluation (via RunCommandSelector), defaulting to "obj" if not available. + string? envPropsFile = hasRuntimeEnvironmentVariableSupport + ? EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath) + : null; + try + { + var buildArgs = MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]); + buildArgs = EnvironmentVariablesToMSBuild.AddPropsFileToArgs(buildArgs, envPropsFile); + buildResult = new RestoringCommand( + buildArgs, + NoRestore || _restoreDoneForDeviceSelection, + advertiseWorkloadUpdates: false + ).Execute(); + } + finally + { + // Clean up temporary props file + EnvironmentVariablesToMSBuild.DeletePropsFile(envPropsFile); + } } if (buildResult != 0) @@ -578,7 +595,7 @@ private ICommand GetTargetCommandForProject(ProjectLaunchProfile? launchSettings { project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger); ValidatePreconditions(project); - InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs); + InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs, EnvironmentVariables); } finally { @@ -670,8 +687,15 @@ static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, s return command; } - static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs) + static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs, IReadOnlyDictionary environmentVariables) { + // Only add environment variables as MSBuild items if the project has opted in via capability + if (project.GetItems(Constants.ProjectCapability) + .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase))) + { + EnvironmentVariablesToMSBuild.AddAsItems(project, environmentVariables); + } + List loggersForBuild = [ CommonRunHelpers.GetConsoleLogger( buildArgs.CloneWithExplicitArgs([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs]) diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs index b2016b765bf4..982fa2c65e93 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs @@ -28,6 +28,7 @@ internal sealed class RunCommandSelector : IDisposable private readonly FacadeLogger? _binaryLogger; private readonly bool _isInteractive; private readonly MSBuildArgs _msbuildArgs; + private readonly IReadOnlyDictionary _environmentVariables; private ProjectCollection? _collection; private Microsoft.Build.Evaluation.Project? _project; @@ -38,20 +39,58 @@ internal sealed class RunCommandSelector : IDisposable /// public bool HasValidProject { get; private set; } + /// + /// Gets the IntermediateOutputPath property from the evaluated project. + /// This will evaluate the project if it hasn't been evaluated yet. + /// Returns null if the project cannot be evaluated or the property is not set. + /// + public string? IntermediateOutputPath + { + get + { + if (OpenProjectIfNeeded(out var projectInstance)) + { + return projectInstance.GetPropertyValue(Constants.IntermediateOutputPath); + } + return null; + } + } + + /// + /// Gets whether the project has opted in to receiving environment variables as MSBuild items. + /// When true, 'dotnet run -e' will pass environment variables as @(RuntimeEnvironmentVariable) items + /// via CustomBeforeMicrosoftCommonProps. + /// + public bool HasRuntimeEnvironmentVariableSupport + { + get + { + if (OpenProjectIfNeeded(out var projectInstance)) + { + return projectInstance.GetItems(Constants.ProjectCapability) + .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase)); + } + return false; + } + } + /// Path to the project file to evaluate /// Whether to prompt the user for selections /// MSBuild arguments containing properties and verbosity settings + /// Environment variables to pass to MSBuild targets as items /// Optional binary logger for MSBuild operations. The logger will not be disposed by this class. public RunCommandSelector( string projectFilePath, bool isInteractive, MSBuildArgs msbuildArgs, + IReadOnlyDictionary environmentVariables, FacadeLogger? binaryLogger = null) { _projectFilePath = projectFilePath; _globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs); _isInteractive = isInteractive; _msbuildArgs = msbuildArgs; + _environmentVariables = environmentVariables; _binaryLogger = binaryLogger; } @@ -488,6 +527,12 @@ public bool TryDeployToDevice() return true; } + // Add environment variables as items before building the target, only if opted in + if (HasRuntimeEnvironmentVariableSupport) + { + EnvironmentVariablesToMSBuild.AddAsItems(projectInstance, _environmentVariables); + } + // Build the DeployToDevice target var buildResult = projectInstance.Build( targets: [Constants.DeployToDevice], diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index 243c5828fb11..c301021fd306 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -46,7 +46,7 @@ public SolutionAddCommand(ParseResult parseResult) _solutionFolderPath = parseResult.GetValue(Definition.SolutionFolderOption); _includeReferences = parseResult.GetValue(Definition.IncludeReferencesOption); SolutionArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SolutionArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath); - _solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory); + _solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory, includeSolutionFilterFiles: true); } public override int Execute() @@ -59,14 +59,22 @@ public override int Execute() // Get project paths from the command line arguments PathUtility.EnsureAllPathsExist(_projects, CliStrings.CouldNotFindProjectOrDirectory, true); - IEnumerable fullProjectPaths = _projects.Select(project => + List fullProjectPaths = _projects.Select(project => { var fullPath = Path.GetFullPath(project); return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath) : fullPath; - }); + }).ToList(); - // Add projects to the solution - AddProjectsToSolutionAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + // Check if we're working with a solution filter file + if (_solutionFileFullPath.HasExtension(SlnfFileHelper.SlnfExtension)) + { + AddProjectsToSolutionFilter(fullProjectPaths); + } + else + { + // Add projects to the solution + AddProjectsToSolutionAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + } return 0; } @@ -264,4 +272,68 @@ static bool IsInSameFolderHierarchy(SolutionItemModel? projectParent, SolutionFo } } } + + private void AddProjectsToSolutionFilter(IEnumerable projectPaths) + { + // Solution filter files don't support --in-root or --solution-folder options + if (_inRoot || !string.IsNullOrEmpty(_solutionFolderPath)) + { + throw new GracefulException(CliCommandStrings.SolutionFilterDoesNotSupportFolderOptions); + } + + // Load the filtered solution to get the parent solution path and existing projects + SolutionModel filteredSolution = SlnFileFactory.CreateFromFilteredSolutionFile(_solutionFileFullPath); + string parentSolutionPath = filteredSolution.Description!; // The parent solution path is stored in Description + + // Load the parent solution to validate projects exist in it + SolutionModel parentSolution = SlnFileFactory.CreateFromFileOrDirectory(parentSolutionPath); + + // Get existing projects in the filter (already normalized to OS separator by CreateFromFilteredSolutionFile) + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); + + // Get solution-relative paths for new projects + var newProjects = ValidateAndGetNewProjects(projectPaths, parentSolution, parentSolutionPath, existingProjects); + + // Add new projects to the existing list and save + var allProjects = existingProjects.Concat(newProjects).OrderBy(p => p); + SlnfFileHelper.SaveSolutionFilter(_solutionFileFullPath, parentSolutionPath, allProjects); + } + + private List ValidateAndGetNewProjects( + IEnumerable projectPaths, + SolutionModel parentSolution, + string parentSolutionPath, + HashSet existingProjects) + { + var newProjects = new List(); + string parentSolutionDirectory = Path.GetDirectoryName(parentSolutionPath) ?? string.Empty; + + foreach (var projectPath in projectPaths) + { + string parentSolutionRelativePath = Path.GetRelativePath(parentSolutionDirectory, projectPath); + + // Normalize to OS separator for consistent comparison + parentSolutionRelativePath = SlnfFileHelper.NormalizePathSeparatorsToOS(parentSolutionRelativePath); + + // Check if project exists in parent solution + var projectInParent = parentSolution.FindProject(parentSolutionRelativePath); + if (projectInParent is null) + { + Reporter.Error.WriteLine(CliStrings.ProjectNotFoundInTheSolution, parentSolutionRelativePath, parentSolutionPath); + continue; + } + + // Check if project is already in the filter + if (existingProjects.Contains(parentSolutionRelativePath)) + { + Reporter.Output.WriteLine(CliStrings.SolutionAlreadyContainsProject, _solutionFileFullPath, parentSolutionRelativePath); + continue; + } + + newProjects.Add(parentSolutionRelativePath); + Reporter.Output.WriteLine(CliStrings.ProjectAddedToTheSolution, parentSolutionRelativePath); + } + + return newProjects; + } } diff --git a/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs b/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs index 9e32962ad7cc..47ef19bdf6dc 100644 --- a/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs @@ -35,7 +35,9 @@ public override int Execute() { ConvertToSlnxAsync(slnFileFullPath, slnxFileFullPath, CancellationToken.None).Wait(); return 0; - } catch (Exception ex) { + } + catch (Exception ex) + { throw new GracefulException(ex.Message, ex); } } diff --git a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs index 28a3e8c26315..8e18861ec3ac 100644 --- a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs @@ -27,7 +27,7 @@ public SolutionRemoveCommand(ParseResult parseResult) public override int Execute() { - string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory); + string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory, includeSolutionFilterFiles: true); if (_projects.Count == 0) { throw new GracefulException(CliStrings.SpecifyAtLeastOneProjectToRemove); @@ -43,7 +43,15 @@ public override int Execute() ? MsbuildProject.GetProjectFileFromDirectory(p) : p)); - RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + // Check if we're working with a solution filter file + if (solutionFileFullPath.HasExtension(SlnfFileHelper.SlnfExtension)) + { + RemoveProjectsFromSolutionFilter(solutionFileFullPath, relativeProjectPaths); + } + else + { + RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + } return 0; } catch (Exception ex) when (ex is not GracefulException) @@ -124,10 +132,41 @@ private static async Task RemoveProjectsAsync(string solutionFileFullPath, IEnum { solution.RemoveFolder(folder); // After removal, adjust index and continue to avoid skipping folders after removal - i--; + i--; } } await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); } + + private static void RemoveProjectsFromSolutionFilter(string slnfFileFullPath, IEnumerable projectPaths) + { + // Load the filtered solution to get the parent solution path and existing projects + SolutionModel filteredSolution = SlnFileFactory.CreateFromFilteredSolutionFile(slnfFileFullPath); + string parentSolutionPath = filteredSolution.Description!; // The parent solution path is stored in Description + + // Get existing projects in the filter + // Use case-insensitive comparer on Windows for file path comparison + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); + + // Remove specified projects + foreach (var projectPath in projectPaths) + { + // Normalize the path to be relative to parent solution + string normalizedPath = projectPath; + + // Try to find and remove the project + if (existingProjects.Remove(normalizedPath)) + { + Reporter.Output.WriteLine(CliStrings.ProjectRemovedFromTheSolution, normalizedPath); + } + else + { + Reporter.Output.WriteLine(CliStrings.ProjectNotFoundInTheSolution, normalizedPath); + } + } + + // Save updated filter + SlnfFileHelper.SaveSolutionFilter(slnfFileFullPath, parentSolutionPath, existingProjects.OrderBy(p => p)); + } } diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildHandler.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildHandler.cs index 724e49b4330f..cb36ef2fdac8 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildHandler.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildHandler.cs @@ -12,7 +12,6 @@ internal sealed class MSBuildHandler(BuildOptions buildOptions) private readonly BuildOptions _buildOptions = buildOptions; private readonly ConcurrentBag _testApplications = []; - private bool _areTestingPlatformApplications = true; public bool RunMSBuild() { @@ -23,65 +22,56 @@ public bool RunMSBuild() return false; } - (IEnumerable projects, bool restored) = GetProjectsProperties(projectOrSolutionFilePath, isSolution); + (IEnumerable projects, int buildExitCode) = isSolution ? + MSBuildUtility.GetProjectsFromSolution(projectOrSolutionFilePath, _buildOptions) : + MSBuildUtility.GetProjectsFromProject(projectOrSolutionFilePath, _buildOptions); - InitializeTestApplications(projects); + LogProjectProperties(projects); - if (!restored || _testApplications.IsEmpty) + if (buildExitCode != 0) { - Reporter.Error.WriteLine(string.Format(CliCommandStrings.CmdMSBuildProjectsPropertiesErrorDescription, ExitCode.GenericFailure)); + Reporter.Error.WriteLine(string.Format(CliCommandStrings.CmdMSBuildProjectsPropertiesErrorDescription, buildExitCode)); return false; } - return true; + return InitializeTestApplications(projects); } - private void InitializeTestApplications(IEnumerable moduleGroups) + private bool InitializeTestApplications(IEnumerable moduleGroups) { // If one test app has IsTestingPlatformApplication set to false (VSTest and not MTP), then we will not run any of the test apps IEnumerable vsTestTestProjects = moduleGroups.SelectMany(group => group.GetVSTestAndNotMTPModules()); if (vsTestTestProjects.Any()) { - _areTestingPlatformApplications = false; - Reporter.Error.WriteLine( string.Format( CliCommandStrings.CmdUnsupportedVSTestTestApplicationsDescription, string.Join(Environment.NewLine, vsTestTestProjects.Select(module => Path.GetFileName(module.ProjectFullPath))).Red().Bold())); - return; + return false; } foreach (ParallelizableTestModuleGroupWithSequentialInnerModules moduleGroup in moduleGroups) { _testApplications.Add(moduleGroup); } - } - public bool EnqueueTestApplications(TestApplicationActionQueue queue) - { - if (!_areTestingPlatformApplications) + if (_testApplications.IsEmpty) { + Reporter.Error.WriteLine(CliCommandStrings.CmdTestNoTestProjectsFound); return false; } - foreach (var testApp in _testApplications) - { - queue.Enqueue(testApp); - } return true; } - private (IEnumerable Projects, bool Restored) GetProjectsProperties(string solutionOrProjectFilePath, bool isSolution) + public void EnqueueTestApplications(TestApplicationActionQueue queue) { - (IEnumerable projects, bool isBuiltOrRestored) = isSolution ? - MSBuildUtility.GetProjectsFromSolution(solutionOrProjectFilePath, _buildOptions) : - MSBuildUtility.GetProjectsFromProject(solutionOrProjectFilePath, _buildOptions); - - LogProjectProperties(projects); - - return (projects, isBuiltOrRestored); + foreach (var testApp in _testApplications) + { + queue.Enqueue(testApp); + } } private static void LogProjectProperties(IEnumerable moduleGroups) diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs index 51f932c3c91b..c789e2ae39a1 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs @@ -26,13 +26,13 @@ internal static class MSBuildUtility [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ProjectShouldBuild")] static extern bool ProjectShouldBuild(SolutionFile solutionFile, string projectFile); - public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) + public static (IEnumerable Projects, int BuildExitCode) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) { - bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(solutionFilePath, buildOptions); + int buildExitCode = BuildOrRestoreProjectOrSolution(solutionFilePath, buildOptions); - if (!isBuiltOrRestored) + if (buildExitCode != 0) { - return (Array.Empty(), isBuiltOrRestored); + return (Array.Empty(), buildExitCode); } var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(buildOptions.MSBuildArgs, CommonOptions.CreatePropertyOption(), CommonOptions.CreateRestorePropertyOption(), CommonOptions.CreateMSBuildTargetOption(), CommonOptions.CreateVerbosityOption(), CommonOptions.CreateNoLogoOption()); @@ -73,16 +73,16 @@ public static (IEnumerable Projects, bool IsBuiltOrRestored) GetProjectsFromProject(string projectFilePath, BuildOptions buildOptions) + public static (IEnumerable Projects, int BuildExitCode) GetProjectsFromProject(string projectFilePath, BuildOptions buildOptions) { - bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(projectFilePath, buildOptions); + int buildExitCode = BuildOrRestoreProjectOrSolution(projectFilePath, buildOptions); - if (!isBuiltOrRestored) + if (buildExitCode != 0) { - return (Array.Empty(), isBuiltOrRestored); + return (Array.Empty(), buildExitCode); } FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb); @@ -94,7 +94,7 @@ public static (IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions, configuration: null, platform: null); logger?.ReallyShutdown(); collection.UnloadAllProjects(); - return (projects, isBuiltOrRestored); + return (projects, buildExitCode); } public static BuildOptions GetBuildOptions(ParseResult parseResult) @@ -231,12 +231,13 @@ private static (string? PositionalProjectOrSolution, string? PositionalTestModul } - private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOptions buildOptions) + private static int BuildOrRestoreProjectOrSolution(string filePath, BuildOptions buildOptions) { if (buildOptions.HasNoBuild) { - return true; + return 0; } + List msbuildArgs = [.. buildOptions.MSBuildArgs, filePath]; if (buildOptions.Verbosity is null) @@ -252,9 +253,7 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption CommonOptions.CreateVerbosityOption(), CommonOptions.CreateNoLogoOption()); - int result = new RestoringCommand(parsedMSBuildArgs, buildOptions.HasNoRestore).Execute(); - - return result == (int)BuildResultCode.Success; + return new RestoringCommand(parsedMSBuildArgs, buildOptions.HasNoRestore).Execute(); } private static ConcurrentBag GetProjectsProperties( diff --git a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs index cb4977dd5cac..2db9c78d02cc 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MicrosoftTestingPlatformTestCommand.cs @@ -70,10 +70,7 @@ private int RunInternal(ParseResult parseResult, bool isHelp) // be slowing us down unnecessarily. // Alternatively, if we can enqueue right after every project evaluation without waiting all evaluations to be done, we can enqueue early. actionQueue = new TestApplicationActionQueue(degreeOfParallelism, buildOptions, testOptions, _output, OnHelpRequested); - if (!msBuildHandler.EnqueueTestApplications(actionQueue)) - { - return ExitCode.GenericFailure; - } + msBuildHandler.EnqueueTestApplications(actionQueue); } actionQueue.EnqueueCompleted(); diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs index 044443fda581..d5d733a17044 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; +using System.IO; using System.IO.Pipes; using System.Threading; using Microsoft.DotNet.Cli.Commands.Test.IPC; @@ -163,6 +164,7 @@ private ProcessStartInfo CreateProcessStartInfo() processStartInfo.Environment[Module.DotnetRootArchVariableName] = Path.GetDirectoryName(new Muxer().MuxerPath); } + processStartInfo.Environment["DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY"] = Directory.GetCurrentDirectory(); return processStartInfo; } diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs index 41ee319317e7..4496703ace28 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs @@ -78,7 +78,7 @@ private async Task Read(BuildOptions buildOptions, TestOptions testOptions, Term { result = ExitCode.GenericFailure; } - + lock (_lock) { if (_aggregateExitCode is null) diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 2dc2bc8b6906..8ec4d74f8564 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -179,7 +179,7 @@ private static int ForwardToVSTestConsole(ParseResult parseResult, string[] args public static TestCommand FromArgs(string[] args, string? testSessionCorrelationId = null, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "test", ..args]); + var parseResult = Parser.Parse(["dotnet", "test", .. args]); // settings parameters are after -- (including --), these should not be considered by the parser string[] settings = [.. args.SkipWhile(a => a != "--")]; @@ -266,7 +266,8 @@ private static TestCommand FromParseResult(ParseResult result, string[] settings Dictionary variables = VSTestForwardingApp.GetVSTestRootVariables(); - foreach (var (rootVariableName, rootValue) in variables) { + foreach (var (rootVariableName, rootValue) in variables) + { testCommand.EnvironmentVariable(rootVariableName, rootValue); VSTestTrace.SafeWriteTrace(() => $"Root variable set {rootVariableName}:{rootValue}"); } diff --git a/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs b/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs index fb81e15466f9..26a021485c97 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs @@ -20,7 +20,7 @@ public VSTestForwardingApp(IEnumerable argsToForward) WithEnvironmentVariable(rootVariableName, rootValue); VSTestTrace.SafeWriteTrace(() => $"Root variable set {rootVariableName}:{rootValue}"); } - + VSTestTrace.SafeWriteTrace(() => $"Forwarding to '{GetVSTestExePath()}' with args \"{argsToForward?.Aggregate((a, b) => $"{a} | {b}")}\""); } diff --git a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs index ca401c280ccd..f394faba7f16 100644 --- a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs @@ -19,7 +19,7 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.Execute; -internal sealed class ToolExecuteCommand : CommandBase +internal sealed class ToolExecuteCommand : CommandBase { const int ERROR_CANCELLED = 1223; // Windows error code for "Operation canceled by user" diff --git a/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs b/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs index ee731e4a385e..a2f03ec7ee31 100644 --- a/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs +++ b/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs @@ -10,9 +10,9 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.List; internal sealed class VersionedDataContract { - /// - /// The version of the JSON format for dotnet tool list. - /// + /// + /// The version of the JSON format for dotnet tool list. + /// [JsonPropertyName("version")] public int Version { get; init; } = 1; diff --git a/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs b/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs index 8ac5c500c371..9aecc47b4806 100644 --- a/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs +++ b/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs @@ -113,7 +113,7 @@ private static bool ManifestCommandMatchesActualInPackage( IReadOnlyList toolPackageCommands) { ToolCommandName[] commandsFromPackage = [.. toolPackageCommands.Select(t => t.Name)]; -return !commandsFromManifest.Any(cmd => !commandsFromPackage.Contains(cmd)) && !commandsFromPackage.Any(cmd => !commandsFromManifest.Contains(cmd)); + return !commandsFromManifest.Any(cmd => !commandsFromPackage.Contains(cmd)) && !commandsFromPackage.Any(cmd => !commandsFromManifest.Contains(cmd)); } public bool PackageHasBeenRestored( diff --git a/src/Cli/dotnet/Commands/Tool/Store/StoreCommand.cs b/src/Cli/dotnet/Commands/Tool/Store/StoreCommand.cs index 512480da7163..1345eb26f2c7 100644 --- a/src/Cli/dotnet/Commands/Tool/Store/StoreCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Store/StoreCommand.cs @@ -20,7 +20,7 @@ private StoreCommand(IEnumerable msbuildArgs, string msbuildPath = null) public static StoreCommand FromArgs(string[] args, string msbuildPath = null) { - var result = Parser.Parse(["dotnet", "store", ..args]); + var result = Parser.Parse(["dotnet", "store", .. args]); return FromParseResult(result, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs index b523f4d47f4d..f8a02960c380 100644 --- a/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs @@ -70,7 +70,7 @@ public override int Execute() TransactionalAction.Run(() => { shellShimRepository.RemoveShim(package.Command); - + toolPackageUninstaller.Uninstall(package.PackageDirectory); }); diff --git a/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs index b8880fbe9e61..0afc696e73ae 100644 --- a/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs @@ -4,12 +4,12 @@ #nullable disable using System.CommandLine; +using Microsoft.DotNet.Cli.Commands.Tool.Install; +using Microsoft.DotNet.Cli.ShellShim; +using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.EnvironmentAbstractions; -using Microsoft.DotNet.Cli.ToolPackage; using CreateShellShimRepository = Microsoft.DotNet.Cli.Commands.Tool.Install.CreateShellShimRepository; -using Microsoft.DotNet.Cli.ShellShim; -using Microsoft.DotNet.Cli.Commands.Tool.Install; namespace Microsoft.DotNet.Cli.Commands.Tool.Update; diff --git a/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs b/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs index b4eb5e84b389..dbc522dd4ff1 100644 --- a/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs @@ -4,11 +4,11 @@ #nullable disable using System.CommandLine; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Cli.Commands.Workload.Install; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Sdk.WorkloadManifestReader; -using Microsoft.Deployment.DotNet.Releases; -using Microsoft.DotNet.Cli.Commands.Workload.Install; namespace Microsoft.DotNet.Cli.Commands.Workload.History; diff --git a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs index e8e59fa51302..76f3881b8acb 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Text.Json; +using Microsoft.DotNet.Cli.Commands.Workload; using Microsoft.DotNet.Cli.Commands.Workload.Config; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; using Microsoft.DotNet.Cli.Extensions; diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallerFactory.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallerFactory.cs index a2f280b8f815..e9f65eabcb2f 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallerFactory.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallerFactory.cs @@ -3,6 +3,7 @@ #nullable disable +using Microsoft.DotNet.Cli.Commands.Workload; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Configurer; @@ -53,7 +54,7 @@ public static IInstaller GetWorkloadInstaller( userProfileDir ??= CliFolderPathCalculator.DotnetUserProfileFolderPath; - return new FileBasedInstaller( + var installer = new FileBasedInstaller( reporter, sdkFeatureBand, workloadResolver, @@ -64,6 +65,25 @@ public static IInstaller GetWorkloadInstaller( verbosity: verbosity, packageSourceLocation: packageSourceLocation, restoreActionConfig: restoreActionConfig); + + // Attach corruption repairer to recover from corrupt workload sets + if (nugetPackageDownloader is not null && + workloadResolver?.GetWorkloadManifestProvider() is SdkDirectoryWorkloadManifestProvider sdkProvider && + sdkProvider.CorruptionRepairer is null) + { + sdkProvider.CorruptionRepairer = new WorkloadManifestCorruptionRepairer( + reporter, + installer, + workloadResolver, + sdkFeatureBand, + dotnetDir, + userProfileDir, + nugetPackageDownloader, + packageSourceLocation, + verbosity); + } + + return installer; } private static bool CanWriteToDotnetRoot(string dotnetDir = null) diff --git a/src/Cli/dotnet/Commands/Workload/Repair/WorkloadRepairCommand.cs b/src/Cli/dotnet/Commands/Workload/Repair/WorkloadRepairCommand.cs index 74337dc143c4..4e523eafb594 100644 --- a/src/Cli/dotnet/Commands/Workload/Repair/WorkloadRepairCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Repair/WorkloadRepairCommand.cs @@ -70,7 +70,8 @@ public override int Execute() { Reporter.WriteLine(); - var workloadIds = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(new SdkFeatureBand(_sdkVersion)); + var sdkFeatureBand = new SdkFeatureBand(_sdkVersion); + var workloadIds = _workloadInstaller.GetWorkloadInstallationRecordRepository().GetInstalledWorkloads(sdkFeatureBand); if (!workloadIds.Any()) { @@ -80,7 +81,7 @@ public override int Execute() Reporter.WriteLine(string.Format(CliCommandStrings.RepairingWorkloads, string.Join(" ", workloadIds))); - ReinstallWorkloadsBasedOnCurrentManifests(workloadIds, new SdkFeatureBand(_sdkVersion)); + ReinstallWorkloadsBasedOnCurrentManifests(workloadIds, sdkFeatureBand); WorkloadInstallCommand.TryRunGarbageCollection(_workloadInstaller, Reporter, Verbosity, workloadSetVersion => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, workloadSetVersion)); @@ -106,4 +107,5 @@ private void ReinstallWorkloadsBasedOnCurrentManifests(IEnumerable w { _workloadInstaller.RepairWorkloads(workloadIds, sdkFeatureBand); } + } diff --git a/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs b/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs index b605a34d3caa..8451a9de5457 100644 --- a/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs @@ -63,7 +63,7 @@ public override int Execute() }); workloadInstaller.Shutdown(); - + return 0; } diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadHistoryRecorder.cs b/src/Cli/dotnet/Commands/Workload/WorkloadHistoryRecorder.cs index ab64b7539912..0690f5e12347 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadHistoryRecorder.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadHistoryRecorder.cs @@ -54,6 +54,10 @@ public void Run(Action workloadAction) private WorkloadHistoryState GetWorkloadState() { var resolver = _workloadResolverFunc(); + if (resolver.GetWorkloadManifestProvider() is SdkDirectoryWorkloadManifestProvider sdkProvider) + { + sdkProvider.CorruptionFailureMode = ManifestCorruptionFailureMode.Ignore; + } var currentWorkloadVersion = resolver.GetWorkloadVersion().Version; return new WorkloadHistoryState() { diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadManifestCorruptionRepairer.cs b/src/Cli/dotnet/Commands/Workload/WorkloadManifestCorruptionRepairer.cs new file mode 100644 index 000000000000..28bfe766a5f0 --- /dev/null +++ b/src/Cli/dotnet/Commands/Workload/WorkloadManifestCorruptionRepairer.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.Commands.Workload.Install; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Cli.Commands.Workload; + +internal sealed class WorkloadManifestCorruptionRepairer : IWorkloadManifestCorruptionRepairer +{ + private readonly IReporter _reporter; + private readonly IInstaller _workloadInstaller; + private readonly IWorkloadResolver _workloadResolver; + private readonly SdkFeatureBand _sdkFeatureBand; + private readonly string _dotnetPath; + private readonly string _userProfileDir; + private readonly INuGetPackageDownloader? _packageDownloader; + private readonly PackageSourceLocation? _packageSourceLocation; + private readonly VerbosityOptions _verbosity; + + private bool _checked; + + public WorkloadManifestCorruptionRepairer( + IReporter reporter, + IInstaller workloadInstaller, + IWorkloadResolver workloadResolver, + SdkFeatureBand sdkFeatureBand, + string dotnetPath, + string userProfileDir, + INuGetPackageDownloader? packageDownloader, + PackageSourceLocation? packageSourceLocation, + VerbosityOptions verbosity) + { + _reporter = reporter ?? NullReporter.Instance; + _workloadInstaller = workloadInstaller; + _workloadResolver = workloadResolver; + _sdkFeatureBand = sdkFeatureBand; + _dotnetPath = dotnetPath; + _userProfileDir = userProfileDir; + _packageDownloader = packageDownloader; + _packageSourceLocation = packageSourceLocation; + _verbosity = verbosity; + } + + public void EnsureManifestsHealthy(ManifestCorruptionFailureMode failureMode) + { + if (_checked) + { + return; + } + + _checked = true; + + if (failureMode == ManifestCorruptionFailureMode.Ignore) + { + return; + } + + // Get the workload set directly from the provider - it was already resolved during construction + // and doesn't require reading the install state file again + var provider = _workloadResolver.GetWorkloadManifestProvider() as SdkDirectoryWorkloadManifestProvider; + var workloadSet = provider?.ResolvedWorkloadSet; + + if (workloadSet is null) + { + // No workload set is being used + return; + } + + if (!provider?.HasMissingManifests(workloadSet) ?? true) + { + return; + } + + if (failureMode == ManifestCorruptionFailureMode.Throw) + { + throw new InvalidOperationException(string.Format(CliCommandStrings.WorkloadSetHasMissingManifests, workloadSet.Version)); + } + + _reporter.WriteLine($"Repairing workload set {workloadSet.Version}..."); + CliTransaction.RunNew(context => RepairCorruptWorkloadSet(context, workloadSet)); + } + + + + private void RepairCorruptWorkloadSet(ITransactionContext context, WorkloadSet workloadSet) + { + var manifestUpdates = CreateManifestUpdatesFromWorkloadSet(workloadSet); + + foreach (var manifestUpdate in manifestUpdates) + { + _workloadInstaller.InstallWorkloadManifest(manifestUpdate, context); + } + + } + + [MemberNotNull(nameof(_packageDownloader))] + private IEnumerable CreateManifestUpdatesFromWorkloadSet(WorkloadSet workloadSet) + { + if (_packageDownloader is null) + { + throw new InvalidOperationException("Package downloader is required to repair workload manifests."); + } + + var manifestUpdater = new WorkloadManifestUpdater( + _reporter, + _workloadResolver, + _packageDownloader, + _userProfileDir, + _workloadInstaller.GetWorkloadInstallationRecordRepository(), + _workloadInstaller, + _packageSourceLocation, + displayManifestUpdates: _verbosity >= VerbosityOptions.detailed); + + return manifestUpdater.CalculateManifestUpdatesForWorkloadSet(workloadSet); + } +} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index 4971fb9bd45d..f6be67c00184 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -92,6 +92,11 @@ .NET Builder + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Jedná se o ekvivalent odstranění project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - Získání vlastností projektů pomocí nástroje MSBuild nebylo správně spuštěno s ukončovacím kódem: {0}. + Build failed with exit code: {0}. + Získání vlastností projektů pomocí nástroje MSBuild nebylo správně spuštěno s ukončovacím kódem: {0}. @@ -2987,6 +2992,11 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Parametry --solution-folder a --in-root nejdou použít společně; použijte jenom jeden z nich. @@ -4040,6 +4050,11 @@ Pokud chcete zobrazit hodnotu, zadejte odpovídající volbu příkazového řá Verze úlohy, která se má zobrazit, nebo jedna nebo více úloh a jejich verze spojené znakem @. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + Ve verzi sady úloh {0} chybí manifesty, které pravděpodobně odstranila správa balíčků. Pokud chcete tento problém vyřešit, spusťte příkaz dotnet workload repair. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Zdroj instalace diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index 057029c7af1a..761708bcbb2c 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -92,6 +92,11 @@ .NET-Generator + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Dies entspricht dem Löschen von "project.assets.json". - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - Das Abrufen von Projekteigenschaften mit MSBuild wurde nicht ordnungsgemäß ausgeführt. Exitcode: {0}. + Build failed with exit code: {0}. + Das Abrufen von Projekteigenschaften mit MSBuild wurde nicht ordnungsgemäß ausgeführt. Exitcode: {0}. @@ -2987,6 +2992,11 @@ Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Die Optionen "--solution-folder" und "--in-root" können nicht zusammen verwendet werden; verwenden Sie nur eine der Optionen. @@ -4040,6 +4050,11 @@ Um einen Wert anzuzeigen, geben Sie die entsprechende Befehlszeilenoption an, oh Eine Workloadversion zum Anzeigen oder mindestens eine Workload und deren Versionen, die mit dem Zeichen "@" verknüpft sind. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + In Version {0} des Arbeitslastsatzes fehlen Manifeste, die wahrscheinlich von der Paketverwaltung entfernt wurden. Führen Sie „dotnet workload repair“ aus, um das Problem zu beheben. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Installationsquelle diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index e18effb56956..11a09cf8daf4 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -92,6 +92,11 @@ Generador para .NET + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Esta acción es equivalente a eliminar project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - La obtención de propiedades de proyectos con MSBuild no se ejecutó correctamente con el código de salida: {0}. + Build failed with exit code: {0}. + La obtención de propiedades de proyectos con MSBuild no se ejecutó correctamente con el código de salida: {0}. @@ -2987,6 +2992,11 @@ Su proyecto tiene como destino varias plataformas. Especifique la que quiere usa SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Las opciones --in-root y --solution-folder no se pueden usar juntas. Utilice solo una de ellas. @@ -4040,6 +4050,11 @@ Para mostrar un valor, especifique la opción de línea de comandos correspondie Una versión de carga de trabajo para mostrar o una o varias cargas de trabajo y sus versiones unidas por el carácter "@". + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + La versión del conjunto de cargas de trabajo {0} tiene manifiestos faltantes, probablemente eliminados por la administración de paquetes. Ejecute "dotnet workload repair" para corregir esto. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Origen de la instalación diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index 6c9a04f60f09..7f994e2eaf83 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -92,6 +92,11 @@ Générateur .NET + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Cela équivaut à supprimer project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - L’obtention des propriétés des projets avec MSBuild ne s’est pas exécutée correctement avec le code de sortie : {0}. + Build failed with exit code: {0}. + L’obtention des propriétés des projets avec MSBuild ne s’est pas exécutée correctement avec le code de sortie : {0}. @@ -2987,6 +2992,11 @@ Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. N'utilisez pas en même temps les options --solution-folder et --in-root. Utilisez uniquement l'une des deux options. @@ -4040,6 +4050,11 @@ Pour afficher une valeur, spécifiez l’option de ligne de commande corresponda Version de charge de travail à afficher ou une ou plusieurs charges de travail et leurs versions jointes par le caractère « @ ». + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + La version {0} de l’ensemble de la charge de travail comporte des manifestes manquants qui ont probablement été supprimés par Package Management. Exécutez « dotnet workload repair » pour corriger ce problème. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Source de l’installation diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index 116c2ef17473..1cf370f1e55e 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -92,6 +92,11 @@ Generatore .NET + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Equivale a eliminare project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - Il recupero delle proprietà dei progetti con MSBuild non è stato eseguito correttamente, con il codice di uscita: {0}. + Build failed with exit code: {0}. + Il recupero delle proprietà dei progetti con MSBuild non è stato eseguito correttamente, con il codice di uscita: {0}. @@ -2987,6 +2992,11 @@ Il progetto è destinato a più framework. Specificare il framework da eseguire SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Non è possibile usare contemporaneamente le opzioni --solution-folder e --in-root. Usare una sola delle opzioni. @@ -4040,6 +4050,11 @@ Per visualizzare un valore, specifica l'opzione della riga di comando corrispond Una versione del carico di lavoro da visualizzare oppure uno o più carichi di lavoro e le relative versioni unite dal carattere '@'. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + Nella versione {0} del set di carichi di lavoro mancano alcuni file manifesto, probabilmente rimossi da Gestione pacchetti. Eseguire "dotnet workload repair" per risolvere il problema. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Origine dell'installazione diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index 09e0b370a21d..a091b0c6774f 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -92,6 +92,11 @@ .NET ビルダー + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ This is equivalent to deleting project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - MSBuild でのプロジェクトプロパティの取得が正しく実行されませんでした。終了コード: {0}。 + Build failed with exit code: {0}. + MSBuild でのプロジェクトプロパティの取得が正しく実行されませんでした。終了コード: {0}。 @@ -2987,6 +2992,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder オプションと --in-root オプションを一緒に使用することはできません。いずれかのオプションだけを使用します。 @@ -4040,6 +4050,11 @@ To display a value, specify the corresponding command-line option without provid 表示するワークロードのバージョン、または '@' 文字で結合された 1 つ以上のワークロードとそのバージョン。 + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + ワークロード セット バージョン {0} で、パッケージ管理によって削除された可能性が高いマニフェストが不足しています。これを修正するには、"dotnet workload repair" を実行してください。 + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source インストール ソース diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index d18f0a43e10d..9354d0ce0248 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -92,6 +92,11 @@ .NET 작성기 + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ project.assets.json을 삭제하는 것과 동일합니다. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - MSBuild를 사용하여 프로젝트 가져오기 속성이 종료 코드에서 제대로 실행되지 않았습니다. {0}. + Build failed with exit code: {0}. + MSBuild를 사용하여 프로젝트 가져오기 속성이 종료 코드에서 제대로 실행되지 않았습니다. {0}. @@ -2987,6 +2992,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder와 --in-root 옵션을 함께 사용할 수 없습니다. 옵션을 하나만 사용하세요. @@ -4040,6 +4050,11 @@ To display a value, specify the corresponding command-line option without provid 표시할 워크로드 버전 또는 '@' 문자로 결합된 하나 이상의 워크로드와 해당 버전입니다. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + 워크로드 집합 버전 {0}에 패키지 관리에 의해 제거될 수 있는 매니페스트가 누락되었습니다. 이 문제를 해결하려면 "dotnet workload repair"를 실행하세요. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source 설치 원본 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 59b0e6816f14..12ffc0b3b46e 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -92,6 +92,11 @@ Konstruktor platformy .NET + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Jest to równoważne usunięciu pliku project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - Pobieranie właściwości projektów za pomocą programu MSBuild nie zostało poprawnie wykonane z kodem zakończenia: {0}. + Build failed with exit code: {0}. + Pobieranie właściwości projektów za pomocą programu MSBuild nie zostało poprawnie wykonane z kodem zakończenia: {0}. @@ -2987,6 +2992,11 @@ Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy u SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Opcji --solution-folder i --in-root nie można używać razem; użyj tylko jednej z tych opcji. @@ -4040,6 +4050,11 @@ Aby wyświetlić wartość, należy podać odpowiednią opcję wiersza poleceń Wersja obciążenia do wyświetlenia lub jedno lub więcej obciążeń i ich wersji połączonych znakiem „@”. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + Wersja zestawu obciążeń {0} ma brakujące manifesty, prawdopodobnie usunięte przez zarządzanie pakietami. Uruchom „dotnet workload repair”, aby to naprawić. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Źródło instalacji diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index 8a5ba6e0315d..f4d5536efb2a 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -92,6 +92,11 @@ Construtor do .NET + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ Isso equivale a excluir o project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - As propriedades de obtenção de projetos com o MSBuild não foram executadas corretamente com o código de saída: {0}. + Build failed with exit code: {0}. + As propriedades de obtenção de projetos com o MSBuild não foram executadas corretamente com o código de saída: {0}. @@ -2987,6 +2992,11 @@ Ele tem diversas estruturas como destino. Especifique que estrutura executar usa SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. As opções --solution-folder e --in-root não podem ser usadas juntas. Use somente uma das opções. @@ -4040,6 +4050,11 @@ Para exibir um valor, especifique a opção de linha de comando correspondente s Uma versão de carga de trabalho para exibir ou uma ou mais cargas de trabalho e suas versões unidas pelo caractere ''@''. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + A versão do conjunto de cargas de trabalho {0} tem manifestos ausentes, provavelmente removidos pelo gerenciamento de pacotes. Execute "dotnet workload repair" para corrigir isso. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Origem da Instalação diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index 544ec14a21f1..1efed9ecc977 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -92,6 +92,11 @@ Построитель .NET + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ This is equivalent to deleting project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - Получение свойств проектов с помощью MSBuild сработало не так, как ожидалось, код завершения: {0}. + Build failed with exit code: {0}. + Получение свойств проектов с помощью MSBuild сработало не так, как ожидалось, код завершения: {0}. @@ -2987,6 +2992,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Параметры --solution-folder и --in-root options не могут быть использованы одновременно; оставьте только один из параметров. @@ -4041,6 +4051,11 @@ To display a value, specify the corresponding command-line option without provid Версия рабочей нагрузки для отображения или одной или нескольких рабочих нагрузок и их версий, соединенных символом "@". + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + В версии набора рабочих нагрузок {0} отсутствуют манифесты, которые, вероятно, были удалены при управлении пакетами. Чтобы устранить эту проблему, выполните команду "dotnet workload repair". + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Источник установки diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index 500968f42977..24240cdf35f6 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -92,6 +92,11 @@ .NET Oluşturucusu + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ project.assets.json öğesini silmeyle eşdeğerdir. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - MSBuild ile proje özelliklerini al işlemi {0} çıkış koduyla çıkarak düzgün şekilde çalışmadı. + Build failed with exit code: {0}. + MSBuild ile proje özelliklerini al işlemi {0} çıkış koduyla çıkarak düzgün şekilde çalışmadı. @@ -2987,6 +2992,11 @@ Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder ve --in-root seçenekleri birlikte kullanılamaz; seçeneklerden yalnızca birini kullanın. @@ -4040,6 +4050,11 @@ Bir değeri görüntülemek için, bir değer sağlamadan ilgili komut satırı Görüntülenecek bir iş yükü sürümü veya bir veya daha fazla iş yükü ve bunların '@' karakteriyle birleştirilen sürümleri. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + İş yükü kümesi {0} sürümü, paket yönetimi tarafından kaldırılmış olabilecek eksik bildirimlere sahip. Bunu düzeltmek için "dotnet workload repair" komutunu çalıştırın. + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source Yükleme Kaynağı diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index f2850c8dd0f2..40e693182a76 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -92,6 +92,11 @@ .NET 生成器 + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ This is equivalent to deleting project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - 使用 MSBuild 获取项目属性未正确执行,退出代码为: {0}。 + Build failed with exit code: {0}. + 使用 MSBuild 获取项目属性未正确执行,退出代码为: {0}。 @@ -2987,6 +2992,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder 和 --in-root 选项不能一起使用;请仅使用其中一个选项。 @@ -4040,6 +4050,11 @@ To display a value, specify the corresponding command-line option without provid 要显示的工作负载版本,或一个或多个工作负载,并且其版本由 ‘@’ 字符联接。 + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + 工作负载集版本 {0} 缺少清单,这些清单可能已被包管理移除。请运行 "dotnet workload repair" 进行修复。 + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source 安装源文件 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index 17a521f0c546..3fe4ac51bcd0 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -92,6 +92,11 @@ .NET 產生器 + + No test projects were found. + No test projects were found. + + The device identifier to use for running the application. The device identifier to use for running the application. @@ -569,8 +574,8 @@ This is equivalent to deleting project.assets.json. - Get projects properties with MSBuild didn't execute properly with exit code: {0}. - 使用 MSBuild 取得專案屬性時未正確執行,結束代碼為: {0}。 + Build failed with exit code: {0}. + 使用 MSBuild 取得專案屬性時未正確執行,結束代碼為: {0}。 @@ -2987,6 +2992,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. 不可同時使用 --solution-folder 和 --in-root 選項; 請只使用其中一個選項。 @@ -4040,6 +4050,11 @@ To display a value, specify the corresponding command-line option without provid 要顯示的工作負載版本,或是一或多個工作負載及其由 '@' 字元連接的版本。 + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + 工作負載集合版本 {0} 缺少資訊清單,其可能已遭套件管理移除。請執行「dotnet workload repair」來修復此問題。 + {0} is the workload set version. {Locked="dotnet workload repair"} + Installation Source 安裝來源 diff --git a/src/Cli/dotnet/DotNetCommandFactory.cs b/src/Cli/dotnet/DotNetCommandFactory.cs index ea5eb912e8f6..dcb70b05e6c9 100644 --- a/src/Cli/dotnet/DotNetCommandFactory.cs +++ b/src/Cli/dotnet/DotNetCommandFactory.cs @@ -38,7 +38,7 @@ private static bool TryGetBuiltInCommand(string commandName, out Func Parser.Invoke([commandName, ..args]); + commandFunc = (args) => Parser.Invoke([commandName, .. args]); return true; } commandFunc = null; diff --git a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs index a5e54ba06bb9..0c606c61dbf7 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs @@ -43,4 +43,4 @@ Task GetBestPackageVersionAsync(PackageId packageId, Task<(NuGetVersion version, PackageSource source)> GetBestPackageVersionAndSourceAsync(PackageId packageId, VersionRange versionRange, PackageSourceLocation packageSourceLocation = null); -} +} diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index a311e88c646d..a0ce16fe6d0b 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -75,7 +75,7 @@ public NuGetPackageDownloader( _retryTimer = timer; _sourceRepositories = new(); // If windows or env variable is set, verify signatures - _verifySignatures = verifySignatures && (OperatingSystem.IsWindows() ? true + _verifySignatures = verifySignatures && (OperatingSystem.IsWindows() ? true : bool.TryParse(Environment.GetEnvironmentVariable(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification), out var shouldVerifySignature) ? shouldVerifySignature : OperatingSystem.IsLinux()); _cacheSettings = new SourceCacheContext @@ -122,7 +122,7 @@ public async Task DownloadPackageAsync(PackageId packageId, throw new ArgumentException($"Package download folder must be specified either via {nameof(NuGetPackageDownloader)} constructor or via {nameof(downloadFolder)} method argument."); } var pathResolver = new VersionFolderPathResolver(resolvedDownloadFolder); - + string nupkgPath = pathResolver.GetPackageFilePath(packageId.ToString(), resolvedPackageVersion); Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)); diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 582eaf14d36d..7755fa463787 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -216,7 +216,8 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject { return projectData; } - }; + } + ; return null; } diff --git a/src/Cli/dotnet/SlnFileFactory.cs b/src/Cli/dotnet/SlnFileFactory.cs index fe8a73f6d2d0..40892bd64575 100644 --- a/src/Cli/dotnet/SlnFileFactory.cs +++ b/src/Cli/dotnet/SlnFileFactory.cs @@ -96,6 +96,8 @@ public static SolutionModel CreateFromFilteredSolutionFile(string filteredSoluti }; JsonElement root = JsonDocument.Parse(File.ReadAllText(filteredSolutionPath), options).RootElement; originalSolutionPath = Uri.UnescapeDataString(root.GetProperty("solution").GetProperty("path").GetString()); + // Normalize path separators to OS-specific for cross-platform compatibility + originalSolutionPath = SlnfFileHelper.NormalizePathSeparatorsToOS(originalSolutionPath); filteredSolutionProjectPaths = [.. root.GetProperty("solution").GetProperty("projects").EnumerateArray().Select(p => p.GetString())]; originalSolutionPathAbsolute = Path.GetFullPath(originalSolutionPath, Path.GetDirectoryName(filteredSolutionPath)); } diff --git a/src/Cli/dotnet/SlnfFileHelper.cs b/src/Cli/dotnet/SlnfFileHelper.cs new file mode 100644 index 000000000000..daa762049fad --- /dev/null +++ b/src/Cli/dotnet/SlnfFileHelper.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli; + +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never)] +[JsonSerializable(typeof(SlnfFileHelper.SlnfRoot))] +internal partial class SlnfJsonSerializerContext : JsonSerializerContext; + +/// +/// Utilities for working with solution filter (.slnf) files +/// +public static class SlnfFileHelper +{ + /// + /// File extension for solution filter files + /// + public const string SlnfExtension = ".slnf"; + + /// + /// Normalizes path separators from backslashes to the OS-specific directory separator + /// + /// The path to normalize + /// Path with OS-specific separators + public static string NormalizePathSeparatorsToOS(string path) + { + return path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + } + + /// + /// Normalizes path separators to backslashes (as used in .slnf files) + /// + /// The path to normalize + /// Path with backslash separators + public static string NormalizePathSeparatorsToBackslash(string path) + { + return path.Replace(Path.DirectorySeparatorChar, '\\'); + } + + internal class SlnfSolution + { + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonPropertyName("projects")] + public List Projects { get; set; } = new(); + } + + internal class SlnfRoot + { + [JsonPropertyName("solution")] + public SlnfSolution Solution { get; set; } = new(); + } + + /// + /// Creates a new solution filter file + /// + /// Path to the solution filter file to create + /// Path to the parent solution file + /// List of project paths to include (relative to the parent solution) + public static void CreateSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects = null) + { + var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)) ?? string.Empty; + var parentSolutionFullPath = Path.GetFullPath(parentSolutionPath, slnfDirectory); + var relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionFullPath); + + // Normalize path separators to backslashes (as per slnf format) + relativeSolutionPath = NormalizePathSeparatorsToBackslash(relativeSolutionPath); + + var root = new SlnfRoot + { + Solution = new SlnfSolution + { + Path = relativeSolutionPath, + Projects = projects?.Select(NormalizePathSeparatorsToBackslash).ToList() ?? new List() + } + }; + + var json = JsonSerializer.Serialize(root, SlnfJsonSerializerContext.Default.SlnfRoot); + File.WriteAllText(slnfPath, json); + } + + /// + /// Saves a solution filter file with the given projects + /// + /// Path to the solution filter file + /// Path to the parent solution (stored in the slnf file) + /// List of project paths (relative to the parent solution) + public static void SaveSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects) + { + var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)) ?? string.Empty; + + // Normalize the parent solution path to be relative to the slnf file + var relativeSolutionPath = parentSolutionPath; + if (Path.IsPathRooted(parentSolutionPath)) + { + relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionPath); + } + + // Normalize path separators to backslashes (as per slnf format) + relativeSolutionPath = NormalizePathSeparatorsToBackslash(relativeSolutionPath); + + var root = new SlnfRoot + { + Solution = new SlnfSolution + { + Path = relativeSolutionPath, + Projects = projects.Select(NormalizePathSeparatorsToBackslash).ToList() + } + }; + + var json = JsonSerializer.Serialize(root, SlnfJsonSerializerContext.Default.SlnfRoot); + File.WriteAllText(slnfPath, json); + } +} diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs index 015af6723629..7960deb22cc7 100644 --- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -85,11 +85,11 @@ private static void CacheDeviceId(string deviceId) // Cache device Id in Windows registry matching the OS architecture using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) { - using(var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + using (var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) { if (key != null) { - key.SetValue("deviceid", deviceId); + key.SetValue("deviceid", deviceId); } } } diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs index d9c3a59bd8a1..38f0d1c7ca19 100644 --- a/src/Cli/dotnet/Telemetry/Telemetry.cs +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -258,6 +258,6 @@ static IDictionary Combine(IDictionary { eventMeasurements[measurement.Key] = measurement.Value; } - return eventMeasurements; - } + return eventMeasurements; + } } diff --git a/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs b/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs index 641c8c583a7c..9da8558f5384 100644 --- a/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs +++ b/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs @@ -62,7 +62,7 @@ private static void EnsureNoLeadingDot(string commandName) } } - + public string CommandName { get; } public string ToolAssemblyEntryPoint { get; } diff --git a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj index 21950ecd89f0..ba27a108b1df 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj +++ b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj @@ -85,7 +85,6 @@ - diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs index 777ed43ee10f..124ee9749f45 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs @@ -58,6 +58,38 @@ internal async Task ExecuteAsync(CancellationToken cancellationToken) return !Log.HasLoggedErrors; } + bool credentialsSet = false; + VSHostObject hostObj = new(HostObject, Log); + if (hostObj.TryGetCredentials() is (string userName, string pass)) + { + // Set credentials for the duration of this operation. + // These will be cleared in the finally block to minimize exposure. + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, userName); + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, pass); + credentialsSet = true; + } + else + { + Log.LogMessage(MessageImportance.Low, Resource.GetString(nameof(Strings.HostObjectNotDetected))); + } + + try + { + return await ExecuteAsyncCore(logger, msbuildLoggerFactory, cancellationToken).ConfigureAwait(false); + } + finally + { + // Clear credentials from environment to minimize exposure window. + if (credentialsSet) + { + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectUser, null); + Environment.SetEnvironmentVariable(ContainerHelpers.HostObjectPass, null); + } + } + } + + private async Task ExecuteAsyncCore(ILogger logger, ILoggerFactory msbuildLoggerFactory, CancellationToken cancellationToken) + { RegistryMode sourceRegistryMode = BaseRegistry.Equals(OutputRegistry, StringComparison.InvariantCultureIgnoreCase) ? RegistryMode.PullFromOutput : RegistryMode.Pull; Registry? sourceRegistry = IsLocalPull ? null : new Registry(BaseRegistry, logger, sourceRegistryMode); SourceImageReference sourceImageReference = new(sourceRegistry, BaseImageName, BaseImageTag, BaseImageDigest); diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs index 5381d2afa590..8bcec281ff38 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImageToolTask.cs @@ -63,8 +63,8 @@ private string DotNetPath /// protected override ProcessStartInfo GetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch) { - VSHostObject hostObj = new(HostObject as System.Collections.Generic.IEnumerable); - if (hostObj.ExtractCredentials(out string user, out string pass, (string s) => Log.LogWarning(s))) + VSHostObject hostObj = new(HostObject, Log); + if (hostObj.TryGetCredentials() is (string user, string pass)) { extractionInfo = (true, user, pass); } diff --git a/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs b/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs index f65843f5ae39..89ea5b16ceae 100644 --- a/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs +++ b/src/Containers/Microsoft.NET.Build.Containers/VSHostObject.cs @@ -1,44 +1,121 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; +using System.Text.Json; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; namespace Microsoft.NET.Build.Containers.Tasks; -internal sealed class VSHostObject +internal sealed class VSHostObject(ITaskHost? hostObject, TaskLoggingHelper log) { private const string CredentialItemSpecName = "MsDeployCredential"; private const string UserMetaDataName = "UserName"; private const string PasswordMetaDataName = "Password"; - IEnumerable? _hostObject; - public VSHostObject(IEnumerable? hostObject) + private readonly ITaskHost? _hostObject = hostObject; + private readonly TaskLoggingHelper _log = log; + + /// + /// Tries to extract credentials from the host object. + /// + /// A tuple of (username, password) if credentials were found with non-empty username, null otherwise. + public (string username, string password)? TryGetCredentials() { - _hostObject = hostObject; + if (_hostObject is null) + { + return null; + } + + IEnumerable? taskItems = GetTaskItems(); + if (taskItems is null) + { + _log.LogMessage(MessageImportance.Low, "No task items found in host object."); + return null; + } + + ITaskItem? credentialItem = taskItems.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName); + if (credentialItem is null) + { + return null; + } + + string username = credentialItem.GetMetadata(UserMetaDataName); + if (string.IsNullOrEmpty(username)) + { + return null; + } + + string password = credentialItem.GetMetadata(PasswordMetaDataName); + return (username, password); } - public bool ExtractCredentials(out string username, out string password, Action logMethod) + private IEnumerable? GetTaskItems() { - bool retVal = false; - username = password = string.Empty; - if (_hostObject != null) + try { - ITaskItem credentialItem = _hostObject.FirstOrDefault(p => p.ItemSpec == CredentialItemSpecName); - if (credentialItem != null) + // This call mirrors the behavior of Microsoft.WebTools.Publish.MSDeploy.VSMsDeployTaskHostObject.QueryAllTaskItems. + // Expected contract: + // - Instance method on the host object named "QueryAllTaskItems". + // - Signature: string QueryAllTaskItems(). + // - Returns a JSON array of objects with the shape: + // [{ "ItemSpec": "", "Metadata": { "": "", ... } }, ...] + // The JSON is deserialized into TaskItemDto records and converted to ITaskItem instances. + // Only UserName and Password metadata are extracted to avoid conflicts with reserved MSBuild metadata. + string? rawTaskItems = (string?)_hostObject!.GetType().InvokeMember( + "QueryAllTaskItems", + BindingFlags.InvokeMethod, + null, + _hostObject, + null); + + if (!string.IsNullOrEmpty(rawTaskItems)) { - retVal = true; - username = credentialItem.GetMetadata(UserMetaDataName); - if (!string.IsNullOrEmpty(username)) + List? dtos = JsonSerializer.Deserialize>(rawTaskItems); + if (dtos is not null && dtos.Count > 0) { - password = credentialItem.GetMetadata(PasswordMetaDataName); + _log.LogMessage(MessageImportance.Low, "Successfully retrieved task items via QueryAllTaskItems."); + return dtos.Select(ConvertToTaskItem).ToList(); } - else + } + + _log.LogMessage(MessageImportance.Low, "QueryAllTaskItems returned null or empty result."); + } + catch (Exception ex) + { + _log.LogMessage(MessageImportance.Low, "Exception trying to call QueryAllTaskItems: {0}", ex.Message); + } + + // Fallback: try to use the host object directly as IEnumerable (legacy behavior). + if (_hostObject is IEnumerable enumerableHost) + { + _log.LogMessage(MessageImportance.Low, "Falling back to IEnumerable host object."); + return enumerableHost; + } + + return null; + + static TaskItem ConvertToTaskItem(TaskItemDto dto) + { + TaskItem taskItem = new(dto.ItemSpec ?? string.Empty); + if (dto.Metadata is not null) + { + if (dto.Metadata.TryGetValue(UserMetaDataName, out string? userName)) { - logMethod("HostObject credentials not detected. Falling back to Docker credential retrieval."); + taskItem.SetMetadata(UserMetaDataName, userName); + } + + if (dto.Metadata.TryGetValue(PasswordMetaDataName, out string? password)) + { + taskItem.SetMetadata(PasswordMetaDataName, password); } } + + return taskItem; } - return retVal; } + + private readonly record struct TaskItemDto(string? ItemSpec, Dictionary? Metadata); } diff --git a/src/Microsoft.DotNet.ProjectTools/LaunchSettings/LaunchSettings.cs b/src/Microsoft.DotNet.ProjectTools/LaunchSettings/LaunchSettings.cs index 5e22df2e18d7..ae1c60161b27 100644 --- a/src/Microsoft.DotNet.ProjectTools/LaunchSettings/LaunchSettings.cs +++ b/src/Microsoft.DotNet.ProjectTools/LaunchSettings/LaunchSettings.cs @@ -20,7 +20,6 @@ public static class LaunchSettings public static IEnumerable SupportedProfileTypes => s_providers.Keys; - public static string GetPropertiesLaunchSettingsPath(string directoryPath, string propertiesDirectoryName) => Path.Combine(directoryPath, propertiesDirectoryName, "launchSettings.json"); diff --git a/src/RazorSdk/Tool/GenerateCommand.cs b/src/RazorSdk/Tool/GenerateCommand.cs index d76d49b0eb10..ee542977d8d8 100644 --- a/src/RazorSdk/Tool/GenerateCommand.cs +++ b/src/RazorSdk/Tool/GenerateCommand.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Threading; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.CSharp; using Microsoft.NET.Sdk.Razor.Tool.CommandLineUtils; diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestCorruptionRepairer.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestCorruptionRepairer.cs new file mode 100644 index 000000000000..4c3a9e907ba9 --- /dev/null +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestCorruptionRepairer.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Sdk.WorkloadManifestReader +{ + /// + /// Provides a hook for the CLI layer to detect and repair corrupt workload manifest installations + /// before the manifests are loaded by the resolver. + /// + public interface IWorkloadManifestCorruptionRepairer + { + /// + /// Ensures that the manifests required by the current resolver are present and healthy. + /// + /// How to handle corruption if detected. + void EnsureManifestsHealthy(ManifestCorruptionFailureMode failureMode); + } +} diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs index 4a2df9d5ecaa..887b170fcd04 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/IWorkloadManifestProvider.cs @@ -3,6 +3,30 @@ namespace Microsoft.NET.Sdk.WorkloadManifestReader { + /// + /// Specifies how the manifest provider should handle corrupt or missing workload manifests. + /// + public enum ManifestCorruptionFailureMode + { + /// + /// Attempt to repair using the CorruptionRepairer if available, otherwise throw. + /// This is the default mode for commands that modify workloads. + /// + Repair, + + /// + /// Throw a helpful error message suggesting how to fix the issue. + /// Use this for read-only/info commands. + /// + Throw, + + /// + /// Silently ignore missing manifests and continue. + /// Use this for history recording or other scenarios where missing manifests are acceptable. + /// + Ignore + } + /// /// This abstracts out the process of locating and loading a set of manifests to be loaded into a /// workload manifest resolver and resolved into a single coherent model. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs index a9e166e3af12..e1cc8aed1238 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs @@ -6,6 +6,7 @@ using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Commands.Workload; using Microsoft.NET.Sdk.Localization; +using Microsoft.DotNet.Cli.Commands; using static Microsoft.NET.Sdk.WorkloadManifestReader.IWorkloadManifestProvider; namespace Microsoft.NET.Sdk.WorkloadManifestReader @@ -31,6 +32,24 @@ public partial class SdkDirectoryWorkloadManifestProvider : IWorkloadManifestPro private bool _useManifestsFromInstallState = true; private bool? _globalJsonSpecifiedWorkloadSets = null; + /// + /// Optional hook that allows the CLI to ensure workload manifests are available (and repaired if necessary) + /// before this provider attempts to enumerate them. + /// + public IWorkloadManifestCorruptionRepairer? CorruptionRepairer { get; set; } + + /// + /// Specifies how this provider should handle corrupt or missing workload manifests. + /// Default is . + /// + public ManifestCorruptionFailureMode CorruptionFailureMode { get; set; } = ManifestCorruptionFailureMode.Repair; + + /// + /// Gets the resolved workload set, if any. This is populated during construction/refresh + /// and does not trigger corruption checking. + /// + public WorkloadSet? ResolvedWorkloadSet => _workloadSet; + // This will be non-null if there is an error loading manifests that should be thrown when they need to be accessed. // We delay throwing the error so that in the case where global.json specifies a workload set that isn't installed, // we can successfully construct a resolver and install that workload set @@ -247,6 +266,19 @@ void ThrowExceptionIfManifestsNotAvailable() public WorkloadVersionInfo GetWorkloadVersion() { + if (CorruptionRepairer != null) + { + CorruptionRepairer.EnsureManifestsHealthy(CorruptionFailureMode); + } + else if (_workloadSet != null && CorruptionFailureMode != ManifestCorruptionFailureMode.Ignore) + { + // No repairer attached - check for missing manifests and throw a helpful error + if (HasMissingManifests(_workloadSet)) + { + throw new InvalidOperationException(string.Format(Strings.WorkloadSetHasMissingManifests, _workloadSet.Version)); + } + } + if (_globalJsonWorkloadSetVersion != null) { // _exceptionToThrow is set to null here if and only if the workload set is not installed. @@ -290,6 +322,18 @@ public WorkloadVersionInfo GetWorkloadVersion() public IEnumerable GetManifests() { + if (CorruptionRepairer != null) + { + CorruptionRepairer.EnsureManifestsHealthy(CorruptionFailureMode); + } + else if (_workloadSet != null && CorruptionFailureMode != ManifestCorruptionFailureMode.Ignore) + { + // No repairer attached - check for missing manifests and throw a helpful error + if (HasMissingManifests(_workloadSet)) + { + throw new InvalidOperationException(string.Format(Strings.WorkloadSetHasMissingManifests, _workloadSet.Version)); + } + } ThrowExceptionIfManifestsNotAvailable(); // Scan manifest directories @@ -365,6 +409,10 @@ void ProbeDirectory(string manifestDirectory, string featureBand) var manifestDirectory = GetManifestDirectoryFromSpecifier(manifestSpecifier); if (manifestDirectory == null) { + if (CorruptionFailureMode == ManifestCorruptionFailureMode.Ignore) + { + continue; + } throw new FileNotFoundException(string.Format(Strings.ManifestFromWorkloadSetNotFound, manifestSpecifier.ToString(), _workloadSet.Version)); } AddManifest(manifestSpecifier.Id.ToString(), manifestDirectory, manifestSpecifier.FeatureBand.ToString(), kvp.Value.Version.ToString()); @@ -382,6 +430,10 @@ void ProbeDirectory(string manifestDirectory, string featureBand) var manifestDirectory = GetManifestDirectoryFromSpecifier(manifestSpecifier); if (manifestDirectory == null) { + if (CorruptionFailureMode == ManifestCorruptionFailureMode.Ignore) + { + continue; + } throw new FileNotFoundException(string.Format(Strings.ManifestFromInstallStateNotFound, manifestSpecifier.ToString(), _installStateFilePath)); } AddManifest(manifestSpecifier.Id.ToString(), manifestDirectory, manifestSpecifier.FeatureBand.ToString(), kvp.Value.Version.ToString()); @@ -512,6 +564,23 @@ void ProbeDirectory(string manifestDirectory, string featureBand) return null; } + /// + /// Checks if the workload set has any manifests that are missing from disk. + /// This checks all manifest roots (including user-local installs). + /// + public bool HasMissingManifests(WorkloadSet workloadSet) + { + foreach (var manifestEntry in workloadSet.ManifestVersions) + { + var manifestSpecifier = new ManifestSpecifier(manifestEntry.Key, manifestEntry.Value.Version, manifestEntry.Value.FeatureBand); + if (GetManifestDirectoryFromSpecifier(manifestSpecifier) == null) + { + return true; + } + } + return false; + } + /// /// Returns installed workload sets that are available for this SDK (ie are in the same feature band) /// @@ -540,7 +609,7 @@ Dictionary GetAvailableWorkloadSetsInternal(SdkFeatureBand? } else { - // Get workload sets for all feature bands + // Get workload sets for all feature bands foreach (var featureBandDirectory in Directory.GetDirectories(manifestRoot)) { AddWorkloadSetsForFeatureBand(availableWorkloadSets, featureBandDirectory); diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx index cf418daa75cb..e583daad115c 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/Strings.resx @@ -1,17 +1,17 @@  - @@ -213,5 +213,9 @@ No manifest with ID {0} exists. + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + {Locked="dotnet workload repair"} + diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf index 1891cc4ff30b..314e15daddb0 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.cs.xlf @@ -142,6 +142,11 @@ Nevyřešený cíl {0} pro přesměrování úlohy {1} v manifestu {2} [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + Ve verzi sady úloh {0} chybí manifesty, které pravděpodobně odstranila správa balíčků. Pokud chcete tento problém vyřešit, spusťte příkaz dotnet workload repair. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. Verze {0} úlohy, která byla zadána v {1}, nebyla nalezena. Spuštěním příkazu dotnet workload restore nainstalujte tuto verzi úlohy. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf index aa35c3838c22..deccb474ffd2 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.de.xlf @@ -142,6 +142,11 @@ Nicht aufgelöstes Ziel „{0}“ für die Workloadumleitung „{1}“ im Manifest „{2}“ [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + In Version {0} des Arbeitslastsatzes fehlen Manifeste, die wahrscheinlich von der Paketverwaltung entfernt wurden. Führen Sie „dotnet workload repair“ aus, um das Problem zu beheben. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. Die Arbeitsauslastungsversion {0}, die in {1} angegeben wurde, wurde nicht gefunden. Führen Sie „dotnet workload restore“ aus, um diese Workloadversion zu installieren. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf index 485e6e0a0cf8..fa5f6c52c089 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.es.xlf @@ -142,6 +142,11 @@ Destino sin resolver '{0}' para redirección de carga de trabajo '{1}' en el manifiesto '{2}' [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + La versión del conjunto de cargas de trabajo {0} tiene manifiestos faltantes, probablemente eliminados por la administración de paquetes. Ejecute "dotnet workload repair" para corregir esto. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. No se encontró la versión de carga de trabajo {0}, que se especificó en {1}. Ejecuta "dotnet workload restore" para instalar esta versión de carga de trabajo. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf index 0ae7f6800bd3..4a4262c69213 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.fr.xlf @@ -142,6 +142,11 @@ Cible non résolue « {0} » pour la redirection de charge de travail « {1} » dans le manifeste « {2} » [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + La version {0} de l’ensemble de la charge de travail comporte des manifestes manquants qui ont probablement été supprimés par Package Management. Exécutez « dotnet workload repair » pour corriger ce problème. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. La version de charge de travail {0}, qui a été spécifiée dans {1}, est introuvable. Exécutez « dotnet workload restore » pour installer cette version de charge de travail. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf index 7c666799e412..0362f8f48f0c 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.it.xlf @@ -142,6 +142,11 @@ Destinazione non risolta '{0}' per il reindirizzamento del carico di lavoro '{1}' nel manifesto '{2}' [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + Nella versione {0} del set di carichi di lavoro mancano alcuni file manifesto, probabilmente rimossi da Gestione pacchetti. Esegui "dotnet workload repair" per risolvere il problema. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. La versione del carico di lavoro {0}, specificata in {1}, non è stata trovata. Eseguire "dotnet workload restore" per installare questa versione del carico di lavoro. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf index cc172807eaa9..cec4a86a8e92 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ja.xlf @@ -142,6 +142,11 @@ マニフェスト '{2}' [{3}] 内のワークロード リダイレクト '{1}' に対する未解決のターゲット '{0}' + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + ワークロード セット バージョン {0} で、パッケージ管理によって削除された可能性が高いマニフェストが不足しています。これを修正するには、"dotnet workload repair" を実行してください。 + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. {1} で指定されたワークロード バージョン {0} が見つかりませんでした。"dotnet workload restore" を実行して、このワークロード バージョンをインストールします。 diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf index 385531b1c48b..85060a626cd5 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ko.xlf @@ -142,6 +142,11 @@ 매니페스트 '{2}' [{3}]의 워크로드 리디렉션 '{1}'에 대한 확인되지 않는 대상 '{0}' + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + 워크로드 집합 버전 {0}에 패키지 관리에 의해 제거될 수 있는 매니페스트가 누락되었습니다. 이 문제를 해결하려면 "dotnet workload repair"를 실행하세요. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. {1}에 지정된 워크로드 버전 {0}을(를) 찾을 수 없습니다. "dotnet workload restore"을 실행하여 이 워크로드 버전을 설치합니다. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf index 2f5dc549ddf0..31f087586422 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pl.xlf @@ -142,6 +142,11 @@ Nierozpoznany element docelowy „{0}” przekierowania obciążenia „{1}” w manifeście „{2}” [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + Wersja zestawu obciążeń {0} ma brakujące manifesty, prawdopodobnie usunięte przez zarządzanie pakietami. Uruchom „dotnet workload repair”, aby to naprawić. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. Nie znaleziono wersji obciążenia {0} określonej w kontenerze {1}. Uruchom polecenie „dotnet workload restore”, aby zainstalować tę wersję obciążenia. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf index f5f0331a25ec..ded314a61d27 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.pt-BR.xlf @@ -142,6 +142,11 @@ Destino '{0}' não resolvido para o redirecionamento de carga de trabalho '{1}' no manifesto '{2}' [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + A versão do conjunto de cargas de trabalho {0} tem manifestos ausentes, provavelmente removidos pelo gerenciamento de pacotes. Execute "dotnet workload repair" para corrigir isso. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. A versão da carga de trabalho {0}, especificada em {1}, não foi localizada. Execute "dotnet workload restore" para instalar esta versão da carga de trabalho. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf index bf88a6af7bbe..7f14a7a2b8b0 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.ru.xlf @@ -142,6 +142,11 @@ Неразрешенный целевой объект "{0}" для перенаправления рабочей нагрузки "{1}" в манифесте "{2}" [{3}] + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + В версии набора рабочих нагрузок {0} отсутствуют манифесты, которые, вероятно, были удалены при управлении пакетами. Чтобы устранить эту проблему, выполните команду "dotnet workload repair". + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. Версия рабочей нагрузки {0}, указанная в {1}, не найдена. Запустите команду "dotnet workload restore", чтобы установить эту версию рабочей нагрузки. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf index 786f001a3435..33acc94ab13d 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.tr.xlf @@ -142,6 +142,11 @@ '{2}' [{3}] bildirimindeki '{1}' iş akışı yeniden yönlendirmesi için '{0}' hedefi çözümlenemedi + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + İş yükü kümesi {0} sürümü, paket yönetimi tarafından kaldırılmış olabilecek eksik bildirimlere sahip. Bunu düzeltmek için "dotnet workload repair" komutunu çalıştırın. + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. {1} konumunda belirtilen {0} iş yükü sürümü bulunamadı. Bu iş yükü sürümünü yüklemek için "dotnet workload restore" komutunu çalıştırın. diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf index 911a2731d284..2ac97ca62ff2 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hans.xlf @@ -142,6 +142,11 @@ 工作负载未解析的目标“{0}”重定向到清单“{2}”[{3}] 中的“{1}” + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + 工作负载集版本 {0} 缺少清单,这些清单可能已被包管理移除。请运行 "dotnet workload repair" 进行修复。 + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. 找不到在 {1} 中指定的工作负载版本 {0}。运行“dotnet workload restore”以安装此工作负载版本。 diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf index 423e6216ecca..6ec848a5c76e 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/xlf/Strings.zh-Hant.xlf @@ -142,6 +142,11 @@ 資訊清單 '{2}' [{3}] 中工作負載重新導向 '{1}' 的未解析目標 '{0}' + + Workload set version {0} has missing manifests likely removed by package management. Run "dotnet workload repair" to fix this. + 工作負載集合版本 {0} 缺少資訊清單,其可能已遭套件管理移除。請執行「dotnet workload repair」來修復此問題。 + {Locked="dotnet workload repair"} + Workload version {0}, which was specified in {1}, was not found. Run "dotnet workload restore" to install this workload version. 找不到 {1} 中指定的工作負載版本 {0}。執行 "dotnet workload restore" 以安裝此工作負載版本。 diff --git a/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs b/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs index af4b56aa899f..138af4457cae 100644 --- a/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs +++ b/src/System.CommandLine.StaticCompletions/DynamicSymbolExtensions.cs @@ -9,6 +9,8 @@ namespace System.CommandLine.StaticCompletions; /// public static class DynamicSymbolExtensions { + private static readonly Lock s_guard = new(); + /// /// The state that is used to track which symbols are dynamic. /// @@ -21,38 +23,44 @@ public static class DynamicSymbolExtensions /// public bool IsDynamic { - get => s_dynamicSymbols.GetValueOrDefault(option, false); - set => s_dynamicSymbols[option] = value; - } - - /// - /// Mark this option as requiring dynamic completions. - /// - /// - public Option RequiresDynamicCompletion() - { - option.IsDynamic = true; - return option; + get + { + lock (s_guard) + { + return s_dynamicSymbols.GetValueOrDefault(option, false); + } + } + set + { + lock (s_guard) + { + s_dynamicSymbols[option] = value; + } + } } } extension(Argument argument) { - /// Indicates whether this argument requires a dynamic call into the dotnet process to compute completions. - public bool IsDynamic - { - get => s_dynamicSymbols.GetValueOrDefault(argument, false); - set => s_dynamicSymbols[argument] = value; - } - /// - /// Mark this argument as requiring dynamic completions. + /// Indicates whether this argument requires a dynamic call into the dotnet process to compute completions. /// - /// - public Argument RequiresDynamicCompletion() + public bool IsDynamic { - argument.IsDynamic = true; - return argument; + get + { + lock (s_guard) + { + return s_dynamicSymbols.GetValueOrDefault(argument, false); + } + } + set + { + lock (s_guard) + { + s_dynamicSymbols[argument] = value; + } + } } } } diff --git a/tasks.code-workspace b/tasks.code-workspace new file mode 100644 index 000000000000..67a217bc7698 --- /dev/null +++ b/tasks.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "tasks.slnf" + } + } diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..6bc180f304f1 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "ParentSolution": { + "longName": "parent-solution", + "shortName": "s" + } + } +} diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json new file mode 100644 index 000000000000..7f33956aab45 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Solution" + ], + "name": "Solution Filter File", + "generatorVersions": "[1.0.0.0-*)", + "description": "Create a solution filter file that references a parent solution", + "groupIdentity": "ItemSolutionFilter", + "precedence": "100", + "identity": "Microsoft.Standard.QuickStarts.SolutionFilter", + "shortName": [ + "slnf", + "solutionfilter" + ], + "sourceName": "SolutionFilter1", + "symbols": { + "ParentSolution": { + "type": "parameter", + "displayName": "Parent solution file", + "description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension).", + "datatype": "string", + "defaultValue": "SolutionFilter1.slnx", + "replaces": "SolutionFilter1.slnx" + } + }, + "defaultName": "SolutionFilter1" +} diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf new file mode 100644 index 000000000000..d633922fb216 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "SolutionFilter1.slnx", + "projects": [] + } +} diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs index 7bc6aaf7ad06..8c4714ce618b 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs @@ -268,8 +268,10 @@ public void PackageToolWithAnyRid() .And.Satisfy(SupportAllOfTheseRuntimes([.. expectedRids, "any"])); } - [Fact] - public void InstallAndRunToolFromAnyRid() + [Theory] + [InlineData("exec")] + [InlineData("dnx")] + public void InstallAndRunToolFromAnyRid(string command) { var toolSettings = new TestToolBuilder.TestToolSettings() { @@ -284,7 +286,12 @@ public void InstallAndRunToolFromAnyRid() var testDirectory = TestAssetsManager.CreateTestDirectory(); var homeFolder = Path.Combine(testDirectory.Path, "home"); - new DotnetToolCommand(Log, "exec", toolSettings.ToolPackageId, "--verbosity", "diagnostic", "--yes", "--source", toolPackagesPath) + string[] args = [command, toolSettings.ToolPackageId, "--verbosity", "diagnostic", "--yes", "--source", toolPackagesPath]; + var testCommand = command == "dnx" + ? new DotnetCommand(Log, args) + : new DotnetToolCommand(Log, args); + + testCommand .WithEnvironmentVariables(homeFolder) .WithWorkingDirectory(testDirectory.Path) .Execute() @@ -292,8 +299,10 @@ public void InstallAndRunToolFromAnyRid() .And.HaveStdOutContaining("Hello Tool!"); } - [Fact] - public void InstallAndRunToolFromAnyRidWhenOtherRidsArePresentButIncompatible() + [Theory] + [InlineData("exec")] + [InlineData("dnx")] + public void InstallAndRunToolFromAnyRidWhenOtherRidsArePresentButIncompatible(string command) { var toolSettings = new TestToolBuilder.TestToolSettings() { @@ -312,7 +321,12 @@ .. expectedRids.Select(rid => $"{toolSettings.ToolPackageId}.{rid}.{toolSettings var testDirectory = TestAssetsManager.CreateTestDirectory(); var homeFolder = Path.Combine(testDirectory.Path, "home"); - new DotnetToolCommand(Log, "exec", toolSettings.ToolPackageId, "--verbosity", "diagnostic", "--yes", "--source", toolPackagesPath) + string[] args = [command, toolSettings.ToolPackageId, "--verbosity", "diagnostic", "--yes", "--source", toolPackagesPath]; + var testCommand = command == "dnx" + ? new DotnetCommand(Log, args) + : new DotnetToolCommand(Log, args); + + testCommand .WithEnvironmentVariables(homeFolder) .WithWorkingDirectory(testDirectory.Path) .Execute() diff --git a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs index a58c43f7dcfe..7bba809b7657 100644 --- a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs +++ b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs @@ -38,43 +38,7 @@ public async ValueTask DisposeAsync() } [Fact] - public async Task ApplyManagedCodeUpdates_ProcessNotSuspended() - { - var moduleId = Guid.NewGuid(); - - var agent = new TestHotReloadAgent() - { - Capabilities = "Baseline AddMethodToExistingType AddStaticFieldToExistingType", - ApplyManagedCodeUpdatesImpl = updates => - { - Assert.Single(updates); - var update = updates.First(); - Assert.Equal(moduleId, update.ModuleId); - AssertEx.SequenceEqual([1, 2, 3], update.MetadataDelta); - } - }; - - await using var test = new Test(output, agent); - - var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None); - AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "AddExplicitInterfaceImplementation"], actualCapabilities); - - var update = new HotReloadManagedCodeUpdate( - moduleId: moduleId, - metadataDelta: [1, 2, 3], - ilDelta: [], - pdbDelta: [], - updatedTypes: [], - requiredCapabilities: ["Baseline"]); - - Assert.Equal(ApplyStatus.AllChangesApplied, await test.Client.ApplyManagedCodeUpdatesAsync([update], isProcessSuspended: false, CancellationToken.None)); - - Assert.Contains("[Debug] Writing capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType", test.AgentLogger.GetAndClearMessages()); - Assert.Contains("[Debug] Updates applied: 1 out of 1.", test.Logger.GetAndClearMessages()); - } - - [Fact] - public async Task ApplyManagedCodeUpdates_ProcessSuspended() + public async Task ApplyManagedCodeUpdates() { var moduleId = Guid.NewGuid(); @@ -98,18 +62,13 @@ public async Task ApplyManagedCodeUpdates_ProcessSuspended() var agentMessage = "[Debug] Writing capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType"; - Assert.Equal(ApplyStatus.AllChangesApplied, await test.Client.ApplyManagedCodeUpdatesAsync([update], isProcessSuspended: true, CancellationToken.None)); - - // emulate process being resumed: - await test.Client.PendingUpdates; + await await test.Client.ApplyManagedCodeUpdatesAsync([update], CancellationToken.None, CancellationToken.None); var clientMessages = test.Logger.GetAndClearMessages(); var agentMessages = test.AgentLogger.GetAndClearMessages(); - // agent log messages not reported to the client logger while the process is suspended: - Assert.Contains("[Debug] Sending update batch #0", clientMessages); - Assert.Contains("[Debug] Updates applied: 1 out of 1.", clientMessages); - Assert.Contains("[Debug] Update batch #0 completed.", clientMessages); + Assert.Contains("[Debug] " + string.Format(LogEvents.SendingUpdateBatch.Message, 0), clientMessages); + Assert.Contains("[Debug] " + string.Format(LogEvents.UpdateBatchCompleted.Message, 0), clientMessages); Assert.Contains(agentMessage, agentMessages); } @@ -135,9 +94,8 @@ public async Task ApplyManagedCodeUpdates_Failure() updatedTypes: [], requiredCapabilities: ["Baseline"]); - Assert.Equal(ApplyStatus.Failed, await test.Client.ApplyManagedCodeUpdatesAsync([update], isProcessSuspended: false, CancellationToken.None)); + await await test.Client.ApplyManagedCodeUpdatesAsync([update], CancellationToken.None, CancellationToken.None); - // agent log messages were reported to the agent logger: var agentMessages = test.AgentLogger.GetAndClearMessages(); Assert.Contains("[Error] The runtime failed to applying the change: Bug!", agentMessages); Assert.Contains("[Warning] Further changes won't be applied to this process.", agentMessages); diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs index cb5ec71b848b..0fee7451304e 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAnAotApp.cs @@ -51,7 +51,7 @@ public void NativeAot_hw_runs_with_no_warnings_when_PublishAot_is_enabled(string .And.NotHaveStdOutContaining("IL2026") .And.NotHaveStdErrContaining("NETSDK1179") .And.NotHaveStdErrContaining("warning") - .And.NotHaveStdOutContaining("warning"); + .And.NotHaveStdOutContaining("warning", new[] { "ld: warning: -ld_classic is deprecated and will be removed in a future release" }); var buildProperties = testProject.GetPropertyValues(testAsset.TestRoot, targetFramework); var rid = buildProperties["NETCoreSdkPortableRuntimeIdentifier"]; diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.files.json b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.files.json index 36188a7e33e2..ce358b8aec11 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.files.json +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.files.json @@ -253,6 +253,7 @@ "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CSharp.wasm.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.CSharp.wasm.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.wasm.gz", + "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.wasm.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.Extensions.Configuration.Abstractions.wasm.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.Extensions.Configuration.Binder.wasm.gz", @@ -491,6 +492,7 @@ "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\zh-Hant\\Microsoft.CodeAnalysis.resources.wasm.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\custom-service-worker-assets.js.gz", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\serviceworkers\\my-service-worker.js.gz", + "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\hotreload\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\service-worker\\custom-service-worker-assets.js.build", "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\service-worker\\my-service-worker.js.build", "${OutputPath}\\wwwroot\\_framework\\Microsoft.AspNetCore.Authorization.wasm", diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.staticwebassets.json b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.staticwebassets.json index 0d4ffdb77e44..fd2b3e682103 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.staticwebassets.json +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/StaticWebAssetsBaselines/Build_SatelliteAssembliesAreCopiedToBuildOutput.Build.staticwebassets.json @@ -5750,29 +5750,6 @@ "FileLength": -1, "LastWriteTime": "0001-01-01T00:00:00+00:00" }, - { - "Identity": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_content\\Microsoft.DotNet.HotReload.WebAssembly.Browser\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js.gz", - "SourceId": "Microsoft.DotNet.HotReload.WebAssembly.Browser", - "SourceType": "Package", - "ContentRoot": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\", - "BasePath": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser", - "RelativePath": "Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js.gz", - "AssetKind": "All", - "AssetMode": "All", - "AssetRole": "Alternative", - "AssetMergeBehavior": "", - "AssetMergeSource": "", - "RelatedAsset": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "AssetTraitName": "Content-Encoding", - "AssetTraitValue": "gzip", - "Fingerprint": "__fingerprint__", - "Integrity": "__integrity__", - "CopyToOutputDirectory": "Never", - "CopyToPublishDirectory": "PreserveNewest", - "OriginalItemSpec": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\_content\\Microsoft.DotNet.HotReload.WebAssembly.Browser\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js.gz", - "FileLength": -1, - "LastWriteTime": "0001-01-01T00:00:00+00:00" - }, { "Identity": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Authorization.wasm.gz", "SourceId": "blazorwasm", @@ -5980,6 +5957,29 @@ "FileLength": -1, "LastWriteTime": "0001-01-01T00:00:00+00:00" }, + { + "Identity": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", + "SourceId": "blazorwasm", + "SourceType": "Computed", + "ContentRoot": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "/", + "RelativePath": "_framework/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", + "AssetKind": "Build", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\hotreload\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\hotreload\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", + "FileLength": -1, + "LastWriteTime": "0001-01-01T00:00:00+00:00" + }, { "Identity": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.wasm.gz", "SourceId": "blazorwasm", @@ -11546,6 +11546,29 @@ "FileLength": -1, "LastWriteTime": "0001-01-01T00:00:00+00:00" }, + { + "Identity": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\hotreload\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "SourceId": "blazorwasm", + "SourceType": "Computed", + "ContentRoot": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\hotreload\\", + "BasePath": "/", + "RelativePath": "_framework/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "AssetKind": "Build", + "AssetMode": "All", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "JSModule", + "AssetTraitValue": "JSLibraryModule", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "obj\\Debug\\${Tfm}\\hotreload\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "FileLength": -1, + "LastWriteTime": "0001-01-01T00:00:00+00:00" + }, { "Identity": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\service-worker\\custom-service-worker-assets.js.build", "SourceId": "blazorwasm", @@ -11752,29 +11775,6 @@ "OriginalItemSpec": "wwwroot\\wwwroot\\exampleJsInterop.js", "FileLength": -1, "LastWriteTime": "0001-01-01T00:00:00+00:00" - }, - { - "Identity": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "SourceId": "Microsoft.DotNet.HotReload.WebAssembly.Browser", - "SourceType": "Package", - "ContentRoot": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\", - "BasePath": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser", - "RelativePath": "Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "AssetKind": "All", - "AssetMode": "All", - "AssetRole": "Primary", - "AssetMergeBehavior": "", - "AssetMergeSource": "", - "RelatedAsset": "", - "AssetTraitName": "JSModule", - "AssetTraitValue": "JSLibraryModule", - "Fingerprint": "__fingerprint__", - "Integrity": "__integrity__", - "CopyToOutputDirectory": "Never", - "CopyToPublishDirectory": "PreserveNewest", - "OriginalItemSpec": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "FileLength": -1, - "LastWriteTime": "0001-01-01T00:00:00+00:00" } ], "Endpoints": [ @@ -21034,8 +21034,8 @@ ] }, { - "Route": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser/Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_content\\Microsoft.DotNet.HotReload.WebAssembly.Browser\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js.gz", + "Route": "_framework/Microsoft.AspNetCore.Authorization.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Authorization.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21052,7 +21052,7 @@ }, { "Name": "Content-Type", - "Value": "text/javascript" + "Value": "application/wasm" }, { "Name": "ETag", @@ -21075,8 +21075,8 @@ ] }, { - "Route": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_content\\Microsoft.DotNet.HotReload.WebAssembly.Browser\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js.gz", + "Route": "_framework/Microsoft.AspNetCore.Authorization.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Authorization.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21099,7 +21099,7 @@ }, { "Name": "Content-Type", - "Value": "text/javascript" + "Value": "application/wasm" }, { "Name": "ETag", @@ -21115,10 +21115,6 @@ } ], "EndpointProperties": [ - { - "Name": "dependency-group", - "Value": "js-initializer" - }, { "Name": "integrity", "Value": "__integrity__" @@ -21126,16 +21122,12 @@ { "Name": "original-resource", "Value": "__original-resource__" - }, - { - "Name": "script-type", - "Value": "module" } ] }, { - "Route": "_framework/Microsoft.AspNetCore.Authorization.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Authorization.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.Forms.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Forms.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21175,8 +21167,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Authorization.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Authorization.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.Forms.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Forms.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21226,8 +21218,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.Forms.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Forms.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.Web.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Web.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21267,8 +21259,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.Forms.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Forms.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.Web.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Web.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21318,8 +21310,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.Web.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Web.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.WebAssembly.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.WebAssembly.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21359,8 +21351,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.Web.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.Web.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.WebAssembly.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.WebAssembly.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21410,8 +21402,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.WebAssembly.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.WebAssembly.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21451,8 +21443,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.WebAssembly.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.WebAssembly.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Components.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21502,8 +21494,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Metadata.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Metadata.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21543,8 +21535,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Components.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Components.wasm.gz", + "Route": "_framework/Microsoft.AspNetCore.Metadata.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Metadata.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21594,8 +21586,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Metadata.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Metadata.wasm.gz", + "Route": "_framework/Microsoft.CSharp.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CSharp.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21635,8 +21627,8 @@ ] }, { - "Route": "_framework/Microsoft.AspNetCore.Metadata.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.AspNetCore.Metadata.wasm.gz", + "Route": "_framework/Microsoft.CSharp.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CSharp.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21686,8 +21678,8 @@ ] }, { - "Route": "_framework/Microsoft.CSharp.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CSharp.wasm.gz", + "Route": "_framework/Microsoft.CodeAnalysis.CSharp.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.CSharp.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21727,8 +21719,8 @@ ] }, { - "Route": "_framework/Microsoft.CSharp.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CSharp.wasm.gz", + "Route": "_framework/Microsoft.CodeAnalysis.CSharp.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.CSharp.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21778,8 +21770,8 @@ ] }, { - "Route": "_framework/Microsoft.CodeAnalysis.CSharp.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.CSharp.wasm.gz", + "Route": "_framework/Microsoft.CodeAnalysis.wasm.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.wasm.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21819,8 +21811,8 @@ ] }, { - "Route": "_framework/Microsoft.CodeAnalysis.CSharp.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.CSharp.wasm.gz", + "Route": "_framework/Microsoft.CodeAnalysis.wasm", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.wasm.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21870,8 +21862,8 @@ ] }, { - "Route": "_framework/Microsoft.CodeAnalysis.wasm.gz", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.wasm.gz", + "Route": "_framework/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", "Selectors": [], "ResponseHeaders": [ { @@ -21888,7 +21880,7 @@ }, { "Name": "Content-Type", - "Value": "application/wasm" + "Value": "text/javascript" }, { "Name": "ETag", @@ -21911,8 +21903,8 @@ ] }, { - "Route": "_framework/Microsoft.CodeAnalysis.wasm", - "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.CodeAnalysis.wasm.gz", + "Route": "_framework/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\compressed\\_framework\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js.gz", "Selectors": [ { "Name": "Content-Encoding", @@ -21935,7 +21927,7 @@ }, { "Name": "Content-Type", - "Value": "application/wasm" + "Value": "text/javascript" }, { "Name": "ETag", @@ -44249,6 +44241,43 @@ } ] }, + { + "Route": "_framework/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\hotreload\\Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Accept-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, { "Route": "custom-service-worker-assets.js", "AssetFile": "${ProjectPath}\\blazorwasm\\obj\\Debug\\${Tfm}\\service-worker\\custom-service-worker-assets.js.build", @@ -44691,104 +44720,6 @@ "Value": "__integrity__" } ] - }, - { - "Route": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js", - "AssetFile": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "Selectors": [], - "ResponseHeaders": [ - { - "Name": "Cache-Control", - "Value": "no-cache" - }, - { - "Name": "Content-Length", - "Value": "__content-length__" - }, - { - "Name": "Content-Type", - "Value": "text/javascript" - }, - { - "Name": "ETag", - "Value": "__etag__" - }, - { - "Name": "Last-Modified", - "Value": "__last-modified__" - }, - { - "Name": "Vary", - "Value": "Accept-Encoding" - } - ], - "EndpointProperties": [ - { - "Name": "dependency-group", - "Value": "js-initializer" - }, - { - "Name": "integrity", - "Value": "__integrity__" - }, - { - "Name": "script-type", - "Value": "module" - } - ] - }, - { - "Route": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser/Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "AssetFile": "${RestorePath}\\microsoft.dotnet.hotreload.webassembly.browser\\${PackageVersion}\\staticwebassets\\Microsoft.DotNet.HotReload.WebAssembly.Browser.__fingerprint__.lib.module.js", - "Selectors": [], - "ResponseHeaders": [ - { - "Name": "Cache-Control", - "Value": "max-age=31536000, immutable" - }, - { - "Name": "Content-Length", - "Value": "__content-length__" - }, - { - "Name": "Content-Type", - "Value": "text/javascript" - }, - { - "Name": "ETag", - "Value": "__etag__" - }, - { - "Name": "Last-Modified", - "Value": "__last-modified__" - }, - { - "Name": "Vary", - "Value": "Accept-Encoding" - } - ], - "EndpointProperties": [ - { - "Name": "dependency-group", - "Value": "js-initializer" - }, - { - "Name": "fingerprint", - "Value": "__fingerprint__" - }, - { - "Name": "integrity", - "Value": "__integrity__" - }, - { - "Name": "label", - "Value": "_content/Microsoft.DotNet.HotReload.WebAssembly.Browser/Microsoft.DotNet.HotReload.WebAssembly.Browser.lib.module.js" - }, - { - "Name": "script-type", - "Value": "module" - } - ] } ] } diff --git a/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs index 03ec40ed5b31..653ed4a04cf2 100644 --- a/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs +++ b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs @@ -436,7 +436,7 @@ public void ItThrowsIfManifestFromWorkloadSetIsNotFound() var sdkDirectoryWorkloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(sdkRootPath: _fakeDotnetRootDirectory, sdkVersion: "8.0.200", userProfileDir: null, globalJsonPath: null); - Assert.Throws(() => GetManifestContents(sdkDirectoryWorkloadManifestProvider).ToList()); + Assert.Throws(() => GetManifestContents(sdkDirectoryWorkloadManifestProvider).ToList()); } [Fact] @@ -710,9 +710,9 @@ public void ItFailsIfManifestFromWorkloadSetFromInstallStateIsNotInstalled() var sdkDirectoryWorkloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(sdkRootPath: _fakeDotnetRootDirectory, sdkVersion: "8.0.200", userProfileDir: null, globalJsonPath: null); - var ex = Assert.Throws(() => sdkDirectoryWorkloadManifestProvider.GetManifests().ToList()); + var ex = Assert.Throws(() => sdkDirectoryWorkloadManifestProvider.GetManifests().ToList()); - ex.Message.Should().Be(string.Format(Strings.ManifestFromWorkloadSetNotFound, "ios: 11.0.2/8.0.100", "8.0.201")); + ex.Message.Should().Be(string.Format(Strings.WorkloadSetHasMissingManifests, "8.0.201")); } [Fact] diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt index 2dd26fe71c1e..c66a73d20e6c 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt @@ -118,6 +118,13 @@ InsertText: sln, Documentation: Create an empty solution containing no projects }, + { + Label: slnf, + Kind: Value, + SortText: slnf, + InsertText: slnf, + Documentation: Create a solution filter file that references a parent solution + }, { Label: tool-manifest, Kind: Value, diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt index ce76573e6a17..6d16601942e3 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt @@ -118,6 +118,13 @@ InsertText: sln, Documentation: Create an empty solution containing no projects }, + { + Label: slnf, + Kind: Value, + SortText: slnf, + InsertText: slnf, + Documentation: Create a solution filter file that references a parent solution + }, { Label: tool-manifest, Kind: Value, diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/InstallTests.cs b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/InstallTests.cs index 93d86edf07d6..e238049d1160 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/InstallTests.cs +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/InstallTests.cs @@ -237,6 +237,5 @@ public void CommandExampleCanShowParentCommandsBeyondNew() ParseResult parseResult = rootCommand.Parse("dotnet new install source"); Assert.Equal("dotnet new install my-source", Example.For(parseResult).WithSubcommand().WithArguments("my-source")); } - } } diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj index 04ce32c6db1d..8c8991768558 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -3,8 +3,22 @@ Exe net9.0;$(CurrentTargetFramework) + + false + + + + + + + + + @@ -49,9 +63,25 @@ + + + + + + + + + + - + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf new file mode 100644 index 000000000000..34cef9585f66 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf @@ -0,0 +1,8 @@ +{ + "solution": { + "path": "App.slnx", + "projects": [ + "src\\App\\App.csproj" + ] + } +} diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx new file mode 100644 index 000000000000..54df61baa606 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj new file mode 100644 index 000000000000..0361aa8cd36d --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj @@ -0,0 +1,6 @@ + + + Exe + net9.0 + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj new file mode 100644 index 000000000000..3043227ce00b --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj @@ -0,0 +1,5 @@ + + + net9.0 + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj new file mode 100644 index 000000000000..eb91b2d48523 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj @@ -0,0 +1,9 @@ + + + net9.0 + + + + + + diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf new file mode 100644 index 000000000000..e66e7dc8b150 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "Parent.slnx", + "projects": [] + } +} diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..70cab17a4b13 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "%TEMPLATE_NAME%" was created successfully. \ No newline at end of file diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf new file mode 100644 index 000000000000..e66e7dc8b150 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "Parent.slnx", + "projects": [] + } +} diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..70cab17a4b13 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "%TEMPLATE_NAME%" was created successfully. \ No newline at end of file diff --git a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt index fd5a8943c574..9483747f5bbb 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt +++ b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt @@ -25,6 +25,7 @@ proto razorclasslib razorcomponent sln +slnf tool-manifest view viewimports diff --git a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt index fd5a8943c574..9483747f5bbb 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt +++ b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt @@ -25,6 +25,7 @@ proto razorclasslib razorcomponent sln +slnf tool-manifest view viewimports diff --git a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt index 5162a1825dad..b063b22b9375 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt +++ b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt @@ -25,6 +25,7 @@ proto razorclasslib razorcomponent sln +slnf tool-manifest view viewimports diff --git a/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs b/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs index d9f6e0da04e0..b55c712a837a 100644 --- a/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs +++ b/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs @@ -54,6 +54,9 @@ public CommonTemplatesTests(SharedHomeDirectory fixture, ITestOutputHelper log) [InlineData("Solution File", "sln", new[] { "--format", "sln" })] [InlineData("Solution File", "sln", new[] { "--format", "slnx" })] [InlineData("Solution File", "solution", null)] + [InlineData("Solution Filter File", "slnf", new[] { "--parent-solution", "Parent.slnx" })] + [InlineData("Solution Filter File", "slnf", new[] { "-s", "Parent.slnx" })] + [InlineData("Solution Filter File", "solutionfilter", new[] { "--parent-solution", "Parent.slnx" })] [InlineData("Dotnet local tool manifest file", "tool-manifest", null)] [InlineData("Web Config", "webconfig", null)] [InlineData("EditorConfig file", "editorconfig", null)] diff --git a/test/dotnet-watch.Tests/Browser/BrowserTests.cs b/test/dotnet-watch.Tests/Browser/BrowserTests.cs index 17e21166bf3f..18511ab7f734 100644 --- a/test/dotnet-watch.Tests/Browser/BrowserTests.cs +++ b/test/dotnet-watch.Tests/Browser/BrowserTests.cs @@ -16,7 +16,7 @@ public async Task LaunchesBrowserOnStart() App.Start(testAsset, [], testFlags: TestFlags.MockBrowser); // check that all app output is printed out: - await App.WaitForOutputLineContaining("Content root path:"); + await App.WaitUntilOutputContains("Content root path:"); Assert.Contains(App.Process.Output, line => line.Contains("Application started. Press Ctrl+C to shut down.")); Assert.Contains(App.Process.Output, line => line.Contains("Hosting environment: Development")); @@ -38,9 +38,9 @@ public async Task BrowserDiagnostics() App.Start(testAsset, ["--urls", url], relativeProjectDirectory: "RazorApp", testFlags: TestFlags.ReadKeyFromStdin); - await App.WaitForOutputLineContaining(MessageDescriptor.ConfiguredToUseBrowserRefresh); - await App.WaitForOutputLineContaining(MessageDescriptor.ConfiguredToLaunchBrowser); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // Verify the browser has been launched. await App.WaitUntilOutputContains($"🧪 Test browser opened at '{url}'."); @@ -60,9 +60,9 @@ public async Task BrowserDiagnostics() var errorMessage = $"{homePagePath}(13,9): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."; var jsonErrorMessage = JsonSerializer.Serialize(errorMessage); - await App.WaitForOutputLineContaining(errorMessage); + await App.WaitUntilOutputContains(errorMessage); - await App.WaitForOutputLineContaining("Do you want to restart your app?"); + await App.WaitUntilOutputContains("Do you want to restart your app?"); await App.WaitUntilOutputContains($$""" 🧪 Received: {"type":"ReportDiagnostics","diagnostics":[{{jsonErrorMessage}}]} @@ -72,7 +72,7 @@ await App.WaitUntilOutputContains($$""" App.SendKey('a'); // browser page is reloaded when the app restarts: - await App.WaitForOutputLineContaining(MessageDescriptor.ReloadingBrowser, $"RazorApp ({tfm})"); + await App.WaitUntilOutputContains(MessageDescriptor.ReloadingBrowser, $"RazorApp ({tfm})"); // browser page was reloaded after the app restarted: await App.WaitUntilOutputContains(""" @@ -82,7 +82,7 @@ await App.WaitUntilOutputContains(""" // no other browser message sent: Assert.Equal(2, App.Process.Output.Count(line => line.Contains("🧪"))); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); App.Process.ClearOutput(); @@ -90,13 +90,13 @@ await App.WaitUntilOutputContains(""" UpdateSourceFile(homePagePath, src => src.Replace("public virtual int F() => 1;", "/* member placeholder */")); errorMessage = $"{homePagePath}(11,5): error ENC0033: Deleting method 'F()' requires restarting the application."; - await App.WaitForOutputLineContaining("[auto-restart] " + errorMessage); + await App.WaitUntilOutputContains("[auto-restart] " + errorMessage); await App.WaitUntilOutputContains($$""" 🧪 Received: {"type":"ReportDiagnostics","diagnostics":["Restarting application to apply changes ..."]} """); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // browser page was reloaded after the app restarted: await App.WaitUntilOutputContains(""" @@ -111,7 +111,7 @@ await App.WaitUntilOutputContains(""" // valid edit: UpdateSourceFile(homePagePath, src => src.Replace("/* member placeholder */", "public int F() => 1;")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); await App.WaitUntilOutputContains($$""" 🧪 Received: {"type":"ReportDiagnostics","diagnostics":[]} diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index 347c61b7c1b7..e7f60417c509 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -21,7 +21,7 @@ public async Task AddSourceFile() App.Start(testAsset, [], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // add a new file: UpdateSourceFile(Path.Combine(dependencyDir, "AnotherLib.cs"), """ @@ -32,7 +32,7 @@ public static void Print() } """); - await App.WaitForOutputLineContaining(MessageDescriptor.ReEvaluationCompleted); + await App.WaitUntilOutputContains(MessageDescriptor.ReEvaluationCompleted); // update existing file: UpdateSourceFile(Path.Combine(dependencyDir, "Foo.cs"), """ @@ -43,7 +43,7 @@ public static void Print() } """); - await App.AssertOutputLineStartsWith("Changed!"); + await App.WaitUntilOutputContains("Changed!"); } [Fact] @@ -56,7 +56,7 @@ public async Task ChangeFileInDependency() App.Start(testAsset, [], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); var newSrc = """ public class Lib @@ -68,9 +68,9 @@ public static void Print() UpdateSourceFile(Path.Combine(dependencyDir, "Foo.cs"), newSrc); - await App.AssertOutputLineStartsWith("Changed!"); + await App.WaitUntilOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); - App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); + await App.WaitUntilOutputContains("Changed!"); } [Fact(Skip="https://github.com/dotnet/sdk/issues/52680")] @@ -83,14 +83,15 @@ public async Task ProjectChange_UpdateDirectoryBuildPropsThenUpdateSource() App.Start(testAsset, [], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); UpdateSourceFile( Path.Combine(testAsset.Path, "Directory.Build.props"), src => src.Replace("false", "true")); - await App.WaitForOutputLineContaining(MessageDescriptor.NoCSharpChangesToApply); - App.AssertOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); + await App.WaitUntilOutputContains(MessageDescriptor.NoCSharpChangesToApply); + await App.WaitUntilOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); + App.Process.ClearOutput(); var newSrc = """ @@ -107,8 +108,8 @@ public static unsafe void Print() UpdateSourceFile(Path.Combine(dependencyDir, "Foo.cs"), newSrc); - await App.AssertOutputLineStartsWith("Changed!"); - await App.WaitUntilOutputContains($"dotnet watch 🔥 [App.WithDeps ({ToolsetInfo.CurrentTargetFramework})] Hot reload succeeded."); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); + await App.WaitUntilOutputContains("Changed!"); } [Theory] @@ -140,15 +141,15 @@ public static void Print() App.Start(testAsset, ["--non-interactive"], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); await App.WaitUntilOutputContains($"{symbolName} set"); App.Process.ClearOutput(); UpdateSourceFile(buildFilePath, src => src.Replace(symbolName, "")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); - App.AssertOutputContains("dotnet watch ⌚ [auto-restart] error ENC1102: Changing project setting 'DefineConstants'"); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); + await App.WaitUntilOutputContains("dotnet watch ⌚ [auto-restart] error ENC1102: Changing project setting 'DefineConstants'"); await App.WaitUntilOutputContains($"{symbolName} not set"); } @@ -173,7 +174,7 @@ public static void Print() { #if BUILD_CONST_IN_PROPS System.Console.WriteLine("BUILD_CONST_IN_PROPS set"); - #else + #else System.Console.WriteLine("BUILD_CONST_IN_PROPS not set"); #endif } @@ -182,7 +183,7 @@ public static void Print() App.Start(testAsset, [], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); await App.WaitUntilOutputContains("BUILD_CONST_IN_PROPS set"); App.Process.ClearOutput(); @@ -190,13 +191,13 @@ public static void Print() directoryBuildProps, src => src.Replace("BUILD_CONST_IN_PROPS", "")); - await App.WaitUntilOutputContains($"dotnet watch 🔥 [App.WithDeps ({ToolsetInfo.CurrentTargetFramework})] Hot reload succeeded."); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); await App.WaitUntilOutputContains("BUILD_CONST not set"); - App.AssertOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); + await App.WaitUntilOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); } - [Fact(Skip = "https://github.com/dotnet/sdk/issues/49545")] + [Fact] public async Task ProjectChange_DirectoryBuildProps_Delete() { var testAsset = TestAssets.CopyTestAsset("WatchAppWithProjectDeps") @@ -220,19 +221,20 @@ public static void Print() } """); - App.Start(testAsset, [], "AppWithDeps"); + App.Start(testAsset, ["--non-interactive"], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); await App.WaitUntilOutputContains("BUILD_CONST_IN_PROPS set"); + // delete Directory.Build.props that defines BUILD_CONST_IN_PROPS Log($"Deleting {directoryBuildProps}"); File.Delete(directoryBuildProps); - await App.WaitForOutputLineContaining(MessageDescriptor.NoCSharpChangesToApply); - App.AssertOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); + // Project needs to be re-evaluated: + await App.WaitUntilOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation); App.Process.ClearOutput(); - await App.AssertOutputLineStartsWith("BUILD_CONST_IN_PROPS not set"); + await App.WaitUntilOutputContains("BUILD_CONST_IN_PROPS not set"); } [Fact] @@ -256,8 +258,8 @@ public async Task DefaultItemExcludes_DefaultItemsEnabled() App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(new Regex(@"dotnet watch ⌚ Exclusion glob: 'AppData/[*][*]/[*][.][*];bin[/\\]+Debug[/\\]+[*][*];obj[/\\]+Debug[/\\]+[*][*];bin[/\\]+[*][*];obj[/\\]+[*][*]")); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(new Regex(@"dotnet watch ⌚ Exclusion glob: 'AppData/[*][*]/[*][.][*];bin[/\\]+Debug[/\\]+[*][*];obj[/\\]+Debug[/\\]+[*][*];bin[/\\]+[*][*];obj[/\\]+[*][*]")); App.Process.ClearOutput(); UpdateSourceFile(appDataFilePath, """ @@ -297,9 +299,9 @@ public async Task DefaultItemExcludes_DefaultItemsDisabled() App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains($"dotnet watch ⌚ Excluded directory: '{binDir}'"); - App.AssertOutputContains($"dotnet watch ⌚ Excluded directory: '{objDir}'"); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains($"dotnet watch ⌚ Excluded directory: '{binDir}'"); + await App.WaitUntilOutputContains($"dotnet watch ⌚ Excluded directory: '{objDir}'"); App.Process.ClearOutput(); UpdateSourceFile(binDirFilePath, "class X;"); @@ -320,7 +322,7 @@ public async Task ProjectChange_GlobalUsings() App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // missing System.Linq import: UpdateSourceFile(programPath, content => content.Replace(""" @@ -330,7 +332,8 @@ public async Task ProjectChange_GlobalUsings() Console.WriteLine($">>> {typeof(XDocument)}"); """)); - await App.WaitForOutputLineContaining(MessageDescriptor.UnableToApplyChanges); + await App.WaitUntilOutputContains(MessageDescriptor.UnableToApplyChanges); + App.Process.ClearOutput(); UpdateSourceFile(projectPath, content => content.Replace(""" @@ -339,11 +342,11 @@ public async Task ProjectChange_GlobalUsings() """)); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded, $"WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})"); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); await App.WaitUntilOutputContains(">>> System.Xml.Linq.XDocument"); - App.AssertOutputContains(MessageDescriptor.ReEvaluationCompleted); + await App.WaitUntilOutputContains(MessageDescriptor.ReEvaluationCompleted); } [Fact] @@ -362,7 +365,7 @@ public async Task BinaryLogs() App.SuppressVerboseLogging(); App.Start(testAsset, ["--verbose", $"-bl:{binLogPath}"], testFlags: TestFlags.None); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); var expectedLogs = new List() { @@ -383,7 +386,7 @@ public async Task BinaryLogs() """)); - await App.WaitForOutputLineContaining(MessageDescriptor.ReEvaluationCompleted); + await App.WaitUntilOutputContains(MessageDescriptor.ReEvaluationCompleted); // project update triggered restore and DTB: expectedLogs.Add(binLogPathBase + "-dotnet-watch.Restore.WatchHotReloadApp.csproj.2.binlog"); @@ -423,23 +426,24 @@ public async Task AutoRestartOnRudeEdit(bool nonInteractive) App.Start(testAsset, nonInteractive ? ["--non-interactive"] : []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + App.Process.ClearOutput(); // rude edit: adding virtual method UpdateSourceFile(programPath, src => src.Replace("/* member placeholder */", "public virtual void F() {}")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); - App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); - App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); + await App.WaitUntilOutputContains(MessageDescriptor.RestartNeededToApplyChanges); + await App.WaitUntilOutputContains($"⌚ [auto-restart] {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); + await App.WaitUntilOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); + await App.WaitUntilOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); App.Process.ClearOutput(); // valid edit: UpdateSourceFile(programPath, src => src.Replace("public virtual void F() {}", "public virtual void F() { Console.WriteLine(1); }")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); } [Theory(Skip = "https://github.com/dotnet/sdk/issues/51469")] @@ -458,7 +462,7 @@ public async Task AutoRestartOnRuntimeRudeEdit(bool nonInteractive) File.WriteAllText(programPath, """ using System; using System.Threading; - + var d = C.F(); while (true) @@ -481,23 +485,28 @@ public static Action F() App.Start(testAsset, nonInteractive ? ["--non-interactive"] : []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); await App.WaitUntilOutputContains("System.Int32"); App.Process.ClearOutput(); UpdateSourceFile(programPath, src => src.Replace("Action", "Action")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - await App.WaitUntilOutputContains("System.Byte"); - - App.AssertOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] HotReloadException handler installed."); - App.AssertOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Runtime rude edit detected:"); + // The following agent messages must be reported in order. + // The HotReloadException handler needs to be installed and update handlers invoked and completed before the + // HotReloadException handler may proceed with runtime rude edit processing and application restart. + await App.WaitForOutputLineContaining($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] HotReloadException handler installed."); + await App.WaitForOutputLineContaining($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Invoking metadata update handlers."); + await App.WaitForOutputLineContaining($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Updates applied."); + await App.WaitForOutputLineContaining($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Runtime rude edit detected:"); - App.AssertOutputContains($"dotnet watch ⚠ [WatchHotReloadApp ({tfm})] " + + await App.WaitUntilOutputContains($"dotnet watch ⚠ [WatchHotReloadApp ({tfm})] " + "Attempted to invoke a deleted lambda or local function implementation. " + "This can happen when lambda or local function is deleted while the application is running."); - App.AssertOutputContains(MessageDescriptor.RestartingApplication, $"WatchHotReloadApp ({tfm})"); + await App.WaitUntilOutputContains(MessageDescriptor.RestartingApplication, $"WatchHotReloadApp ({tfm})"); + + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains("System.Byte"); } [Fact] @@ -510,21 +519,22 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt() App.Start(testAsset, [], testFlags: TestFlags.ReadKeyFromStdin); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); App.Process.ClearOutput(); // rude edit: adding virtual method UpdateSourceFile(programPath, src => src.Replace("/* member placeholder */", "public virtual void F() {}")); - await App.AssertOutputLineStartsWith(" ❔ Do you want to restart your app? Yes (y) / No (n) / Always (a) / Never (v)", failure: _ => false); + // the prompt is printed into stdout while the error is printed into stderr, so they might arrive in any order: + await App.WaitUntilOutputContains(" ❔ Do you want to restart your app? Yes (y) / No (n) / Always (a) / Never (v)"); + await App.WaitUntilOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"❌ {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); + await App.WaitUntilOutputContains($"❌ {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); App.Process.ClearOutput(); App.SendKey('a'); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); @@ -533,12 +543,12 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt() // rude edit: deleting virtual method UpdateSourceFile(programPath, src => src.Replace("public virtual void F() {}", "")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(39,1): error ENC0033: Deleting method 'F()' requires restarting the application."); - App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); - App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); + await App.WaitUntilOutputContains(MessageDescriptor.RestartNeededToApplyChanges); + await App.WaitUntilOutputContains($"⌚ [auto-restart] {programPath}(39,1): error ENC0033: Deleting method 'F()' requires restarting the application."); + await App.WaitUntilOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); + await App.WaitUntilOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); } [Theory] @@ -565,25 +575,25 @@ public async Task AutoRestartOnNoEffectEdit(bool nonInteractive) App.Start(testAsset, nonInteractive ? ["--non-interactive"] : []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); App.Process.ClearOutput(); // top-level code change: UpdateSourceFile(programPath, src => src.Replace("Started", "")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(17,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted."); - App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); - App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); - App.AssertOutputContains(""); + await App.WaitUntilOutputContains(MessageDescriptor.RestartNeededToApplyChanges); + await App.WaitUntilOutputContains($"⌚ [auto-restart] {programPath}(17,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted."); + await App.WaitUntilOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); + await App.WaitUntilOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); + await App.WaitUntilOutputContains(""); App.Process.ClearOutput(); // valid edit: UpdateSourceFile(programPath, src => src.Replace("/* member placeholder */", "public void F() {}")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); } /// @@ -603,7 +613,7 @@ public async Task BaselineCompilationError() App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForFileChangeBeforeRestarting); UpdateSourceFile(programPath, """ System.Console.WriteLine(""); @@ -620,7 +630,7 @@ public async Task ChangeFileInFSharpProject() App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForFileChangeBeforeRestarting); UpdateSourceFile(Path.Combine(testAsset.Path, "Program.fs"), content => content.Replace("Hello World!", "")); @@ -653,17 +663,19 @@ open System.Threading App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + App.Process.ClearOutput(); UpdateSourceFile(sourcePath, content => content.Replace("Waiting", "")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - await App.AssertOutputLineStartsWith(""); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(""); + App.Process.ClearOutput(); UpdateSourceFile(sourcePath, content => content.Replace("", "")); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - await App.AssertOutputLineStartsWith(""); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(""); } // Test is timing out on .NET Framework: https://github.com/dotnet/sdk/issues/41669 @@ -675,7 +687,7 @@ public async Task HandleTypeLoadFailure() App.Start(testAsset, [], "App"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); var newSrc = """ class DepSubType : Dep @@ -694,7 +706,7 @@ public static void Print() UpdateSourceFile(Path.Combine(testAsset.Path, "App", "Update.cs"), newSrc); - await App.AssertOutputLineStartsWith("Updated types: Printer"); + await App.WaitUntilOutputContains("Updated types: Printer"); } [Fact] @@ -719,11 +731,11 @@ class AppUpdateHandler App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); UpdateSourceFile(sourcePath, source.Replace("Console.WriteLine(\".\");", "Console.WriteLine(\"\");")); - await App.WaitForOutputLineContaining(""); + await App.WaitUntilOutputContains(""); await App.WaitUntilOutputContains( $"dotnet watch ⚠ [WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Expected to find a static method 'ClearCache', 'UpdateApplication' or 'UpdateContent' on type 'AppUpdateHandler, WatchHotReloadApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' but neither exists."); @@ -758,17 +770,17 @@ class AppUpdateHandler App.Start(testAsset, []); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); UpdateSourceFile(sourcePath, source.Replace("Console.WriteLine(\".\");", "Console.WriteLine(\"\");")); - await App.WaitForOutputLineContaining(""); + await App.WaitUntilOutputContains(""); await App.WaitUntilOutputContains($"dotnet watch ⚠ [WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exception from 'AppUpdateHandler.ClearCache': System.InvalidOperationException: Bug!"); if (verbose) { - await App.WaitUntilOutputContains(MessageDescriptor.UpdatesApplied); + await App.WaitUntilOutputContains(MessageDescriptor.UpdateBatchCompleted); } else { @@ -798,7 +810,7 @@ public async Task GracefulTermination_Windows() App.Start(testAsset, [], testFlags: TestFlags.ReadKeyFromStdin); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Windows Ctrl+C handling enabled."); @@ -806,7 +818,7 @@ public async Task GracefulTermination_Windows() App.SendControlC(); - await App.WaitForOutputLineContaining("Ctrl+C detected! Performing cleanup..."); + await App.WaitUntilOutputContains("Ctrl+C detected! Performing cleanup..."); await App.WaitUntilOutputContains("exited with exit code 0."); } @@ -829,7 +841,7 @@ public async Task GracefulTermination_Unix() App.Start(testAsset, [], testFlags: TestFlags.ReadKeyFromStdin); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Posix signal handlers registered."); @@ -837,7 +849,7 @@ public async Task GracefulTermination_Unix() App.SendControlC(); - await App.WaitForOutputLineContaining("SIGTERM detected! Performing cleanup..."); + await App.WaitUntilOutputContains("SIGTERM detected! Performing cleanup..."); await App.WaitUntilOutputContains("exited with exit code 0."); } @@ -863,21 +875,21 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities) var port = TestOptions.GetTestPort(); App.Start(testAsset, ["--urls", "http://localhost:" + port], testFlags: TestFlags.MockBrowser); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); - App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); // Browser is launched based on blazor-devserver output "Now listening on: ...". await App.WaitUntilOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); // Middleware should have been loaded to blazor-devserver before the browser is launched: - App.AssertOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorWasmHotReloadMiddleware[0]"); - App.AssertOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserScriptMiddleware[0]"); - App.AssertOutputContains("Middleware loaded. Script /_framework/aspnetcore-browser-refresh.js"); - App.AssertOutputContains("Middleware loaded. Script /_framework/blazor-hotreload.js"); - App.AssertOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware"); - App.AssertOutputContains("Middleware loaded: DOTNET_MODIFIABLE_ASSEMBLIES=debug, __ASPNETCORE_BROWSER_TOOLS=true"); + await App.WaitUntilOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorWasmHotReloadMiddleware[0]"); + await App.WaitUntilOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserScriptMiddleware[0]"); + await App.WaitUntilOutputContains("Middleware loaded. Script /_framework/aspnetcore-browser-refresh.js"); + await App.WaitUntilOutputContains("Middleware loaded. Script /_framework/blazor-hotreload.js"); + await App.WaitUntilOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware"); + await App.WaitUntilOutputContains("Middleware loaded: DOTNET_MODIFIABLE_ASSEMBLIES=debug, __ASPNETCORE_BROWSER_TOOLS=true"); // shouldn't see any agent messages (agent is not loaded into blazor-devserver): App.AssertOutputDoesNotContain("🕵️"); @@ -888,16 +900,16 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities) """; UpdateSourceFile(Path.Combine(testAsset.Path, "Pages", "Index.razor"), newSource); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadSucceeded, $"blazorwasm ({ToolsetInfo.CurrentTargetFramework})"); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); // check project specified capapabilities: if (projectSpecifiesCapabilities) { - App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddMethodToExistingType Baseline."); + await App.WaitUntilOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddMethodToExistingType Baseline."); } else { - App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); + await App.WaitUntilOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); } } @@ -919,8 +931,8 @@ public async Task BlazorWasm_MSBuildWarning() var port = TestOptions.GetTestPort(); App.Start(testAsset, ["--urls", "http://localhost:" + port], testFlags: TestFlags.MockBrowser); - await App.AssertOutputLineStartsWith("dotnet watch ⚠ msbuild: [Warning] Duplicate source file"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains("dotnet watch ⚠ msbuild: [Warning] Duplicate source file"); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); } [PlatformSpecificFact(TestPlatforms.Windows)] // "https://github.com/dotnet/sdk/issues/49307" https://github.com/dotnet/aspnetcore/issues/63759 @@ -932,11 +944,11 @@ public async Task BlazorWasm_Restart() var port = TestOptions.GetTestPort(); App.Start(testAsset, ["--urls", "http://localhost:" + port, "--non-interactive"], testFlags: TestFlags.ReadKeyFromStdin | TestFlags.MockBrowser); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); - App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); - App.AssertOutputContains(MessageDescriptor.PressCtrlRToRestart); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); + await App.WaitUntilOutputContains(MessageDescriptor.PressCtrlRToRestart); // Browser is launched based on blazor-devserver output "Now listening on: ...". await App.WaitUntilOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); @@ -957,17 +969,17 @@ public async Task BlazorWasmHosted() var port = TestOptions.GetTestPort(); App.Start(testAsset, ["--urls", "http://localhost:" + port], "blazorhosted", testFlags: TestFlags.MockBrowser); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); - App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); - App.AssertOutputContains(MessageDescriptor.ApplicationKind_BlazorHosted); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); + await App.WaitUntilOutputContains(MessageDescriptor.ApplicationKind_BlazorHosted); // client capabilities: - App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Project specifies capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType AddExplicitInterfaceImplementation."); + await App.WaitUntilOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Project specifies capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType AddExplicitInterfaceImplementation."); // server capabilities: - App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva AddExplicitInterfaceImplementation."); + await App.WaitUntilOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva AddExplicitInterfaceImplementation."); } [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 @@ -979,11 +991,11 @@ public async Task Razor_Component_ScopedCssAndStaticAssets() var port = TestOptions.GetTestPort(); App.Start(testAsset, ["--urls", "http://localhost:" + port], relativeProjectDirectory: "RazorApp", testFlags: TestFlags.MockBrowser); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); - App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); - App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); - App.AssertOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); + await App.WaitUntilOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); + await App.WaitUntilOutputContains(MessageDescriptor.LaunchingBrowser.GetMessage($"http://localhost:{port}", "")); App.Process.ClearOutput(); var scopedCssPath = Path.Combine(testAsset.Path, "RazorClassLibrary", "Components", "Example.razor.css"); @@ -995,21 +1007,19 @@ public async Task Razor_Component_ScopedCssAndStaticAssets() """; UpdateSourceFile(scopedCssPath, newCss); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); + await App.WaitUntilOutputContains(MessageDescriptor.StaticAssetsChangesApplied); + await App.WaitUntilOutputContains(MessageDescriptor.NoCSharpChangesToApply); - App.AssertOutputContains(MessageDescriptor.SendingStaticAssetUpdateRequest.GetMessage("wwwroot/RazorClassLibrary.bundle.scp.css")); - App.AssertOutputContains(MessageDescriptor.StaticAssetsReloaded); - App.AssertOutputContains(MessageDescriptor.NoCSharpChangesToApply); + await App.WaitUntilOutputContains(MessageDescriptor.SendingStaticAssetUpdateRequest.GetMessage("wwwroot/RazorClassLibrary.bundle.scp.css")); App.Process.ClearOutput(); var cssPath = Path.Combine(testAsset.Path, "RazorApp", "wwwroot", "app.css"); UpdateSourceFile(cssPath, content => content.Replace("background-color: white;", "background-color: red;")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); + await App.WaitUntilOutputContains(MessageDescriptor.StaticAssetsChangesApplied); + await App.WaitUntilOutputContains(MessageDescriptor.NoCSharpChangesToApply); - App.AssertOutputContains(MessageDescriptor.SendingStaticAssetUpdateRequest.GetMessage("wwwroot/app.css")); - App.AssertOutputContains(MessageDescriptor.StaticAssetsReloaded); - App.AssertOutputContains(MessageDescriptor.NoCSharpChangesToApply); + await App.WaitUntilOutputContains(MessageDescriptor.SendingStaticAssetUpdateRequest.GetMessage("wwwroot/app.css")); App.Process.ClearOutput(); } @@ -1035,37 +1045,33 @@ public async Task MauiBlazor() var tfm = $"{ToolsetInfo.CurrentTargetFramework}-{platform}"; App.Start(testAsset, ["-f", tfm]); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // update code file: var razorPath = Path.Combine(testAsset.Path, "Components", "Pages", "Home.razor"); UpdateSourceFile(razorPath, content => content.Replace("Hello, world!", "Updated")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); - // TODO: Warning is currently reported because UpdateContent is not recognized - App.AssertOutputContains("Updates applied: 1 out of 1."); - App.AssertOutputContains("Microsoft.AspNetCore.Components.HotReload.HotReloadManager.UpdateApplication"); + await App.WaitUntilOutputContains("Microsoft.AspNetCore.Components.HotReload.HotReloadManager.UpdateApplication"); App.Process.ClearOutput(); // update static asset: var cssPath = Path.Combine(testAsset.Path, "wwwroot", "css", "app.css"); UpdateSourceFile(cssPath, content => content.Replace("background-color: white;", "background-color: red;")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); - App.AssertOutputContains("Updates applied: 1 out of 1."); - App.AssertOutputContains("Microsoft.AspNetCore.Components.WebView.StaticContentHotReloadManager.UpdateContent"); - App.AssertOutputContains("No C# changes to apply."); + await App.WaitUntilOutputContains(MessageDescriptor.StaticAssetsChangesApplied); + await App.WaitUntilOutputContains("Microsoft.AspNetCore.Components.WebView.StaticContentHotReloadManager.UpdateContent"); + await App.WaitUntilOutputContains(MessageDescriptor.NoCSharpChangesToApply); App.Process.ClearOutput(); // update scoped css: var scopedCssPath = Path.Combine(testAsset.Path, "Components", "Pages", "Counter.razor.css"); UpdateSourceFile(scopedCssPath, content => content.Replace("background-color: green", "background-color: red")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); - App.AssertOutputContains("Updates applied: 1 out of 1."); - App.AssertOutputContains("Microsoft.AspNetCore.Components.WebView.StaticContentHotReloadManager.UpdateContent"); - App.AssertOutputContains("No C# changes to apply."); + await App.WaitUntilOutputContains(MessageDescriptor.StaticAssetsChangesApplied); + await App.WaitUntilOutputContains("Microsoft.AspNetCore.Components.WebView.StaticContentHotReloadManager.UpdateContent"); + await App.WaitUntilOutputContains(MessageDescriptor.NoCSharpChangesToApply); } // Test is timing out on .NET Framework: https://github.com/dotnet/sdk/issues/41669 @@ -1077,7 +1083,7 @@ public async Task HandleMissingAssemblyFailure() App.Start(testAsset, [], "App"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); var newSrc = /* lang=c#-test */""" using System; @@ -1102,7 +1108,7 @@ public static void Print() File.WriteAllText(Path.Combine(testAsset.Path, "App", "Update.cs"), newSrc); - await App.AssertOutputLineStartsWith("Updated types: Printer"); + await App.WaitUntilOutputContains("Updated types: Printer"); } [Theory] @@ -1139,7 +1145,7 @@ public static void PrintFileName([CallerFilePathAttribute] string filePath = nul App.Start(testAsset, [], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // rename the file: if (useMove) @@ -1193,7 +1199,7 @@ public static void PrintDirectoryName([CallerFilePathAttribute] string filePath App.Start(testAsset, ["--non-interactive"], "AppWithDeps"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // rename the directory: if (useMove) @@ -1212,7 +1218,7 @@ public static void PrintDirectoryName([CallerFilePathAttribute] string filePath // dotnet-watch may observe the delete separately from the new file write. // If so, rude edit is reported, the app is auto-restarted and we should observe the final result. - await App.AssertOutputLineStartsWith("> NewSubdir", failure: _ => false); + await App.WaitUntilOutputContains("> NewSubdir"); } [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 @@ -1231,18 +1237,18 @@ public async Task Aspire_BuildError_ManualRestart() App.Start(testAsset, ["-lp", "http"], relativeProjectDirectory: "WatchAspire.AppHost", testFlags: TestFlags.ReadKeyFromStdin); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); // check that Aspire server output is logged via dotnet-watch reporter: await App.WaitUntilOutputContains("dotnet watch ⭐ Now listening on:"); // wait until after all DCP sessions have started: await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #3"); - App.AssertOutputContains("dotnet watch ⭐ Session started: #1"); - App.AssertOutputContains("dotnet watch ⭐ Session started: #2"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #1"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #2"); // MigrationService terminated: - App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); + await App.WaitUntilOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); // working directory of the service should be it's project directory: await App.WaitUntilOutputContains($"ApiService working directory: '{Path.GetDirectoryName(serviceProjectPath)}'"); @@ -1252,12 +1258,9 @@ public async Task Aspire_BuildError_ManualRestart() serviceSourcePath, serviceSource.Replace("Enumerable.Range(1, 5)", "Enumerable.Range(1, 10)")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); - App.AssertOutputContains("Using Aspire process launcher."); - App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, $"WatchAspire.AppHost ({tfm})"); - App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, $"WatchAspire.ApiService ({tfm})"); - App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, $"WatchAspire.Web ({tfm})"); + await App.WaitUntilOutputContains("Using Aspire process launcher."); // Only one browser should be launched (dashboard). The child process shouldn't launch a browser. Assert.Equal(1, App.Process.Output.Count(line => line.StartsWith("dotnet watch ⌚ Launching browser: "))); @@ -1268,24 +1271,25 @@ public async Task Aspire_BuildError_ManualRestart() serviceSourcePath, serviceSource.Replace("record WeatherForecast", "record WeatherForecast2")); - await App.WaitForOutputLineContaining(" ❔ Do you want to restart these projects? Yes (y) / No (n) / Always (a) / Never (v)"); + // the prompt is printed into stdout while the error is printed into stderr, so they might arrive in any order: + await App.WaitUntilOutputContains(" ❔ Do you want to restart these projects? Yes (y) / No (n) / Always (a) / Never (v)"); + await App.WaitUntilOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"dotnet watch ❌ {serviceSourcePath}(40,1): error ENC0020: Renaming record 'WeatherForecast' requires restarting the application."); - App.AssertOutputContains("dotnet watch ⌚ Affected projects:"); - App.AssertOutputContains("dotnet watch ⌚ WatchAspire.ApiService"); + await App.WaitUntilOutputContains($"dotnet watch ❌ {serviceSourcePath}(40,1): error ENC0020: Renaming record 'WeatherForecast' requires restarting the application."); + await App.WaitUntilOutputContains("dotnet watch ⌚ Affected projects:"); + await App.WaitUntilOutputContains("dotnet watch ⌚ WatchAspire.ApiService"); App.Process.ClearOutput(); App.SendKey('y'); - await App.WaitForOutputLineContaining(MessageDescriptor.FixBuildError); + await App.WaitUntilOutputContains(MessageDescriptor.FixBuildError); - App.AssertOutputContains("Application is shutting down..."); + await App.WaitUntilOutputContains("Application is shutting down..."); - App.AssertOutputContains($"[WatchAspire.ApiService ({tfm})] Exited"); + await App.WaitUntilOutputContains($"[WatchAspire.ApiService ({tfm})] Exited"); - App.AssertOutputContains(MessageDescriptor.Building.GetMessage(serviceProjectPath)); - App.AssertOutputContains("error CS0246: The type or namespace name 'WeatherForecast' could not be found"); + await App.WaitUntilOutputContains(MessageDescriptor.Building.GetMessage(serviceProjectPath)); + await App.WaitUntilOutputContains("error CS0246: The type or namespace name 'WeatherForecast' could not be found"); App.Process.ClearOutput(); // fix build error: @@ -1293,27 +1297,28 @@ public async Task Aspire_BuildError_ManualRestart() serviceSourcePath, serviceSource.Replace("WeatherForecast", "WeatherForecast2")); - await App.WaitForOutputLineContaining(MessageDescriptor.ProjectsRestarted.GetMessage(1)); + await App.WaitUntilOutputContains(MessageDescriptor.ProjectsRestarted.GetMessage(1)); - App.AssertOutputContains(MessageDescriptor.BuildSucceeded.GetMessage(serviceProjectPath)); - App.AssertOutputContains(MessageDescriptor.ProjectsRebuilt); - App.AssertOutputContains($"dotnet watch ⭐ Starting project: {serviceProjectPath}"); + await App.WaitUntilOutputContains(MessageDescriptor.BuildSucceeded.GetMessage(serviceProjectPath)); + await App.WaitUntilOutputContains(MessageDescriptor.ProjectsRebuilt); + await App.WaitUntilOutputContains($"dotnet watch ⭐ Starting project: {serviceProjectPath}"); App.Process.ClearOutput(); App.SendControlC(); - await App.WaitForOutputLineContaining(MessageDescriptor.ShutdownRequested); + await App.WaitUntilOutputContains(MessageDescriptor.ShutdownRequested); await App.WaitUntilOutputContains($"[WatchAspire.ApiService ({tfm})] Exited"); + await App.WaitUntilOutputContains($"[WatchAspire.Web ({tfm})] Exited"); await App.WaitUntilOutputContains($"[WatchAspire.AppHost ({tfm})] Exited"); await App.WaitUntilOutputContains("dotnet watch ⭐ Waiting for server to shutdown ..."); - App.AssertOutputContains("dotnet watch ⭐ Stop session #1"); - App.AssertOutputContains("dotnet watch ⭐ Stop session #2"); - App.AssertOutputContains("dotnet watch ⭐ Stop session #3"); - App.AssertOutputContains("dotnet watch ⭐ [#2] Sending 'sessionTerminated'"); - App.AssertOutputContains("dotnet watch ⭐ [#3] Sending 'sessionTerminated'"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Stop session #1"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Stop session #2"); + await App.WaitUntilOutputContains("dotnet watch ⭐ Stop session #3"); + await App.WaitUntilOutputContains("dotnet watch ⭐ [#2] Sending 'sessionTerminated'"); + await App.WaitUntilOutputContains("dotnet watch ⭐ [#3] Sending 'sessionTerminated'"); } [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759 @@ -1329,18 +1334,18 @@ public async Task Aspire_NoEffect_AutoRestart() App.Start(testAsset, ["-lp", "http", "--non-interactive"], relativeProjectDirectory: "WatchAspire.AppHost"); - await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + await App.WaitUntilOutputContains(MessageDescriptor.WaitingForChanges); + + await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #1"); + await App.WaitUntilOutputContains(MessageDescriptor.Exited, $"WatchAspire.MigrationService ({tfm})"); + await App.WaitUntilOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); + + // migration service output should not be printed to dotnet-watch output, it should be sent via DCP as a notification: + await App.WaitUntilOutputContains("dotnet watch ⭐ [#1] Sending 'serviceLogs': log_message=' Migration complete', is_std_err=False"); // wait until after DCP sessions have been started for all projects: await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #3"); - // other services are waiting for completion of MigrationService: - App.AssertOutputContains("dotnet watch ⭐ Session started: #1"); - App.AssertOutputContains(MessageDescriptor.Exited, $"WatchAspire.MigrationService ({tfm})"); - App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'sessionTerminated'"); - - // migration service output should not be printed to dotnet-watch output, it hsould be sent via DCP as a notification: - App.AssertOutputContains("dotnet watch ⭐ [#1] Sending 'serviceLogs': log_message=' Migration complete', is_std_err=False"); App.AssertOutputDoesNotContain(new Regex("^ +Migration complete")); App.Process.ClearOutput(); @@ -1348,10 +1353,9 @@ public async Task Aspire_NoEffect_AutoRestart() // no-effect edit: UpdateSourceFile(webSourcePath, src => src.Replace("/* top-level placeholder */", "builder.Services.AddRazorComponents();")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); await App.WaitUntilOutputContains("dotnet watch ⭐ Session started: #3"); - - App.AssertOutputContains(MessageDescriptor.ProjectsRestarted.GetMessage(1)); + await App.WaitUntilOutputContains(MessageDescriptor.ProjectsRestarted.GetMessage(1)); App.AssertOutputDoesNotContain("⚠"); // The process exited and should not participate in Hot Reload: @@ -1363,8 +1367,8 @@ public async Task Aspire_NoEffect_AutoRestart() // lambda body edit: UpdateSourceFile(webSourcePath, src => src.Replace("Hello world!", "")); - await App.WaitForOutputLineContaining(MessageDescriptor.HotReloadChangeHandled); - App.AssertOutputContains($"dotnet watch 🕵️ [WatchAspire.Web ({tfm})] Updates applied."); + await App.WaitUntilOutputContains(MessageDescriptor.ManagedCodeChangesApplied); + await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchAspire.Web ({tfm})] Updates applied."); App.AssertOutputDoesNotContain(MessageDescriptor.ProjectsRebuilt); App.AssertOutputDoesNotContain(MessageDescriptor.ProjectsRestarted); App.AssertOutputDoesNotContain("⚠"); diff --git a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs index 93b8c0931611..537543bd21ec 100644 --- a/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs +++ b/test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs @@ -99,7 +99,7 @@ private RunningWatcher StartWatcher(TestAsset testAsset, string[] args, string? var reporter = new TestReporter(Logger); var loggerFactory = new LoggerFactory(reporter, LogLevel.Trace); var environmentOptions = TestOptions.GetEnvironmentOptions(workingDirectory ?? testAsset.Path, SdkTestContext.Current.ToolsetUnderTest.DotNetHostPath, testAsset); - var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true)); + var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout()); var program = Program.TryCreate( TestOptions.GetCommandLineOptions(["--verbose", ..args]), @@ -195,7 +195,8 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger) var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); - var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); + var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); + var projectsRestarted = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectsRestarted); var sessionStarted = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadSessionStarted); var projectBaselinesUpdated = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectsRebuilt); await launchCompletionA.Task; @@ -210,8 +211,8 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger) await MakeRudeEditChange(); - Log("Waiting for changed handled ..."); - await changeHandled.WaitAsync(w.ShutdownSource.Token); + Log("Waiting for projects restarted ..."); + await projectsRestarted.WaitAsync(w.ShutdownSource.Token); // Wait for project baselines to be updated, so that we capture the new solution snapshot // and further changes are treated as another update. @@ -330,8 +331,8 @@ public async Task UpdateAppliedToNewProcesses(bool sharedOutput) await using var w = StartWatcher(testAsset, ["--non-interactive", "--project", hostProject], workingDirectory); var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); - var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); - var updatesApplied = w.Reporter.RegisterSemaphore(MessageDescriptor.UpdatesApplied); + var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); + var updatesApplied = w.Reporter.RegisterSemaphore(MessageDescriptor.UpdateBatchCompleted); var hasUpdateA = new SemaphoreSlim(initialCount: 0); var hasUpdateB = new SemaphoreSlim(initialCount: 0); @@ -420,7 +421,7 @@ public async Task HostRestart(UpdateLocation updateLocation) await using var w = StartWatcher(testAsset, args: ["--project", hostProject], workingDirectory); var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); - var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); + var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); var restartNeeded = w.Reporter.RegisterSemaphore(MessageDescriptor.ApplyUpdate_ChangingEntryPoint); var restartRequested = w.Reporter.RegisterSemaphore(MessageDescriptor.RestartRequested); @@ -510,7 +511,7 @@ public async Task RudeEditInProjectWithoutRunningProcess() var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); - var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); + var projectsRebuilt = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectsRebuilt); var sessionStarted = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadSessionStarted); var applyUpdateVerbose = w.Reporter.RegisterSemaphore(MessageDescriptor.ApplyUpdate_Verbose); @@ -534,8 +535,8 @@ public async Task RudeEditInProjectWithoutRunningProcess() [assembly: System.Reflection.AssemblyMetadata("TestAssemblyMetadata", "2")] """); - Log("Waiting for change handled ..."); - await changeHandled.WaitAsync(w.ShutdownSource.Token); + Log("Waiting for projects rebuilt ..."); + await projectsRebuilt.WaitAsync(w.ShutdownSource.Token); Log("Waiting for verbose rude edit reported ..."); await applyUpdateVerbose.WaitAsync(w.ShutdownSource.Token); @@ -601,7 +602,7 @@ public async Task IgnoredChange(bool isExisting, bool isIncluded, DirectoryKind await using var w = StartWatcher(testAsset, ["--no-exit"], workingDirectory); var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); - var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); + var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); var ignoringChangeInHiddenDirectory = w.Reporter.RegisterSemaphore(MessageDescriptor.IgnoringChangeInHiddenDirectory); var ignoringChangeInExcludedFile = w.Reporter.RegisterSemaphore(MessageDescriptor.IgnoringChangeInExcludedFile); var fileAdditionTriggeredReEvaluation = w.Reporter.RegisterSemaphore(MessageDescriptor.FileAdditionTriggeredReEvaluation); @@ -663,7 +664,7 @@ public async Task ProjectAndSourceFileChange() var waitingForChanges = w.Reporter.RegisterSemaphore(MessageDescriptor.WaitingForChanges); - var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadChangeHandled); + var changeHandled = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); var hasUpdatedOutput = w.CreateCompletionSource(); w.Reporter.OnProcessOutput += line => @@ -721,7 +722,7 @@ public async Task ProjectAndSourceFileChange_AddProjectReference() var projectChangeTriggeredReEvaluation = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectChangeTriggeredReEvaluation); var projectsRebuilt = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectsRebuilt); var projectDependenciesDeployed = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectDependenciesDeployed); - var hotReloadSucceeded = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadSucceeded); + var managedCodeChangesApplied = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); var hasUpdatedOutput = w.CreateCompletionSource(); w.Reporter.OnProcessOutput += line => @@ -759,7 +760,7 @@ public async Task ProjectAndSourceFileChange_AddProjectReference() Assert.Equal(1, projectChangeTriggeredReEvaluation.CurrentCount); Assert.Equal(1, projectsRebuilt.CurrentCount); Assert.Equal(1, projectDependenciesDeployed.CurrentCount); - Assert.Equal(1, hotReloadSucceeded.CurrentCount); + Assert.Equal(1, managedCodeChangesApplied.CurrentCount); } [Fact] @@ -780,7 +781,7 @@ public async Task ProjectAndSourceFileChange_AddPackageReference() var projectChangeTriggeredReEvaluation = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectChangeTriggeredReEvaluation); var projectsRebuilt = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectsRebuilt); var projectDependenciesDeployed = w.Reporter.RegisterSemaphore(MessageDescriptor.ProjectDependenciesDeployed); - var hotReloadSucceeded = w.Reporter.RegisterSemaphore(MessageDescriptor.HotReloadSucceeded); + var managedCodeChangesApplied = w.Reporter.RegisterSemaphore(MessageDescriptor.ManagedCodeChangesApplied); var hasUpdatedOutput = w.CreateCompletionSource(); w.Reporter.OnProcessOutput += line => @@ -816,6 +817,6 @@ public async Task ProjectAndSourceFileChange_AddPackageReference() Assert.Equal(1, projectChangeTriggeredReEvaluation.CurrentCount); Assert.Equal(0, projectsRebuilt.CurrentCount); Assert.Equal(1, projectDependenciesDeployed.CurrentCount); - Assert.Equal(1, hotReloadSucceeded.CurrentCount); + Assert.Equal(1, managedCodeChangesApplied.CurrentCount); } } diff --git a/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs b/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs index fd846476e25b..9116bbecb0b5 100644 --- a/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs +++ b/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs @@ -15,7 +15,7 @@ public static int GetTestPort() public static readonly ProjectOptions ProjectOptions = GetProjectOptions([]); public static EnvironmentOptions GetEnvironmentOptions(string workingDirectory = "", string muxerPath = "", TestAsset? asset = null) - => new(workingDirectory, muxerPath, TimeSpan.Zero, IsPollingEnabled: true, TestFlags: TestFlags.RunningAsTest, TestOutput: asset != null ? asset.GetWatchTestOutputPath() : ""); + => new(workingDirectory, muxerPath, ProcessCleanupTimeout: null, IsPollingEnabled: true, TestFlags: TestFlags.RunningAsTest, TestOutput: asset != null ? asset.GetWatchTestOutputPath() : ""); public static CommandLineOptions GetCommandLineOptions(string[] args) => CommandLineOptions.Parse(args, NullLogger.Instance, TextWriter.Null, out _) ?? throw new InvalidOperationException(); diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs index e84814fb2cf4..df3c68ab5133 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsDevice.cs @@ -376,4 +376,135 @@ public void ItPassesRuntimeIdentifierToDeployToDeviceTarget() .And.HaveStdOutContaining($"Device: {deviceId}") .And.HaveStdOutContaining($"RuntimeIdentifier: {rid}"); } + + [Fact] + public void ItPassesEnvironmentVariablesToTargets() + { + var testInstance = TestAssetsManager.CopyTestAsset("DotnetRunDevices", identifier: "EnvVarTargets") + .WithSource(); + + string deviceId = "test-device-1"; + string buildBinlogPath = Path.Combine(testInstance.Path, "msbuild.binlog"); + string runBinlogPath = Path.Combine(testInstance.Path, "msbuild-dotnet-run.binlog"); + + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute("--framework", ToolsetInfo.CurrentTargetFramework, "--device", deviceId, + "-e", "FOO=BAR", "-e", "ANOTHER=VALUE", + "-bl"); + + result.Should().Pass(); + + // Verify the binlog files were created + File.Exists(buildBinlogPath).Should().BeTrue("the build binlog file should be created"); + File.Exists(runBinlogPath).Should().BeTrue("the run binlog file should be created"); + + // Verify environment variables were passed to Build target (out-of-process build) + AssertTargetInBinlog(buildBinlogPath, "_LogRuntimeEnvironmentVariableDuringBuild", + targets => + { + targets.Should().NotBeEmpty("_LogRuntimeEnvironmentVariableDuringBuild target should have executed"); + var messages = targets.First().FindChildrenRecursive(); + var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("Build: RuntimeEnvironmentVariable=") == true); + envVarMessage.Should().NotBeNull("the Build target should have logged the environment variables"); + envVarMessage.Text.Should().Contain("FOO=BAR").And.Contain("ANOTHER=VALUE"); + }); + + // Verify environment variables were passed to ComputeRunArguments target (in-process) + AssertTargetInBinlog(runBinlogPath, "_LogRuntimeEnvironmentVariableDuringComputeRunArguments", + targets => + { + targets.Should().NotBeEmpty("_LogRuntimeEnvironmentVariableDuringComputeRunArguments target should have executed"); + var messages = targets.First().FindChildrenRecursive(); + var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("ComputeRunArguments: RuntimeEnvironmentVariable=") == true); + envVarMessage.Should().NotBeNull("the ComputeRunArguments target should have logged the environment variables"); + envVarMessage.Text.Should().Contain("FOO=BAR").And.Contain("ANOTHER=VALUE"); + }); + + // Verify environment variables were passed to DeployToDevice target (in-process) + AssertTargetInBinlog(runBinlogPath, "DeployToDevice", + targets => + { + targets.Should().NotBeEmpty("DeployToDevice target should have executed"); + var messages = targets.First().FindChildrenRecursive(); + var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("DeployToDevice: RuntimeEnvironmentVariable=") == true); + envVarMessage.Should().NotBeNull("the DeployToDevice target should have logged the environment variables"); + envVarMessage.Text.Should().Contain("FOO=BAR").And.Contain("ANOTHER=VALUE"); + }); + + // Verify the props file was created in the correct IntermediateOutputPath location + string tempPropsFile = Path.Combine(testInstance.Path, "obj", "Debug", ToolsetInfo.CurrentTargetFramework, "dotnet-run-env.props"); + var build = BinaryLog.ReadBuild(buildBinlogPath); + var propsFile = build.SourceFiles?.FirstOrDefault(f => f.FullPath.EndsWith("dotnet-run-env.props", StringComparison.OrdinalIgnoreCase)); + propsFile.Should().NotBeNull("dotnet-run-env.props should be embedded in the binlog"); + propsFile.FullPath.Should().Be(tempPropsFile, "the props file should be in the IntermediateOutputPath"); + File.Exists(tempPropsFile).Should().BeFalse("the temporary props file should be deleted after build"); + } + + [Fact] + public void ItDoesNotPassEnvironmentVariablesToTargetsWithoutOptIn() + { + var testInstance = TestAssetsManager.CopyTestAsset("DotnetRunDevices", identifier: "EnvVarNoOptIn") + .WithSource(); + + string deviceId = "test-device-1"; + string buildBinlogPath = Path.Combine(testInstance.Path, "msbuild.binlog"); + string runBinlogPath = Path.Combine(testInstance.Path, "msbuild-dotnet-run.binlog"); + + // Run with EnableRuntimeEnvironmentVariableSupport=false to opt out of the capability + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute("--framework", ToolsetInfo.CurrentTargetFramework, "--device", deviceId, + "-e", "FOO=BAR", "-e", "ANOTHER=VALUE", + "-p:EnableRuntimeEnvironmentVariableSupport=false", + "-bl"); + + result.Should().Pass(); + + // Verify the binlog files were created + File.Exists(buildBinlogPath).Should().BeTrue("the build binlog file should be created"); + File.Exists(runBinlogPath).Should().BeTrue("the run binlog file should be created"); + + // Verify _LogRuntimeEnvironmentVariableDuringBuild target did NOT execute (condition failed due to no items) + AssertTargetInBinlog(buildBinlogPath, "_LogRuntimeEnvironmentVariableDuringBuild", + targets => + { + // The target should either not execute, or execute with no environment variable message + if (targets.Any()) + { + var messages = targets.First().FindChildrenRecursive(); + var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("Build: RuntimeEnvironmentVariable=") == true); + envVarMessage.Should().BeNull("the Build target should NOT have logged the environment variables when not opted in"); + } + }); + + // Verify _LogRuntimeEnvironmentVariableDuringComputeRunArguments target did NOT log env vars + AssertTargetInBinlog(runBinlogPath, "_LogRuntimeEnvironmentVariableDuringComputeRunArguments", + targets => + { + if (targets.Any()) + { + var messages = targets.First().FindChildrenRecursive(); + var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("ComputeRunArguments: RuntimeEnvironmentVariable=") == true); + envVarMessage.Should().BeNull("the ComputeRunArguments target should NOT have logged the environment variables when not opted in"); + } + }); + + // Verify DeployToDevice target did NOT log actual env var values + AssertTargetInBinlog(runBinlogPath, "DeployToDevice", + targets => + { + targets.Should().NotBeEmpty("DeployToDevice target should have executed"); + var messages = targets.First().FindChildrenRecursive(); + // The message may appear (target has no condition) but should NOT contain actual env var values + var envVarMessage = messages.FirstOrDefault(m => m.Text?.Contains("FOO=BAR") == true || m.Text?.Contains("ANOTHER=VALUE") == true); + envVarMessage.Should().BeNull("the DeployToDevice target should NOT have logged the actual environment variable values when not opted in"); + }); + + // Verify no props file was created (since opt-in is false) + string tempPropsFile = Path.Combine(testInstance.Path, "obj", "Debug", ToolsetInfo.CurrentTargetFramework, "dotnet-run-env.props"); + var build = BinaryLog.ReadBuild(buildBinlogPath); + var propsFile = build.SourceFiles?.FirstOrDefault(f => f.FullPath.EndsWith("dotnet-run-env.props", StringComparison.OrdinalIgnoreCase)); + propsFile.Should().BeNull("dotnet-run-env.props should NOT be created when not opted in"); + } } diff --git a/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs b/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs index 9673540cd726..8fcb4bedd3d2 100644 --- a/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs +++ b/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs @@ -1353,5 +1353,71 @@ private string GetSolutionFileTemplateContents(string templateFileName) .Path; return File.ReadAllText(Path.Join(templateContentDirectory, templateFileName)); } + + // SLNF TESTS + [Theory] + [InlineData("sln")] + [InlineData("solution")] + public void WhenAddingProjectToSlnfItAddsOnlyIfInParentSolution(string solutionCommand) + { + var projectDirectory = TestAssetsManager + .CopyTestAsset("TestAppWithSlnfFiles", identifier: $"GivenDotnetSlnAdd-Slnf-{solutionCommand}") + .WithSource() + .Path; + + var slnfFullPath = Path.Combine(projectDirectory, "App.slnf"); + + // Try to add Lib project which is in parent solution + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute(solutionCommand, "App.slnf", "add", Path.Combine("src", "Lib", "Lib.csproj")); + cmd.Should().Pass(); + cmd.StdOut.Should().Contain(string.Format(CliStrings.ProjectAddedToTheSolution, Path.Combine("src", "Lib", "Lib.csproj"))); + + // Verify the project was added to the slnf file + var slnfContent = File.ReadAllText(slnfFullPath); + slnfContent.Should().Contain("src\\\\Lib\\\\Lib.csproj"); + } + + [Theory] + [InlineData("sln")] + [InlineData("solution")] + public void WhenRemovingProjectFromSlnfItRemovesSuccessfully(string solutionCommand) + { + var projectDirectory = TestAssetsManager + .CopyTestAsset("TestAppWithSlnfFiles", identifier: $"GivenDotnetSlnAdd-SlnfRemove-{solutionCommand}") + .WithSource() + .Path; + + var slnfFullPath = Path.Combine(projectDirectory, "App.slnf"); + + // Remove the App project from the filter + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute(solutionCommand, "App.slnf", "remove", Path.Combine("src", "App", "App.csproj")); + cmd.Should().Pass(); + cmd.StdOut.Should().Contain(string.Format(CliStrings.ProjectRemovedFromTheSolution, Path.Combine("src", "App", "App.csproj"))); + + // Verify the project was removed from the slnf file + var slnfContent = File.ReadAllText(slnfFullPath); + slnfContent.Should().NotContain("src\\\\App\\\\App.csproj"); + } + + [Theory] + [InlineData("sln")] + [InlineData("solution")] + public void WhenAddingProjectToSlnfWithInRootOptionItErrors(string solutionCommand) + { + var projectDirectory = TestAssetsManager + .CopyTestAsset("TestAppWithSlnfFiles", identifier: $"GivenDotnetSlnAdd-SlnfInRoot-{solutionCommand}") + .WithSource() + .Path; + + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute(solutionCommand, "App.slnf", "add", "--in-root", Path.Combine("src", "Lib", "Lib.csproj")); + cmd.Should().Fail(); + cmd.StdErr.Should().Contain(CliCommandStrings.SolutionFilterDoesNotSupportFolderOptions); + } } } diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs index 46dcb9d1f376..875abee43d96 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs @@ -479,7 +479,7 @@ public void RunMultiTFMsProjectSolutionWithPreviousFramework_ShouldReturnExitCod // Output looks similar to the following /* error NETSDK1005: Assets file 'path\to\OtherTestProject\obj\project.assets.json' doesn't have a target for 'net9.0'. Ensure that restore has run and that you have included 'net9.0' in the TargetFrameworks for your project. - Get projects properties with MSBuild didn't execute properly with exit code: 1. + Build failed with exit code: 1. */ if (!SdkTestContext.IsLocalized()) { diff --git a/test/dotnet.Tests/CommandTests/Workload/Install/CorruptWorkloadSetTestHelper.cs b/test/dotnet.Tests/CommandTests/Workload/Install/CorruptWorkloadSetTestHelper.cs new file mode 100644 index 000000000000..e42c4ea223cd --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Workload/Install/CorruptWorkloadSetTestHelper.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using ManifestReaderTests; +using Microsoft.DotNet.Cli.Commands.Workload; +using Microsoft.DotNet.Cli.Commands.Workload.Install; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.NET.Sdk.WorkloadManifestReader; + +namespace Microsoft.DotNet.Cli.Workload.Install.Tests +{ + /// + /// Test helper for setting up corrupt workload set scenarios. + /// + internal static class CorruptWorkloadSetTestHelper + { + /// + /// Sets up a corrupt workload set scenario where manifests are missing but workload set is configured. + /// This simulates package managers deleting manifests during SDK updates. + /// Returns a real SdkDirectoryWorkloadManifestProvider so the corruption repairer can be attached. + /// + public static (string dotnetRoot, string userProfileDir, MockPackWorkloadInstaller mockInstaller, IWorkloadResolver workloadResolver, SdkDirectoryWorkloadManifestProvider manifestProvider) + SetupCorruptWorkloadSet( + TestAssetsManager testAssetsManager, + bool userLocal, + out string sdkFeatureVersion) + { + var testDirectory = testAssetsManager.CreateTestDirectory(identifier: userLocal ? "userlocal" : "default").Path; + var dotnetRoot = Path.Combine(testDirectory, "dotnet"); + var userProfileDir = Path.Combine(testDirectory, "user-profile"); + sdkFeatureVersion = "6.0.100"; + var workloadSetVersion = "6.0.100"; + + // Create workload set contents JSON for the current (corrupt) version + var workloadSetJson = """ +{ + "xamarin-android-build": "8.4.7/6.0.100", + "xamarin-ios-sdk": "10.0.1/6.0.100" +} +"""; + + // Create workload set contents for the updated version + var workloadSetJsonUpdated = """ +{ + "xamarin-android-build": "8.4.8/6.0.100", + "xamarin-ios-sdk": "10.0.2/6.0.100" +} +"""; + + // Create workload set contents for the mock installer + var workloadSetContents = new Dictionary + { + [workloadSetVersion] = workloadSetJson, + ["6.0.101"] = workloadSetJsonUpdated + }; + + // Set up mock installer with workload set support + // Note: Don't pre-populate installedWorkloads - the test focuses on manifest repair, not workload installation + var mockInstaller = new MockPackWorkloadInstaller( + dotnetDir: dotnetRoot, + installedWorkloads: new List(), + workloadSetContents: workloadSetContents); + + string installRoot = userLocal ? userProfileDir : dotnetRoot; + if (userLocal) + { + WorkloadFileBasedInstall.SetUserLocal(dotnetRoot, sdkFeatureVersion); + } + + // Create install state with workload set version + var installStateDir = Path.Combine(installRoot, "metadata", "workloads", RuntimeInformation.ProcessArchitecture.ToString(), sdkFeatureVersion, "InstallState"); + Directory.CreateDirectory(installStateDir); + var installStatePath = Path.Combine(installStateDir, "default.json"); + var installState = new InstallStateContents + { + UseWorkloadSets = true, + WorkloadVersion = workloadSetVersion, + Manifests = new Dictionary + { + ["xamarin-android-build"] = "8.4.7", + ["xamarin-ios-sdk"] = "10.0.1" + } + }; + File.WriteAllText(installStatePath, installState.ToString()); + + // Create workload set folder so the real provider can find it + var workloadSetsRoot = Path.Combine(dotnetRoot, "sdk-manifests", sdkFeatureVersion, "workloadsets", workloadSetVersion); + Directory.CreateDirectory(workloadSetsRoot); + File.WriteAllText(Path.Combine(workloadSetsRoot, "workloadset.workloadset.json"), workloadSetJson); + + // Create mock manifest directories but WITHOUT manifest files to simulate ruined install + var manifestRoot = Path.Combine(dotnetRoot, "sdk-manifests", sdkFeatureVersion); + var androidManifestDir = Path.Combine(manifestRoot, "xamarin-android-build", "8.4.7"); + var iosManifestDir = Path.Combine(manifestRoot, "xamarin-ios-sdk", "10.0.1"); + Directory.CreateDirectory(androidManifestDir); + Directory.CreateDirectory(iosManifestDir); + + // Verify manifests don't exist (simulating the ruined install) + if (File.Exists(Path.Combine(androidManifestDir, "WorkloadManifest.json")) || + File.Exists(Path.Combine(iosManifestDir, "WorkloadManifest.json"))) + { + throw new InvalidOperationException("Test setup failed: manifest files should not exist"); + } + + // Create a real SdkDirectoryWorkloadManifestProvider + var manifestProvider = new SdkDirectoryWorkloadManifestProvider(dotnetRoot, sdkFeatureVersion, userProfileDir, globalJsonPath: null); + var workloadResolver = WorkloadResolver.Create(manifestProvider, dotnetRoot, sdkFeatureVersion, userProfileDir); + mockInstaller.WorkloadResolver = workloadResolver; + + return (dotnetRoot, userProfileDir, mockInstaller, workloadResolver, manifestProvider); + } + } +} diff --git a/test/dotnet.Tests/CommandTests/Workload/Install/MockPackWorkloadInstaller.cs b/test/dotnet.Tests/CommandTests/Workload/Install/MockPackWorkloadInstaller.cs index c1e7354d9d21..9c7bed6b7e9a 100644 --- a/test/dotnet.Tests/CommandTests/Workload/Install/MockPackWorkloadInstaller.cs +++ b/test/dotnet.Tests/CommandTests/Workload/Install/MockPackWorkloadInstaller.cs @@ -139,7 +139,11 @@ public IEnumerable GetWorkloadHistoryRecords(string sdkFe return HistoryRecords; } - public void RepairWorkloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, DirectoryPath? offlineCache = null) => throw new NotImplementedException(); + public void RepairWorkloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, DirectoryPath? offlineCache = null) + { + // Repair is essentially a reinstall of existing workloads + CliTransaction.RunNew(context => InstallWorkloads(workloadIds, sdkFeatureBand, context, offlineCache)); + } public void GarbageCollect(Func getResolverForWorkloadSet, DirectoryPath? offlineCache = null, bool cleanAllPacks = false) { @@ -160,6 +164,28 @@ public IWorkloadInstallationRecordRepository GetWorkloadInstallationRecordReposi public void InstallWorkloadManifest(ManifestVersionUpdate manifestUpdate, ITransactionContext transactionContext, DirectoryPath? offlineCache = null) { InstalledManifests.Add((manifestUpdate, offlineCache)); + + // Also create the actual manifest file on disk so that SdkDirectoryWorkloadManifestProvider can find it + if (_dotnetDir != null) + { + var manifestDir = Path.Combine(_dotnetDir, "sdk-manifests", manifestUpdate.NewFeatureBand, + manifestUpdate.ManifestId.ToString(), manifestUpdate.NewVersion.ToString()); + Directory.CreateDirectory(manifestDir); + + var manifestPath = Path.Combine(manifestDir, "WorkloadManifest.json"); + if (!File.Exists(manifestPath)) + { + // Write a minimal manifest file + string manifestContents = $$""" +{ + "version": "{{manifestUpdate.NewVersion}}", + "workloads": {}, + "packs": {} +} +"""; + File.WriteAllText(manifestPath, manifestContents); + } + } } public IEnumerable GetDownloads(IEnumerable workloadIds, SdkFeatureBand sdkFeatureBand, bool includeInstalledItems) diff --git a/test/dotnet.Tests/CommandTests/Workload/Repair/GivenDotnetWorkloadRepair.cs b/test/dotnet.Tests/CommandTests/Workload/Repair/GivenDotnetWorkloadRepair.cs index cd0646f2bbd7..84807718c497 100644 --- a/test/dotnet.Tests/CommandTests/Workload/Repair/GivenDotnetWorkloadRepair.cs +++ b/test/dotnet.Tests/CommandTests/Workload/Repair/GivenDotnetWorkloadRepair.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.Text.Json; using ManifestReaderTests; @@ -11,8 +9,10 @@ using Microsoft.DotNet.Cli.Commands.Workload.Install; using Microsoft.DotNet.Cli.Commands.Workload.Repair; using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Workload.Install.Tests; using Microsoft.NET.Sdk.WorkloadManifestReader; +using static Microsoft.NET.Sdk.WorkloadManifestReader.IWorkloadManifestProvider; namespace Microsoft.DotNet.Cli.Workload.Repair.Tests { @@ -86,7 +86,7 @@ public void GivenExtraPacksInstalledRepairGarbageCollects(bool userLocal) // Add extra pack dirs and records var extraPackRecordPath = Path.Combine(installRoot, "metadata", "workloads", "InstalledPacks", "v1", "Test.Pack.A", "1.0.0", sdkFeatureVersion); - Directory.CreateDirectory(Path.GetDirectoryName(extraPackRecordPath)); + Directory.CreateDirectory(Path.GetDirectoryName(extraPackRecordPath)!); var extraPackPath = Path.Combine(installRoot, "packs", "Test.Pack.A", "1.0.0"); Directory.CreateDirectory(extraPackPath); var packRecordContents = JsonSerializer.Serialize(new WorkloadResolver.PackInfo(new WorkloadPackId("Test.Pack.A"), "1.0.0", WorkloadPackKind.Sdk, extraPackPath, "Test.Pack.A"), PackInfoJsonSerializerContext.Default.PackInfo); @@ -152,5 +152,43 @@ public void GivenMissingPacksRepairFixesInstall(bool userLocal) Directory.GetDirectories(Path.Combine(installRoot, "packs")).Length.Should().Be(7); Directory.GetDirectories(Path.Combine(installRoot, "metadata", "workloads", "InstalledPacks", "v1")).Length.Should().Be(8); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GivenMissingManifestsInWorkloadSetModeRepairReinstallsManifests(bool userLocal) + { + var (dotnetRoot, userProfileDir, mockInstaller, workloadResolver, manifestProvider) = + CorruptWorkloadSetTestHelper.SetupCorruptWorkloadSet(TestAssetsManager, userLocal, out string sdkFeatureVersion); + + mockInstaller.InstalledManifests.Should().HaveCount(0); + + var workloadResolverFactory = new MockWorkloadResolverFactory(dotnetRoot, sdkFeatureVersion, workloadResolver, userProfileDir); + + // Attach the corruption repairer to the manifest provider + var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot, manifestDownload: true); + var corruptionRepairer = new WorkloadManifestCorruptionRepairer( + _reporter, + mockInstaller, + workloadResolver, + new SdkFeatureBand(sdkFeatureVersion), + dotnetRoot, + userProfileDir, + nugetDownloader, + packageSourceLocation: null, + VerbosityOptions.detailed); + manifestProvider.CorruptionRepairer = corruptionRepairer; + + // Directly trigger the manifest health check and repair + corruptionRepairer.EnsureManifestsHealthy(ManifestCorruptionFailureMode.Repair); + + // Verify that manifests were installed by the corruption repairer + mockInstaller.InstalledManifests.Should().HaveCount(2, "Manifests should be installed after EnsureManifestsHealthy call"); + mockInstaller.InstalledManifests.Should().Contain(m => m.manifestUpdate.ManifestId.ToString() == "xamarin-android-build"); + mockInstaller.InstalledManifests.Should().Contain(m => m.manifestUpdate.ManifestId.ToString() == "xamarin-ios-sdk"); + + // Verify the repair process was triggered (the corruption repairer shows this message) + _reporter.Lines.Should().Contain(line => line.Contains("Repairing workload set")); + } } } diff --git a/test/dotnet.Tests/CommandTests/Workload/Search/MockWorkloadResolver.cs b/test/dotnet.Tests/CommandTests/Workload/Search/MockWorkloadResolver.cs index ca5c48b55f71..52f0dc4f56cd 100644 --- a/test/dotnet.Tests/CommandTests/Workload/Search/MockWorkloadResolver.cs +++ b/test/dotnet.Tests/CommandTests/Workload/Search/MockWorkloadResolver.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Reflection.Metadata.Ecma335; using Microsoft.NET.Sdk.WorkloadManifestReader; namespace Microsoft.DotNet.Cli.Workload.Search.Tests @@ -14,19 +15,22 @@ public class MockWorkloadResolver : IWorkloadResolver private readonly Func> _getPacksInWorkload; private readonly Func _getPackInfo; private readonly Func _getManifest; + private readonly IWorkloadManifestProvider _manifestProvider; public MockWorkloadResolver( IEnumerable availableWorkloads, IEnumerable installedManifests = null, Func> getPacks = null, Func getPackInfo = null, - Func getManifest = null) + Func getManifest = null, + IWorkloadManifestProvider manifestProvider = null) { _availableWorkloads = availableWorkloads; _installedManifests = installedManifests; _getPacksInWorkload = getPacks; _getPackInfo = getPackInfo; _getManifest = getManifest; + _manifestProvider = manifestProvider; } public IEnumerable GetAvailableWorkloads() => _availableWorkloads; @@ -48,6 +52,6 @@ public void RefreshWorkloadManifests() { /* noop */ } public IEnumerable GetUpdatedWorkloads(WorkloadResolver advertisingManifestResolver, IEnumerable installedWorkloads) => throw new NotImplementedException(); WorkloadResolver IWorkloadResolver.CreateOverlayResolver(IWorkloadManifestProvider overlayManifestProvider) => throw new NotImplementedException(); WorkloadManifest IWorkloadResolver.GetManifestFromWorkload(WorkloadId workloadId) => _getManifest?.Invoke(workloadId) ?? throw new NotImplementedException(); - public IWorkloadManifestProvider GetWorkloadManifestProvider() => throw new NotImplementedException(); + public IWorkloadManifestProvider GetWorkloadManifestProvider() => _manifestProvider ?? throw new NotImplementedException(); } } diff --git a/test/dotnet.Tests/CommandTests/Workload/Update/GivenDotnetWorkloadUpdate.cs b/test/dotnet.Tests/CommandTests/Workload/Update/GivenDotnetWorkloadUpdate.cs index 23338e0ddf3f..c1f24f283207 100644 --- a/test/dotnet.Tests/CommandTests/Workload/Update/GivenDotnetWorkloadUpdate.cs +++ b/test/dotnet.Tests/CommandTests/Workload/Update/GivenDotnetWorkloadUpdate.cs @@ -67,13 +67,15 @@ public void GivenWorkloadUpdateFromHistory() IEnumerable installedManifests = new List() { new WorkloadManifestInfo("microsoft.net.sdk.android", "34.0.0-rc.1", "androidDirectory", "8.0.100-rc.1"), new WorkloadManifestInfo("microsoft.net.sdk.ios", "16.4.8825", "iosDirectory", "8.0.100-rc.1") }; + var manifestProvider = new MockManifestProvider(Array.Empty()) { SdkFeatureBand = new SdkFeatureBand("8.0.100-rc.1") }; var workloadResolver = new MockWorkloadResolver( new string[] { "maui-android", "maui-ios" }.Select(s => new WorkloadInfo(new WorkloadId(s), null)), installedManifests, id => new List() { new WorkloadPackId(id.ToString() + "-pack") }, id => id.ToString().Contains("android") ? mauiAndroidPack : - id.ToString().Contains("ios") ? mauiIosPack : null); + id.ToString().Contains("ios") ? mauiIosPack : null, + manifestProvider: manifestProvider); IWorkloadResolverFactory mockResolverFactory = new MockWorkloadResolverFactory( Path.Combine(Path.GetTempPath(), "dotnetTestPath"), @@ -304,7 +306,8 @@ public void UpdateViaWorkloadSet(bool upgrade, bool? installStateUseWorkloadSet, } "; var nugetPackageDownloader = new MockNuGetPackageDownloader(); - var workloadResolver = new MockWorkloadResolver([new WorkloadInfo(new WorkloadId("android"), string.Empty)], getPacks: id => [], installedManifests: []); + var manifestProvider = new MockManifestProvider(Array.Empty()) { SdkFeatureBand = new SdkFeatureBand(sdkVersion) }; + var workloadResolver = new MockWorkloadResolver([new WorkloadInfo(new WorkloadId("android"), string.Empty)], getPacks: id => [], installedManifests: [], manifestProvider: manifestProvider); var workloadInstaller = new MockPackWorkloadInstaller( dotnetDir, installedWorkloads: [new WorkloadId("android")], @@ -375,13 +378,16 @@ public void GivenWorkloadUpdateItFindsGreatestWorkloadSetWithSpecifiedComponents WorkloadManifest iosManifest = WorkloadManifest.CreateForTests("Microsoft.NET.Sdk.iOS"); WorkloadManifest macosManifest = WorkloadManifest.CreateForTests("Microsoft.NET.Sdk.macOS"); WorkloadManifest mauiManifest = WorkloadManifest.CreateForTests("Microsoft.NET.Sdk.Maui"); + var manifestProvider = new MockManifestProvider(Array.Empty()) { SdkFeatureBand = new SdkFeatureBand("9.0.100") }; + MockWorkloadResolver resolver = new([new WorkloadInfo(new WorkloadId("ios"), ""), new WorkloadInfo(new WorkloadId("macos"), ""), new WorkloadInfo(new WorkloadId("maui"), "")], installedManifests: [ new WorkloadManifestInfo("Microsoft.NET.Sdk.iOS", "17.4.3", Path.Combine(testDirectory, "iosManifest"), "9.0.100"), new WorkloadManifestInfo("Microsoft.NET.Sdk.macOS", "14.4.3", Path.Combine(testDirectory, "macosManifest"), "9.0.100"), new WorkloadManifestInfo("Microsoft.NET.Sdk.Maui", "14.4.3", Path.Combine(testDirectory, "mauiManifest"), "9.0.100") ], - getManifest: id => id.ToString().Equals("ios") ? iosManifest : id.ToString().Equals("macos") ? macosManifest : mauiManifest); + getManifest: id => id.ToString().Equals("ios") ? iosManifest : id.ToString().Equals("macos") ? macosManifest : mauiManifest, + manifestProvider: manifestProvider); MockNuGetPackageDownloader nugetPackageDownloader = new(packageVersions: [new NuGetVersion("9.103.0"), new NuGetVersion("9.102.0"), new NuGetVersion("9.101.0"), new NuGetVersion("9.100.0")]); WorkloadUpdateCommand command = new( parseResult, @@ -654,5 +660,55 @@ public void GivenInvalidVersionInRollbackFileItErrors() return (dotnetRoot, installManager, installer, workloadResolver, manifestUpdater, nugetDownloader, workloadResolverFactory); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GivenMissingManifestsInWorkloadSetModeUpdateReinstallsManifests(bool userLocal) + { + var (dotnetRoot, userProfileDir, mockInstaller, workloadResolver, manifestProvider) = + CorruptWorkloadSetTestHelper.SetupCorruptWorkloadSet(TestAssetsManager, userLocal, out string sdkFeatureVersion); + + mockInstaller.InstalledManifests.Should().HaveCount(0); + + var workloadResolverFactory = new MockWorkloadResolverFactory(dotnetRoot, sdkFeatureVersion, workloadResolver, userProfileDir); + + // Attach the corruption repairer to the manifest provider + var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot, manifestDownload: true); + manifestProvider.CorruptionRepairer = new WorkloadManifestCorruptionRepairer( + _reporter, + mockInstaller, + workloadResolver, + new SdkFeatureBand(sdkFeatureVersion), + dotnetRoot, + userProfileDir, + nugetDownloader, + packageSourceLocation: null, + VerbosityOptions.detailed); + + // Advertise a NEWER workload set version than what's currently installed (6.0.100) + // so that the update command proceeds with manifest installation + var workloadManifestUpdater = new MockWorkloadManifestUpdater( + manifestUpdates: [ + new ManifestUpdateWithWorkloads(new ManifestVersionUpdate(new ManifestId("xamarin-android-build"), new ManifestVersion("8.4.8"), "6.0.100"), Enumerable.Empty>().ToDictionary()), + new ManifestUpdateWithWorkloads(new ManifestVersionUpdate(new ManifestId("xamarin-ios-sdk"), new ManifestVersion("10.0.2"), "6.0.100"), Enumerable.Empty>().ToDictionary()) + ], + fromWorkloadSet: true, workloadSetVersion: "6.0.101"); + + var parseResult = Parser.Parse(new string[] { "dotnet", "workload", "update" }); + + // Run update command + var updateCommand = new WorkloadUpdateCommand(parseResult, reporter: _reporter, workloadResolverFactory, + workloadInstaller: mockInstaller, workloadManifestUpdater: workloadManifestUpdater); + updateCommand.Execute(); + + // Verify that manifests were reinstalled + mockInstaller.InstalledManifests.Should().HaveCount(2); + mockInstaller.InstalledManifests.Should().Contain(m => m.manifestUpdate.ManifestId.ToString() == "xamarin-android-build"); + mockInstaller.InstalledManifests.Should().Contain(m => m.manifestUpdate.ManifestId.ToString() == "xamarin-ios-sdk"); + + // Verify command succeeded + _reporter.Lines.Should().NotContain(line => line.Contains("failed", StringComparison.OrdinalIgnoreCase)); + } } } diff --git a/test/trustedroots.Tests/trustedroots.Tests.csproj b/test/trustedroots.Tests/trustedroots.Tests.csproj index a14af8d295cd..fb8e2e36037c 100644 --- a/test/trustedroots.Tests/trustedroots.Tests.csproj +++ b/test/trustedroots.Tests/trustedroots.Tests.csproj @@ -4,6 +4,7 @@ $(SdkTargetFramework) Exe + false $(TestHostFolder)